Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

算法基础

高精度

高精度加法

#include <bits/stdc++.h>
using namespace std;
const int N = 550;
vector<int> add(string a, string b) {
    reverse(a.begin(), a.end());
    reverse(b.begin(), b.end());
    int lena = a.length(), lenb = b.length();
    int len = max(lena, lenb);
    vector<int> res(len + 1);
    for (int i = 0; i < len; i++) {
        res[i] += i < lena ? a[i] - '0' : 0;
        res[i] += i < lenb ? b[i] - '0' : 0;
        res[i + 1] += res[i] / 10;
        res[i] %= 10;
    }
    while (res.size() > 1 && res.back() == 0) res.pop_back();
    reverse(res.begin(), res.end());
    return res;
}
int main() {
    string a, b;
    cin >> a >> b;
    vector<int> res = add(a, b);
    for (int i = 0; i < res.size(); i++) {
        cout << res[i];
    }
    cout << endl;
    return 0;
}

高精度减法

#include <bits/stdc++.h>
using namespace std;

vector<int> sub(string a, string b) {
    reverse(a.begin(), a.end());
    reverse(b.begin(), b.end());
    int lena = a.length(), lenb = b.length();
    int len = max(lena, lenb);
    vector<int> res(len + 1);
    for (int i = 0; i < len; i++) {
        res[i] += i < lena ? a[i] - '0' : 0;
        res[i] -= i < lenb ? b[i] - '0' : 0;
        if (res[i] < 0) {
            res[i + 1]--;
            res[i] += 10;
        }
    }
    while (res.size() > 1 && res.back() == 0) res.pop_back();
    reverse(res.begin(), res.end());
    return res;
}
int main() {
    string a, b;
    cin >> a >> b;
    vector<int> res = sub(a, b);
    for (int i = 0; i < res.size(); i++) {
        cout << res[i];
    }
    cout << endl;
    return 0;
}

高精度乘法

#include <bits/stdc++.h>
using namespace std;

vector<int> mul(string a, string b) {
    reverse(a.begin(), a.end());
    reverse(b.begin(), b.end());
    int lena = a.length(), lenb = b.length();
    vector<int> res(lena + lenb + 1);
    for (int i = 0; i < lena; i++) {
        for (int j = 0; j < lenb; j++) {
            res[i + j] += (a[i] - '0') * (b[j] - '0');
        }
    }
    for (int i = 0; i + 1 < res.size(); i++) {
        res[i + 1] += res[i] / 10;
        res[i] %= 10;
    }
    while (res.size() > 1 && res.back() == 0) res.pop_back();
    reverse(res.begin(), res.end());
    return res;
}

int main() {
    string a, b;
    cin >> a >> b;
    vector<int> res = mul(a, b);
    for (int i = 0; i < res.size(); i++) {
        cout << res[i];
    }
    cout << endl;
    return 0;
}

高精度乘低精度

#include <bits/stdc++.h>
using namespace std;

vector<int> mul(string a, int b) {
    reverse(a.begin(), a.end());
    int lena = a.length();
    vector<int> res(lena);
    for (int i = 0; i < lena; i++) {
        res[i] = b * (a[i] - '0');
    }
    for (int i = 0; i + 1 < res.size(); i++) {
        res[i + 1] += res[i] / 10;
        res[i] %= 10;
    }
    while (res.back() > 10) {
        int x = res.back();
        res.back() = x % 10;
        res.push_back(x / 10);
    }
    while (res.size() > 1 && res.back() == 0) res.pop_back();
    reverse(res.begin(), res.end());
    return res;
}

int main() {
    string a;
    int b;
    cin >> a >> b;
    vector<int> res = mul(a, b);
    for (int i = 0; i < res.size(); i++) {
        cout << res[i];
    }
    cout << endl;
    return 0;
}

高精度除以低精度

#include <bits/stdc++.h>
using namespace std;

vector<int> div(string a, int b) {
    int lena = a.length();
    vector<int> res;
    long long x = 0;
    for (int i = 0; i < lena; i++) {
        x = x * 10 + (a[i] - '0');
        if (!res.empty() || x / b > 0) {
            res.push_back(x / b);
        }
        x %= b;
    }
    if (res.empty()) res.push_back(0);
    return res;
}

int main() {
    string a;
    int b;
    cin >> a >> b;
    vector<int> res = div(a, b);
    for (int i = 0; i < res.size(); i++) {
        cout << res[i];
    }
    cout << endl;
    return 0;
}

高精度除法

#include <bits/stdc++.h>
using namespace std;
struct BigInt {
    vector<int> d;

    BigInt() {}

    BigInt(const string& s) {
        for (int i = s.size() - 1; i >= 0; i--) {
            d.push_back(s[i] - '0');
        }
    }

    void trim() {
        while (d.size() > 1 && d.back() == 0) d.pop_back();
    }

    friend BigInt operator+(const BigInt& lhs, const BigInt& rhs) {
        BigInt res;
        const vector<int>& a = lhs.d;
        const vector<int>& b = rhs.d;
        vector<int>& c = res.d;
        int lena = a.size(), lenb = b.size();
        int len = max(lena, lenb);
        c.resize(len + 1, 0);

        for (int i = 0; i < len; i++) {
            if (i < lena) c[i] += a[i];
            if (i < lenb) c[i] += b[i];
            c[i + 1] += c[i] / 10;
            c[i] %= 10;
        }
        while (c.size() > 1 && c.back() == 0) c.pop_back();
        return res;
    }

    friend BigInt operator-(const BigInt& lhs, const BigInt& rhs) {
        BigInt res;
        const vector<int>& a = lhs.d;
        const vector<int>& b = rhs.d;
        vector<int>& c = res.d;
        int lena = a.size(), lenb = b.size();
        int len = max(lena, lenb);
        c.resize(len, 0);
        for (int i = 0; i < len; i++) {
            if (i < lena) c[i] += a[i];
            if (i < lenb) c[i] -= b[i];
            if (c[i] < 0) {
                c[i] += 10;
                c[i + 1]--;
            }
        }
        while (c.size() > 1 && c.back() == 0) c.pop_back();
        return res;
    }

    friend BigInt operator*(const BigInt& lhs, int b) {
        BigInt res;
        const vector<int>& a = lhs.d;
        vector<int>& c = res.d;
        int lena = a.size();
        int len = lena;
        c.resize(len + 1, 0);

        for (int i = 0; i < len; i++) {
            c[i] += a[i] * b;
            c[i + 1] += c[i] / 10;
            c[i] %= 10;
        }
        while (c[len] >= 10) {
            c.push_back(c[len] / 10);
            c[len++] %= 10;
        }
        while (c.size() > 1 && c.back() == 0) c.pop_back();
        return res;
    }

    bool operator<(const BigInt& b) const {
        if (d.size() != b.d.size()) return d.size() < b.d.size();
        for (int i = d.size() - 1; i >= 0; i--)
            if (d[i] != b.d[i]) return d[i] < b.d[i];
        return false;
    }

    friend istream& operator>>(istream& is, BigInt& x) {
        string s;
        is >> s;
        x = BigInt(s);
        return is;
    }
    friend ostream& operator<<(ostream& os, const BigInt& x) {
        for (int i = x.d.size() - 1; i >= 0; i--) {
            os << x.d[i];
        }
        return os;
    }
};

pair<BigInt, BigInt> divmod(BigInt A, BigInt B) {
    BigInt Q, R("0");
    Q.d.assign(A.d.size(), 0);
    for (int i = A.d.size() - 1; i >= 0; i--) {
        R.d.insert(R.d.begin(), A.d[i]);
        R.trim();
        int q = 0;
        while (!(R < B)) {
            R = R - B;
            q++;
        }
        Q.d[i] = q;
    }
    Q.trim();
    return {Q, R};
}

int main() {
    BigInt a, b;
    cin >> a >> b;
    auto [c, d] = divmod(a, b);
    cout << c << endl;
    cout << d << endl;
    return 0;
}

压位高精度乘法

高精度乘法加强版

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int LEN = 5000;
const int w = 9;
const int base = 1000000000;
vector<int> mul(const vector<int>& a, const vector<int>& b) {
    vector<int> c(a.size() + b.size() + 1);
    for (int i = 0; i < a.size(); i++) {
        for (int j = 0; j < b.size(); j++) {
            // 避免爆long long
            ll now = c[i + j] + 1LL * a[i] * b[j];
            c[i + j] = now % base;
            c[i + j + 1] += now / base;
        }
    }
    for (int i = 0; i + 1 < c.size(); i++) {
        c[i + 1] += c[i] / base;
        c[i] %= base;
    }
    // 去掉前导0
    while (c.size() > 1 && c.back() == 0) c.pop_back();
    return c;
}
int calc(const vector<int>& a, int k) {
    int x = a[k / w];
    for (int i = 0; i < k % w; i++) {
        x = x / 10;
    }
    return x % 10;
}
int main() {
    string a, b;
    cin >> a >> b;
    // 翻转
    reverse(a.begin(), a.end());
    reverse(b.begin(), b.end());
    // 压位
    vector<int> va, vb;
    for (int i = 0; i < a.size(); i += w) {
        int x = 0;
        for (int j = i + w - 1; j >= i; j--) {
            x = x * 10 + (j < a.size() ? a[j] - '0' : 0);
        }
        va.push_back(x);
    }
    for (int i = 0; i < b.size(); i += w) {
        int x = 0;
        for (int j = i + w - 1; j >= i; j--) {
            x = x * 10 + (j < b.size() ? b[j] - '0' : 0);
        }
        vb.push_back(x);
    }
    vector<int> c = mul(va, vb);
    // 输出补0
    printf("%d", c.back());
    for (int i = (int)c.size() - 2; i >= 0; i--) {
        printf("%09d", c[i]);
    }
    return 0;
}

重载运算符写法

#include <bits/stdc++.h>
using namespace std;
struct BigInt {
    vector<int> d;

    // 默认构造函数
    BigInt() {}

    // 用 string 构造
    BigInt(const string& s) {
        for (int i = s.size() - 1; i >= 0; i--) {
            d.push_back(s[i] - '0');
        }
    }

    // 用 int 构造,需要是非负整数
    BigInt(int x) {
        if (x == 0) {
            d.push_back(0);
            return;
        }
        while (x > 0) {
            d.push_back(x % 10);
            x /= 10;
        }
    }

    // 重载加法运算符
    friend BigInt operator+(const BigInt& lhs, const BigInt& rhs) {
        BigInt res;
        const vector<int>& a = lhs.d;
        const vector<int>& b = rhs.d;
        vector<int>& c = res.d;
        int lena = a.size(), lenb = b.size();
        int len = max(lena, lenb);
        c.resize(len + 1, 0);

        for (int i = 0; i < len; i++) {
            if (i < lena) c[i] += a[i];
            if (i < lenb) c[i] += b[i];
            c[i + 1] += c[i] / 10;
            c[i] %= 10;
        }
        while (c.size() > 1 && c.back() == 0) c.pop_back();
        return res;
    }

    // 重载减法运算符
    friend BigInt operator-(const BigInt& lhs, const BigInt& rhs) {
        BigInt res;
        const vector<int>& a = lhs.d;
        const vector<int>& b = rhs.d;
        vector<int>& c = res.d;
        int lena = a.size(), lenb = b.size();
        int len = max(lena, lenb);
        c.resize(len, 0);
        for (int i = 0; i < len; i++) {
            if (i < lena) c[i] += a[i];
            if (i < lenb) c[i] -= b[i];
            if (c[i] < 0) {
                c[i] += 10;
                c[i + 1]--;
            }
        }
        while (c.size() > 1 && c.back() == 0) c.pop_back();
        return res;
    }

    friend BigInt operator*(const BigInt& lhs, int b) {
        BigInt res;
        const vector<int>& a = lhs.d;
        vector<int>& c = res.d;
        int lena = a.size();
        int len = lena;
        c.resize(len + 1, 0);

        for (int i = 0; i < len; i++) {
            c[i] += a[i] * b;
            c[i + 1] += c[i] / 10;
            c[i] %= 10;
        }
        while (c[len] >= 10) {
            c.push_back(c[len] / 10);
            c[len++] %= 10;
        }
        while (c.size() > 1 && c.back() == 0) c.pop_back();
        return res;
    }

    friend BigInt operator*(const BigInt& lhs, const BigInt& rhs) {
        BigInt res;
        const vector<int>& a = lhs.d;
        const vector<int>& b = rhs.d;
        vector<int>& c = res.d;
        int lena = a.size();
        int lenb = b.size();
        int len = lena + lenb + 1;
        c.resize(len, 0);

        for (int i = 0; i < lena; i++) {
            for (int j = 0; j < lenb; j++) {
                c[i + j] += a[i] * b[j];
            }
        }
        for (int i = 0; i + 1 < len; i++) {
            c[i + 1] += c[i] / 10;
            c[i] %= 10;
        }
        while (c.size() > 1 && c.back() == 0) c.pop_back();
        return res;
    }

    friend BigInt operator/(const BigInt& lhs, int b) {
        BigInt res;
        const vector<int>& a = lhs.d;
        vector<int>& c = res.d;
        int lena = a.size();
        long long rem = 0;
        for (int i = lena - 1; i >= 0; i--) {
            rem = rem * 10 + a[i];
            if (!c.empty() || rem / b > 0) {
                c.push_back(rem / b);
            }
            rem %= b;
        }
        if (c.empty()) c.push_back(0);
        reverse(c.begin(), c.end());
        return res;
    }

    // 重载输出流运算符
    friend ostream& operator<<(ostream& os, const BigInt& x) {
        for (int i = x.d.size() - 1; i >= 0; i--) {
            os << x.d[i];
        }
        return os;
    }
};

int main() {
    string a;
    int b;
    cin >> a >> b;
    BigInt A = a;
    BigInt C = A / b;
    cout << C << endl;
    return 0;
}

题目

排序算法

基础排序算法

插入排序

插入排序(代码填空)

#include <vector>
using namespace std;

void insertion_sort(vector<int> &a) {
    int n = a.size();
    for (int i = 1; i < n; i++) {
        int key = a[i];
        int j = i - 1;
        while (j >= 0 && a[j] > key) {
            a[j + 1] = a[j];
            j--;
        }
        a[j + 1] = key;
    }
}

冒泡排序

冒泡排序(代码填空)

#include <vector>
using namespace std;

void bubble_sort(vector<int> &a) {
    int n = a.size();
    for (int i = 0; i < n - 1; i++) {
        bool swapped = false;
        for (int j = 0; j < n - i - 1; j++) {
            if (a[j] > a[j + 1]) {
                swap(a[j], a[j + 1]);
                swapped = true;
            }
        }
        if (!swapped) {
            break;
        }
    }
}

选择排序

选择排序(代码填空)

#include <vector>
using namespace std;

void selection_sort(vector<int> &a) {
    int n = a.size();
    for (int i = 0; i < n - 1; i++) {
        int k = i;
        for (int j = i + 1; j < n; j++) {
            if (a[j] < a[k]) {
                k = j;
            }
        }
        if (i != k) {
            swap(a[i], a[k]);
        }
    }
}

计数排序

计数排序(代码填空)

#include <cstdio>
#include <iostream>
#include <vector>
using namespace std;

// 计数排序函数
void counting_sort(vector<int>& a) {
    if (a.empty()) return;

    // 找到数组中的最大值
    int maxv = a[0];
    for (int i = 1; i < a.size(); i++) {
        maxv = max(maxv, a[i]);
    }

    // 创建计数数组并初始化
    vector<int> count(maxv + 1, 0);

    // 统计每个元素的出现次数
    for (int i = 0; i < a.size(); i++) {
        count[a[i]]++;
    }

    // 根据计数数组重新排序
        int idx = 0;
    for (int i = 0; i <= maxv; i++) {
        while (count[i] > 0) {
            a[idx++] = i;
            count[i]--;
        }
    }
}

int main() {
    int n;
    while (scanf("%d", &n) != EOF) {
        vector<int> a;
        for (int i = 0; i < n; i++) {
            int x;
            scanf("%d", &x);
            a.push_back(x);
        }
        counting_sort(a);
        for (int i = 0; i < n; i++) {
            printf("%d%c", a[i], (i == n - 1) ? '\n' : ' ');
        }
    }
    return 0;
}

基数排序

最后的排序

[!warning] TODO

添加算法解释

#include <vector>
using namespace std;

void my_sort(vector<int> &a) {
	const int base = 65536;
    int n = a.size();
    vector<int> b(n);
    vector<int> c(base, 0);
    for (int exp = 1; exp <= base; exp *= base) {
        for (int i = 0; i < a.size(); i++) {
            c[a[i] / exp % base]++;
        }
        for (int i = 1; i < base; i++) {
            c[i] += c[i - 1];
        }
        for (int i = n - 1; i >= 0; i--) {
            b[--c[a[i] / exp % base]] = a[i];
        }
        swap(a, b);
        for (int i = 0; i < base; i++) {
            c[i] = 0;
        }
    }
}

排序题目

反向字典

归并排序

二路归并

[!info] 二路归并

给两个按非降序排列的整数数组 ab。你需要将它们合并成一个数组,使合并后的数组同样按非降序排列。

#include <cstdio>
#include <iostream>
#include <vector>

using namespace std;

vector<int> merge(const vector<int>& a, const vector<int>& b) {
    vector<int> c;
    int n = a.size(), m = b.size();
    int i = 0, j = 0;
    while (i < n && j < m) {
        if (a[i] <= b[j]) {
            c.push_back(a[i++]);
        } else {
            c.push_back(b[j++]);
        }
    }
    while (i < n) {
        c.push_back(a[i++]);
    }
    while (j < m) {
        c.push_back(b[j++]);
    }
    return c;
}

[!question] 思维拓展

K 路合并怎么做?

归并排序思路

归并排序(代码填空)

例子:

初始数组:[8, 4, 5, 7, 1, 3, 6, 2]

分:
[8, 4, 5, 7]      [1, 3, 6, 2]
[8, 4]  [5, 7]    [1, 3]  [6, 2]
[8] [4]  [5] [7]  [1] [3]  [6] [2]

治:
[4, 8]  [5, 7]    [1, 3]  [2, 6]
[4, 5, 7, 8]      [1, 2, 3, 6]
[1, 2, 3, 4, 5, 6, 7, 8]
#include <cstdio>
#include <iostream>
using namespace std;

int a[100005], b[100005];

void merge_sort(int l, int r) {
    if (l == r) {
        return;
    }
    int mid = (l + r) / 2;
    merge_sort(l, mid);
    merge_sort(mid + 1, r);
    int p1 = l, p2 = mid + 1, p = l;
    while (p1 <= mid && p2 <= r) {
        if (a[p1] <= a[p2]) {
            b[p++] = a[p1++];
        } else {
            b[p++] = a[p2++];
        }
    }
    while (p1 <= mid) {
        b[p++] = a[p1++];
    }
    while (p2 <= r) {
        b[p++] = a[p2++];
    }
    for (int i = l; i <= r; i++) {
        a[i] = b[i];
    }
}

int main() {
    int n;
    while (scanf("%d", &n) != EOF) {
        for (int i = 0; i < n; i++) {
            scanf("%d", &a[i]);
        }
        merge_sort(0, n - 1);
        for (int i = 0; i < n; i++) {
            printf("%d%c", a[i], (i == n - 1) ? '\n' : ' ');
        }
    }
    return 0;
}

归并排序应用

逆序对(Inversion Pair)

[4, 5, 7, 8]      [1, 2, 3, 6]

比如比较 (5, 2) 时,发现有三个逆序对需要统计 (5, 2), (7, 2), (8, 2)

答案需要加上

[!question] 易错辨析

为什么不是加上 ,统计 (5, 1), (5, 2)

int mid = (l + r) / 2;
merge_sort(l, mid);
merge_sort(mid + 1, r);
int p1 = l, p2 = mid + 1, p = l;
while (p1 <= mid && p2 <= r) {
    if (a[p1] <= a[p2]) {
        b[p++] = a[p1++];
    } else {
        ans += mid - p1 + 1;  // 只需要加这一行
        b[p++] = a[p2++];
    }
}

快速排序

Partition

[!info] Partition

给一个数组 nums,要求把其中小于等于 0 的元素放到前面,把大于 0 的元素放到后面,内部顺序不做要求。答案为不唯一,返回任意一个合法解即可。

void partition(vector<int>& nums) {
	int left = 0, right = 0;
    while (right < nums.size()) {
        if (nums[right] <= 0) {
            swap(nums[left], nums[right]);
            left++;
        }
        right++;
    }
}

快速排序

快速排序(代码填空)

#include <cstdio>
#include <iostream>
#include <vector>
using namespace std;

int partition(vector<int>& arr, int low, int high) {
    // 选取某个元素作为基准值(pivot)
    // 遍历这段区间,把小于等于 pivot 的元素放到左侧
    // 最后把 pivot 放到正确位置,返回它的索引
	swap(arr[rand() % (high - low + 1) + low], arr[high]);
    int pivot = arr[high];
    int i = low;
    for (int j = low; j < high; j++) {
        if (arr[j] <= pivot) {
            swap(arr[i++], arr[j]);
        }
    }
    swap(arr[i], arr[high]);
    return i;
}

// 快速排序
void quick_sort(vector<int>& arr, int low, int high) {
    if (low >= high) return;
    int pivotIndex = partition(arr, low, high);  // 获取 pivot 索引
    quick_sort(arr, low, pivotIndex - 1);        // 递归排序左部分
    quick_sort(arr, pivotIndex + 1, high);       // 递归排序右部分
}

如果每次都选第一个数作为 pivot,最坏情况是输入为 [1,2,3,4,5][5,4,3,2,1]

快速排序最坏时间复杂度是 ,平均时间复杂度是

求无序数组第 k 小

查找第 K 小元素

TODO

各类排序算法的对比

算法最坏运行时间平均/期望运行时间空间stableIn-place
插入排序 insertion sortyesyes
归并排序 merge Sortyesno
堆排序 heapsortnoyes
快速排序 quick sort最坏,平均noyes
计数排序 counting Sortyesno
基数排序 radix sort-no
桶排序 bucket sortyesno

稳定性:具有相同值的元素在输出数组中的相对次序与它们在输入数组中的相对次序相同。

计数排序中的 表示元素值范围大小,基数排序中的 表示数位个数, 表示每个数位的取值范围大小。

基数排序的稳定性取决于底层排序算法,一般使用稳定的计数排序。

基于比较的排序:

  • 插入排序、归并排序、堆排序、快速排序…

非基于比较的排序:

  • 计数排序、基数排序、桶排序…

比较次数下界

[!question] 思考

4 个小球,假设他们重量各不相同,你每次可以用天平称出两个小球的轻重,想要将这 4 个小球按照重量从小到大排序,运气最差的情况下要称多少次?

一共 5 次:

  • 比较
  • 比较
  • 比较 ,确定最小值
  • 比较 ,确定最大值
  • 比较中间的两个

[!question] 思考

如果改成 8 个小球,能否构造/估算出最小次数?(16 次)

决策树:将比较的执行过程抽象为一棵二叉树(每个内部节点对应一次比较;左分支代表“<”的结果,右分支代表“>”的结果),叶子节点对应一种可能的输出(即一种排序结果)。

树的深度(height)即为最坏情况下比较的次数。

一棵高度为 的二叉树,最多拥有 个叶子。因此,为了区分所有 种可能的输入排列,必须满足

从信息论的角度理解:通过比较来确定出正确的排列,完全消除这 种可能性中的不确定性。需要 次比较,每次消除一半可能。

重要结论:任意的基于比较的排序算法排序 个元素的最坏情况运行时间的下界为

Note

A036604: Sorting numbers: minimal number of comparisons needed to sort n elements.

二分

整数二分

二分查找 1

二分查找 1

#include <bits/stdc++.h>
using namespace std;
int n, m;
int a[1000010];
int binary_search(int x) {
    int l = 0, r = n - 1;
    while (l <= r) {
        int mid = (l + r) / 2;
        if (a[mid] == x) return mid;
        if (a[mid] < x) {
            l = mid + 1;
        } else {
            r = mid - 1;
        }
    }
    return -1;
}
int main() {
    scanf("%d%d", &n, &m);
    for (int i = 0; i < n; i++) {
        scanf("%d", a + i);
    }
    while (m--) {
        int x;
        scanf("%d", &x);
        printf("%d\n", binary_search(x));
    }
}

二分查找 2

二分查找 2

求最大的 使

#include <bits/stdc++.h>
using namespace std;
int n, m;
int a[1000010];
int binary_search(int x) {
    int l = -1, r = n - 1;
    while (l < r) {
        int mid = (l + r + 1) / 2;
        if (a[mid] < x) {
            l = mid;
        } else {
            r = mid - 1;
        }
    }
    return l;
}
int main() {
    scanf("%d%d", &n, &m);
    for (int i = 0; i < n; i++) {
        scanf("%d", a + i);
    }
    while (m--) {
        int x;
        scanf("%d", &x);
        printf("%d\n", binary_search(x));
    }
}

二分查找 3

二分查找 3

求最小的 使

#include <bits/stdc++.h>
using namespace std;
int n, m;
int a[1000010];
int binary_search(int x) {
    int l = 0, r = n - 1;
    while (l < r) {
        int mid = (l + r) / 2;
        if (a[mid] > x) {
            r = mid;
        } else {
            l = mid + 1;
        }
    }
    if (a[l] > x) return l;
    return -1;
}
int main() {
    scanf("%d%d", &n, &m);
    for (int i = 0; i < n; i++) {
        scanf("%d", a + i);
    }
    while (m--) {
        int x;
        scanf("%d", &x);
        printf("%d\n", binary_search(x));
    }
}

实数二分

求一元五次方程的解 求方程 的根。

#include <bits/stdc++.h>
using namespace std;

int main() {
    double l = 0, r = 1;
    while (r - l > 1e-12) {
        double mid = (l + r) / 2;
        if (mid * mid * mid * mid * mid + mid < 1) {
            l = mid;
        } else {
            r = mid;
        }
    }
    printf("%.10f\n", l);
}

二分答案

关键词:

  • 最大值最小
  • 答案跟某个变量之间存在单调性

题目:

  • NOIP2015 跳石头

    一年一度的“跳石头”比赛又要开始了! 这项比赛将在一条笔直的河道中进行,河道中分布着一些巨大岩石。组委会已经选择好了两块岩石作为比赛起点和终点。在起点和终点之间,有 N 块岩石(不含起点和终点的岩石)。在比赛过程中,选手们将从起点出发,每一步跳向相邻的岩石,直至到达终点。 为了提高比赛难度,组委会计划移走一些岩石,使得选手们在比赛过程中的最短跳跃距离尽可能长。由于预算限制,组委会至多从起点和终点之间移走 M 块岩石(不能移走起点和终点的岩石)。

  • 分配房间

  • 打包

  • 小木棍

  • 数列分段 Section Ⅱ

倍增

快速幂

快速幂

分块思想

其实求 也可以用 的分块。

递归版

#include <bits/stdc++.h>
using namespace std;

int power(int a, long long b, int mod) {
	if (b == 0) {
		return 1 % mod;
	}
	int x = power(a, b / 2, mod);
	if (b & 1) {
		return 1LL * x * x % mod * a % mod;
	} else {
		return 1LL * x * x % mod;
	}
}

int main() {
	long long a, b;
	int mod;
	cin >> a >> b >> mod;
	cout << power(a % mod, b, mod) << endl;
}

非递归版

#include <bits/stdc++.h>
using namespace std;

int power(int a, long long n, int mod) {
    int ans = 1 % mod;
    while (n) {
        if (n & 1) ans = 1LL * ans * a % mod;
        a = 1LL * a * a % mod;
        n >>= 1;
    }
    return ans;
}

int main() {
    long long a, b;
    int mod;
    cin >> a >> b >> mod;
    cout << power(a % mod, b, mod) << endl;
}

分块思想

其实求 也可以用 的分块。

题目

ST 算法

RMQ 问题:给定 个数,个询问,每次询问区间 内的最大值。

时间复杂度:预处理 ,查询

注意也可以用二进制拆分的方法,将任意一个询问区间拆成 个区间。

模板题

HDU 5443 The Water Problem

RMQ

练习题

进阶

HDU 5726 GCD

#include <bits/stdc++.h>
using namespace std;
using ll = long long;
const int N = 100010;
int n, m;
int q[N];
int f[N][21];
unordered_map<int, ll> mp;
int gcd(int x, int y) {
    if (y == 0) return x;
    return gcd(y, x % y);
}
int calc(int l, int r) {
    int k = q[r - l + 1];
    return gcd(f[l][k], f[r - (1 << k) + 1][k]);
}
void work() {
    mp.clear();
    scanf("%d", &n);
    for (int i = 1; i <= n; i++) {
        scanf("%d", &f[i][0]);
    }
    for (int j = 1; j <= 20; j++) {
        for (int i = 1; i + (1 << j) - 1 <= n; i++) {
            f[i][j] = gcd(f[i][j - 1], f[i + (1 << (j - 1))][j - 1]);
            // cout << i << " " << j << " " << f[i][j] << endl;
        }
    }
    for (int i = 1; i <= n; i++) {
        int x = i;
        while (x <= n) {
            // cout << "i = " << i << " x = " << x << endl;
            int g = calc(i, x);
            int l = x;
            int r = n;
            while (l < r) {
                int mid = (l + r + 1) / 2;
                if (calc(i, mid) == g) {
                    l = mid;
                } else {
                    r = mid - 1;
                }
            }
            // g: [i, x] ~ [i, r]
            mp[g] += r - x + 1;
            x = l + 1;
        }
    }
    scanf("%d", &m);
    while (m--) {
        int l, r;
        scanf("%d%d", &l, &r);
        int g = calc(l, r);
        printf("%d %lld\n", g, mp[g]);
    }
}
int main() {
    q[0] = -1;
    for (int i = 1; i < N; i++) {
        q[i] = ((i & (i - 1)) == 0) ? q[i - 1] + 1 : q[i - 1];
    }
    int T;
    scanf("%d", &T);
    for (int cas = 1; cas <= T; cas++) {
        printf("Case #%d:\n", cas);
        work();
    }
    return 0;
}

子序列倍数统计

#include <bits/stdc++.h>
using namespace std;
const int N = 100010;
const int mod = 1e9 + 7;
int n, m, q;
int f[N][18][6];
int ans[6];
inline void add(int& x, int y) {
    x += y;
    if (x >= mod) x -= mod;
}
void merge(int* c, int* a, int* b) {
    static int tmp[6];
    memset(tmp, 0, sizeof(tmp));
    for (int i = 0; i <= m; i++) {
        for (int j = 0; j <= m; j++) {
            add(tmp[(i + j) % m], 1LL * a[i] * b[j] % mod);
        }
    }
    for (int i = 0; i <= m; i++) {
        c[i] = tmp[i];
    }
}
int main() {
    cin >> n >> m;
    for (int i = 1; i <= n; i++) {
        int x;
        cin >> x;
        f[i][0][0] = 1;
        f[i][0][x % m]++;
    }
    for (int j = 1; j <= 17; j++) {
        for (int i = 1; i + (1 << j) - 1 <= n; i++) {
            merge(f[i][j], f[i][j - 1], f[i + (1 << (j - 1))][j - 1]);
        }
    }
    cin >> q;
    while (q--) {
        int l, r;
        cin >> l >> r;
        memset(ans, 0, sizeof(ans));
        ans[0] = 1;
        for (int j = 17; j >= 0; j--) {
            if (l + (1 << j) - 1 <= r) {
                merge(ans, ans, f[l][j]);
                l += 1 << j;
            }
        }
        cout << ans[0] << endl;
    }
}

倍增

stikl

题意: 个数 次询问,每次问从下标 ,往右跳 步到哪个点,每一步只能跳到下一个不小于当前的数字的位置。

表示节点往右跳步到达的点,初始的 需要用单调队列求一下。

#include <bits/stdc++.h>
using namespace std;
using ll = long long;
const int N = 100010;
int n, m;
int a[N], q[N];
int f[N][20];
int main() {
    scanf("%d%d", &n, &m);
    int r = 0;
    for (int i = 1; i <= n; i++) {
        scanf("%d", a + i);
        while (r > 0 && a[q[r]] <= a[i]) {
            f[q[r]][0] = i;
            r--;
        }
        q[++r] = i;
    }
    while (r > 0) {
        f[q[r]][0] = n + 1;
        r--;
    }
    f[n + 1][0] = n + 1;
    for (int j = 1; j <= 19; j++) {
        for (int i = 1; i <= n + 1; i++) {
            f[i][j] = f[f[i][j - 1]][j - 1];
        }
    }
    while (m--) {
        int x, d;
        scanf("%d%d", &x, &d);
        for (int i = 19; i >= 0; i--) {
            if (d >= (1 << i)) {
                d -= (1 << i);
                x = f[x][i];
            }
        }
        if (x == n + 1) {
            puts("leik lokid");
        } else {
            printf("%d\n", x);
        }
    }
}

贪心

基础

排队打水

[!info] 排队打水

个人排队打水,第 个人打水需要打 分钟。请问如何安排他们打水的顺序,使得所有人等待的时间和最小。

结论:按打水时间从小到大让人打水是最小化总等待时间的最优策略。

证明用相邻交换法:任意逆序相邻对交换都能降低(或不增)总完成时间,从而最终得到非降序排列为最优。

也可以用排序不等式证明:

神牛果

[!info] 神牛果

个正整数,分成 组使得 sum 最大的那组 sum 最小

奥利凡德

[!info] 奥利凡德

哈利波特在与伏地魔的战斗中毁坏了自己的魔杖,于是他决定去奥利凡德的魔杖店买个新的。他在店里看到 个魔杖和 个盒子,每个魔杖的长度为 ,每个盒子的长度为。一个长度为 的魔杖要能放进长度为 的盒子里必须满足 。哈利波特想知道他能否把所有的魔杖都放进盒子里,并且每个盒子只能放一根魔杖。

硬币问题

[!info] 硬币问题

有 1 元、5 元、10 元、50 元、100 元硬币各有若干枚,现在要用这些硬币支付 元,问最少需要多少枚硬币。

反悔贪心

[!info] [CSP-S 2025] 社团招新 / club

其他

删数字 弱化版

删数字

grass 区间覆盖问题变形

kvallsfika 排序贪心

bolir n 个区间 n 个点问是否能一一匹配

回文数组

数袋鼠

前缀和&差分

前缀和

一维前缀和

前缀和(小数据)

前缀和

#include <bits/stdc++.h>
using namespace std;
#define N 200010
int n, q;
int a[N], sum[N];
int main() {
    scanf("%d", &n);
    for (int i = 1; i <= n; i++) {
        scanf("%d", a + i);
        sum[i] = sum[i - 1] + a[i];
    }
    scanf("%d", &q);
    while (q--) {
        int l, r;
        scanf("%d%d", &l, &r);
        printf("%d\n", sum[r] - sum[l - 1]);
    }
}

二维前缀和

矩阵前缀和

#include <bits/stdc++.h>
using namespace std;
#define N 1010
int n, m, q;
int a[N][N], sum[N][N];
int main() {
    scanf("%d%d", &n, &m);
    for (int i = 1; i <= n; i++) {
        for (int j = 1; j <= m; j++) {
            scanf("%d", &a[i][j]);
            sum[i][j] = sum[i - 1][j] + sum[i][j - 1] - sum[i - 1][j - 1] + a[i][j];
        }
    }
    scanf("%d", &q);
    while (q--) {
        int x1, y1, x2, y2;
        scanf("%d%d%d%d", &x1, &y1, &x2, &y2);
        printf("%d\n", sum[x2][y2] - sum[x1 - 1][y2] - sum[x2][y1 - 1] + sum[x1 - 1][y1 - 1]);
    }
}

[HNOI2003] 激光炸弹

一种新型的激光炸弹,可以摧毁一个边长为 的正方形内的所有目标。现在地图上有 个目标,用整数 表示目标在地图上的位置,每个目标都有一个价值 。激光炸弹的投放是通过卫星定位的,但其有一个缺点,就是其爆破范围,即那个边长为 的边必须与 轴, 轴平行。若目标位于爆破正方形的边上,该目标不会被摧毁。 现在你的任务是计算一颗炸弹最多能炸掉地图上总价值为多少的目标。 可能存在多个目标在同一位置上的情况。

#include <bits/stdc++.h>
using namespace std;
using ll = long long;
const int N = 5000;
int n, m;
int val[N + 5][N + 5], sum[N + 5][N + 5];
int main() {
    scanf("%d%d", &n, &m);
    for (int i = 1; i <= n; i++) {
        int x, y, c;
        scanf("%d%d%d", &x, &y, &c);
        val[x][y] += c;
    }
    for (int i = 0; i <= N; i++) {
        for (int j = 0; j <= N; j++) {
            sum[i + 1][j + 1] = sum[i + 1][j] + sum[i][j + 1] - sum[i][j] + val[i][j];
        }
    }
    int ans = 0;
    for (int i = 1; i <= N + 1; i++) {
        for (int j = 1; j <= N + 1; j++) {
            int now = sum[i][j] - sum[i][max(0, j - m)] - sum[max(0, i - m)][j] + sum[max(0, i - m)][max(0, j - m)];
            ans = max(ans, now);
        }
    }
    printf("%d", ans);
}

差分

区间加法

有一个长度为 的数组 a,初始元素值都为 0。现在有 次操作,每次给定三个参数 ,你需要给 a[l], a[l+1], …, a[r] 都加上 。问 次操作之后,数组的每个元素值是多少。

#include <bits/stdc++.h>
using namespace std;
using ll = long long;
int a[100010];
int n, m;
int main() {
    scanf("%d%d", &n, &m);
    while (m--) {
        int l, r, x;
        scanf("%d%d%d", &l, &r, &x);
        a[l] += x;
        a[r + 1] -= x;
    }
    for (int i = 1; i <= n; i++) {
        a[i] += a[i - 1];
    }
    for (int i = 1; i <= n; i++) {
        printf("%lld ", a[i]);
    }
}

二维差分

扩展

结合模运算

区间计数:给一个长度为 的数组,求有多少个区间 满足 的倍数。

知识点:同余,计数

结合二分/双指针

进阶

  • 每次给区间加上一个等差数列

  • 菱形和

其他题目

递推

平面分割

[!info] 平面分割

条直线分割平面,问最多分割出多少个区域。

答案是

骨牌覆盖简单版

[!info] 骨牌覆盖简单版

有一个长度为 的长方形方格,用 的骨牌完全覆盖,可横放或竖放,但不能重叠,有多少种不同的覆盖的方法。因为答案可能很大,你只需要输出答案模 的结果。

f = [0 for i in range(100 + 1)]
f[0] = 1
f[1] = 1
for i in range(2, 100 + 1):
    f[i] = (f[i - 1] + f[i - 2]) % 10007
n = int(input())
print(f[n])

汉诺塔问题(求步数)

[!info] 汉诺塔问题(求步数)

求汉诺塔的移动步数。

答案是

棋盘格数

[!info] 棋盘格数

设有一个 方格的棋盘。求出该棋盘中包含有多少个正方形、多少个长方形(不包括正方形)。

#include <bits/stdc++.h>
using namespace std;
using ll = long long;
__int128 read() {
    char s[30];
    __int128 res = 0;
    scanf("%s", s);
    int len = strlen(s);
    for (int i = 0; i < len; i++) {
        res = res * 10 + s[i] - '0';
    }
    return res;
}

void print(__int128 num) {
    if (num > 9) {
        print(num / 10);
    }
    putchar(num % 10 + '0');
}

int main() {
    __int128 n = read(), m = read();
    __int128 ans1 = 0, ans2 = 0;
    __int128 size = min(n, m);
    ans1 += n * m * size;
    ans1 -= (n + m) * size * (size - 1) / 2;
    ans1 += (size - 1) * size * (2 * size - 1) / 6;
    ans2 = n * (n + 1) * m * (m + 1) / 4 - ans1;
    print(ans1);
    cout << endl;
    print(ans2);
    return 0;
}

Neighbouring Ones

[!info] Neighbouring Ones

对于一个二进制 01 串来说,如果其中所有的 1 都有一个与它相邻的 1,那么称这个 01 串为好串。现在问长度为 的好串有多少个,答案需要对 取模。

f = [0 for i in range(100 + 1)]
f[1] = 1
f[2] = 2
f[3] = 4
f[4] = 7
for i in range(5, 100 + 1):
    f[i] = (f[i - 1] + f[i - 2] + f[i - 4]) % 1000000007
  • 第一类:末尾是 0
  • 第二类:末尾是 11
  • 第三类:末尾是 0111

汉诺双塔

[!info] 汉诺双塔

个圆盘,求移动步数。

n = int(input().strip())
f = [0 for i in range(n + 1)]
f[0] = 0
for i in range(1, n + 1):
	f[i] = 2 * f[i - 1] + 2
print(f[n])

[NOIP2001 普及组] 数的计数

[!info] 数的计数

我们要求找出具有下列性质数的个数(包含输入的自然数 n): 先输入一个自然数 n(n<=1000),然后对此自然数按照如下方法进行处理: 1. 不作任何处理; 2. 在它的左边加上一个自然数,但该自然数不能超过原数的一半; 3. 加上数后,继续按此规则进行处理,直到不能再加自然数为止。

n = int(input().strip())
f = [0 for i in range(n + 1)]
for i in range(1, n + 1):
    f[i] = 1
    for j in range(1, i // 2 + 1):
        f[i] += f[j]
print(f[n])

骨牌覆盖升级版

[!info] 骨牌覆盖升级版

有一个长度为 的长方形方格,用 的骨牌完全覆盖,要求不能重叠,问有多少种不同的覆盖的方法。因为答案可能很大,你只需要输出答案模 的结果。

#include <bits/stdc++.h>
using namespace std;
int f[1010];
int main() {
	f[1] = 1;
	f[2] = 3;
	for (int i = 3; i <= 1000; i++) {
		f[i] = (f[i - 1] + 2LL * f[i - 2]) % 1000000007;
	}
	int T;
	cin >> T;
	while (T--) {
		int n;
		cin >> n;
		cout << f[n] << endl;
	}
}

覆盖墙壁

[!info] 覆盖墙壁

和 L型的牌覆盖 的墙壁,有多少种不同的覆盖方法。答案取模。

递归

一般递归

汉诺塔

汉诺塔

#include <bits/stdc++.h>
using namespace std;
int cnt;
void hanoi(int n, char from, char mid, char to) {
    if (n == 0) return;
    hanoi(n - 1, from, to, mid);
    ++cnt;
    cout << cnt << ".Move " << n << " from " << from << " to " << to << endl;
    hanoi(n - 1, mid, from, to);
}
int main() {
    int n;
    cin >> n;
    hanoi(n, 'a', 'b', 'c');
}

FJ 的字符串

生成递归构造的字符串

FJ 的字符串
FJ 在沙盘上写了这样一些字符串:
A1 = “A”
A2 = “ABA”
A3 = “ABACABA”
A4 = “ABACABADABACABA”
你能找出其中的规律并写所有的数列 AN 吗?

#include <iostream>
using namespace std;
string dfs(int n) {
	if (n == 1) {
		return "A";
	}
	string mid;
	mid += 'A' + n - 1;
	return dfs(n - 1) + mid + dfs(n - 1);
}
int main() {
	int n;
	cin >> n;
	cout << dfs(n) << endl;
}

其他题目

递归枚举

分治

基础模型

  • 快速幂
  • 汉诺塔
  • 快速排序
  • 归并排序

题目

数论

链表

TODO

队列

题目

数学

数论

素数筛

埃氏筛

筛法求素数

#include <bits/stdc++.h>
using namespace std;
const int N = 1000000;
bool isp[N + 5];
int sum[N + 5];
void pre() {
    memset(isp, 1, sizeof(isp));
    isp[0] = isp[1] = false;
    for (int i = 2; i <= N / i; i++) {
        if (isp[i]) {
            for (int j = i * i; j <= N; j += i) {
                isp[j] = false;
            }
        }
    }
    for (int i = 1; i <= N; i++) {
        sum[i] = sum[i - 1] + isp[i];
    }
}

int main() {
    pre();
    int t;
    scanf("%d", &t);
    while (t--) {
        int l, r;
        scanf("%d%d", &l, &r);
        printf("%d\n", sum[r] - sum[l - 1]);
    }
}

[!info] 质数统计(数据加强版)

给定 ,求出 内质数的个数。

#include <bits/stdc++.h>
using namespace std;
using ll = long long;
const int N = 1000000;
bool isp[N + 5];
vector<int> prime;
void pre() {
    memset(isp, 1, sizeof(isp));
    isp[0] = isp[1] = false;
    for (int i = 2; i <= N; i++) {
        if (isp[i]) {
            prime.push_back(i);
        }
        for (int x : prime) {
            if (x > N / i) break;
            isp[i * x] = false;
            if (i % x == 0) break;
        }
    }
}

int main() {
    pre();
    ll l, r;
    cin >> l >> r;
    memset(isp, 1, sizeof(isp));
    for (int x : prime) {
        if (x > r / x) break;
        ll k = max(2ll, (l + x - 1) / x);
        for (ll i = k * x; i <= r; i += x) {
            isp[i - l] = false;
        }
    }
    int sum = 0;
    for (ll i = l; i <= r; i++) {
        sum += (i != 1 && isp[i - l]);
    }
    cout << sum << endl;
}

[!info] 约数和加强版

给定 个数,分别求出他们所有约数之和。

先枚举约数,再枚举倍数,可以用 时间求出所有数的约数和。证明是经典的调和级数。

#include <bits/stdc++.h>
using namespace std;
using ll = long long;
const int N = 10000000;
ll f[N + 5];
int main() {
    for (int i = 1; i <= N; i++) {
        for (int j = i; j <= N; j += i) {
            f[j] += i;
        }
    }
    int T;
    scanf("%d", &T);
    while (T--) {
        int n;
        scanf("%d", &n);
        printf("%lld\n", f[n]);
    }
}

[!info] 园艺题解

求任意两个数 的最大公约数。

for (int d = 1; d <= n; ++d) {
    for (int a = d; a <= n; a += d) {
        for (int b = d; b <= n; b += d) {
            g[a][b] = d;
            g[b][a] = d;
        }
    }
}

对于固定的 ,最里层两个循环次数约为

循环总次数:

注意到 是一个收敛的调和级数,极限为 (巴塞尔问题)。

所以这个代码的时间复杂度是

线性筛

每个合数只被它的最小素因子筛一次,时间复杂度 O(n)。

const int N = 10000000;
int prime[N + 5], cnt;
bool vis[N + 5];
void getprime(int n) {
    for (int i = 2; i <= n; ++i) {
        if (!vis[i]) {
            prime[++cnt] = i;
        }
        for (int j = 1; j <= cnt && prime[j] <= n / i; ++j) {
            vis[i * prime[j]] = true;
            if (i % prime[j] == 0) break;
        }
    }
}

i * prime[j] 的素因子一定是 prime[j]

题目:

组合数计算

给定 , , ,求

组合数计算 0

不取模,数据范围

做法:高精度

#include <bits/stdc++.h>
using namespace std;
using ll = long long;
struct BigInt {
    vector<int> d;

    BigInt() {}
    BigInt(const string &s) { *this = s; }
    BigInt(int x) {
        d.clear();
        if (x == 0) {
            d.push_back(0);
            return;
        }
        while (x > 0) {
            d.push_back(x % 10);
            x /= 10;
        }
    }
    BigInt &operator=(const string &s) {
        d.clear();
        for (int i = s.size() - 1; i >= 0; i--) {
            d.push_back(s[i] - '0');
        }
        trim();
        return *this;
    }

    void trim() {
        while (d.size() > 1 && d.back() == 0) d.pop_back();
    }

    string str() const {
        string s;
        for (int i = d.size() - 1; i >= 0; i--) s.push_back(d[i] + '0');
        return s.empty() ? "0" : s;
    }

    friend BigInt operator+(const BigInt &a, const BigInt &b) {
        BigInt c;
        int lena = a.d.size();
        int lenb = b.d.size();
        int len = max(lena, lenb);
        c.d.resize(len + 1, 0);
        for (int i = 0; i < len; i++) {
            c.d[i] += (i < lena) ? a.d[i] : 0;
            c.d[i] += (i < lenb) ? b.d[i] : 0;
            c.d[i + 1] += c.d[i] / 10;
            c.d[i] %= 10;
        }
        c.trim();
        return c;
    }

    friend BigInt operator*(const BigInt &a, const BigInt &b) {
        BigInt c;
        int lena = a.d.size();
        int lenb = b.d.size();
        c.d.resize(lena + lenb + 1, 0);
        for (int i = 0; i < lena; i++) {
            for (int j = 0; j < lenb; j++) {
                c.d[i + j] += a.d[i] * b.d[j];
            }
        }
        for (int i = 0; i + 1 < c.d.size(); i++) {
            c.d[i + 1] += c.d[i] / 10;
            c.d[i] %= 10;
        }
        c.trim();
        return c;
    }

    friend BigInt operator*(const BigInt &a, const int &b) {
        BigInt c;
        int lena = a.d.size();
        c.d.resize(lena, 0);
        for (int i = 0; i < lena; i++) {
            c.d[i] = b * a.d[i];
        }
        for (int i = 0; i + 1 < c.d.size(); i++) {
            c.d[i + 1] += c.d[i] / 10;
            c.d[i] %= 10;
        }
        while (c.d.back() >= 10) {
            int x = c.d.back();
            c.d.back() = x % 10;
            c.d.push_back(x / 10);
        }
        c.trim();
        return c;
    }

    friend BigInt operator/(const BigInt &a, const int &b) {
        BigInt c;
        int lena = a.d.size();
        ll x = 0;
        for (int i = lena - 1; i >= 0; i--) {
            x = x * 10 + a.d[i];
            if (c.d.empty() && x / b == 0) {
                continue;
            }
            c.d.push_back(x / b);
            x %= b;
        }
        if (c.d.empty()) c.d.push_back(0);
        reverse(c.d.begin(), c.d.end());
        return c;
    }
};
int main() {
    int n, m;
    cin >> n >> m;
    BigInt ans(1);
    for (int i = 1; i <= m; i++) {
        ans = ans * (n - i + 1);
        ans = ans / i;
    }
    cout << ans.str() << endl;
    return 0;
}

组合数计算 1

多组询问,

做法:递推 时间复杂度

#include <bits/stdc++.h>
using namespace std;
const int N = 2000;
int c[N + 5][N + 5];

int main() {
    int T, P;
    cin >> T >> P;
    c[0][0] = 1;
    for (int i = 1; i <= N; i++) {
        c[i][0] = 1;
        for (int j = 1; j <= i; j++) {
            c[i][j] = (c[i - 1][j - 1] + c[i - 1][j]) % P;
        }
    }
    while (T--) {
        int n, m;
        cin >> n >> m;
        cout << c[n][m] << endl;
    }
    return 0;
}

组合数计算 2

数据范围:

是质数。

做法:

因为模数是质数,直接用费马小定理求乘法逆元。

时间复杂度

#include <bits/stdc++.h>
using namespace std;
using ll = long long;
int power(int a, ll n, int mod) {
    int res = 1;
    while (n) {
        if (n & 1) res = 1LL * res * a % mod;
        a = 1LL * a * a % mod;
        n >>= 1;
    }
    return res;
}
int main() {
    int T;
    cin >> T;
    while (T--) {
        ll n;
        int m, P;
        cin >> n >> m >> P;
        if (m > n / 2) m = n - m;
        ll ans = 1;
        for (int i = 1; i <= m; i++) {
            ans = ans * ((n - i + 1) % P) % P;
            ans = ans * power(i, P - 2, P) % P;
        }
        cout << ans << endl;
    }
    return 0;
}

组合数计算 3

数据范围:

模数

做法:

预处理

则每次询问可以做到 ,预处理时间复杂度可以,也可以优化到

总时间复杂度

#include <bits/stdc++.h>
using namespace std;
using ll = long long;
const int mod = 1e9 + 7;
const int N = 1e6;
int fac[N + 10], inv[N + 10];
int power(int a, ll n, int mod) {
    int res = 1;
    while (n) {
        if (n & 1) res = 1LL * res * a % mod;
        a = 1LL * a * a % mod;
        n >>= 1;
    }
    return res;
}
int main() {
    fac[0] = 1;
    for (int i = 1; i <= N; i++) fac[i] = 1LL * fac[i - 1] * i % mod;
    inv[N] = power(fac[N], mod - 2, mod);
    for (int i = N - 1; i >= 0; i--) inv[i] = 1LL * inv[i + 1] * (i + 1) % mod;
    int T;
    cin >> T;
    while (T--) {
        int n, m;
        cin >> n >> m;
        cout << 1LL * fac[n] * inv[m] % mod * inv[n - m] % mod << endl;
    }
    return 0;
}

组合数计算 4

数据范围:

是质数。

做法:lucas 定理

时间复杂度 (TODO)

#include <bits/stdc++.h>
using namespace std;
using ll = long long;
int power(int a, ll n, int mod) {
    int res = 1;
    while (n) {
        if (n & 1) res = 1LL * res * a % mod;
        a = 1LL * a * a % mod;
        n >>= 1;
    }
    return res;
}
ll com(ll n, ll m, ll p) {
    if (n < m) return 0;
    ll ans = 1;
    for (int i = 1; i <= m; i++) {
        ans = ans * ((n - i + 1) % p) % p;
        ans = ans * power(i, p - 2, p) % p;
    }
    return ans;
}

ll lucas(ll n, ll m, ll p) {
    ll ans = 1;
    while (n && m) {
        ans = ans * com(n % p, m % p, p) % p;
        n /= p;
        m /= p;
    }
    return ans;
}
int main() {
    int T;
    cin >> T;
    while (T--) {
        ll n, m, p;
        cin >> n >> m >> p;
        cout << lucas(n, m, p) << endl;
    }
    return 0;
}

组合数计算 5

数据范围:

做法:线性筛+勒让德定理+分解质因数。

时间复杂度:

#include <bits/stdc++.h>
using namespace std;
using ll = long long;
const int N = 10000000;
int prime[N + 5], cnt;
bool vis[N + 5];
void getprime(int n = N) {
    for (int i = 2; i <= n; ++i) {
        if (!vis[i]) {
            prime[++cnt] = i;
        }
        for (int j = 1; j <= cnt && prime[j] <= n / i; ++j) {
            vis[i * prime[j]] = true;
            if (i % prime[j] == 0) break;
        }
    }
}

ll calc(ll n, ll p) {
    ll ans = 0;
    while (n) {
        ans += n / p;
        n /= p;
    }
    return ans;
}

int main() {
    getprime();
    int T;
    cin >> T;
    while (T--) {
        int n, m, p;
        cin >> n >> m >> p;
        int ans = 1;
        for (int i = 1; i <= cnt; i++) {
            int x = calc(n, prime[i]) - calc(m, prime[i]) - calc(n - m, prime[i]);
            for (int j = 1; j <= x; j++) {
                ans = 1LL * ans * prime[i] % p;
            }
        }
        cout << ans << endl;
    }
}

组合数计算 6

给定 , , ,求

做法:二项式定理+卷积快速幂

求多项式的前 项系数。

时间复杂度

#include <bits/stdc++.h>
using namespace std;
using ll = long long;
ll n;
int m, p;
void add(int& x, int y) {
    x += y;
    if (x >= p) x -= p;
}
vector<int> mul(const vector<int>& a, const vector<int>& b, int m) {
    vector<int> c(m, 0);
    for (int i = 0; i < m; i++) {
        for (int j = 0; i + j < m; j++) {
            add(c[i + j], 1LL * a[i] * b[j] % p);
        }
    }
    return c;
}
int main() {
    cin >> n >> m >> p;
    m++;
    vector<int> v(m, 0);
    v[0] = v[1] = 1;
    vector<int> ans(m, 0);
    ans[0] = 1;
    while (n) {
        if (n & 1) {
            ans = mul(ans, v, m);
        }
        v = mul(v, v, m);
        n >>= 1;
    }
    for (int i = 0; i < m; i++) {
        cout << ans[i];
        cout << (i + 1 < m ? " " : "\n");
    }
    return 0;
}

组合数计算 7

组合数学基础

广义的组合数学(Combinatorics)就是离散数学,狭义的组合数学是组合计数、图论、代数结构、数理逻辑等的总称。 但这只是不同学者在叫法上的区别。总之,组合数学是一门研究可数或离散对象的科学。 随着计算机科学的日益发展,组合数学的重要性也日渐凸显,因为计算机科学的核心内容是使用算法处理离散数据。

加法原理乘法原理

加法原理

如果事件 种产生方式,事件 种产生方式,当 产生的方式不重叠时,则事件 种产生方式。

使用加法原理要注意事件 和事件 产生的方式不能重叠,即每种产生方式不能同时属于两个事件。

加法原理可以推广到 个事件:设 个事件,他们的产生方式分别有 种, 当其中任意两个事件产生的方式都不重叠时, 则事件“ ”有 种产生方式。

在运用加法原理时,我们通常把问题分隔成若干穷尽所有可能的互相排斥的情况。

从北京前往上海旅游,有三种交通工具可供选择,分别为飞机、高铁、长途汽车。而飞机的班次有 种,高铁的班次有 种,长途汽车的班次有 种,那么一共有:

种不同的交通方式。

乘法原理

如果事件 种产生方式,事件 种产生方式,当 产生的方式相互独立时,则事件有:

种产生方式。

这里独立的意思是,事件 与事件 对产生方式的选择彼此没有影响。乘法原理也可以推广到 个事件。设 个事件,他们的产生方式分别有 种,当其中任何两个事件产生的方式彼此独立时,事件“”有:

种产生方式。

粉笔的长度有 种,颜色有 种,直径有 种,那么一共有:

种不同的粉笔。

正因为长度、颜色、直径是相互独立的,所以才能用乘法原理。如果两个事件之间是相关的,比如一种长度对应几种直径,那就不能直接用乘法原理计算了。

练习题

加法原理乘法原理例题


[!info] 例1

之间,有多少个各位不相同的奇数?

首先考虑个位,因为这个数必须是奇数,所以个位有 种选择:

再来看千位,原本的选择范围是 ,除去个位的选择后,剩下还有 种选择。

再来看百位,原本的选择范围是 ,除去个位和千位的选择后,只剩下 种选择。

最后来看十位,只有 种选择方案,因此最终答案为 种。

注意我们首先选择的是个位,然后再是千位,这是因为他们的约束性比较强。如果先选十位或者百位,你会发现就不能用乘法原理解决了(要分类讨论有没有用过 和奇数)。所以,计算的顺序很重要。


[!info] 例2

的约数个数为多少。

根据算术基本定理,该数的某个因数可以表示为

其中 ,那么 有 4 种选择。同理 有 4 种选择, 有 3 种选择, 有 2 种选择, 有 3 种选择。 那么总共的因数个数为 个。


[!info] 例3

由五个数字 可以组成多少个不同的五位数?

可以放在五位数中的任何一位中, 可以放在剩下四个位置中的任何一位,那么剩下都是 了只有一种放法。 一共能组成 个不同的五位数。

排列数

线性排列

元集合,从 中有序选取 个元素称为 的一个 排列。

排列总数记作 ,当 时称为全排列。 也称为从 个不同的元素取 个元素的排列数。

个不同的元素取 个元素按顺便排成一列,选择第 1 个元素可以有 种方法,选择第 2 个元素可以有 种方法,以此类推。所以 的计算方法如下:

这里只考虑 都是整数,且 的情况,则有:

举个例子:用 26 个字母构成一个长度为 5 的英文单词,要求每个字母最多使用一次,那这样不同的单词个数为

圆排列

刚才我们所讨论的都是把对象排成一条线,称为线性排列。如果把对象排成环形呢?这种环形的排列我们称为圆排列(也可以叫环排列,循环排列)。

有 6 个小朋友手拉手围成一个圈,编号分别为 1, 2, 3, 4, 5, 6,问有多少种不同的方案?

很显然 123456 和 234561 是同一种方案,也就是说方案数比原来 种可能要少。再具体一点,一种圆排列对应了 6 种线性排列。

因此, 个元素的 圆排列的数目为

特别地,当 时,圆排列数目为

练习题

排列数例题


[!info] 例4

将字母表中的 26 个字母排成一行,使得元音字母 中任意两个都不能连续出现,这样的方案数有多少?

可以先排 21 个辅音字母,这样一共有 种排列方法。 然后把元音字母进行插空,来保证不相邻。 这样的空位一共有 22 个,然后把 5 个元音字母放进空位里。 第一个元音字母有 22 种放法,第二个元音字母有 21 种放法…… 也就是说元音字母有 种方案数,那么根据乘法原理,总方案数为 种。


[!info] 例5

个数字组成的七位数中,有多少个数满足各位数字互不相同,且数字 不连续?

暂且不考虑数字 5 和 6 不连续出现,由 1~9 组成各位数字互不相同的七位数有 种。那么再减去一定出现 5 和 6 连续的情况,就是答案了。

5 和 6 两个数字本身的排列有 种,在七位数中一共有 6 种放法,而剩余的数放置的方案为 种。那么一定出现 5 和 6 连续的情况的方案数就为 种。

因此,最终答案为


[!info] 例6

A 和 B 参加同学聚餐,一共有 个人要围坐一张桌子。在路上 A 和 B 吵架了,因此他们不想坐在相邻的位置上,那么一共要多少种分配座位的方法?

让 A 先随便坐一个位置,那他左边有 8 个人选,右边有 7 人选,剩下的七个位置一共有 种方案,那么一共有 种方案。

也可以这么想,总共圆排列有 种,然后减去 A 和 B 相邻的方案数。他们两人的顺序有 种,捆绑在一起和其他人组成的圆排列有 8! 种,那么答案为 种方案。

组合数

元集合,从 中无序选取 个元素,称作 的一个 组合。 组合总数记作 也称为从 个不同的元素取 个元素的组合数,也可以记作 ,称为二项式系数。

这里只考虑 都是整数,且 的情况,则有:

根据上面的式子可以得出:

举个例子:教室里有 30 个座位,进来了 10 个学生,每个人会选择坐一个座位,那么每个座位的空闲情况一共有 种。

帕斯卡恒等式是组合数中非常重要的公式:

尝试证明一下?

可以用组合分析的方法证明:

个元素中取 个一共有 种取法,现在换一种角度来考虑:对于其中一个元素,如果取它,那么剩余的 个元素里就要取 个;如果不取它,那么剩余 个元素里就要取 个。那么总共的取法一共有 种。

问题:一个包含 个元素的集合的子集个数为多少?(一个长度为 的数组的子序列个数有多少个?)

一个包含 个元素的集合的子集数目为 ,因为每个元素可以取或者不取。

也可以分别统计大小为 的子集数目,大小为 的子集一共有 个,那么根据加法原理,所以子集数目为

两种计算方法的结果是相等的,所以推出了一个恒等式

多重集的排列组合

为了讨论允许重复的选取问题需要引入多重集合的概念。多重集合 , 其中 种不同的元素, 表示 中出现的次数,称为 的重复度。

为多重集合,元素的总数为

  • 中有序选取的 个元素称为多重集合 的一个 排列。 的排列称为 的全排列。

  • 中无序选取的 个元素称为多重集合 的一个 组合。

  • 的全排列个数是

  • ,那么 排列数是

练习题

[!info] 例7

用 4,4,4,4,3,3,3,2,2,1 这十个数字能组成多少个不同的十位数?

由多重集合全排列公式,得出答案为


[!info] 例8

的棋盘上放置 2 个红车,4 个黄车,2 个绿车,要求所有车之间彼此不能攻击(任意两个车不在同一行且不在同一列), 同种颜色的车之间没有区别,问有多少种摆放的方案。

首先只考虑摆放的位置,8 个车显然每行要放一个,假设他们的坐标分别为 。因为列号不能重复, 的一个排列。那么在所有车一样的情况下,摆放位置有 种。

现在我们要给每行分配某种颜色的车,而 8 个车其实是一个多重集合 。也就是说,在位置固定的情况下,一种颜色方案就是这个多重集合的一个排列,其总的排列数为

因此,总共的摆放方案有 种。


[!info] 例9

多重集合 ,求 的 8 排列个数。

多重集合的非全排列个数没有直接的公式,但某些情况下还是能够求解的,这里采用分类讨论的方法。

一共 9 个元素,8 排列是少用了一个元素,那么可以分成三种独立的情况:

  • 少用一个 的全排列个数为
  • 少用一个 的全排列个数为
  • 少用一个 的全排列个数为

所以 的 8 排列个数为

多重集合与不定方程的解

为多重集合,若 ,那么 组合数是

证明:

组合是 的一个子集,也是一个多重集合 ,其中满足 都为非负整数。

那么组合数的个数就等于这个不定方程解的个数,即每一个组合和每一个解是一一对应的。

  • 正整数解的个数:隔板法,现在有 个元素,用 个隔板把他们分成 个部分,每个部分不能为空,那么答案就是

  • 非负整数解的个数:隔板法,现在有 个元素,用 个隔板把他们分成 个部分,每个部分允许为空,那么答案就是

练习题

[!info] 例10

有一家面包店有 种面包圈,一个面包盒内装有 个面包圈,那么能够装配多少不同类型的面包盒?

显然,每种面包圈有无限个,也就保证了 。这个问题其实是求 8 种元素的多重集合里选出 12 个元素的组合有多少个,那么答案为 种。


[!info] 例11

不定方程 的正整数解的个数有多少个? 直接用隔板法,现在有 10 个元素,用 3 个隔板把他们分成 4 个非空部分,答案为


[!info] 例12

不定方程 的正整数解的个数有多少个?

个人分成 组,每组 2 人,求不同的分组方法数,比如 的情况。

比如现在 ,有三种分法:

  1. 考虑总的排列数:

  1. 每组组内的顺序不重要:

  1. 组与组之间的顺序不重要:

更一般的情况,我们有如下定理:

个不同的元素划分成 个不同的组,使得第 个组有 个元素,并且满足 。那么这样的划分方案数等于

如果组之间没有区别,且 ,那么划分方案数为

抽屉原理

定义及证明

鸽巢原理又称为抽屉原理。其最简单的形式如下:

如果 个物体被放进 个盒子,那么至少有一个盒子包含两个或者两个以上的物体。

可以用反证法来证明:如果这 个盒子中每个都至多含有一个物体,那么物体的总数最多是 ,和已知的有 个物体矛盾,故某个盒子必然包含两个及以上的物体。

  • 种颜色给 个物体染色,那么必有两个物体颜色相同。
  • 某年级有 367 个人,那么必有 2 人的生日是相同的。
  • 对已婚夫妇,至少选出其中 个人才能保证能够选出一对夫妇。

一个合格的水果篮必须满足下面三个条件之一

  • 至少有 8 个苹果;
  • 至少有 6 个橙子;
  • 至少有 9 个桃子。

现在有一个只装苹果、橙子、桃子的水果篮,那么篮子里最少有多少水果时,能保证是合格的?

最坏情况是包含 7 个苹果、5 个橙子、8 个桃子,此时再随便多一个水果就是合格的了,所以最少 个水果能保证合格。

[!example] Erdős–Szekeres定理

对于一个长度为 的序列,如果 ,那么序列中一定存在一个长度至少为 非降序子序列非增序子序列

例题

[!info] 区间最小差值查询

给定一个包含 个整数的数组,你会得到该数组中的两个下标 )。你需要找出下标范围 内的所有整数中,差值最小的两个数的差值,并输出这个最小值。数组的下标从 开始到 结束。

保证 ,数组元素取值范围

[!info] 糖果分配

个数,选出一个子集使得子集和是 的倍数,保证

容斥原理

[!example] 例子

一个班有 名学生, 人喜欢数学, 人喜欢物理, 人两科都喜欢。数学物理都不喜欢的人有多少?

设全集 中元素有 种不同的属性,而第 种属性称为 ,拥有属性 的元素构成集合 ,那么

容斥原理例题

[!example] 例1

中能被 整除的数的个数。

  1. 定义集合
  • :能被 整除的数,
  • :能被 整除的数,
  • :能被 整除的数,
  1. 计算交集的大小
  • :能被 整除的数,
  • :能被 整除的数,
  • :能被 整除的数,
  1. 计算三者交集的大小

:能被 整除的数,

  1. 容斥公式:


[!example] 例2

个不同的物品需要放入 个不同的盒子中,要求所有盒子都不为空。问有多少种放置方法?

提示:设事件 表示盒子 为空。


[!example] 例3

个信件需要装入 个信封,要求没有一个信件被装入正确的信封,有多少种装法?

提示:设事件 表示第 个物品在原来位置。

根据容斥原理,错排数 可以表示为:

进一步化简

概率与期望

多项式相关

快速傅里叶变换

一、写在前面

两个 次多项式相加的最直接方法所需的时间是 ,但是相乘的最直接方法所需的时间为 。用快速傅里叶变换(Fast Fourier Transform,FFT)可以使多项式相乘的时间复杂度降低为

需要的一些前置技能:复数、多项式、线性代数。


二、多项式

一个以 为变量的多项式定义在一个代数域 上,将函数 表示为形式和:

我们称 为如上多项式的系数,所有系数都属于域 ,典型的情形是复数集合

如果一个多项式 的最高次的非零系数是 ,则称 次数,记 。任何严格大于一个多项式次数的整数都是该多项式的次数界,因此,对于次数界为 的多项式,其次数可以是 之间的任何整数。

多项式加法

如果 是次数界为 的多项式,那么它们的和也是一个次数界为 的多项式 ,对所有属于定义域的 ,都有 。也就是说,

例如,如果有多项式 ,那么

多项式乘法

如果 是次数界为 的多项式,那么它们的乘积 是一个次数界为 的多项式 ,对所有属于定义域的 ,都有 。方法类似还是用上一个例子,那么得到

形式化的式子有

其中

此时

多项式的表示

从某种意义上,多项式的系数表达与点值表达式等价的。

系数表达

对一个次数界为 的多项式 而言,其系数表达是一个由系数组成的(列)向量 。对于多项式乘法,系数向量 成为输入向量 的卷积,表示成

点值表达

一个次数界为 的多项式 点值表达就是一个由个点值对组成的集合

使得对 ,所有 各不相同,且

一个多项式可以有很多不同的点值表达。如果采用的点都相同的话,用点值表达多项式做乘法只需 的时间。

求值与插值

从一个多项式的系数表达转化为点值表达的过程是求值,其逆运算称为插值。

定理(插值多项式的唯一性):对于任意 个点值对组成的集合 ,其中所有的 都不同,那么存在唯一的次数界为 的多项式 ,满足

证明列出矩阵方程,然后结合范德蒙德矩阵的性质。

简单的求值和插值(拉格朗日插值)的时间复杂度都是 的。

我们之后就要通过巧妙选取点来加速这两个过程,使其运行时间变为


三、单位复数根

次单位复数根是满足 的复数

次单位复数根恰好有 个:

其中主 次单位复数根为

其他 次单位复数根都是 的幂次。

[!theroem] 消去引理

对于任何整数 ,有

[!theroem] 推论

对于任意偶数 ,有

[!theroem] 折半引理

如果 为偶数,那么 次单位复数根的平方的集合就是 次单位复数根的集合

[!theroem] 求和引理

对任意整数 和不能被 整除的非负整数 ,有


四、快速傅里叶变换

DFT

现在我们希望计算次数界 的多项式

处的值,记为

向量 就是系数向量 离散傅里叶变换(DFT),记为

FFT

快速傅里叶变换(FFT) 利用复数单位根的特殊性质,可以在时间内计算出 。首先通篇假设 恰好是 的整数幂。

FFT 利用了分治策略,采用 中偶数下标的系数与奇数下标的系数,分别定义两个新的次数界为 的多项式 :

于是有

所以,求 处的值转换为求次数界为 的多项式 在点 的值。可以发现其实是 次单位复数根,且每个根恰好出现两次:

IDFT

将点值表达的多项式转换回系数表达,是相似的过程。

我们把 DFT 写成矩阵乘积

其中 是一个范德蒙德矩阵,在 处的元素为

对于逆运算 ,我们把 乘以 的逆矩阵来处理。

[!theroem] 定理

元素为

证明 时用求和引理即可,注意使用条件。

所以可以推导出

可以看出只需将单位根取倒数,做一次 FFT,最后将结果都除以 ,就做完逆变换了。


五、代码实现

首先是手写复数类,也可以用 std::complex<T>

struct Complex {
    double x, y;
    Complex(double x_ = 0, double y_ = 0) {
        x = x_;
        y = y_;
    }
    Complex operator-(const Complex& t) const {
        return Complex(x - t.x, y - t.y);
    }
    Complex operator+(const Complex& t) const {
        return Complex(x + t.x, y + t.y);
    }
    Complex operator*(const Complex& t) const {
        return Complex(x * t.x - y * t.y, x * t.y + y * t.x);
    }
};

递归实现

void fft(Complex y[], int n) {
    if (n == 1) return;
    static Complex c[MAXN];
    int m = n / 2;
    for (int i = 0; i < m; ++i) {
        c[i] = y[i * 2];
        c[i + m] = y[i * 2 + 1];
    }
    copy(c, c + n, y);
    Complex *a0 = y, *a1 = y + m;
    fft(a0, m);
    fft(a1, m);
    for (int i = 0; i < m; ++i) {
        Complex w(cos(-2 * PI / n * i), sin(-2 * PI / n * i));
        c[i] = a0[i] + w * a1[i];
        c[i + m] = a0[i] - w * a1[i];
    }
    copy(c, c + n, y);
}

合并过程的推导:

对于

前半段没什么问题,再来看后半段

迭代实现

递归实际运行起来常数很大,我们需要更高效的实现方法。

先来观察一下递归过程中输入向量的下标变化,以举例,可以将这个过程自行脑补成一个完全二叉树的样子:

[0   1   2   3   4   5   6   7]

[0   2   4   6] [1   3   5   7]

[0   4] [2   6] [1   5] [3   7]

[0] [4] [2] [6] [1] [5] [3] [7]

如果观察二进制的话会发现对应的下标是反转二进制位得到的,比如“011”变成“110”,即下标 3 变成了 6。

[000   001   010   011   100   101   110   111]

[000   010   100   110] [001   011   101   111]

[000   100] [010   110] [001   101] [011   111]

[000] [100] [010] [110] [001] [101] [011] [111]

代码实现举例两种

  1. 直接求出对应位置反转二进制位后的数,然后交换,时间复杂度
void change(Complex y[], int len) {
    int k = 0;
    while ((1 << k) < len) ++k;
    for (int i = 0; i < len; ++i) {
        int t = 0;
        for (int j = 0; j < k; ++j)
            if (i >> j & 1)
                t |= 1 << (k - j - 1);
        if (i < t) swap(y[i], y[t]);
    }
}
  1. 从高位模拟二进制加一,用经典的摊还分析可以证明复杂度是
void change(Complex y[], int len) {
    int i, j, k;
    for (i = 1, j = len / 2; i < len - 1; i++) {
        if (i < j) swap(y[i], y[j]);
        k = len / 2;
        while (j >= k) {
            j -= k;
            k /= 2;
        }
        if (j < k) j += k;
    }
}

之后我们再考虑自底向上的合并,在之前的递归版本中,有一个公用子表达式 计算了两次,我们可以只计算一次乘积,存放在临时变量 里,然后从 中增加或者减去 ,这一系列操作称为一个蝴蝶操作

代码:

void fft(Complex y[], int len, int on) {
    change(y, len);
    for (int h = 2; h <= len; h <<= 1) {
        Complex wn(cos(-on * 2 * PI / h), sin(-on * 2 * PI / h));
        for (int j = 0; j < len; j += h) {
            Complex w(1, 0);
            for (int k = j; k < j + h / 2; k++) {
                Complex u = y[k];
                Complex t = w * y[k + h / 2];
                y[k] = u + t;
                y[k + h / 2] = u - t;
                w = w * wn;
            }
        }
    }
    if (on == -1)
        for (int i = 0; i < len; i++)
            y[i].x /= len;
}

on 取值 on 代表逆变换。

实际上,预处理单位根代替每次旋转精度会更好。

void init() {
    for (int i = 0; i <= MAXN; ++i)
        wn[i] = Complex(cos(-2 * PI * i / MAXN), sin(-2 * PI * i / MAXN));
}

void fft(Complex y[], int len, int on) {
    change(y, len);
    for (int h = 2; h <= len; h <<= 1) {
        int st = MAXN / h;
        for (int j = 0; j < len; j += h) {
            int ptr = 0;
            for (int k = j; k < j + h / 2; k++) {
                Complex w = wn[on == 1 ? ptr : MAXN - ptr];
                Complex u = y[k], t = w * y[k + h / 2];
                y[k] = u + t;
                y[k + h / 2] = u - t;
                ptr += st;
            }
        }
    }
    if (on == -1)
        for (int i = 0; i < len; i++)
            y[i].x /= len;
}

六、模板

多项式乘法

#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const double PI = acos(-1.0);
const int MAXN = 1 << 18;
struct Complex {
    double x, y;
    Complex(double x_ = 0, double y_ = 0) {
        x = x_;
        y = y_;
    }
    Complex operator-(const Complex& t) const {
        return Complex(x - t.x, y - t.y);
    }
    Complex operator+(const Complex& t) const {
        return Complex(x + t.x, y + t.y);
    }
    Complex operator*(const Complex& t) const {
        return Complex(x * t.x - y * t.y, x * t.y + y * t.x);
    }
} x1[MAXN + 5], x2[MAXN + 5], wn[MAXN + 5];
void init() {
    for (int i = 0; i <= MAXN; ++i)
        wn[i] = Complex(cos(-2 * PI * i / MAXN), sin(-2 * PI * i / MAXN));
}
void change(Complex y[], int len) {
    int i, j, k;
    for (i = 1, j = len / 2; i < len - 1; i++) {
        if (i < j) swap(y[i], y[j]);
        k = len / 2;
        while (j >= k) {
            j -= k;
            k /= 2;
        }
        if (j < k) j += k;
    }
}
void fft(Complex y[], int len, int on) {
    change(y, len);
    for (int h = 2; h <= len; h <<= 1) {
        int st = MAXN / h;
        for (int j = 0; j < len; j += h) {
            int ptr = 0;
            for (int k = j; k < j + h / 2; k++) {
                Complex w = wn[on == 1 ? ptr : MAXN - ptr];
                Complex u = y[k], t = w * y[k + h / 2];
                y[k] = u + t;
                y[k + h / 2] = u - t;
                ptr += st;
            }
        }
    }
    if (on == -1)
        for (int i = 0; i < len; i++)
            y[i].x /= len;
}
int n, m;
int main() {
    init();
    scanf("%d%d", &n, &m);
    ++n;
    ++m;
    int len = 1;
    while (len < (n << 1) || len < (m << 1)) len <<= 1;
    for (int i = 0; i < n; ++i) {
        int x;
        scanf("%d", &x);
        x1[i].x = x;
    }
    for (int i = 0; i < m; ++i) {
        int x;
        scanf("%d", &x);
        x2[i].x = x;
    }
    fft(x1, len, 1);
    fft(x2, len, 1);
    for (int i = 0; i < len; ++i) x1[i] = x1[i] * x2[i];
    fft(x1, len, -1);
    for (int i = 0; i < n + m - 1; ++i)
        printf("%d%c", (int)(x1[i].x + 0.5), " \n"[i == n + m - 2]);
    return 0;
}

七、进阶

拆系数 FFT

#include <bits/stdc++.h>
using namespace std;
using ll = long long;
using db = long double;
const int MAXL = 1 << 18;

namespace FFT {
const db PI = acos(-1.0);
const int base = 32768;
struct Complex {
    db x, y;
    Complex(db x_ = 0, db y_ = 0) {
        x = x_;
        y = y_;
    }
    Complex operator-(const Complex& t) const {
        return Complex(x - t.x, y - t.y);
    }
    Complex operator+(const Complex& t) const {
        return Complex(x + t.x, y + t.y);
    }
    Complex operator*(const Complex& t) const {
        return Complex(x * t.x - y * t.y, x * t.y + y * t.x);
    }
    Complex operator*(const db& t) const {
        return Complex(x * t, y * t);
    }
} wn[MAXL + 5];
Complex x1[MAXL + 5], x2[MAXL + 5], x3[MAXL + 5], x4[MAXL + 5];
Complex x5[MAXL + 5], x6[MAXL + 5], x7[MAXL + 5];
int rev[MAXL + 5];
int len, mod;
void init(int n, int p) {
    mod = p;
    int L = 0;
    while ((1 << L) < (n << 1)) ++L;
    len = 1 << L;
    for (int i = 0; i <= MAXL; i++) {
        rev[i] = rev[i >> 1] >> 1 | ((i & 1) << (L - 1));
        wn[i] = Complex(cos(-2 * i * PI / MAXL), sin(-2 * i * PI / MAXL));
    }
}
void fft(Complex y[], int on) {
    for (int i = 0; i < len; ++i)
        if (i < rev[i])
            swap(y[i], y[rev[i]]);
    for (int h = 2; h <= len; h <<= 1) {
        int st = MAXL / h;
        for (int j = 0; j < len; j += h) {
            int ptr = 0;
            for (int k = j; k < j + h / 2; k++) {
                Complex u = y[k];
                Complex t = wn[on == 1 ? ptr : MAXL - ptr] * y[k + h / 2];
                y[k] = u + t;
                y[k + h / 2] = u - t;
                ptr += st;
            }
        }
    }
    if (on == -1)
        for (int i = 0; i < len; i++)
            y[i].x /= len;
}
void solve(int* a, int n, int* b, int m, int* c) {
    for (int i = 0; i < n; ++i) {
        x1[i] = Complex(a[i] / base, 0);
        x2[i] = Complex(a[i] % base, 0);
    }
    for (int i = n; i < len; ++i) x1[i] = x2[i] = Complex(0, 0);
    for (int i = 0; i < m; ++i) {
        x3[i] = Complex(b[i] / base, 0);
        x4[i] = Complex(b[i] % base, 0);
    }
    for (int i = m; i < len; ++i) x3[i] = x4[i] = Complex(0, 0);
    fft(x1, 1);
    fft(x2, 1);
    fft(x3, 1);
    fft(x4, 1);
    for (int i = 0; i < len; ++i) {
        x5[i] = x1[i] * x3[i];
        x6[i] = x1[i] * x4[i] + x2[i] * x3[i];
        x7[i] = x2[i] * x4[i];
    }
    fft(x5, -1);
    fft(x6, -1);
    fft(x7, -1);
    for (int i = 0; i < len; ++i) {
        c[i] = 1LL * base * base * ((ll)(x5[i].x + 0.5) % mod) % mod;
        c[i] += 1LL * base * ((ll)(x6[i].x + 0.5) % mod) % mod;
        if (c[i] >= mod) c[i] -= mod;
        c[i] += (ll)(x7[i].x + 0.5) % mod;
        if (c[i] >= mod) c[i] -= mod;
    }
}
}  // namespace FFT

ll n;
int m, p;
int ans[MAXL + 5], v[MAXL + 5];
int main() {
    cin >> n >> m >> p;
    m++;
    FFT::init(m, p);
    v[0] = v[1] = 1;
    ans[0] = 1;
    while (n) {
        if (n & 1) {
            FFT::solve(ans, m, v, m, ans);
        }
        FFT::solve(v, m, v, m, v);
        n >>= 1;
    }
    for (int i = 0; i < m; i++) {
        cout << ans[i] << (i + 1 < m ? " " : "\n");
    }
    return 0;
}

数据结构

单调队列

动态规划

背包

综合练习

背包基础

01 背包

普通写法

01 背包

01 背包:每个物品只有一个。

表示考虑前 个物品,用容量为 的背包能背出的最大价值。

时间复杂度 ,空间复杂度

#include <bits/stdc++.h>
using namespace std;
int n, m;
int dp[205][5005];
int main() {
    cin >> n >> m;
    for (int i = 1; i <= n; i++) {
        int w, v;
        cin >> w >> v;
        for (int j = 1; j <= m; j++) {
            if (j < w)
                dp[i][j] = dp[i - 1][j];
            else
                dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - w] + v);
        }
    }
    cout << dp[n][m] << endl;
}

空间优化

因为 只由上一行的之前某列转移过来,所以可以优化掉第一维,倒序更新 dp 值。空间复杂度

#include <bits/stdc++.h>
using namespace std;
int n, m;
int dp[5005];
int main() {
    cin >> n >> m;
    for (int i = 1; i <= n; i++) {
        int w, v;
        cin >> w >> v;
        for (int j = m; j >= w; j--) {
            dp[j] = max(dp[j], dp[j - w] + v);
        }
    }
    cout << dp[m] << endl;
}

完全背包

完全背包问题

完全背包:每种物品有无限个。

直接把 01 背包的倒序改成正序 for 循环即可。

#include <bits/stdc++.h>
using namespace std;
int n, m;
int dp[5005];
int main() {
    cin >> n >> m;
    for (int i = 1; i <= n; i++) {
        int w, v;
        cin >> w >> v;
        for (int j = w; j <= m; j++) {
            dp[j] = max(dp[j], dp[j - w] + v);
        }
    }
    cout << dp[m] << endl;
}

多重背包

部分背包问题

多重背包:每种物品有个数限制。

二进制拆分优化:把 个物品拆成 个, 个, 个, 个…直到最后剩下的捆绑到一起,这样分成 个物品,一定能组合出 之间任意一个整数。

时间复杂度

#include <bits/stdc++.h>
using namespace std;
using ll = long long;
const int N = 105;
int n, m;
int c[N], w[N], v[N];
ll dp[10010];
void pack(int w, ll v) {
    for (int i = m; i >= w; i--) {
        dp[i] = max(dp[i], dp[i - w] + v);
    }
}
void multi_pack(int c, int w, int v) {
    for (int i = 1; i <= c; i *= 2) {
        pack(i * w, 1LL * i * v);
        c -= i;
    }
    if (c > 0) {
        pack(c * w, 1LL * c * v);
    }
}
int main() {
    cin >> n;
    for (int i = 1; i <= n; i++) {
        cin >> c[i] >> w[i] >> v[i];
    }
    cin >> m;
    for (int i = 1; i <= n; i++) {
        multi_pack(c[i], w[i], v[i]);
    }
    cout << dp[m] << endl;
}

分组背包

分组背包

#include <bits/stdc++.h>
using namespace std;
int n, W, K;
vector<pair<int, int>> a[1010];
int dp[1010];
int main() {
    cin >> n >> W >> K;
    for (int i = 1; i <= n; i++) {
        int w, v, k;
        cin >> w >> v >> k;
        a[k].emplace_back(w, v);
    }
    for (int i = 1; i <= K; i++) {
        for (int j = W; j >= 0; j--) {
            for (auto [w, v] : a[i]) {
                if (j >= w) {
                    dp[j] = max(dp[j], dp[j - w] + v);
                }
            }
        }
    }
    cout << dp[W] << endl;
    return 0;
}

背包合并

背包合并

类似多项式乘法的卷积,两层循环

#include <bits/stdc++.h>
using namespace std;
const int N = 1010;
int n, m;
int a[N];
int dp[N];
int c[N];
void merge(int* a, int* b) {
    memset(c, 0, sizeof(c));
    for (int i = 0; i <= m; i++) {
        for (int j = 0; i + j <= m; j++) {
            c[i + j] = max(c[i + j], a[i] + b[j]);
        }
    }
    for (int i = 0; i <= m; i++) {
        dp[i] = c[i];
    }
}
int main() {
    scanf("%d%d", &n, &m);
    for (int i = 1; i <= n; i++) {
        for (int j = 0; j <= m; j++) {
            scanf("%d", &a[j]);
        }
        merge(dp, a);
    }
    for (int i = 0; i <= m; i++) {
        printf("%d ", dp[i]);
    }
    return 0;
}

例题

可行性背包

装物品(小内存)

件物品,第 件物品的重量为 (整数)。对于给定的整数 , 请选择一些物品,使得拼出的重量不超过 ,请问在此前提下能拼出的最大重量是多少?

一种方法是把物品的重量也当成价值,直接背包求最大价值。

#include <bits/stdc++.h>
using namespace std;
int n, m;
int w[1010];
int dp[1000010];
int main() {
    cin >> n;
    for (int i = 1; i <= n; i++) {
        cin >> w[i];
    }
    cin >> m;
    for (int i = 1; i <= n; i++) {
        for (int j = m; j >= w[i]; j--) {
            dp[j] = max(dp[j], dp[j - w[i]] + w[i]);
        }
    }
    cout << dp[m] << endl;
    return 0;
}

另一种方法是用可行性背包, 只记 true/false 表示能否恰好背出重量为 的背包。

#include <bits/stdc++.h>
using namespace std;
int n, m;
int w[1010];
bool dp[1000010];
int main() {
    cin >> n;
    for (int i = 1; i <= n; i++) {
        cin >> w[i];
    }
    cin >> m;
    dp[0] = true;
    for (int i = 1; i <= n; i++) {
        for (int j = m; j >= w[i]; j--) {
            dp[j] |= dp[j - w[i]];
        }
    }
    int ans = m;
    while (!dp[ans]) ans--;
    cout << ans << endl;
    return 0;
}

bitset 优化

TODO

计数背包

整数分拆

为将整数 分拆为若干个正整数之和的不同方法数。

#include <bits/stdc++.h>
using namespace std;
const int mod = 1e9 + 7;
int n;
int dp[5010];
int main() {
    cin >> n;
    dp[0] = 1;
    for (int i = 1; i <= n; i++) {
        for (int j = i; j <= n; j++) {
            dp[j] += dp[j - i];
            if (dp[j] >= mod) dp[j] -= mod;
        }
    }
    cout << dp[n] << endl;
    return 0;
}

树形DP

树形DP基础

记忆化搜索

Fibonacci

#include <bits/stdc++.h>
using namespace std;
using ll = long long;
ll f[95];

ll fib(int n) {
    if (f[n] != 0) return f[n];
    if (n <= 2) return f[n] = 1;
    return f[n] = fib(n - 1) + fib(n - 2);
}
int main() {
    int n;
    cin >> n;
    cout << fib(n) << endl;
    return 0;
}

数字三角形

#include <bits/stdc++.h>
using namespace std;
int n;
int a[105][105];
int dp[105][105];
int dfs(int x, int y) {
    if (dp[x][y] != -1) return dp[x][y];
    if (x == n) return dp[x][y] = a[x][y];
    return dp[x][y] = max(dfs(x + 1, y), dfs(x + 1, y + 1)) + a[x][y];
}
int main() {
    cin >> n;
    for (int i = 1; i <= n; i++) {
        for (int j = 1; j <= i; j++) {
            cin >> a[i][j];
        }
    }
    memset(dp, -1, sizeof(dp));
    cout << dfs(1, 1) << endl;
    return 0;
}

合并石子

#include <bits/stdc++.h>
using namespace std;
using ll = long long;
const int MAXN = 1000;
int n;
ll a[MAXN + 5], sum[MAXN + 5];
ll dp[MAXN + 5][MAXN + 5];

inline ll calc(int l, int r) {
    return sum[r] - sum[l - 1];
}

ll dfs(int l, int r) {
    if (l == r) return dp[l][r] = 0;
    if (dp[l][r] != -1) return dp[l][r];
    ll ans = 1e18;
    for (int k = l; k < r; k++) {
        ans = min(ans, dfs(l, k) + dfs(k + 1, r) + calc(l, r));
    }
    return dp[l][r] = ans;
}

int main() {
    cin >> n;
    for (int i = 1; i <= n; i++) {
        cin >> a[i];
        sum[i] = sum[i - 1] + a[i];
    }
    memset(dp, -1, sizeof(dp));
    cout << dfs(1, n) << endl;
    return 0;
}

滑雪

#include <bits/stdc++.h>
using namespace std;
int n, m;
int a[105][105];
int dp[105][105];
int dfs(int x, int y) {
    if (dp[x][y]) return dp[x][y];
    int ans = 0;
    if (a[x][y] > a[x - 1][y]) ans = max(ans, dfs(x - 1, y));
    if (a[x][y] > a[x + 1][y]) ans = max(ans, dfs(x + 1, y));
    if (a[x][y] > a[x][y - 1]) ans = max(ans, dfs(x, y - 1));
    if (a[x][y] > a[x][y + 1]) ans = max(ans, dfs(x, y + 1));
    return dp[x][y] = ans + 1;
}
int main() {
    cin >> n >> m;
    memset(a, 0x3f, sizeof(a));
    for (int i = 1; i <= n; i++) {
        for (int j = 1; j <= m; j++) {
            cin >> a[i][j];
        }
    }
    int ans = 0;
    for (int i = 1; i <= n; i++) {
        for (int j = 1; j <= m; j++) {
            ans = max(ans, dfs(i, j));
        }
    }
    cout << ans << endl;
    return 0;
}

[HAOI2016] 食物链

#include <bits/stdc++.h>
using namespace std;
using ll = long long;
const int N = 100010;
int n, m;
vector<int> e[N];
int in[N], out[N], dp[N];
int dfs(int x) {
    if (dp[x] != -1) return dp[x];
    if (out[x] == 0) return dp[x] = 1;
    int ans = 0;
    for (auto& y : e[x]) {
        ans += dfs(y);
    }
    return dp[x] = ans;
}
int main() {
    scanf("%d%d", &n, &m);
    while (m--) {
        int x, y;
        scanf("%d%d", &x, &y);
        out[x]++;
        in[y]++;
        e[x].push_back(y);
    }
    memset(dp, -1, sizeof(dp));
    int ans = 0;
    for (int i = 1; i <= n; i++) {
        if (in[i] == 0 && out[i] > 0) {
            ans += dfs(i);
        }
    }
    printf("%d\n", ans);
    return 0;
}

图论

宽度优先搜索

bfs基础

并查集

连通性

Tarjan 算法

有向图强连通分量

在有向图 中,如果两个顶点间至少存在一条路径,称两个顶点强连通

若有向图 的每两个顶点都强连通,称 是一个强连通图

非强连通图有向图的极大强连通子图,称为 强连通分量(SCC)

Tarjan 算法

Tarjan 算法是基于对图深度优先搜索的算法。它维护了一个栈,搜索时,把与当前点相连的未访问的节点入栈,回溯时可以判断栈顶到栈中的节点是否为一个强连通分量。该算法引入了两个很重要的变量:

  • Dfn(u)是节点 u 的时间戳,表示点 u 是第几个被访问的点
  • Low(u)是 u 或 u 的子树能追溯到的最早的栈中节点的 Dfn

当 Dfn(u)=Low(u)时,以 u 为根的搜索子树上所有节点(栈顶到 u 的所有节点)是一个强连通分量。

tarjan

强连通分量

#include <bits/stdc++.h>
using namespace std;
const int N = 10010;
int n, m;
int dfn[N], low[N], stk[N], v[N], num, size, tot;
int color[N];
vector<int> e[N];
vector<vector<int>> scc;
void tarjan(int x) {
    int y;
    dfn[x] = low[x] = ++num;
    stk[++size] = x;
    v[x] = true;
    for (auto &y : e[x]) {
        if (!dfn[y]) {
            tarjan(y);
            low[x] = min(low[x], low[y]);
        } else if (v[y]) {
            low[x] = min(low[x], dfn[y]);
        }
    }
    if (low[x] == dfn[x]) {
        scc.emplace_back();
        do {
            y = stk[size--];
            color[y] = scc.size();
            scc.back().emplace_back(y);
            v[y] = false;
        } while (y != x);
    }
}
int main() {
    scanf("%d%d", &n, &m);
    while (m--) {
        int x, y;
        scanf("%d%d", &x, &y);
        e[x].push_back(y);
    }
    for (int i = 1; i <= n; i++)
        if (!dfn[i])
            tarjan(i);
    printf("%d\n", (int)scc.size());
    for (int i = 1; i <= n; i++) {
        if (color[i] > 0) {
            auto &vec = scc[color[i] - 1];
            sort(vec.begin(), vec.end());
            for (int j = 0; j < vec.size(); j++) {
                printf("%d ", vec[j]);
                color[vec[j]] = 0;
            }
            printf("\n");
        }
    }
}

计算几何

基础

Cetvrta: 平面直角坐标系上有一个矩形,现在已知这个矩形的其中三点的坐标,你需要求出第四个点。

杂项

杂项技巧

数位模拟

数位和游戏 给一个正整数 ,问所有与 长度相同且数位和相同的数中,最小以及最大分别是多少?

分段打表

阶乘之和 这道题目很简单,给定非负整数,求 ,答案可能很大,所以你只需要输出答案对 取模后的结果。

#include <bits/stdc++.h>
using namespace std;
const int mod = 1e9 + 7;
const int N = 1e9;
const int s[] = {1, 390799048, 370927277, ...};  // 省略
const int p[] = {1, 154425679, 641102369, ...};  // 省略
const int M = 500000;
int main() {
    int T;
    scanf("%d", &T);
    while (T--) {
        int n;
        scanf("%d", &n);
        int now = p[n / M], sum = s[n / M];
        for (int i = n / M * M + 1; i <= n; i++) {
            now = 1LL * now * i % mod;
            sum += now;
            if (sum >= mod) sum -= mod;
        }
        printf("%d\n", sum);
    }
}

时间复杂度之递归式求解

三种方法:

  • Substitution method (代入法)
  • Recursion tree (递归树法)
  • The master method (主方法)

代入法这里不予讨论。

递归树法

例一

递归式:

场景:比如归并排序

画出递归树(假设 中隐含的常数为 ):

IMG_0349

每层代价之和都为 ,一共 层,所以总代价

例二

递归式:

场景:假设快速排序每次划分的比例都为 1:9

递归树:

IMG_0350

结论:

推论:任何一种常数比例的划分都会产生深度为 的递归树,其中每一层的时间代价都是 ,总运行时间都是

主方法

是常数, 是一个函数, 是定义在非负整数上的递归式:

分三种情况讨论:

  • case 1. for some constant grows polynomially slower than

    • 的增长速度多项式地慢于
    • 此时
  • case 2. and grow at similar rates)

    • 增长速度相同
    • 此时
  • case 3. for some constant grows polynomially faster than

    • 的增长速度多项式地快于
    • 此时

注意条件:这三种情况并未覆盖 的所有可能性。可能存在 渐进小于 ,但并非多项式意义上的小于。同理,也可能存在 大于 ,但并非多项式意义上的大于。举个例子, 大于 ,但并不是多项式意义的大于。因此,这种情况的话不能使用主方法来求解递归式。

例三

递归式

套用主方法:

  • 因为
  • 所以应用情况 1,得到

例四

递归式

套用主方法:

  • 因为
  • 所以应用情况 2,得到

其他练习

递归式结果
情况 3,
情况 2,
不能使用主方法,用数学方法可知 ,可以参考这里

出题

下载 testlib.h

方式一:根据参数造不同的数据

g++ gen.cpp -o gen

for i in {1..20}; do
    inf="$i.in"
    case $i in
    # 1
        1)  ./gen -number=1 -minn=1 -maxn=20 -minq=1 -maxq=100 -m=2 > "$inf" ;;
        2)  ./gen -number=2 -minn=500 -maxn=1000 -minq=900 -maxq=1000 -m=5 > "$inf" ;;
        3)  ./gen -number=3 -minn=500 -maxn=1000 -minq=900 -maxq=1000 -m=18 > "$inf" ;;
        4)  ./gen -number=4 -minn=500 -maxn=1000 -minq=900 -maxq=1000 -m=19 > "$inf" ;;
        5)  ./gen -number=5 -minn=500 -maxn=1000 -minq=900 -maxq=1000 -m=20 > "$inf" ;;
        6)  ./gen -number=6 -minn=1000 -maxn=1000 -minq=1000 -maxq=1000 -m=20 > "$inf" ;;
    # 2
        7)  ./gen -number=7 -minn=100000 -maxn=200000 -minq=190000 -maxq=200000 -m=1 > "$inf" ;;
        8)  ./gen -number=8 -minn=100000 -maxn=200000 -minq=190000 -maxq=200000 -m=2 > "$inf" ;;
        9)  ./gen -number=9 -minn=100000 -maxn=200000 -minq=190000 -maxq=200000 -m=3 > "$inf" ;;
        10) ./gen -number=10 -minn=100000 -maxn=200000 -minq=190000 -maxq=200000 -m=4 > "$inf" ;;
        11) ./gen -number=11 -minn=100000 -maxn=200000 -minq=190000 -maxq=200000 -m=5 > "$inf" ;;
        12) ./gen -number=12 -minn=100000 -maxn=200000 -minq=200000 -maxq=200000 -m=5 > "$inf" ;;
        13) ./gen -number=13 -minn=200000 -maxn=200000 -minq=200000 -maxq=200000 -m=4 > "$inf" ;;
        14) ./gen -number=14 -minn=200000 -maxn=200000 -minq=200000 -maxq=200000 -m=5 > "$inf" ;;
    # 3
        15) ./gen -number=15 -minn=150000 -maxn=200000 -minq=190000 -maxq=200000 -m=17 > "$inf" ;;
        16) ./gen -number=16 -minn=150000 -maxn=200000 -minq=190000 -maxq=200000 -m=20 > "$inf" ;;
        17) ./gen -number=17 -minn=150000 -maxn=200000 -minq=190000 -maxq=200000 -m=18 > "$inf" ;;
        18) ./gen -number=18 -minn=190000 -maxn=200000 -minq=190000 -maxq=200000 -m=18 > "$inf" ;;
        19) ./gen -number=19 -minn=200000 -maxn=200000 -minq=200000 -maxq=200000 -m=19 > "$inf" ;;
        20) ./gen -number=20 -minn=200000 -maxn=200000 -minq=200000 -maxq=200000 -m=20 > "$inf" ;;
    esac
    echo "$inf generated"
done

g++ std.cpp -o std
for i in {1..20}; do
    inf="$i.in"
    ouf="$i.out"
    if [[ -f "$inf" ]]; then
        ./std < "$inf" > "$ouf"
        echo "$inf ok"
    else
        echo "$inf does not exist"
    fi
done

方式二:通过 startTest() 一次运行生成全部数据

g++ gen.cpp -o gen
./gen

# 遍历当前目录中所有只以数字命名的文件
for file in [0-9]*; do
    if [[ -f "$file" && ! "$file" =~ \. ]]; then
        mv "$file" "$file.in"
    fi
done

g++ std.cpp -o std

for i in {0..9}; do
    inf="$i.in"
    ouf="$i.out"
    if [[ -f "$inf" ]]; then
    ./std < "$inf" > "$ouf"
    else
    echo "$inf does not exist"
    fi
done

字符串

生成长度为 100 的 01 随机串,其中 1 的概率是十分之一。

println(rnd.next("[0000000001]{100}"));

随机包含大小写字母、数字的字符串

int length = rnd.wnext(1, 1000, 1);
println(rnd.next("[a-zA-Z0-9]{1,%d}", length));

基于 AI 的诗歌创作

最长公共子序列:先随机一个 LCS 串,再分别随机剩余串,然后随机合并。

#include <bits/stdc++.h>

#include "testlib.h"
using namespace std;

string add(const string &s, const string &t) {
    int weight = rnd.next(1, 5);
    int n = s.length();
    int m = t.length();
    string res = "";
    int i = 0, j = 0;
    while (i < n || j < m) {
        int op = rnd.next(0, weight);
        if (op == 0 && i < n) {
            res += s[i++];
        }
        if (op > 0 && j < m) {
            res += t[j++];
        }
    }

    return res;
}

void writeTest(int test, int maxn, int maxm) {
    startTest(test);
    int T = 10;
    println(T);
    T--;
    println("a");
    println("a");
    T--;
    println("a");
    println("b");
    while (T--) {
        int n = rnd.wnext(1, maxn, T);
        int m = rnd.wnext(1, maxm, T);
        int common_length = rnd.wnext(1, min(n, m), 2);
        if (test == 3 || test == 9) {
            if (rnd.next(0, 1)) n = maxn;
            if (rnd.next(0, 1)) m = maxm;
            if (rnd.next(0, 1))
                common_length = min(n, m);
            else
                common_length = rnd.wnext(1, min(n, m), 1);
        }
        auto lcs = rnd.next("[a-z]{%d}", common_length);
        auto s0 = rnd.next("[a-z]{%d}", n - common_length);
        auto t0 = rnd.next("[a-z]{%d}", m - common_length);
        auto s = add(s0, lcs);
        auto t = add(t0, lcs);
        println(s);
        println(t);
        assert(s.length() == n);
        assert(t.length() == m);
    }
}

int main(int argc, char *argv[]) {
    registerGen(argc, argv, 1);
    int seed = opt<int>("seed", 20241127);
    writeTest(0, 1000, 1000);
    writeTest(1, 1000, 1000);
    writeTest(2, 1000, 1000);
    writeTest(3, 1000, 1000);
    writeTest(4, 1000000, 1000);
    writeTest(5, 1000000, 1000);
    writeTest(6, 1000000, 1000);
    writeTest(7, 1000000, 1000);
    writeTest(8, 1000000, 1000);
    writeTest(9, 1000000, 1000);
    return 0;
}

排列

个元素随机选

template <typename T>
std::vector<T> random_sample(const std::vector<T>& input, int k) {
    assert(k <= input.size());
    std::vector<T> copy = input;

    shuffle(copy.begin(), copy.end());
    copy.resize(k);
    sort(copy.begin(), copy.end());
    return copy;
}

的随机路径,每次往右或者往左(随机的方式目前不太正确)。

vector<pair<int, int>> random_path(int n, int m) {
    vector<pair<int, int>> path;
    int x = 0, y = 0;
    while (true) {
        path.push_back({x, y});
        if (x == n - 1 && y == m - 1) break;
        if (x == n - 1) {
            y++;
            continue;
        }
        if (y == m - 1) {
            x++;
            continue;
        }
        int dx = rnd.next(0, 1);
        int dy = 1 - dx;
        int nx = x + dx, ny = y + dy;
        x = nx, y = ny;
    }
    assert(path.size() == n + m - 1);
    return path;
}

树图

  • -sum-n 多组数据的点数之和,必须指定
  • test-count 默认为 0,表示单组数据;大于 0 时,多组数据,会先输出组数
./gen-tree -sum-n=1000 -random=true -value-bias=0 -chain=0.2 -flower=0.6 -start-index=1 > 5.in
/**
* gen-tree       -sum-n <num>
*                [-test-count <num>]
*                [-min-n <num>]
*                [-min-value <num>]
*                [-max-value <num>]
*                [-value-bias <num>]
*                [-is-weighted <bool>]
*                [-chain <num>]
*                [-flower <num>]
*                [-start-index <num>]
*
*/
#include <bits/stdc++.h>

#include "testlib.h"

using namespace std;

namespace Graph {

using VTIII = vector<tuple<int, int, int>>;

/**
* @param chain = 0 -> 1 means the tree is a chain
* @param flower = 0 -> 1 means the tree is a flower
* NOTICE: only either chain or flower can be True
* @param start_index = 1
* @param value_bias = 0
* @param weight_limit = (1,1) -> the limit of weight. index 0 is the min limit,
                        and index 1 is the max limit(both included)
*/
struct TreeParams {
    double chain = 0.;
    double flower = 0.;
    // bool directed = true;
    int start_index = 1;
    int value_bias = 0;
    pair<int, int> weight_limit = {1, 1};
};

VTIII tree(int n, TreeParams para) {
    double chain = para.chain;
    double flower = para.flower;
    int start_index = para.start_index;
    int value_bias = para.value_bias;
    pair<int, int> weight_limit = para.weight_limit;

    if (!(chain >= 0 && chain <= 1) || !(flower >= 0 && flower <= 1)) {
        throw std::invalid_argument("chain and flower must be between 0 and 1");
    }
    if (chain + flower > 1) {
        throw std::invalid_argument("chain plus flower must be smaller than 1");
    }
    int chain_count = (n - 1) * chain;
    int flower_count = (n - 1) * flower;
    chain_count = min(chain_count, n - 1);
    flower_count = min(flower_count, n - 1 - chain_count);
    int random_count = n - 1 - chain_count - flower_count;

    vector<int> p(n);
    for (int i = 1; i < chain_count + 1; i++) {
        p[i] = i - 1;
    }
    for (int i = chain_count + 1; i < chain_count + flower_count + 1; i++) {
        p[i] = 0;
    }
    for (int i = n - random_count; i < n; i++) {
        p[i] = rnd.next(i);  // 0 ~ i-1
    }
    vector<int> perm = rnd.perm(n);
    VTIII edges;
    for (int i = 1; i < n; i++) {
        int x = perm[i];
        int y = perm[p[i]];
        int c = rnd.wnext(weight_limit.first, weight_limit.second, value_bias);
        if (rnd.next(2)) {
            swap(x, y);
        }
        edges.emplace_back(x + start_index, y + start_index, c);
    }
    return edges;
}

}  // namespace Graph

int main(int argc, char* argv[]) {
    registerGen(argc, argv, 1);

    int test_count = opt<int>("test-count", 0);
    if (test_count == 0) {
        test_count = 1;
    } else {
        println(test_count);
    }

    int sum_n = opt<int>("sum-n");  // Required
    bool random = opt<bool>("random", false);
    int min_n = opt<int>("min-n", 1);
    bool weighted = opt<bool>("is-weighted", false);
    int min_value = opt<int>("min-value", 1);
    int max_value = opt<int>("max-value", weighted ? 1000 * 1000 * 1000 : 1);
    int value_bias = opt<int>("value-bias", 0);
    if (random) {
        sum_n = max(2, rnd.wnext(sum_n, value_bias) + 1);
    }

    double chain = opt<double>("chain", 0.);
    double flower = opt<double>("flower", 0.);
    int start_index = opt<int>("start-index", 1);

    vector<int> n_list = rnd.partition(test_count, sum_n, min_n);
    for (int test_case = 0; test_case < test_count; ++test_case) {
        int n = n_list[test_case];
        auto edges = Graph::tree(n, {.chain = chain,
                                    .flower = flower,
                                    .start_index = start_index,
                                    .value_bias = value_bias,
                                    .weight_limit = {min_value, max_value}});
        println(n);
        for (auto edge : edges) {
            if (weighted) {
                println(std::get<0>(edge), std::get<1>(edge), std::get<2>(edge));
            } else {
                println(std::get<0>(edge), std::get<1>(edge));
            }
        }
    }
}

SPJ

科学可视化

#include <bits/stdc++.h>

#include "testlib.h"
using namespace std;
#define N 1010
using ll = long long;
int n;

int sgn(ll x) {
    if (x == 0) return 0;
    if (x > 0)
        return 1;
    else
        return -1;
}

struct Point {
    int x, y;
    Point() {}
    Point(int x_, int y_) {
        x = x_;
        y = y_;
    }
    Point operator-(const Point &b) const {
        return Point(x - b.x, y - b.y);
    }
    ll operator*(const Point &b) const {
        return 1LL * x * b.x + 1LL * y * b.y;
    }
    ll operator^(const Point &b) const {
        return 1LL * x * b.y - 1LL * b.x * y;
    }
    bool operator==(const Point &b) const {
        return x == b.x && y == b.y;
    }
} t[N];

struct Line {
    Point s, e;
    Line() {}
    Line(Point s_, Point e_) {
        s = s_;
        e = e_;
    }
} a[N];

bool inter(Line l1, Line l2) {
    return max(l1.s.x, l1.e.x) >= min(l2.s.x, l2.e.x) &&
            max(l2.s.x, l2.e.x) >= min(l1.s.x, l1.e.x) &&
            max(l1.s.y, l1.e.y) >= min(l2.s.y, l2.e.y) &&
            max(l2.s.y, l2.e.y) >= min(l1.s.y, l1.e.y) &&
            sgn((l2.s - l1.e) ^ (l1.s - l1.e)) * sgn((l2.e - l1.e) ^ (l1.s - l1.e)) <= 0 &&
            sgn((l1.s - l2.e) ^ (l2.s - l2.e)) * sgn((l1.e - l2.e) ^ (l2.s - l2.e)) <= 0;
}

bool same_point() {
    for (int i = 1; i <= n; ++i) {
        for (int j = 1; j < i; ++j) {
            if (t[i] == t[j]) {
                return true;
            }
        }
    }
    return false;
}

bool line_cross() {
    for (int i = 1; i <= n - 1; ++i)
        for (int j = i + 1; j <= n - 1; ++j) {
            if (!inter(a[i], a[j])) continue;
            // 相交,但排除掉只交在端点的情况
            if (a[i].s == a[j].e) swap(a[j].s, a[j].e);
            if (a[i].e == a[j].s) swap(a[i].s, a[i].e);
            if (a[i].e == a[j].e) swap(a[i].s, a[i].e), swap(a[j].s, a[j].e);
            if (!(a[i].s == a[j].s)) return true;
            if (sgn((a[i].s - a[i].e) ^ (a[j].s - a[j].e)) == 0 &&
                sgn((a[i].s - a[i].e) * (a[j].s - a[j].e)) > 0)
                return true;
            // 只交在端点是ok的
        }
    return false;
}

ll calc_area() {
    int L = ~0U >> 1, R = 0;
    int U = 0, D = ~0U >> 1;
    for (int i = 1; i <= n; ++i) {
        L = min(L, t[i].x);
        R = max(R, t[i].x);
        U = max(U, t[i].y);
        D = min(D, t[i].y);
    }
    return 1LL * (R - L) * (U - D);
}

int main(int argc, char *argv[]) {
    registerTestlibCmd(argc, argv);
    n = inf.readInt();
    for (int i = 1; i <= n; ++i) {
        t[i].x = ouf.readInt(0, 100000000, "x");
        t[i].y = ouf.readInt(0, 100000000, "y");
    }
    // ouf.readEof();
    ll area = calc_area();
    if (area > 9 * n) {
        quitf(_wa, "The area is too large.");
    }
    if (same_point()) {
        quitf(_wa, "Two points are at the same position.");
    }
    for (int i = 1; i < n; ++i) {
        int u = inf.readInt();
        int v = inf.readInt();
        a[i].s = t[u];
        a[i].e = t[v];
    }
    if (line_cross()) {
        quitf(_wa, "Two segments intersect");
    }
    double ratio = (double)area / n;
    std::string str = "ok. area = " + std::to_string(ratio);
    quitf(_ok, str.c_str());
}