• 算法基础模板


    时空复杂度分析

    一般ACM或者笔试题的时间限制是1秒或2秒。在这种情况下,C++代码中的操作次数控制在107~108为最佳。下面给出在不同数据范围下,代码的时间复杂度和算法该如何选择:

    1. n ≤ 30,指数级别,dfs+剪枝,状态压缩dp
    2. n ≤ 100 => O(n3),floyd,dp,高斯消元
    3. n ≤ 1000 =>O(n2),O(n2logn),dp,二分,朴素版Dijkstra、朴素版Prim、Bellman-Ford
    4. n ≤ 10000 => o(n * \(\sqrt{n}\)),块状链表、分块、莫队
    5. n ≤ 100000 => O(nlogn) => 各种sort,线段树、树状数组、set/map、heap、拓扑排序、dijkstra+heap、prim+heap、Kruskal、spfa、求凸包、求半平面交、二分、CDQ分治、整体二分、后缀数组、树链剖分、动态树
    6. n ≤ 1000000 =>O(n),以及常数较小的O(nlogn)算法 => 单调队列、hash、双指针扫描、并查集,kmp、AC自动机,常数比较小的O(nlogn)的做法: sort、树状数组、heap、dijkstra、spfa
    7. n ≤ 10000000 => o(n),双指针扫描、kmp、AC自动机、线性筛素数
    8. n ≤ 109=> o(\(\sqrt{n}\)),判断质数
    9. n ≤ 1018 => o(logn),最大公约数,快速幂,数位DP
    10. n ≤ 101000 => o((logn)2),高精度加减乘除
    11. n ≤ 10100000 => o(logk x loglogk),k表示位数,高精度加减、FFT/NTT

    基础算法-模板

    排序

    //快排
    void quick_sort(int q[], int l, int r){
        if (l >= r) return;
        int i = l - 1, j = r + 1, x = q[l + r >> 1];
        while (i < j){
            do i ++; while (q[i] < x);
            do j --; while (q[j] > x);
            if (i < j) swap(q[i], q[j]);
        }
        quick_sort(q, l, j);
        quick_sort(q, j + 1, r);
    }
    //归排
    void merge_sort(int q[], int l, int r){
        if (l >= r) return;
        int mid = l + r >> 1;
        merge_sort(q, l, mid);
        merge_sort(q, mid + 1, r);
        
        int k = 0, i = l, j = mid + 1;
        while (i <= mid && j <= r)
            if (q[i] <= q[j]) tmp[k ++] = q[i ++];
        	else tmp[k ++] = q[j ++];
        while(i <= mid) tmp[k ++] = q[i ++];
        while(j <= r) tmp[k ++] = q[j ++];
        
        for (i = l, j = 0; i <= r; i ++, j ++) q[i] = tmp[j];
    }
    

    二分

    // 整数二分
    bool check(int x){} // 查找x是否满足某种性质
    
    // 区间[l, r]被划分成[l, mid]和[mid + 1, r]时使用:符合条件的第一个位置
    int bsearch_1(int l, int r){
        while (l < r){
            int mid = l + r >> 1;
            if (check(mid)) r = mid;  // check(mid) 判断 [l,mid] 这个区间是否满足条件
            else l = mid + 1;
        }
        return l;
    }
    // 区间[l, r]被划分成[l, mid - 1]和[mid, r]时使用:符合条件的最后一个位置
    int bsearch_2(int l, int r){
        while (l < r){
            int mid = l + r + 1 >> 1; // + 1 的原因是 l + r >> 1 有可能 == l , l = mid 这条就会导致死循环
            if (check(mid)) l = mid;  // check(mid) 判断 [mid,r] 这个区间是否满足条件
            else r = mid - 1;
        }
        return l;
    }
    
    // 浮点数二分
    double bsearch_3(double l, double r){
        const double eps = 1e-6;	// 查找的精度
        while (r - l > eps){
            double mid = (l + r) / 2;
            if (check(mid)) r = mid;
            else l = mid;
        }
        return l;
    }
    

    高精度

    // 加法
    vector<int> add_plus(vector<int> &a, vector<int> &b){		//数组元素都是倒置
        if (a.size() < b.size()) return add_plus(b,a);
        vector<int> c;
        int t = 0;
        for (int i = 0; i < a.size(); i ++){
            t += a[i];
            if (i < b.size()) t += b[i];
            c.push_back(t % 10);
            t /= 10;
        }
        if (t) c.push_back(t);
        return c;
    }
    //减法   C = A - B, 满足A >= B, A >= 0, B >= 0
    vector<int> sub(vector<int> &a, vector<int> &b){
        vector<int> c;
        for (int i = 0, t = 0; i < a.size(); i ++ ){
            t = a[i] - t;
            if (i < b.size()) t -= b[i];
            c.push_back((t + 10) % 10);
            if (t < 0) t = 1;
            else t = 0;
        }
    
        while (c.size() > 1 && c.back() == 0) c.pop_back();
        return c;
    }
    //高精×低精 C = A * b, A >= 0, b >= 0
    vector<int> mul(vector<int> &a, int b){
        vector<int> c;
        int t = 0;
        for (int i = 0; i < a.size() || t; i ++ ){
            if (i < a.size()) t += A[i] * b;
            c.push_back(t % 10);
            t /= 10;
        }
        while (c.size() > 1 && c.back() == 0) c.pop_back();
        return c;
    }
    //高精÷低精 A / b = C ... r, A >= 0, b > 0
    vector<int> div(vector<int> &a, int b, int &r){
        vector<int> c;
        r = 0;
        for (int i = a.size() - 1; i >= 0; i -- ){
            r = r * 10 + A[i];
            c.push_back(r / b);
            r %= b;
        }
        reverse(c.begin(), c.end());
        while (c.size() > 1 && c.back() == 0) c.pop_back();
        return c;
    }
    

    前缀和

    //一维
    s[i] = s[i - 1] + a[i];
    [x1,x2]的和 sum = s[x2] - s[x1 - 1];
    //二维
    s[i][j] += s[i - 1][j] + s[i][j - 1] - s[i - 1][j - 1];
    [(x1,y1),(x2,y2)]的和 sum = s[x2][y2] - s[x2][y1 - 1] - s[x1 - 1][y2] + s[x1 - 1][y1 - 1];
    

    差分

    //一维差分			思路: 存数按询问方式操作
    给区间[l, r]中的每个数加上c: B[l] += c, B[r + 1] -= c
    //二维差分
    给以(x1, y1)为左上角,(x2, y2)为右下角的子矩阵中的所有元素加上c:
    S[x1, y1] += c, S[x2 + 1, y1] -= c, S[x1, y2 + 1] -= c, S[x2 + 1, y2 + 1] += c
    

    位运算

    &: 与 |: 或 ^: 异或同0异1 ~: 取反 <<: 左移 >>: 右移

    //求n的二进制的第k位数字:n >> k & 1; 19 10011
    cout << (19 >> 4 & 1) << endl; //1
    cout << (19 >> 3 & 1) << endl; //0
    cout << (19 >> 2 & 1) << endl; //0
    cout << (19 >> 1 & 1) << endl; //1
    cout << (19 >> 0 & 1) << endl; //1
    //求n的二进制的最后一位1的位置lowbit(n) = n&-n; 20 10100
    cout << (20&-20) << endl;//4
    

    双指针

    for (int i = 0, j = 0; i < n; i ++ ){
        while (j < i && check(i, j)) j ++ ;
        // 具体问题的逻辑
    }
    常见问题分类:
        (1) 对于一个序列,用两个指针维护一段区间
        (2) 对于两个序列,维护某种次序,比如归并排序中合并两个有序序列的操作
    

    离散化

    vector<int> alls; // 存储所有待离散化的值
    sort(alls.begin(), alls.end()); // 将所有值排序
    alls.erase(unique(alls.begin(), alls.end()), alls.end());   // 去掉重复元素
    
    // 二分求出x对应的离散化的值
    int find(int x) // 找到第一个大于等于x的位置{
        int l = 0, r = alls.size() - 1;
        while (l < r){
            int mid = l + r >> 1;
            if (alls[mid] >= x) r = mid;
            else l = mid + 1;
        }
        return r + 1; // 映射到1, 2, ...n
    }
    

    区间合并

    void merge(vector &segs){
        vector res;
        sort(segs.begin(), segs.end());
        int st = -2e9, ed = -2e9;
        for (auto seg : segs){
            if (ed < seg.first){
                if (st != -2e9) res.push_back({st, ed});
                st = seg.first, ed = seg.second;
            }
            else ed = max(ed, seg.second);
        }
        if (st != -2e9) res.push_back({st, ed});
        segs = res;
    }
    

    数据结构-模板

    单链表

    // head存储链表头,e[]存储节点的值,ne[]存储节点的next指针,idx表示当前用到了哪个节点
    int head, e[N], ne[N], idx;
    // 初始化
    void init(){
        head = -1;
        idx = 0;
    }
    // 在链表头插入一个数a
    void insert(int a){
        e[idx] = a, ne[idx] = head, head = idx ++ ;
    }
    // 在结点k后插入一个数x
    void add(int k, int x){
        e[idx] = x, ne[idx] = ne[k], ne[k] = idx ++ ;
    }
    // 将头结点删除,需要保证头结点存在
    void remove(){
        head = ne[head];
    }
    

    双链表

    // e[]表示节点的值,l[]表示节点的左指针,r[]表示节点的右指针,idx表示当前用到了哪个节点
    int e[N], l[N], r[N], idx;
    
    // 初始化
    void init(){
        //0是左端点,1是右端点
        r[0] = 1, l[1] = 0;
        idx = 2;
    }
    
    // 在节点a的右边插入一个数x
    void insert(int a, int x){
        e[idx] = x;
        l[idx] = a, r[idx] = r[a];	// 新节点连旧
        l[r[a]] = idx, r[a] = idx ++; // 旧节点连新	
    }
    
    // 删除节点a
    void remove(int a){
        l[r[a]] = l[a];
        r[l[a]] = r[a];
    }
    
    // 遍历单链表
    for (int i = h[k]; i != -1; i = ne[i])
            x = e[i];
    

    单调栈

    //常见模型:找出每个数左边离它最近的比它大/小的数
    int stk[N],tt = 0;	// 栈中存数据或下标
    for (int i = 1; i <= n; i ++){
        int x; cin >> x;
        while (tt && stk[tt] >= x) tt -- ;	// 左边比它小的数
        stk[ ++ tt] = i;	// 把当前值放在合适地方
    }
    

    单调队列

    //常见模型:找出滑动窗口中的最大值/最小值
    int a[N],q[N]; // q[N] 存的是a数组的下标
    int hh = 0, tt = -1;   // hh 队头(左) tt 队尾(右) 
    for (int i = 0; i < n; i ++){
        while (hh <= tt && check_out(q[hh])) hh ++ ;  	// 判断队头是否滑出窗口
        while (hh <= tt && check(q[tt], i)) tt -- ; 	// 舍去不合理数据
        q[ ++ tt] = i;					// 把当前数据的坐标插入适合的地方
    }
    

    KMP

    // s[1-m]是长文本,p[1-n]是模式串,m是s的长度,n是p的长度
    // 求next
    for (int i = 2, j = 0; i <= n; i ++){
        while (j && p[i] != p[j + 1]) j = ne[j];
        if (p[i] == p[j+1]) j ++;
        ne[i] = j;
    }
    // 匹配
    for (int i = 1, j = 0; i <= m; i ++){
        while (j && s[i] != p[j + 1]) j = ne[j];
        if (s[i] == p[j + 1]) j ++;
        if (j == n) {
            printf("%d ",i - n);
            j = ne[j];
        }
    }
    

    Tree树

    int son[N][26], cnt[N], idx;
    // 0号点既是根节点,又是空节点
    // son[][]存储树中每个节点的子节点	【实质是多开*26空间记录每个节点的信息】【这个26是根据提目要求具体有所变化】
    // cnt[]存储以每个节点结尾的单词数量
    // idx 节点编号
    
    // 插入一个字符串
    void insert(char *str){
        int p = 0;
        for (int i = 0; str[i]; i ++ ){
            int u = str[i] - 'a';
            if (!son[p][u]) son[p][u] = ++ idx;// 该节点是否存过
            p = son[p][u];
        }
        cnt[p] ++;
    }
    
    // 查询字符串出现的次数
    int query(char *str){
        int p = 0;
        for (int i = 0; str[i]; i ++ ){
            int u = str[i] - 'a';
            if (!son[p][u]) return 0;
            p = son[p][u];
        }
        return cnt[p];
    }
    

    并查集

    (1)朴素并查集:
        int p[N]; //存储每个点的祖宗节点
    
        // 返回x的祖宗节点
        int find(int x){
            if (p[x] != x) p[x] = find(p[x]);	// 路径压缩
            return p[x];
        }
    
        // 初始化,假定节点编号是1~n
        for (int i = 1; i <= n; i ++ ) p[i] = i;
    
        // 合并a和b所在的两个集合:
        p[find(a)] = find(b);
    
    (2)维护size的并查集:
        int p[N], size[N];
        //p[]存储每个点的祖宗节点, size[]只有祖宗节点的有意义,表示祖宗节点所在集合中的点的数量
    
        // 返回x的祖宗节点
        int find(int x){
            if (p[x] != x) p[x] = find(p[x]);
            return p[x];
        }
    
        // 初始化,假定节点编号是1~n
        for (int i = 1; i <= n; i ++ ){
            p[i] = i;
            size[i] = 1;
        }
    
        // 合并a和b所在的两个集合:
        size[find(b)] += size[find(a)];
        p[find(a)] = find(b);
    
    (3)维护到祖宗节点距离的并查集:
        int p[N], d[N];
        //p[]存储每个点的祖宗节点, d[x]存储x到p[x]的距离
    
        // 返回x的祖宗节点
        int find(int x){
            if (p[x] != x){
                int u = p[x];  // u记录旧的父节点
    			p[x] = find(p[x]); // 路径压缩,新父节点变成根节点了
    			d[x] += d[u];  // x到新父节点的距离等于x到旧父节点的距离加上旧父节点到根节点的距离
            }
            return p[x];
        }
    
        // 初始化,假定节点编号是1~n
        for (int i = 1; i <= n; i ++ ){
            p[i] = i;
            d[i] = 0;
        }
    
        // 合并a和b所在的两个集合:
        p[find(a)] = find(b);
        d[find(a)] = distance; // 根据具体问题,初始化find(a)的偏移量
    
    // 240. 食物链 ------ (3)维护到祖宗节点距离的并查集
    #include 
    using namespace std;
    const int N = 50010;
    int n, m;
    int p[N], d[N];
    int find(int x) {
        if (p[x] != x) {
            int u = p[x];  // u记录旧的父节点
    		p[x] = find(p[x]); // 路径压缩,新父节点变成根节点了
    		d[x] += d[u];  // x到新父节点的距离等于x到旧父节点的距离加上旧父节点到根节点的距离
        }
        return p[x];
    }
    int main(){
        scanf("%d%d", &n, &m);
        for (int i = 1; i <= n; i ++ ) p[i] = i;
        int res = 0;
        while (m -- ){
            int t, x, y;
            scanf("%d%d%d", &t, &x, &y);
            if (x > n || y > n) res ++ ;
            else{
                int px = find(x), py = find(y);
                if (t == 1) {                                       //x和y是同类
                    if (px == py && (d[x] - d[y]) % 3) res ++ ;     //如果d[x]=d[y]说明距离相等
                    else if (px != py) {                            //更新
                        p[px] = py;
                        d[px] = d[y] - d[x];                        //(d[x]+?-d[y])%3==0
                    }
                }else {                                             //x和y不是同类
                    if (px == py && (d[x] - d[y] - 1) % 3) res ++ ;
                    else if (px != py) {
                        p[px] = py;
                        d[px] = d[y] + 1 - d[x];                    //(d[x]+?-d[y]-1)%3==0
                    }
                }
            }
        }
        printf("%d\n", res);
        return 0;
    }
    

    // h[N]存储堆中的值, h[1]是堆顶,x的左儿子是2x, 右儿子是2x + 1
    // ph[k]存储第 k 个插入的点在堆中的位置
    // hp[J]存储堆中下标为 J 的点是第几个插入的
    int h[N], ph[N], hp[N], size;
    
    // 交换两个点,及其映射关系
    void heap_swap(int i, int j){	        // 交换i节点和j节点(附带更新是第几个插入的节点)
        swap(ph[hp[i]],ph[hp[j]]);    	//更新 i 和 j ph 信息 
        swap(hp[i], hp[j]);			//更新 i 和 j hp 信息
        swap(h[i], h[j]);			//交换 i 和 j 数值
    }
    
    void down(int u){	// 向下更新
        int t = u;
        if (u * 2 <= size && h[u * 2] < h[t]) t = u * 2;	// 左孩子
        if (u * 2 + 1 <= size && h[u * 2 + 1] < h[t]) t = u * 2 + 1; // 右孩子
        if (u != t){
            heap_swap(u, t);
            down(t);	// 向下递归继续更新
        }
    }
    
    void up(int u){	// 向上更新
        while (u / 2 && h[u] < h[u / 2]){
            heap_swap(u, u / 2);
            u >>= 1;
        }
    }
    
    // O(n)建堆
    for (int i = n / 2; i; i -- ) down(i);
    

    哈希表

    (1) 拉链法
        int h[N], e[N], ne[N], idx;
    
        // 向哈希表中插入一个数
        void insert(int x){
            int k = (x % N + N) % N;
            e[idx] = x;
            ne[idx] = h[k];
            h[k] = idx ++;
        }
    
        // 在哈希表中查询某个数是否存在
        bool find(int x){
            int k = (x % N + N) % N;
            for (int i = h[k]; i != -1; i = ne[i])
                if (e[i] == x)
                    return true;
    
            return false;
        }
    
    (2) 开放寻址法
        int h[N];
    
        // 如果x在哈希表中,返回x的下标;如果x不在哈希表中,返回x应该插入的位置
        int find(int x){
            int t = (x % N + N) % N;	// N 一般取 大于数据范围的素数
            while (h[t] != null && h[t] != x){
                t ++ ;
                if (t == N) t = 0;
            }
            return t;
        }
    

    字符串哈希

    核心思想:将字符串看成P进制数,P的经验值是13113331,取这两个值的冲突概率低
    小技巧:取模的数用2^64,这样直接用unsigned long long存储,溢出的结果就是取模的结果
    
    typedef unsigned long long ULL;
    ULL h[N], p[N]; // h[k]存储字符串前k个字母的哈希值, p[k]存储 P^k mod 2^64
    
    // 初始化
    p[0] = 1;
    for (int i = 1; i <= n; i ++ ){
        h[i] = h[i - 1] * P + str[i];// 这个str[i]只要不是0就行任意值都行,因此不需要转成1-26
        p[i] = p[i - 1] * P;
    }
    
    // 计算子串 str[l ~ r] 的哈希值
    // 由于h数组的特殊定义,h数组前面都是哈希值的高位,所以l-r的哈希值可以通过
    // 类似, l=123,r=123456,r-l哈希值等于123456-123000
    ULL get(int l, int r){
        return h[r] - h[l - 1] * p[r - l + 1];
    }
    

    STL

    vector:变长数组,倍增的思想
         vector<int> a(10),a(10,1); // 长度10,且初始化为1
         vector<int> a[10];	// 10个vector
        【size()返回元素个数】   【empty()返回是否为空】   【clear()清空】   【front()/back()】
        【push_back()/pop_back()】   【begin()/end()】   【[数组]】   【支持比较运算,按字典序】
    
    pair<int, int>
        【first, 第一个元素】   【second, 第二个元素】
        【支持比较运算,以first为第一关键字,以second为第二关键字(字典序)】
        【p = make_pair(10,20); p = {10,20};】
    
    string,字符串
        【size()/length()返回字符串长度】  【empty()】  【clear()】
        【substr(起始下标,(子串长度))返回子串】  【c_str()返回字符串所在字符数组的起始地址】
    
    queue, 队列
        【size()】      【empty()】     【push()向队尾插入一个元素】     【front()返回队头元素】
        【back()  返回队尾元素】      【pop()  弹出队头元素】
    
    priority_queue, 优先队列,默认是大根堆		【黑科技:插入负数就是小根堆】
        【size()】      【empty()】      【push()插入一个元素】       【top()返回堆顶元素】
        【pop()弹出堆顶元素】    【定义成小根堆的方式:priority_queue<int, vector<int>, greater<int>> q;】
    
    stack, 栈
        【size()】 【empty()】  【push()向栈顶插入一个元素】  【top()返回栈顶元素】  【pop()弹出栈顶元素】
    
    deque, 双端队列
        【size()】   【empty()】   【clear()】   【front()/back()】    【push_back()/pop_back()】
        【push_front()/pop_front()】    【begin()/end()】    【[数组/随机访问]】
    
    set, map, multiset, multimap, 基于平衡二叉树(红黑树),动态维护有序序列
        【size()】  【empty()】  【clear()】  【begin()/end()】  【++,-- 返回前驱和后继,时间复杂度 O(logn)】
    
    set(无重复)/multiset(可重复)
        【insert()  插入一个数】  【find()  查找一个数】  【count()  返回某一个数的个数】
        【erase()】
             (1) 输入是一个数x,删除所有x   O(k + logn)
             (2) 输入一个迭代器,删除这个迭代器
    lower_bound()/upper_bound()
        【lower_bound(x)  返回大于等于x的最小的数的迭代器】
        【upper_bound(x)  返回大于x的最小的数的迭代器】
    map/multimap
        【insert()  插入的数是一个pair】   【erase()  输入的参数是pair或者迭代器】    【find()】
        【[下标索引]  注意multimap不支持此操作。 时间复杂度是 O(logn)】  【lower_bound()/upper_bound()】
    
    unordered_set, unordered_map, unordered_multiset, unordered_multimap, 哈希表
        和上面类似,增删改查的时间复杂度是 O(1)
        不支持 lower_bound()/upper_bound(), 迭代器的++,--
    
    bitset, 圧位
        bitset<10000> s;
        ~, &, |, ^
        >>, <<
        ==, !=
        []
        count()  返回有多少个1
        any()  判断是否至少有一个1
        none()  判断是否全为0
        set()  把所有位置成1
        set(k, v)  将第k位变成v
        reset()  把所有位变成0
        flip()  等价于~
        flip(k) 把第k位取反
    

    搜索与图论-模板

    树与图的存储

    树是一种特殊的图,与图的存储方式相同。
    对于无向图中的边ab,存储两条有向边a->b, b->a。
    因此我们可以只考虑有向图的存储。

    1. 邻接矩阵: g[a][b]存储边a->b

    2. 邻接表:

      // 对于每个点k,开一个单链表,存储k所有可以走到的点。h[k]存储这个单链表的头结点
      int h[N], e[N], ne[N], idx;
      
      // 添加一条边a->b
      void add(int a, int b){
          e[idx] = b, ne[idx] = h[a], h[a] = idx ++ ;
      }
      
      // 初始化
      idx = 0;
      memset(h, -1, sizeof h);
      

    树与图的遍历

    时间复杂度O(n + m), n表示点数,m表示边数

    1. 深度优先搜索

      int dfs(int u){
          st[u] = true; // st[u] 表示点u已经被遍历过
      
          for (int i = h[u]; i != -1; i = ne[i]){
              int j = e[i];
              if (!st[j]) dfs(j);
          }
      }
      
    2. 宽度优先搜索

      queue<int> q;
      st[1] = true; // 表示1号点已经被遍历过
      q.push(1);
      
      while (q.size()){
          int t = q.front();
          q.pop();
      
          for (int i = h[t]; i != -1; i = ne[i]){
              int j = e[i];
              if (!st[j]){
                  st[j] = true; // 表示点j已经被遍历过
                  q.push(j);
              }
          }
      }
      

    拓扑排序

    时间复杂度 O(n+m),n表示点数,m表示边数

    int q[N],d[N]; // q模拟队列,d记录入度
    bool topsort(){
        int hh = 0, tt = -1;
        
        for (int i = 1; i <= n; i ++ )
            if (!d[i])
                q[ ++ tt] = i;	// 度为0的点队尾入队
    
        while (hh <= tt){
            int t = q[hh ++ ];	// 队头出队
    
            for (int i = h[t]; i != -1; i = ne[i]){
                int j = e[i];
                if (-- d[j] == 0)	// 度为0的点入队
                    q[ ++ tt] = j;
            }
        }
    
        // 如果所有点都入队了,说明存在拓扑序列;否则不存在拓扑序列。
        return tt == n - 1; // 1 说明有n个节点入过队列
    }
    

    朴素dijkstra算法

    时间复杂是O(n2+m), n表示点数, m表示边数

    int g[N][N];  // 存储每条边
    int dist[N];  // 存储1号点到每个点的最短距离
    bool st[N];   // 存储每个点的最短路是否已经确定
    
    // 求1号点到n号点的最短路,如果不存在则返回-1
    int dijkstra(){
        memset(dist, 0x3f, sizeof dist);
        dist[1] = 0;
    
        for (int i = 0; i < n - 1; i ++ ){
            int t = -1;     // 在还未确定最短路的点中,寻找距离最小的点
            for (int j = 1; j <= n; j ++ )
                if (!st[j] && (t == -1 || dist[t] > dist[j]))
                    t = j;
    
            // 用t更新其他点的距离
            for (int j = 1; j <= n; j ++ )
                dist[j] = min(dist[j], dist[t] + g[t][j]);
            
            st[t] = true;
        }
    
        if (dist[n] == 0x3f3f3f3f) return -1;
        return dist[n];
    }
    

    堆优化版dijkstra

    时间复杂度O(mlogn), n表示点数, m表示边数

    typedef pair<int, int> PII;
    
    int n;      // 点的数量
    int h[N], w[N], e[N], ne[N], idx;       // 邻接表存储所有边
    int dist[N];        // 存储所有点到1号点的距离
    bool st[N];     // 存储每个点的最短距离是否已确定
    
    // 求1号点到n号点的最短距离,如果不存在,则返回-1
    int dijkstra(){
        memset(dist, 0x3f, sizeof dist);
        dist[1] = 0;
        priority_queue, greater> heap;
        heap.push({0, 1});      // first存储距离,second存储节点编号
    
        while (heap.size()){
            auto t = heap.top();
            heap.pop();
    
            int ver = t.second, distance = t.first;
    
            if (st[ver]) continue;
            st[ver] = true;
    
            for (int i = h[ver]; i != -1; i = ne[i]){
                int j = e[i];
                if (dist[j] > distance + w[i]){
                    dist[j] = distance + w[i];
                    heap.push({dist[j], j});
                }
            }
        }
    
        if (dist[n] == 0x3f3f3f3f) return -1;
        return dist[n];
    }
    

    Bellman-ford算法

    时间复杂度O(nm), n表示点数, m表示边数

    int n, m;       // n表示点数,m表示边数
    int dist[N];        // dist[x]存储1到x的最短路距离
    
    struct Edge{     // 边,a表示出点,b表示入点,w表示边的权重
        int a, b, w;
    }edges[M];
    
    // 求1到n的最短路距离,如果无法从1走到n,则返回-1。
    int bellman_ford(){
        memset(dist, 0x3f, sizeof dist);
        dist[1] = 0;
    
        // 如果第n次迭代仍然会松弛三角不等式,就说明存在一条长度是n+1的最短路径,由抽屉原理,路径中至少存在两个相同的点,说明图中存在负权回路。
        for (int i = 0; i < n; i ++ ){
            for (int j = 0; j < m; j ++ ){
                int a = edges[j].a, b = edges[j].b, w = edges[j].w;
                if (dist[b] > dist[a] + w)
                    dist[b] = dist[a] + w;
            }
        }
    
        if (dist[n] > 0x3f3f3f3f / 2) return -1;
        return dist[n];
    }
    

    求边数限制的最短路算法 通过k次松弛,所求得的最短路,就是边数限制的最短路

    const int N = 510, M = 10010;
    struct Edge{
        int a, b, c;
    }edges[M];
    int n, m, k;
    int dist[N];
    int last[N];
    
    void bellman_ford(){
        memset(dist, 0x3f, sizeof dist);	// 初始化
        dist[1] = 0;
        
        for (int i = 0; i < k; i ++ ){
            // 为了防止发生串联 如: 1→2→3,在一次循环里1更新2,2有就可能更新3,这是不允许的,所以保存初始dist数组
            memcpy(last, dist, sizeof dist);
            for (int j = 0; j < m; j ++ ){
                auto e = edges[j];
                dist[e.b] = min(dist[e.b], last[e.a] + e.c);  // 松弛
            }
        }
    }
    int main(){
        scanf("%d%d%d", &n, &m, &k);
        for (int i = 0; i < m; i ++ ){
            int a, b, c;
            scanf("%d%d%d", &a, &b, &c);
            edges[i] = {a, b, c};
        }
        bellman_ford();
        
    	//不可dist[n]==0x3f3f3f3f 因为有可能出现1到不了2,2到3为负数,所以大于无穷的一半就可以判定无法到达
        if (dist[n] > 0x3f3f3f3f / 2) puts("impossible");
        else printf("%d\n", dist[n]);
        return 0;
    }
    

    spfa 算法

    队列优化的Bellman-Ford算法: 时间复杂度平均情况下O(m),最坏情况下O(nm), n表示点数, m表示边数

    int n;      // 总点数
    int h[N], w[N], e[N], ne[N], idx;       // 邻接表存储所有边
    int dist[N];        // 存储每个点到1号点的最短距离
    bool st[N];     // 存储每个点是否在队列中
    
    // 求1号点到n号点的最短路距离,如果从1号点无法走到n号点则返回-1
    int spfa(){
        memset(dist, 0x3f, sizeof dist);
        dist[1] = 0;
    
        queue<int> q;
        q.push(1);
        st[1] = true;	// st 数组记录哪些点在队列里
    
        while (q.size()){
            auto t = q.front();
            q.pop();
    
            st[t] = false;
    
            for (int i = h[t]; i != -1; i = ne[i]){
                int j = e[i];
                if (dist[j] > dist[t] + w[i]){	// 松弛:对于队列中所有符合条件的边进行松弛
                    dist[j] = dist[t] + w[i];
                    if (!st[j]){                    // 如果队列中已存在j,则不需要将j重复插入
                        q.push(j);    	        // 只要是符合条件就进队列
                        st[j] = true;
                    }
                }
            }
        }
    
        if (dist[n] == 0x3f3f3f3f) return -1;
        return dist[n];
    }
    

    spfa 求负环

    int n;      // 总点数
    int h[N], w[N], e[N], ne[N], idx;       // 邻接表存储所有边
    int dist[N], cnt[N];        // dist[x]存储1号点到x的最短距离,cnt[x]存储1到x的最短路中经过的点数
    bool st[N];     // 存储每个点是否在队列中
    
    // 如果存在负环,则返回true,否则返回false。
    bool spfa(){
        // 不需要初始化dist数组,因为不用求具体数值,只需要矢量的比较就行
        // 原理:如果某条最短路径上有n个点(除了自己),那么加上自己之后一共有n+1个点,由抽屉原理一定有两个点相同,所以存在环。
    
        queue<int> q;
        for (int i = 1; i <= n; i ++ ){	// 求整个图中的负环
            q.push(i);
            st[i] = true;
        }
    
        while (q.size()){
            auto t = q.front();
            q.pop();
    
            st[t] = false;
    
            for (int i = h[t]; i != -1; i = ne[i]){
                int j = e[i];
                if (dist[j] > dist[t] + w[i]){	// 松弛
                    dist[j] = dist[t] + w[i];
                    cnt[j] = cnt[t] + 1;
                    if (cnt[j] >= n) return true; // 如果从1号点到x的最短路中包含至少n个点(不包括自己),则说明存在环
                    if (!st[j]){
                        q.push(j);
                        st[j] = true;
                    }
                }
            }
        }
    
        return false;
    }
    

    Floyd算法

    // 初始化:
        for (int i = 1; i <= n; i ++ )
            for (int j = 1; j <= n; j ++ )
                if (i == j) d[i][j] = 0;
                else d[i][j] = INF;
    
    // 算法结束后,d[a][b]表示a到b的最短距离
    void floyd(){
        for (int k = 1; k <= n; k ++ )
            for (int i = 1; i <= n; i ++ )
                for (int j = 1; j <= n; j ++ )
                    d[i][j] = min(d[i][j], d[i][k] + d[k][j]);
    }
    
    // 输出结果
    	if(g[a][b] > INF/2)puts("impossible");
    	else printf("%d\n",g[a][b]);
    

    朴素版Prim算法

    时间复杂度是O(n2+m), n表示点数,m表示边数

    用堆优化版prim,和用堆优化版Dijkstra差不多

    int n;      // n表示点数
    int g[N][N];        // 邻接矩阵,存储所有边
    int dist[N];        // 存储其他点到当前最小生成树的距离
    bool st[N];     // 存储每个点是否已经在生成树中
    
    
    // 如果图不连通,则返回INF(值是0x3f3f3f3f), 否则返回最小生成树的树边权重之和
    int prim(){
        memset(dist, 0x3f, sizeof dist);
    	dist[1] = 0;
        int res = 0;
        for (int i = 0; i < n; i ++ ){
            int t = -1;
            for (int j = 1; j <= n; j ++ )
                if (!st[j] && (t == -1 || dist[t] > dist[j]))
                    t = j;
    
            if (dist[t] == INF) return INF;
    
            res += dist[t];
            st[t] = true;
    		// 放在下面,是因为数据中有自环,容易造成误算
            for (int j = 1; j <= n; j ++ ) dist[j] = min(dist[j], g[t][j]);	// 把t所连且距离更短的放入集合
        }
        return res;
    }
    

    堆优化版Prim

    不用刻意优化

    const int N = 510, INF = 0x3f3f3f3f;
    int n, m;
    int g[N][N];
    bool st[N];
    int prim(){
        int res = 0, cnt = 0;
        priority_queue, greater> heap;
        heap.push({0,1});
        while (heap.size()){
            auto t = heap.top();
            heap.pop();
            if(st[t.second]) continue;
            st[t.second] = true;
            res += t.first;
            cnt ++;
            for (int i = 1; i <= n; i ++){
                if (!st[i] && g[t.second][i] != INF){
                    heap.push({g[t.second][i], i});
                }
            }
            
        }
        if(cnt != n)return INF;
        return res;
    }
    

    Kruskal算法

    int n, m;       // n是点数,m是边数
    int p[N];       // 并查集的父节点数组
    
    struct Edge{     // 存储边
        int a, b, w;
        bool operator< (const Edge &W)const{
            return w < W.w;
        }
    }edges[M];
    
    int find(int x){     // 并查集核心操作
        if (p[x] != x) p[x] = find(p[x]);
        return p[x];
    }
    
    int kruskal(){
        sort(edges, edges + m);
        for (int i = 1; i <= n; i ++ ) p[i] = i;    // 初始化并查集
    
        int res = 0, cnt = 0;
        for (int i = 0; i < m; i ++ ){
            int a = edges[i].a, b = edges[i].b, w = edges[i].w;
    
            a = find(a), b = find(b);
            if (a != b){     // 如果两个连通块不连通,则将这两个连通块合并
                p[a] = b;
                res += w;
                cnt ++ ;
            }
        }
    
        if (cnt < n - 1) return INF;
        return res;
    }
    

    二分图-染色判定法

    • 二分图定义: 图中不存在奇数环;或图可被分为两部分,两部分内部不存在边,只在中间存在边。
    • 时间复杂度是O(n + m), n表示点数,m表示边数
    int n;      // n表示点数
    int h[N], e[M], ne[M], idx;     // 邻接表存储图
    int color[N];       // 表示每个点的颜色,0表示未染色,1表示白色,2表示黑色
    
    // 参数:u表示当前节点,c表示当前点的颜色
    bool dfs(int u, int c){
    	color[u] = c;
    	for (int i = h[u]; i != -1; i = ne[i]){
    		int j = e[i];
    		if (!color[j] && !dfs(j, 3 - c)) return false;
    		else if (color[u] == color[j]) return false;
    	}
    	return true;
    }
    
    bool check(){
        bool flag = true;
        for (int i = 1; i <= n; i ++ )
            if (!color[i] && !dfs(i, 0)){
                    flag = false;
                    break;
            }
        return flag;
    }
    

    二分图-匈牙利算法

    • 匈牙利算法为了解决二分图两部分的节点的最大匹配数。
    • 匈牙利算法: 二分图的两部分,一方男同志,一方女同志,两方匹配,一方按顺序匹配,有心仪的女生(即有边),即匹配成功,到某个男生匹配时,发现心仪的女生已经匹配了,那么这个男生就要女生问问她的配偶是否有备胎,递归去问备胎是否单身....。若备胎也没匹配,那么她男朋友和他备胎在一起,直到所有有联系的人都问完。---给人找到下家,才去挖墙脚。(做错不重要,重要的是错过)
    • 时间复杂度是O(nm), n表示点数,m表示边数
    int n1, n2;     // n1表示第一个集合中的点数,n2表示第二个集合中的点数
    int h[N], e[M], ne[M], idx;     // 邻接表存储所有边,匈牙利算法中只会用到从第一个集合指向第二个集合的边,所以这里只用存一个方向的边
    bool st[N];			// 男生匹配每个女生只尝试一次
    int match[N];		// 该女生匹配了哪个男生
    
    bool find(int x){
    	for (int i = h[x]; i != -1; i = ne[i]){
    		int j = e[i];
    		if (!st[j]){
    			st[j] = true; // 只尝试一次
    			if (match[j] == 0 || find(match[j])){ // 没匹配或者对象有备胎
    				match[j] = x;		// 匹配成功
    				return true;
    			}
    		}
    
    	}
    	return false;
    }
    
    // 求最大匹配数,依次枚举第一个集合中的每个点能否匹配第二个集合中的点
    int res = 0;
    for (int i = 1; i <= n1; i ++ ){
        memset(st, false, sizeof st);	// 每个都尝试
        if (find(i)) res ++ ;
    }
    

    数学知识-模板

    试除法判定质数

    时间复杂度大O(sqrt(n))

    bool is_prime(int x){
        if (x < 2) return false;
        for (int i = 2; i <= x / i; i ++ )
            if (x % i == 0)
                return false;
        return true;}
    

    试除法分解质因数

    时间复杂度O(log(n) - sqrt(n))

    void divide(int x){
        for (int i = 2; i <= x / i; i ++ )
            if (x % i == 0){	// i 一定是质数,因为合数在前面已经除完了
                int s = 0;
                while (x % i == 0) x /= i, s ++ ;
                cout << i << ' ' << s << endl;
            }
        if (x > 1) cout << x << ' ' << 1 << endl;// 一个数最多有一个大于sqrt(n)的质因子,因为若是有两个那么乘积就大于n了
        cout << endl;
    }
    

    朴素筛法求素数

    时间复杂度O(nlog(log(n)))近似O(n)

    int primes[N], cnt;     // primes[]存储所有素数
    bool st[N];         // st[x]存储x是否被筛掉
    
    void get_primes(int n){
        for (int i = 2; i <= n; i ++ ){
            if (st[i]) continue;
            primes[cnt ++ ] = i;
            for (int j = i + i; j <= n; j += i)	// 只需要以素数筛就可以,因为前面的素数会将后面的合数筛掉
                st[j] = true;
        }
    }
    

    线性筛法求素数

    原理: n只会被它的最小质因子筛掉 时间复杂度 O(n)

    int primes[N], cnt;     // primes[]存储所有素数
    bool st[N];         // st[x]存储x是否被筛掉
    
    void get_primes(int n){
        for (int i = 2; i <= n; i ++ ){
            if (!st[i]) primes[cnt ++ ] = i;
            // 用已经筛出的素数去筛,保证了每次都是最小质因子筛掉合数
            /* 
            	primes[j]<=n/i是合理的, 因为j永远小于i, 即primes已存的素数都是小于i的
                1、当 i 是合数, 那么一定在下面的break出去, 因为一定有最小质因子。
                2、当 i 是质数, 如果不从primes[j] <= n / i退出,那一定在下面break退出(因为总会j++到primes[j] == i时)
            */
            for (int j = 0; primes[j] <= n / i; j ++ ){
                st[primes[j] * i] = true;	//在下面注释
                if (i % primes[j] == 0) break;	// 只能被自己最小质因子筛掉
            }
        }
    }
    

    注释:

    1. 若i % primes[j] == 0 那么primes[j] 一定是 i 的最小质因子,此时i可以直接被筛掉,且primes[j] * i 的最小质因子也是primes[j]。
    2. 若i % primes[j] != 0 说明前面筛出的素数都不是i最小质因子,但primes[j] * i 的最小质因子也是 primes[j]。
    3. 总之,primes[j] * i 的最小质因子始终是 primes[j] 对应代码 st[primes[j] * i] = true;

    试除法求所有约数

    时间复杂度为O(logn)

    vector<int> get_divisors(int x){
        vector<int> res;
        for (int i = 1; i <= x / i; i ++ )
            if (x % i == 0){
                res.push_back(i);
                if (i != x / i) res.push_back(x / i);
            }
        sort(res.begin(), res.end());
        return res;
    }
    

    约数个数和约数之和

    如果 N = p1^c1 * p2^c2 * ... *pk^ck		// p为质因子
    约数个数:(c1 + 1) * (c2 + 1) * ... * (ck + 1)	// 组合数
    // 按照组合数选数, 展开的每一项就是约数, 总和就是约数之和
    约数之和:(p1^0 + p1^1 + ... + p1^c1) * ... * (pk^0 + pk^1 + ... + pk^ck)
    
    const int N = 110;
    const int mod = 1e9+7;
    int main(){
        int n; cin >> n;
        unordered_map<int, int> primes;
        while (n --){
            int x; cin >> x;
            for (int i = 2; i <= x / i; i ++){
                while (x % i == 0){
                    primes[i] ++;
                    x /= i;
                }
            }
            if (x > 1) primes[x] ++;
        }
        LL res = 1;
        // 约数个数
        for (auto prime:primes)res = res * (prime.second + 1) % mod;
        
        cout << res << endl;
        res = 1;
        // 约数之和
        for (auto prime:primes){
            int p = prime.first,k = prime.second;
            LL t = 1;
            while (k --) t = (t * p + 1) % mod;  // 这里要取模所以用等比数列前n项和不合适
            res = res * t % mod;
        }
        cout << res << endl;
        return 0;
    }
    

    欧几里得算法

    假设d为任意两个数的最大公约数

    1. 定理: 若d|a 和 d|b, 即d|ax + by |: 整除的意思 ↔ d|a a整除b 裴蜀定理: 对于任意正整数a, b, 一定存在非零整数x, y, 使得ax + by = (a, b) 即a, b组合的最小的正整数为a和b的最大公约数。
    2. 推理: a mod b = a - a / b * b = a - c * b 令c = a / b
    3. 论证: (b, a % b) == (b, a - c * b) 由(1)得 d|(a - c * b) 和 d|b 得d|(a- c * b + c * b),即d|a,
      所以(b, a % b) == (a, b) (a,b)即a和b最大公约数
    4. 结论: (a, b) == (a, a % b)
    int gcd(int a, int b){	// (a,b) == (a, a % b) 递归下去, 即求最大公约数 递归结束条件 b == 0
        return b ? gcd(b, a % b) : a;// b不等于0, 返回gcd(b, a % b), 否者返回a, 因为a和0的最大公约数为a
    }
    

    欧拉函数

    1. 极性函数证明

    2. 容斥原理证明

    代码如下

    int phi(int x){
        int res = x;
        for (int i = 2; i <= x / i; i ++ )
            if (x % i == 0){
                res = res / i * (i - 1);	// res / i * (i - 1) == res * (1 - 1 / i);
                while (x % i == 0) x /= i;
            }
        if (x > 1) res = res / x * (x - 1);
    
        return res;
    }
    

    筛法求欧拉筛

    int primes[N], cnt;     // primes[]存储所有素数
    int euler[N];           // 存储每个数的欧拉函数
    bool st[N];         // st[x]存储x是否被筛掉
    
    
    void get_eulers(int n){
        euler[1] = 1;	// 定义的
        for (int i = 2; i <= n; i ++ ){
            if (!st[i]){
                primes[cnt ++ ] = i;
                euler[i] = i - 1;	// 质数的欧拉值为 i - 1
            }
            for (int j = 0; primes[j] <= n / i; j ++ ){
                int t = primes[j] * i;
                st[t] = true;
                if (i % primes[j] == 0){	// primes[j] 是 i的最小质因子
                    /*
                    	phi[i] = i*(1-1/p1)*(1-1/p2)*...*(1-1/pk),且primes[j]是i的质因子,
                    	所以phi[t] = primes[j]*i*(1-1/p1)*(1-1/p2)*...*(1-1/pk) = primes[j]*phi[i]
                    */
                    euler[t] = euler[i] * primes[j];
                    break;
                }
                /*
                	解释一:
                		i 不能整除 primes[j], 那么 i 就和 primes[j] 互质, 根据积性函数得 φ(t) = φ(i) * φ(primes[j])
                	解释二:
                		i 不能整除 primes[j], 但是primes[j]仍是t的最小质因子, 因此不仅需要将基数N修正为primes[j]倍, 还需要				补上1 - 1 / primes[j]这一项, 因此最终结果phi[i] * (primes[j] - 1)
                */
                euler[t] = euler[i] * (primes[j] - 1);
            }
        }
    }
    

    快速幂

    求 m^k mod p,时间复杂度 O(log(k))

    原理: 预处理m的1,2,4,8,16....次方,进行k的二进制规律进行组合相乘

    int qmi(int m, int k, int p){
        int res = 1 % p, t = m;
        while (k){ // k次, k转成二进制
            if (k&1) res = res * t % p;	// 每次看末位是否为1,为1则进行累乘
            t = t * t % p;
            k >>= 1;
        }
        return res;
    }
    

    快速幂求逆元(p质数)

    ≡ : 同余

    a / b ≡ a * x (mod p)
    两边同乘b可得 a ≡ a * b * x (mod p)
    即 1 ≡ b * x (mod p)
    同 b * x ≡ 1 (mod p)
    由费马小定理可知,当p为质数时
    b(p-1) ≡ 1 (mod p)
    拆一个b出来可得 b * b(p-2) ≡ 1 (mod p)
    故当n为质数时,b的乘法逆元 x = b(p-2)

    LL qmi(int m, int k, int p){
        LL res = 1 % p, t = m;
        while (k){
            if (k&1) res = res * t % p;
            t = t * t % p;
            k >>= 1;
        }
        return res;
    }
    int main(){
        int n;
        scanf("%d", &n);
        while (n -- ){
            int a, p;
            scanf("%d%d", &a, &p);
            if (a % p == 0) puts("impossible");	// 质数只和自己的倍数不互质
            else printf("%lld\n", qmi(a, p - 2, p));
        }
        return 0;
    }
    

    扩展欧几里得算法

    证明1:

    写法一

    int exgcd(int a, int b, int &x, int &y){//返回gcd(a,b) 并求出解(引用带回)
        if(b==0){
            x = 1, y = 0;
            return a;
        }
        int x1,y1,gcd;
        gcd = exgcd(b, a%b, x1, y1);
        x = y1, y = x1 - a/b*y1;	// 递归回溯回时记录答案
        return gcd; 
    }
    

    写法二

    // 求x, y,使得ax + by = gcd(a, b)
    int exgcd(int a, int b, int &x, int &y){
        if (!b){
            x = 1; y = 0;	// 当 b = 0时, a和b的最大公约数为 a, 系数为 x = 1, y = 0;
            return a;
        }
        int d = exgcd(b, a % b, y, x);
        y -= (a / b) * x;	// y = y' - a/b * x'  y'和x'都是回溯上层的结果
        return d;
    }
    

    线性同余方程

    求同余方程ax ≡ b(mod m)的系数 x

    推理: ax ≡ b(mod m)↔(ax % m = b % m),知存在yk使得 ax = myk + b,得ax - myk = b,令 y = -yk,即 ax + my = b。ax + my = b有解的必要条件是gcd(a, m)|b。设求出ax0 + my0 = gcd(a,m) ,即得 x = b / gcd(a,m) * x0 = b * x0 / gcd(a, m)

    while (n -- ){
        int a, b, m;
        scanf("%d%d%d", &a, &b, &m);
        int x, y;
        int d = exgcd(a, m, x, y);
        if (b % d) puts("impossible");	        // 说明b不能整除gcd(a, m)
        else printf("%d\n", (LL)b * x / d % m);	// 题目要求在int范围内,且(a*x)%m = (a*(x%m))%m, 所以最后需要%m
    }
    

    扩展欧几里得求逆元(p非质数)

    求ax ≡ 1 (mod p)的x,根据线性同余方程等价求ax + py = 1的x

    while (n--) {
        cin >> a >> p;
        if (exgcd(a, p, x, y) == 1) cout << (x + p) % p << endl;
        else  cout << "impossible" << endl;//如果 exgcd(a,p,x,y) != 1, 说明ax+py=1无解, 因为1只能整除1
    }
    

    高斯消元

    // a[N][N]是增广矩阵
    int gauss(){
        int c, r;
        for (c = 0, r = 0; c < n; c ++ ){
            int t = r;
            for (int i = r; i < n; i ++ )//找到绝对值最大的行,寻找最大的数值是因为可以避免系数变得太大,精度较高.
                if (fabs(a[i][c]) > fabs(a[t][c]))
                    t = i;
    
            if (fabs(a[t][c]) < eps) continue;
    
            for (int i = c; i <= n; i ++ ) swap(a[t][i], a[r][i]); // 将绝对值最大的行换到最顶端 r
            for (int i = n; i >= c; i -- ) a[r][i] /= a[r][c];     // 将当前行的首位变成 1
            for (int i = r + 1; i < n; i ++ )                      // 用当前行将下面所有的列消成 0
                if (fabs(a[i][c]) > eps)
                    for (int j = n; j >= c; j -- )
                        a[i][j] -= a[r][j] * a[i][c];
    
            r ++ ;
        }
    
        if (r < n){
            for (int i = r; i < n; i ++ )
                if (fabs(a[i][n]) > eps)  // 最后一列有非零则无解
                    return 2; // 无解
            return 1; // 有无穷多组解
        }
    
        for (int i = n - 1; i >= 0; i -- )
            for (int j = i + 1; j < n; j ++ )
                a[i][n] -= a[i][j] & a[j][n];	// 回解每个未知数
    
        return 0; // 有唯一解
    }
    

    递推法求组合数

    \(C_{m}^{n}\) = \(C_{m-1}^{n}\) + \(C_{m-1}^{n-1}\) : m个数选n个,可分为两种情况,某数x,① 确定选 x 再在m-1个中选n-1个,即\(C_{m-1}^{n-1}\)确定不选 x 再在m-1个中选n个\(C_{m-1}^{n}\)

    数据范围: 10000次询问,1 <= b <= a <= 2000

    // c[a][b] 表示从a个苹果中选b个的方案数
    for (int i = 0; i < N; i ++ )
        for (int j = 0; j <= i; j ++ )
            if (!j) c[i][j] = 1;	// c[i][0] = 1;
            else c[i][j] = (c[i - 1][j] + c[i - 1][j - 1]) % mod;
    

    预处理逆元的方式求组合数

    首先预处理出所有阶乘取模的余数fact[N],以及所有阶乘取模的逆元infact[N]
    如果取模的数是质数,可以用费马小定理求逆元

    数据范围: 10000次询问,1 <= b <= a <= 105

    int qmi(int a, int k, int p){    // 快速幂模板
        int res = 1;
        while (k){
            if (k & 1) res = (LL)res * a % p;
            a = (LL)a * a % p;
            k >>= 1;
        }
        return res;
    }
    
    // 预处理阶乘的余数和阶乘逆元的余数
    fact[0] = infact[0] = 1;
    for (int i = 1; i < N; i ++ ){
        fact[i] = (LL)fact[i - 1] * i % mod;
        infact[i] = (LL)infact[i - 1] * qmi(i, mod - 2, mod) % mod;
    }
    

    Lucas定理求组合数

    若p是质数,则对于任意整数 1 <= m <= n,有: \(C_{m}^{n}\) = \(C_{m\%p}^{n\%p}\) * \(C_{m/p}^{n/p}\) (mod p)

    数据范围: 20次询问,1 <= b <= a <= 1018,1 <= p <= 105

    int qmi(int a, int k, int p){  // 快速幂模板
        int res = 1 % p;
        while (k){
            if (k & 1) res = (LL)res * a % p;
            a = (LL)a * a % p;
            k >>= 1;
        }
        return res;
    }
    int C(int a, int b, int p){  // 通过定理求组合数C(a, b)
        if (a < b) return 0;
    
        LL x = 1, y = 1;  // x是分子,y是分母
        for (int i = a, j = 1; j <= b; i --, j ++ ){
            x = (LL)x * i % p;
            y = (LL) y * j % p;
        }
        
        return x * (LL)qmi(y, p - 2, p) % p;
    }
    int lucas(LL a, LL b, int p){
        if (a < p && b < p) return C(a, b, p);
        return (LL)C(a % p, b % p, p) * lucas(a / p, b / p, p) % p;
    }
    

    分解质因数法求组合数

    当我们需要求出组合数的真实值,而非对某个数的余数时,分解质因数的方式比较好用:

    1. 筛法求出范围内的所有质数

    2. 通过 C(a, b) = a! / b! / (a - b)! 这个公式求出每个质因子的次数。 \(\lfloor {n \over p} \rfloor\) + \(\lfloor {n \over p^{2}} \rfloor\) + \(\lfloor {n \over p^{3}} \rfloor\) + ... \(\lfloor {n \over p^{k}(p^{k}\leqslant n)} \rfloor\)

      n*(n-1)*(n-2)*...2*1 中 p 的次数: p为质因子

      • \({n \over p}\) 代表 1 - n 中 p倍数的数字个数 1p,2p,3p,...xp \({\leqslant}\) n 这个x=\({n \over p}\)
      • \({n \over p^{2}}\) 代表1 - n/p 中 p倍数的数字个数1p,2p,...mp\({\leqslant}\) n/p 其中m=\({n \over p^{2}}\)
      • .....
      • \({n \over p^{k}}\) 代表1 - n/pk-1 中 p倍数的数字个数1p,2p,3p,....,kp\({\leqslant}\) n/pk-1 其中k=\({n \over p^{k}}\) (循环结束条件: pk+1 > n)
      • 所以 n! 中p的次数是 \(\lfloor {n \over p} \rfloor\) + \(\lfloor {n \over p^{2}} \rfloor\) + \(\lfloor {n \over p^{3}} \rfloor\) + ... \(\lfloor {n \over p^{k}(p^{k}\leqslant n)} \rfloor\)
    3. 高精度乘法将所有质因子相乘

    // 线性筛求素数
    int primes[N], cnt;
    int sum[N];
    bool st[N];
    void get_primes(int n){
        for (int i = 2; i <= n; i ++ ){
            if (!st[i]) primes[cnt ++ ] = i;
            for (int j = 0; primes[j] <= n / i; j ++ ){
                st[primes[j] * i] = true;
                if (i % primes[j] == 0) break;
            }
        }
    }
    
    // 求n!中的次数(核心代码)
    int get(int n, int p){
        int res = 0;
        while (n){
            res += n / p;	        // 累计一次p的数量
            n /= p;			// 增加一次方
        }
        return res;
    }
    
    // 高精度乘低精度模板
    vector<int> mul(vector<int> a, int b){
        vector<int> c;
        int t = 0;
        for (int i = 0; i < a.size(); i ++ ){
            t += a[i] * b;
            c.push_back(t % 10);
            t /= 10;
        }
        while (t){
            c.push_back(t % 10);
            t /= 10;
        }
        return c;
    }
    
    /*************************************************************************/
    get_primes(a);  // 预处理范围内的所有质数
    for (int i = 0; i < cnt; i ++ ){// 求每个质因数的次数
        int p = primes[i];
        sum[i] = get(a, p) - get(b, p) - get(a - b, p);	// 分子的p次数 减去 分母p的次数
    }
    
    // 剩余的质因子相乘(高精度乘低精度)
    vector<int> res;
    res.push_back(1);
    for (int i = 0; i < cnt; i ++ )     // 用高精度乘法将所有质因子相乘
        for (int j = 0; j < sum[i]; j ++ )
            res = mul(res, primes[i]);
    

    卡特兰数(组合数)

    给定n个0和n个1,它们按照某种顺序排成长度为2n的序列,满足任意前缀中0的个数都不少于1的个数的序列的数量为:Cat(n) = \(C_{2n}^{n} \over n + 1\)

    将01序列置于坐标系中,起点定于原点。若0表示向右走,1表示向上走,那么任何前缀中0的个数不少于1的个数就转化为,路径上的任意一点,横坐标大于等于纵坐标。题目所求即为这样的合法路径数量。
    下图中,表示从(0,0)走到(n, n)的路径,在绿线及以下表示合法,若触碰红线即不合法。

    由图可知,任何一条不合法的路径(如黑色路径),都对应一条从(0,0)走到(n - 1,n + 1)的一条路径(如灰色路径)。而任何一条(0,0)走到(n - 1,n+1)的路径,也对应了一条从(0,0)走到(n,n)的不合法路径。

    结论: 所有(0,0)到(n,n)且不经过红线的路线即为答案,所有经历红线并到达(n,n)的路线数 等价于 所有从(0,0)到(n-1,n+1)路线数,因为(0,0)到(n-1,n+1)一定经历红线

    证明: \(C_{2n}^{n}\) - \(C_{2n}^{n-1}\) = \(C_{2n}^{n} \over n + 1\)
    \(C_{2n}^{n}\) - \(C_{2n}^{n-1}\) = \((2n)! \over n! n!\) - \((2n)! \over (n-1)!(n+1)!\) = \((2n)!(n+1) - (2n)!n\over (n+1)!n!\) = \((2n)! \over (n+1)!n!\) = \(1 \over n+1\)\((2n)!\over n!n!\) = \(C_{2n}^{n} \over n + 1\)

    int a = n * 2, b = n;
    int res = 1;
    // 2n!/(n+1)!n! = 2n*(2n-1)*...*(2n-n+1)/(n+1)!
    for (int i = a; i > a - b; i -- ) res = (LL)res * i % mod;	// 2n*(2n-1)*...*(2n-n+1)
    
    for (int i = 1; i <= b + 1; i ++ ) res = (LL)res * qmi(i, mod - 2, mod) % mod;	// res*((n+1)!的逆元)
    
    cout << res << endl;
    

    容斥原理

    应用: 能被整除的数

    给定一个整数n和m个不同的质数p1,p2,... ,pm,请你求出1~n中能被p1,p2,...,pm中的至少一个数整除的整数有多少个。

    解题思路:

    实现思路:

    // 二进制枚举
    #include 
    using namespace std;
    typedef long long LL;
    
    const int N = 20;
    int p[N], n, m;
    
    int main() {
        cin >> n >> m;
        for(int i = 0; i < m; i ++) cin >> p[i];
    
        int res = 0;
        //枚举从1 到 1111...(m个1)的每一个集合状态, (至少选中一个集合)
        for(int i = 1; i < 1 << m; i ++) {
            int t = 1;             //选中集合对应质数的乘积
            int s = 0;             //选中的集合数量
    
            //枚举当前状态的每一位
            for(int j = 0; j < m; j ++){
                //选中一个集合
                if(i >> j & 1){
                    if((LL)t * p[j] > n){    
                        t = -1;
                        break;//乘积大于n, 则n / t = 0, 跳出这轮循环
                    }
                    s++;            //有一个1, 集合数量+1
                    t *= p[j];
                }
            }
    
            if(t == -1) continue;  
    
            if(s & 1) res += n / t; //选中奇数个集合, 则系数应该是1, n/t为当前这种状态的集合数量
            else res -= n / t;      //反之则为 -1
        }
    
        cout << res << endl;
        return 0;
    }
    

    博弈论-NIM游戏

    经典NIM游戏

    for(int i = 0; i < n; i++) {
        int x;
        scanf("%d", &x);
        res ^= x;
    }
    if(res) puts("Yes");
    else puts("No");
    

    NIM游戏拓展

    题目描述: 现在,有一个n级台阶的楼梯,每级台阶上都有若干个石子,其中第 i 级台阶上有 a 个石子(i ≥ 1)。两位玩家轮流操作,每次操作可以从任意一级台阶上拿若干个石子放到下一级台阶中(不能不拿)。已经拿到地面上的石子不能再拿,最后无法进行操作的人视为失败。问如果两人都采用最优策略,先手是否必胜。

    最优策略:

    1. 把所有奇数台阶看做经典NIM游戏,若是所有奇数台阶异或和为0,则必败,否者先手将奇数台阶拿走若干石子到下一台阶(偶数台阶),把所有奇数台阶的异或和恢复为 0 。
    2. 将经典NIM游戏中的拿走某堆中若干,看做两种情况,① 拿奇数台阶到下一台阶(偶数台阶),就相当于NIM游戏中拿走某堆中若干 ② 拿偶数台阶到下一台阶(奇数台阶),那后手就将拿过去的都拿到下一台阶(偶数),那么奇数台阶又恢复异或为0的状态。
    3. 为什么不用偶数台阶计算?因为最后都落到0号台阶且不能再移动,0号台阶是偶数台阶。
    int f = 0;
    for (int i = 1,x; i <= n; i++){
        cin >> x;
        if(i%2)f^=x;
    }
    if (f)puts("Yes");
    else puts("No");
    

    博弈论-SG函数

    例子: 若干堆石子,每一次只能拿2, 5个,其他规则和NIM游戏相同

    SG函数过程:

    结合代码重点理解Mex运算,以及SG函数如何利用Mex运算

    应用一: 集合-Nim游戏

    给定n堆石子以及一个由k个不同正整数构成的数字集合S。
    现在有两位玩家轮流操作,每次操作可以从任意一堆石子中拿取石子,每次拿取的石子数量必须包含于集合S,最后无法进行操作的人视为失败。
    问如果两人都采用最优策略,先手是否必胜。

    #include 
    #include 
    #include 
    #include 
    
    using namespace std;
    
    const int N=110,M=10010;
    int n,m;
    int f[M],s[N];//s存储的是可供选择的集合,f存储的是所有可能出现过的情况的sg值
    
    int sg(int x){
        if(f[x] != -1) return f[x];// 如果此sg值出现过就不再重复计算
        unordered_set<int> S; // set代表的是有序集合,记录所有子节点的sg值
        for(int i = 0;i < m;i ++){
            int sum = s[i];
            if(x >= sum) S.insert(sg(x - sum));// 当x大于sum是才可以"拿"递归下去
        }
    
        /*************************************重点Mex运算***************************************
        	循环完之后可以进行选出最小的没有出现的自然数的操作,这里就保证了sg值可以像Nim游戏一样,
        	Nim游戏中可以拿任意数量,sg(x)节点可以走到小于它的任何节点,这是一个有向图
        ***************************************************************************************/
        for(int i=0;;i++)
            if(!S.count(i)) return f[x] = i;
    }
    
    int main(){
        cin >> m;
        for (int i = 0;i < m;i ++)
        cin >> s[i];
    
        cin >> n;
        memset(f,-1,sizeof(f));//初始化f均为-1,方便在sg函数中查看x是否被记录过
    
        int res = 0;
        for (int i = 0;i < n; i++){
            int x;
            cin >> x;
            res ^= sg(x);//观察异或值的变化,基本原理与Nim游戏相同
        }
    
        if(res) printf("Yes");
        else printf("No");
    
        return 0;
    }
    

    应用二: 拆分-Nim游戏

    题目描述:

    给定n堆石子,两位玩家轮流操作,每次操作可以拿走其中的一堆石子,然后重新放置两堆规模更小的石子(新堆规模可以为0,且两个新堆的石子总数可以大于取走的那堆石子数),最后无法进行操作的人视为失败。
    问如果两人都采用最优策略,先手是否必胜。

    加黑解释: 新的两堆,不是以原来的石子分的,是重新放的两堆石子,只是要求这两堆每一堆都小于原来那堆的数量。

    题目分析:

    相比于集合-Nim,这里的每一堆可以变成小于原来那堆的任意大小的两堆,即ai可以拆分成(bi, bj),为了避免重复规定bi >= bj
    相当于一个局面拆分成了两个局面,由SG函数理论,多个独立局面的SG值,等于这些局面SG值的异或和。因此需要存储的状态就是sg(bi)^sg(bj) (与集合-Nim的唯一区别)

    int f[N];
    unordered_set<int> S;
    /*****************************为什么可以把 S 定义成全局变量********************************
    	因为这个sg函数的特殊性, 求sg(100)时, 它会将1-100的所有sg(1)-sg(100)都计算出来,
    	当 x <= 100 的都会直接return f[x]; 当x > 100 的会因为sg是递归性质, 因此会按顺序求出sg(101),
    	sg(102),...,sg(x), 所以把S设置成全局变量更好.
    **************************************************************************************/
    
    int sg(int x){
        if(f[x] != -1) return f[x];
    
        for(int i = 0 ; i < x ; i++)
            for(int j = 0 ; j <= i ; j++)//规定j不大于i,避免重复
                //相当于一个局面拆分成了两个局面,由SG函数理论,多个独立局面的SG值,等于这些局面SG值的异或和
                S.insert(sg(i) ^ sg(j));
        for(int i = 0 ; ; i++)
            if(!S.count(i))
                return f[x] = i;
    }
    

    动态规划-模型

    背包问题

    01背包

    每件物品只能选一次,在不超过体积 j 的前提下可以选择的最大价值

    朴素版

    int v[N],w[M];
    int f[N][N];	// 在1-i中选出体积不超过j的最大价值
        
    for (int i = 1; i <= n; i ++){
        for (int j = 0; j <= m; j ++){
            f[i][j] = f[i - 1][j];		// 不选第i个物品: 只从1-i-1中选, 且体积不超过j
            if (j > v[i]) f[i][j] = max(f[i][j],f[i - 1][j - v[i]] + w[i]); // 选第i个物品: f[i - 1][j - v[i]] + w[i]
        }
    }
    cout << f[n][m] << endl;
    

    优化版

    int v[N],w[M];
    int f[N];
    for (int i = 1; i <= n; i ++){
        for (int j = m; j >= v[i]; j --)	// 倒着循环保证f[j-v[i]]是上一轮的数据没有被覆盖
            /*
            	1. 本轮没选第i个物品 f[i - 1][j] == f[j]
            	2. 本轮选第i个物品 f[i - 1][j - v[i]] + w[i] == f[j - v[i]] + w[i]
            	3. 两者取max
            */
            f[j] = max(f[j], f[j - v[i]] + w[i]);
    }
    cout << f[m] << endl;
    

    完全背包

    每件物品可以被选无数个,在不超过体积 j 的前提下可以选择的最大价值

    朴素版

    int v[N],w[M];
    int f[N][N];	// 在1-i中选出体积不超过j的最大价值
    for (int i = 1; i <= n; i ++)
        for (int j = 0; j <= m; j ++)
            for (int k = 0; k * v[i] <= j; k ++)
                f[i][j] = max(f[i][j], f[i - 1][j - k * v[i]] + k * w[i]);
    cout << f[n][m] << endl;
    

    优化版1

    int v[N],w[M];
    int f[N][N];	// 在1-i中选出体积不超过j的最大价值
    for (int i = 1; i <= n; i ++)
        for (int j = 0; j <= m; j ++){
            /*
            	f[i][j] = max{f[i-1][j],f[i-1][j-v]+w,f[i-1][j-2*v]+2*w,f[i-1][j-3*v]+3*w,...}
            	f[i][j-v] = max{        f[i-1][j-v]  ,f[i-1][j-2*v]+  w,f[i-1][j-3*v]+2*w,...}
            	所以 f[i][j] = max{f[i-1][j],f[i][j-v]+w}
            */
            f[i][j] = f[i - 1][j];
            if (j >= v[i]) f[i][j] = max(f[i][j], f[i][j - v[i]] + w[i]);
        }
    cout << f[n][m] << endl;
    

    优化版2

    int v[N],w[M];
    int [N];	// 在1-i中选出体积不超过j的最大价值
    for (int i = 1; i <= n; i ++)
        for (int j = v[i]; j <= m; j ++){	// 和01背包唯一的区别就是循环的顺序
            /*
            	1. f[i][j] = f[i - 1][j]; == f[j] = f[j]
            	2. f[i][j] = max(f[j], f[i][j - v[i]] + w[i]); == f[j] = max(f[j], f[j - v[i]] + w[i])
            	3. 循环不用倒着是因为f[i][j - v[i]]就是需要本层已经更新过的, 因此不用担心覆盖问题
            */
            f[j] = max(f[j], f[j - v[i]] + w[i]);
        }
    cout << f[m] << endl;
    

    多重背包

    每件物品可以被选xi个,在不超过体积 j 的前提下可以选择的最大价值

    朴素版 时间复杂度O(n*m*s)

    int v[N], w[N], s[N];
    int f[N][N];
    for (int i = 1; i <= n; i ++)
        for (int j = 0; j <= m; j ++)
            for (int k = 0; k <= s[i] && k * v[i] <= j; k ++)
                f[i][j] = max(f[i][j], f[i - 1][j - k * v[i]] + k * w[i]);
    cout << f[m] << endl;
    

    为什么不用完全背包优化的方法优化多重背包?

    优化版 时间复杂度O(n*m*log(s)) 利用二进制将多重背包优化成01背包
    例子: x = 200 = 1 + 2 + 4 + 8 + 16 + 32 + 64 + 73,且200以内所有的数都可以用这些数组和表示

    // N = 1000*log(2,2000)
    int n,m,cnt;
    int v[N],w[N];
    int f[N];
    for (int i = 0; i < n; i ++){
        int a, b, s; cin >> a >> b >> s;
        int k = 1; 
        while (k <= s){
            cnt ++;
            v[cnt] = a * k;
            w[cnt] = b * k;
            s -= k;
            k *= 2;
        }
        if (s > 0){
            v[++ cnt] = a * s;
            v[cnt] = b * s;
        }
    }
    for (int i = 1; i <= cnt; i ++)
        for (int j = m; j >= v[i]; j --)
            f[j] = max(f[j], f[j - v[i]] + w[i]);
    cout << f[m] << endl;
    

    分组背包

    每组有多种物品,每种物品只有一个,每组只能选一个,,在不超过体积 j 的前提下可以选择的最大价值

    多重背包是每组选几个,而分组背包是每组选哪个。

    f[i][j] = max{f[i - 1][j], f[i - 1][j - ki] + w[i][ki]} 类似01背包

    int n,m;
    int v[N][N],w[N][N];
    int f[N],s[N];
    for (int i = 1; i <= n; i ++)
        for (int j = m; j > 0; j --)
            for (int k = 1; k <= s[i]; k ++)
                if (v[i][k] <= j) f[j] = max(f[j], f[j - v[i][k]] + w[i][k]);
    cout << f[m] << endl;
    

    PS: 第三重循环和第二重循环是不可以换位子的,因为第二重循环是从m开始的,为了避免覆盖上层,而不能使用上层。如果换位置,f[j] 就会循环s[i]次导致上层数据被覆盖。但是如果是没有进行一维优化的话,用二维i,j,k就可以交换位置了,那样就不会覆盖上层数据。

    线性DP

    数字三角形

    for (int i = n - 1; i >= 1; i --)
        for (int j = 1; j <= i; j ++)
            a[i][j] += max(a[i + 1][j], a[i + 1][j + 1]);
    cout << a[1][1] << endl;
    

    最长上升子序列

    数据范围 1 <= N <= 1000

    int n,a[1010],f[1010],g[1010];	// g[i] 记录 f[i] 是从哪个状态转移过来的, 最后可以倒着推出序列是什么
    for(int i = 1; i <= n; i ++){
        f[i] = 1; 
        g[i] = 0;	// 以i为起点的最长子序列
        for(int j = 1; j < i; j ++)
            if(a[j] < a[i] && f[i] < f[j] + 1){
                f[i] = f[j] + 1;		// 若是a[j] < a[i] 那么以i结尾的最长子序列长度 = f[j] + 1
                g[i] = j;
            }
    }
    int ans = 0;
    for(int i = 1; i <= n; i ++) ans = max(ans, f[i]);
    cout << ans << endl;
    

    数据范围 1 <= N <= 100000

    /************************** DP ----> 贪心 **************************
    	q数组下标: 代表最长子序列长度
    	q数组的值: 记录下标len的子序列最后一个数的最小值
    	因为q数组的定义可知,所以q[len]一定小于q[len+1],因此数组q具有单调递增性质,
    	可以利用二分找到第一个大于a[i]的值, q[i + 1] = a[i]
    *******************************************************************/
    int n,a[N],q[N];
    int len = 0;
    for (int i = 0; i < n; i ++ ){
        int l = 0, r = len;
        while (l < r){
            int mid = l + r + 1 >> 1;
            if (q[mid] < a[i]) l = mid;
            else r = mid - 1;
        }
        len = max(len, r + 1);
        q[r + 1] = a[i];
    }
    printf("%d\n", len);
    

    最长公共子序列

    闫式DP分析

    • 状态表示f[i, j]
      ① 集合: 所有在第一个序列的前i个字母出现,且在第二个序列的前j个字母出现的子序列
      ② 属性: Max
    • 状态计算: f[i, j] 分为4种状态 00(i不选, j不选),01(i不选, j选),10(i选, j不选),11(i选, j选)
      00: 这个状态好表示 f[i, j] = f[i - 1, j - 1]
      01: 这个状态表示为 f[i, j] = f[i - 1, j]
      10: 这个状态通过为 f[i, j] = f[i, j - 1]
      11: 这个状态好表示 f[i, j] = f[i - 1, j - 1] + 1
    • 通过对f[i, j]的定义,可以发现f[i - 1, j - 1] 这种状态属于 f[i - 1, j] 和 f[i, j - 1] 这两种状态中。
    int n, m;
    char a[N], b[N];
    int f[N][N];
    scanf("%s%s", a + 1, b + 1);
    for (int i = 1; i <= n; i ++)	// 双重循环--二维dp
        for (int j = 1; j <= m; j ++){
            f[i][j] = max(f[i - 1][j], f[i][j - 1]);
            if (a[i] == b[j]) f[i][j] = max(f[i][j], f[i - 1][j - 1] + 1);
        }
    cout << f[n][m] << endl;
    /*
    acbd
    abedc
    */
    

    最短编辑距离

    题意: 两个字符串a, b,有三种操作: 增,删,改。问最少的操作次数使得字符串a变成b

    • 状态表示 f[i, j]
      1. 集合: 所有将a[1~i]变成b[1~j]的操作方式
      2. 属性: Min
    • 状态计算
      1. 删除: f[i - 1, j] + 1 要保证a[0,i - 1]b[0,j] 相等的条件下
      2. 增加: f[i, j - 1] + 1 要保证a[0,i]b[0,j - 1] 相等的条件下
      3. 修改: f[i - 1, j - 1] + 1不同 or 0相同 要保证 a[0,i - 1]b[0,j - 1] 相等的条件下
    int f[N][N];
    char a[N],b[N];
    scanf("%s%s", a + 1, b + 1);
    // 初始化
    for(int i = 0; i <= m; i ++)f[0][i] = i;	// a[0,0] 到 b[0,i] 需要添加操作i次
    for(int i = 0; i <= n; i ++)f[i][0] = i;	// a[0,i] 到 b[0,0] 需要删除操作i次
    
    for(int i = 1; i <= n; i ++){
        for(int j = 1; j <= m; j ++){
            if(a[i] != b[j]) f[i][j] = f[i - 1][j - 1] + 1; // a[i] == b[i] 修改 +1
            else f[i][j] = f[i - 1][j - 1];	// 修改
            f[i][j] = min(f[i][j], min(f[i - 1][j] + 1,f[i][j - 1] + 1));	// 比较三种情况选出最小值
        }
    }
    cout << f[n][m] << endl;
    

    区间DP-石子合并

    • 题意: 合并 N 堆石子,每次只能合并相邻的两堆石子,求最小代价

    • 解题思路:

      关键点: 最后一次合并一定是左边连续区间和右边连续区间进行合并

      1. 状态表示: f[i][j] 表示将 i 到 j 这一区间的石子合并成一个区间的集合,属性时Min

      2. 状态计算:

        f[i][j] = min{f[i][ki] + f[ki + 1][j] + s[j] - s[i - 1]} (i ≤ ki ≤ j - 1) 至少 ki[i, j]分成两个区间

    int s[N];
    int f[N][N];
    for (int i = 1; i <= n; i ++ ) s[i] += s[i - 1];  // 前缀和
    for (int len = 2; len <= n; len ++ )	// 枚举区间长度
        for (int i = 1; i + len - 1 <= n; i ++ ){
            int l = i, r = i + len - 1;
            f[l][r] = 1e8;
            for (int k = l; k < r; k ++ )	//k ∈ [l, r - 1]
                f[l][r] = min(f[l][r], f[l][k] + f[k + 1][r] + s[r] - s[l - 1]);
        }
    printf("%d\n", f[1][n]);
    

    记忆化搜索做法

    int dp(int i, int j) {
        if (i == j) return 0; // 判断边界
        int &v = f[i][j];
    
        if (v != -1) return v;// 减枝避免重复计算,因为下面循环会出现区间重叠
    
        v = 1e8;
        for (int k = i; k < j; k ++)
            v = min(v, dp(i, k) + dp(k + 1, j) + s[j] - s[i - 1]);
    
        return v;
    }
    memset(f, -1, sizeof f);
    cout << dp(1, n) << endl;
    

    区间DP常用模板

    for (int len = 1; len <= n; len ++) {         // 区间长度
        for (int i = 1; i + len - 1 <= n; i ++) { // 枚举起点
            int j = i + len - 1;                 // 区间终点
            if (len == 1) {
                dp[i][j] = 初始值
                continue;
            }
    
            for (int k = i; k < j; k ++) {        // 枚举分割点,构造状态转移方程
                dp[i][j] = min(dp[i][j], dp[i][k] + dp[k + 1][j] + w[i][j]);
            }
        }
    }
    

    计数类DP-整数划分

    问题描述: 一个正整数n可以表示成若干个正整数之和,形如:n = n1 + n2 + … + nk,其中 n1 ≥ n2 ≥ … ≥ nk, k≥1,我们将这样的一种表示称为正整数 n 的一种划分,现在给定一个正整数n,请你求出n共有多少种不同的划分方法

    方法一

    /***************************利用完全背包的推理***********************
    f[i][j]: 表示前i个整数(1,2…,i)恰好拼成j的方案数
    f[i][j] = f[i-1][j]+f[i-1][j-i]+f[i-1][j-i*2]...f[i-1][j-i*s]	i*s <= j < i*(s+1)
    f[i][j-i] = 		f[i-1][j-i]+f[i-1][j-i*2]...f[i-1][j-i*s]
    得出转移方程 f[i][j] = f[i-1][j]+f[i][j-i]
    优化维度	f[j] = f[j]+f[j-i]
    ******************************************************************/
    int f[N];
    f[0] = 1;  //总和为0的方案数,也就是f[i][0]前i个整数(1,2…,i)恰好拼成0的方案数,只有一种就是一个都不选
    for (int i = 1; i <= n; i ++)
        for (int j = i; j <= n; j ++)
            f[j] = (f[j] + f[j - i]) % mod;
    cout << f[n] << endl;
    

    方法二

    /*********************计数DP****************************
    f[i][j]表示和为i,恰好选j个数的方案数
    划分为两种情况
    1.最小值为1 那把为1的情况去掉 就是f[i-1][j-1]这种情况的方案数
    2.最小值大于1 那把i个数都减去1 就是f[i-j][j] 这个情况的方案数
    转移方程: f[i][j] = f[i-1][j-1] + f[i-j][j]
    		ans = f[n][1] + f[n][2] + ... + f[n][n]
    *******************************************************/
    int f[N][N];
    f[1][1] = 1;	//初始化源头
    for (int i = 2; i <= n; i ++)
        for (int j = 1; j <= i; j ++)
            f[i][j] = (f[i - 1][j - 1] + f[i - j][j]) % mod;
    int res = 0;
    for (int i = 1; i <= n; i++)res = (res + f[n][i]) % mod;	//枚举每种情况相加
    cout << res << endl;
    

    数位统计DP-计数问题

    题目描述:

    给定两个整数 a 和 b,求 a 和 b 之间的所有数字中0~9的出现次数。
    例如,a=1024,b = 1032,则a和b之间共有9个数如下:
    1024 1025 1026 1027 1028 1029 1030 1031 1032
    其中0出现10次,1出现10次,2出现7次,3出现3次等等...

    算法思想: 前缀和,数位dp

    例如 n = abcdefg 求 0 ~ n 中x出现的次数,记作count(n, x),核心思想是计算x在abcdefg上每一位出现的次数之和

    计算x在数字'd'这个位置出现的次数

    1. ① 'abc'位置是[000, abc - 1] 此时ans += abc*10^3

      ② 当x = 0时要特判,因为多算了000x这种情况,所以ans -= 10^3

    2. 'abc'位置是abc时

      • ③ d < x 时,那么abcxefg就大于abcdefg,此时不符合条件不计入ans
      • ④ d = x 时,那么efg就是所求x在数字d所在位置的次数 ans += efg+1 (000 ~ efg)
      • ⑤ d > x 时,那么efg所在的位置可以填任何数字, ans += 1000 (000 ~ 999)

    最后将x在n的每一位上计算的次数相加,就是0~n中x出现的次数

    所以求 a~b之间的x出现的次数,利用前缀和原理,即等于求0~b出现x的次数减去0~a-1出现x的次数: ans = count(b, x) - count(a - 1, x)

    int get(vector<int> num, int l, int r){	// 计算num[l],num[l+1],...,num[r]十进制数
        int res = 0;
        for (int i = l; i >= r; i --) res = res * 10 + num[i];
        return res;
    }
    int power10(int x){						// 计算10^x
        int res = 1;
        while (x -- ) res *= 10;
        return res;
    }
    int count(int n, int x){				// 计算0~n中x出现的次数
        if (!n) return 0;
        vector<int> num;					// 把n的每一位拆分放进num数组中
        while (n){
            num.push_back(n % 10);
            n /= 10;
        }
        n = num.size();
        int res = 0;	
        for (int i = n - 1 - !x; i >= 0; i --){
            if (i < n - 1){	// 计算i的前缀是0 ~ (abc-1)
                res += get(num, n - 1, i + 1) * power10(i);	//① 0~(abc-1)数量等于abc   res+="前缀数量"*power10(i)
                if (!x) res -= power10(i);			//② 如果x是0, 那么就会多数一种情况000xefg, 即多加一个 power10(i)
            }
            if (num[i] == x) res += get(num, i - 1, 0) + 1;	//④ 前缀是abc且d = x
            else if (num[i] > x) res += power10(i);			//⑤ 前缀是abc且d > x
        }
        return res;
    }
    int main(){
        int a, b;
        while (cin >> a >> b , a){
            if (a > b) swap(a, b);
            for (int i = 0; i <= 9; i ++)
                cout << count(b, i) - count(a - 1, i) << ' ';
            cout << endl;
        }
        return 0;
    }
    

    状态压缩DP

    蒙德里安的猜想-DP

    题意: n x m的棋盘可以摆放不同的1 × 2小方格的种类数。

    题目分析:

    • 摆放方块的时候,先放横着的,再放竖着的。总方案数等于只放横着的小方块的合法方案数。
    • 如何判断,当前方案数是否合法? 所有剩余位置能否填充满竖着的小方块。可以按列来看,每一列内部所有连续的空着的小方块需要是偶数个。
    • 这是一道动态规划的题目,并且是一道状态压缩的dp: 用一个N位的二进制数,每一位表示一个物品,0/1表示不同的状态。因此可以用0 →2N -1中的所有数来枚举全部的状态。

    状态表示: f[i][j]表示已经将前i-1列摆好,且从第i-1列,伸出到第i列的状态是j的所有方案。其中j是一个二进制数,用来表示第i-1列转化成第i列的状态(j对应二进制中的1表示从i-1列横着放一个方块,0表示从i-1类到i列没变化),其位数和棋盘的行数一致。

    状态转移: f[i][j] += f[i - 1][ki] (0 ≤ ki ≤ 2n-1) 表示第i列的状态j的方案数等于所有符合条件的第i-1列的状态ki之和。其中状态ki 表示第i-2列转化到第i-1列的状态,状态j表示第i-1列转化到第i列的状态

    typedef long long LL;
    const int N = 12, M = 1 << N;
    int n, m;
    LL f[N][M];// 第一维表示列, 第二维表示所有可能的状态
    vector<int> state[M];
    bool st[M];//存储每种状态是否有奇数个连续的0, 如果奇数个0是无效状态, 如果是偶数个零置为true
    
    int main(){
        while (cin >> n >> m, n || m){
            // 预处理(一): 预处理出[0,1<
            for (int i = 0; i < 1 << n; i ++ ){
                int cnt = 0;
                bool is_valid = true;
                for (int j = 0; j < n; j ++ )
                    if (i >> j & 1){
                        if (cnt & 1){
                            is_valid = false;
                            break;
                        }
                        cnt = 0; // 这一步可以不写, 因为上面if不满足的话, cnt一定是偶数
                    }
                    else cnt ++ ;
                if (cnt & 1) is_valid = false;
                st[i] = is_valid;
            }
    		// 预处理(二): 预处理出f[k-1,i]到f[k,j]状态转移的所有合法方案, 此时属于减少不必要的枚举
            for (int i = 0; i < 1 << n; i ++ ){
                state[i].clear();
                for (int j = 0; j < 1 << n; j ++ )
                    /*
                    	i & j == 1 说明i和j的n位上有同时为1的情况, 这是不允许的, 若是i的某位为1,
                    	说明在那个位置有从i的前一种状态伸出, 那么此时就不能在这个位置填一个块伸出到j对应位置
                    	
                    	st[i|j]==true 标明在i转换成j状态后,i中剩余连续的0是否符合偶数,因为剩下的0要填竖着的方块
                    	例如i='10101' j='01000' i|j=='11101' 这个就是不符合条件的, 即i不能转化为j, 排除
                    */
                    if ((i & j) == 0 && st[i | j])
                        state[i].push_back(j);
            }
            memset(f, 0, sizeof f);
            f[0][0] = 1;
            /*******************************为什么f[0][0] = 1**********************************
    			1. 题目中输入参数的列数是从1开始到m,即范围为1~m,但我们写的时候是将其先映射到数组0~m-1里
    			2. 对于第一列,也就是数组中的第0列,是需要初始化的;也就是我们需要初始化f[0][x] = ?回到定义,
    			f[0][x] 表示从-1列伸到0列(此处说的都是数组下标)状态为x的方案。
    			3. 我们发现,合法的方案只能是不伸过来,因为根本没有-1列。即x只能取0的时候方案合法,f[0][0] = 1;
    			接着dp过程就从第1列(数组下标)开始。
    			4. 那么答案为什么是f[m][0] 呢,因为横放的时候方块最多够到第m-1列(数组下标),不能从m-1再往外伸,
    			所以是f[m][0];
            **********************************************************************************/
            for (int i = 1; i <= m; i ++ )
                for (int j = 0; j < 1 << n; j ++ )
                    for (auto k : state[j])
                        f[i][j] += f[i - 1][k];	// 枚举所有符合从i-1的k状态且能成功转化i的j状态, 并累加
    
            cout << f[m][0] << endl;
        }
        return 0;
    }
    

    蒙德里安的猜想-记忆化搜索

    定义状态: dp[i][j]表示前i - 1列的方格都已完全覆盖,第i列方格被第i - 1列伸出的方块覆盖后状态为j的所有方案数。

    例如,上图表示的就是dp[3][010010]的状态(红色为2 * 1方块,绿色为1 * 2方块) 0 表示没有覆盖,1 表示覆盖。

    状态转移:

    我们采用由底至上的递推方式,即由当前状态推出下一列状态的方案数。

    以某一列的状态而言

    1. 情况一】如果当前行的格子已被上一列伸出的方块覆盖,则跳过
    2. 情况二】如果当前行的格子未被覆盖,说明可以放一个1 * 2的方块
    3. 情况三】如果当前行的格子和下一行的格子都未被覆盖,说明可以放一个2 * 1的方块
    4. 总结】此列所有行的格子都覆盖完后,我们便可以得出下一列的合法状态

    如上图,我们对第3列的状态进行搜索后可到达的其中一种状态

    为什么要搜索?

    根据dp数组的定义可知,第一列不可能被上一列伸出的方块覆盖,所以初始化为dp[1][000] = 1,搜索下一列可得:

    可知第二列可到达的状态只有3种,于是进行第三列的搜索时只需从这3种状态开始dfs,当前阶段总是影响下一阶段,我们只对可到达的进行讨论,并不需要枚举每一种情况。

    时间复杂度:

    1. 外层循环时间: m * (1<
    2. 递归时间: 最坏情况是一个不满的二叉树 20+21+22+...+210 = (211 - 1)
    3. 总时间 = 外层循环时间*递归时间 ≈ 10 * 211 * 211 = 46137344 ≈ 4e7
    int n, m;
    long long dp[12][2500];
    void dfs(int row, int col, int state, int next) {
        //row为当前行, col为当前列, state为当前列的状态, next为可到达的下一列的状态
        //当前列全覆盖后可到达的下一个状态加上当前状态的方案数
        if (row == n) {
            //当前列所有行都已覆盖完毕
            dp[col + 1][next] += dp[col][state];
            return;
        }
        //情况一: 如果当前行(state二进制中第row位等于1)的格子已被覆盖,跳过
        if (state & (1 << row)) dfs(row + 1, col, state, next);
        else {
            //当前行未被覆盖,可放一个1*2的方块
            dfs(row + 1, col, state, next | (1 << row));// 情况二
            //当前行和下一行都未被覆盖,可放一个2*1的方块
            if (row + 1 < n && (state & (1 << (row + 1))) == 0) dfs(row + 2, col, state, next);// 情况三
        }
    }
    int main() {
        while (scanf("%d%d", &n, &m) && n && m) {
            if (n > m) swap(n, m);
            //因为n行m列和n列m行的方案数等价, 所以我们不妨将min(n, m)作为二进制枚举的指数, 减少方案数
            memset(dp, 0, sizeof(dp));
            dp[0][0] = 1;
            for (int i = 0; i < m; i++) {
                for (int j = 0; j < (1 << n); j ++) {
                    if (dp[i][j] > 0) {     //筛选出之前搜索过可到达的状态
                        dfs(0, i, j, 0);
                    }
                }
            }
            //因为下标从0开始,所以dp[m][0]表示第m + 1列没有任何第m列的方块伸出的方案数
            cout << dp[m][0] << endl;
        }
        return 0;
    }
    

    最短Hamilton路径

    题目描述: 给定一张n个点的带权无向图,点从0 ~ n-1标号,求起点0到终点n-1的最短Hamilton路径。Hamilton路径的定义是从0到n-1不重不漏地经过每个点恰好一次。

    状态表示: f[i][j]

    • 集合: 所有从0走到j,走过的所有点是i的所有路径
    • 属性: Min

    状态计算: 0→...→k→j f[ i ][ j ] = min(f[ i ][ j ], f[ i - (1 << j) ][ ki ] + w[ ki ][ j ])

    int f[M][N],w[N][N];
    int main(){
        cin >> n;
        for (int i = 0; i < n; i ++)
            for (int j = 0; j < n; j ++)
                cin >> w[i][j];
        memset(f, 0x3f, sizeof f);	// 初始化费用最大值
        f[1][0] = 0;				// 0 到 0 路径只有 0 的费用是 0
        //for (int i = 0; i < 1 << n; i ++)
        for (int i = 1; i < 1 << n; i += 2)	//优化: 0001 + 10 = 0011 因为第0位始终只有是1才是合法的, 所以+2是符合条件的
            for (int j = 0; j < n; j ++)
                if (i >> j & 1)		// i的第j位二进制是否为1
                    for (int k = 0; k < n; k ++)  // 节点j的前一个路径节点k
                        if(i - (1 << j) >> k & 1)	// i - 1 << j 的第k位是否为1
                           f[i][j] = min(f[i][j], f[i - (1 << j)][k] + w[k][j]);
        cout << f[(1 << n) - 1][n - 1] << endl;	// f[11...11][n - 1]是答案
        return 0;
    }
    

    树形DP-没有上司的舞会

    题目描述: Ural大学有N名职员,编号为1~N。他们的关系就像─棵以校长为根的树,父节点就是子节点的直接上司。每个职员有一个快乐指数,用整数Hi给出,其中1≤i≤N。现在要召开一场周年庆宴会,不过,没有职员愿意和直接上司一起参会。在满足这个条件的前提下,主办方希望邀请一部分职员参会,使得所有参会职员的快乐指数总和最大,求这个最大值。

    状态表示: f[u][0],f[u][1]

    • 集合: f[u][0]表示以u为根节点且u不参加的快乐指数最大值,f[u][1]表示以u为根节点且u参加的快乐指数最大值
    • 属性: Max

    状态计算: f[u][0] += max(f[ui][0], f[ui][1]),f[u][1] += f[ui][0]

    最后的结果ans = max(f[u, 0], f[u, 1]])

    void dfs(int u){
        f[u][0] = 0;       // 不加当前结点
        f[u][1] = a[u];    // 加上当前结点
        for(int i = h[u]; i != -1; i = ne[i]){
            int j = e[i];
            dfs(j);       // 一直递归到最深处
            f[u][0] += max(f[j][0], f[j][1]); // 不加当前结点,那么他的子结点就可以选或者不选
            f[u][1] += f[j][0];                     // 加上当前结点,那么他的子结点只能不选
        }
    }
    for(int i = 1; i <= n; i++) if(!ru[i]) root = i; // 找出根节点
    dfs(root);
    printf("%d\n", max(f[root][0], f[root][1]));
    
    

    记忆化搜索

    题目描述: 一张n*m的图,图上每一个点都有一个高度,a点走到b点的要求是a点高度要大于b点高度,求某个点可以走的最大步数。

    5 5
    1 2 3 4 5
    16 17 18 19 6
    15 24 25 20 7
    14 23 22 21 8
    13 12 11 10 9

    如上图最大的步数是从25走,螺旋路线,最远走到1,一共25步

    int n, m;
    int g[N][N],f[N][N];
    int dx[4] = {-1, 0, 1, 0}, dy[4] = {0, 1, 0, -1};
    
    int dp(int x, int y){
        int &v = f[x][y];
        if (v != -1) return v;
    
        v = 1;	// 最次也可以走一步
        for (int i = 0; i < 4; i ++ ){
            int a = x + dx[i], b = y + dy[i];
            if (a >= 1 && a <= n && b >= 1 && b <= m && g[x][y] > g[a][b])
                v = max(v, dp(a, b) + 1);
        }
        return v;
    }
    memset(f, -1, sizeof f);
    int res = 0;
    for (int i = 1; i <= n; i ++ )
        for (int j = 1; j <= m; j ++ )
            res = max(res, dp(i, j));
    printf("%d\n", res);
    

    贪心-思想

    区间问题

    区间选点

    题目描述: 给定N个闭区间[ai,bi],请你在数轴上选择尽量少的点,使得每个区间内至少包含一个选出的点。输出选择的点的最小数量。位于区间端点上的点也算作区间内。

    struct Range{
        int l, r;
        bool operator< (const Range &W)const{
            return r < W.r;
        }
    }range[N];
    sort(range, range + n);
    int res = 0, ed = -2e9;
    for (int i = 0; i < n; i ++ )
        if (range[i].l > ed){
            res ++ ;
            ed = range[i].r;
        }
    
    printf("%d\n", res);
    

    最大不相交区间数量

    题目描述: 给定N个闭区间[ai,bi],请你在数轴上选择若干区间,使得选中的区间之间互不相交(包括端点)。输出可选取区间的最大数量。

    计算方法和区间选点一模一样。

    struct Range{
        int l, r;
        bool operator< (const Range &W)const{
            return r < W.r;
        }
    }range[N];
    sort(range, range + n);
    int res = 0, ed = -2e9;
    for (int i = 0; i < n; i ++ )
        if (range[i].l > ed){
            res ++ ;
            ed = range[i].r;
        }
    
    printf("%d\n", res);
    

    区间分组

    题目描述: 给定N个闭区间[ai,bi],请你将这些区间分成若干组,使得每组内部的区间两两之间(包括端点)没有交集,并使得组数尽可能小。输出最小组数。

    思路:

    1. 将所有区间按左端点从小到大排序
    2. 从前往后处理每个区间,判断能否将其放到某个现有的组中L[i] > Max_r
      1. 如果不存在这样的组,则开新组,然后再将其放进去;
      2. 如果存在这样的组,将其放进去,并更新当前组的Max_r
    sort(range, range + n);
    priority_queue<int, vector<int>, greater<int>> heap;// 小根堆里存在每个组右端点
    for (int i = 0; i < n; i ++ ){
        auto r = range[i];
        if (heap.empty() || heap.top() >= r.l) heap.push(r.r);// 最小的右端点都大于r.l那就需要新开一个组
        else{		// 否者就把这个组加入右端点最小的那个组, 并且更新
            heap.pop();
            heap.push(r.r);
        }
    }
    printf("%d\n", heap.size());
    

    区间覆盖

    题目描述: 给定N个闭区间[ai, bi]以及一个线段区间[s, t],请你选择尽量少的区间,将指定线段区间完全覆盖。输出最少区间数,如果无法完全覆盖则输出 -1

    核心思想: 在左端点l都小于等于st的情况下, 取右端点最大的小区间

    1. 将所有区间按照左端点从小到大进行排序
    2. 从前往后枚举每个区间,在所有能覆盖start的区间中,选择右端点的最大区间,然后将start更新成右端点的最大值 这—步用到了贪心决策
    int n;
    int st, ed;
    struct Range{
        int l, r;
        bool operator< (const Range &W)const{
            return l < W.l;
        }
    }range[N];
    
    sort(range, range + n);
    
    int res = 0;
    bool success = false;
    int i = 0;
    while (i < n){
        int r = -2e9;
        /*********************************************************************************
            int r = -2e9 不能放在外面
            例如: 
            4 10
            2
            4 5
            11 12  这个样例不会执行里面的while,i 不会 ++, 且if (r < st) 永远不会执行, 就会陷入死循环
        **********************************************************************************/
        while (i < n && range[i].l <= st){	//在左端点l都小于等于st的情况下, 取右端点最大的小区间
            r = max(r, range[i].r);
            i ++ ;
        }
    
        if (r < st){	// 若 r < st 即说明while循环结束条件是 i < n, 所以说明所有的区间都不在[st, ed]里面
            res = -1;
            break;
        }
    
        res ++ ;		// 成功找到合适的一个区间预设res ++
        if (r >= ed){	// 若 r >= ed 说明已经找到一个合适的区间, 此时退出, 贪心停止
            success = true;
            break;
        }
    
        st = r;			// st值设定成当前寻找的符合条件的右端点
    }
    
    if (!success) res = -1;
    printf("%d\n", res);
    

    Huffman树-合并果子

    priority_queue<int, vector<int>, greater<int>> heap;
    while (n --){
        int x;
        scanf("%d", &x);
        heap.push(x);
    }
    
    int res = 0;
    while (heap.size() > 1){
        int a = heap.top(); heap.pop();
        int b = heap.top(); heap.pop();
        res += a + b;
        heap.push(a + b);
    }
    
    printf("%d\n", res);
    

    排序不等式-排队打水

    题目描述: 有n 个人排队到1个水龙头处打水,第i个人装满水桶所需的时间是t,请问如何安排他们的打水顺序才能使所有人的等待时间之和最小?

    sort(t, t + n);
    reverse(t, t + n);
    LL res = 0;
    for (int i = 0; i < n; i ++ ) res += t[i] * i;
    printf("%lld\n", res);
    

    绝对值不等式-货仓选址

    题目描述: 在—条数轴上有N家商店,它们的坐标分别为A1~ AN。现在需要在数轴上建立一家货仓,每天清晨,从货仓到每家商店都要运送一车商品。为了提高效率,求把货仓建在何处,可以使得货仓到每家商店的距离之和最小。

    sort(a,a + t);
    int ans = 0;
    for(int i = 0; i < t; i ++) 
        /*
        	1. 当n为奇数时, 站点放在中位数a[t/2]时ans最小
        	2. 当n为偶数时, 站点放在范围为a[(t-1)/2]~a[t/2]中任意位置都行,设[a,b]中有一个x,即|a - x| + |b - x| = b - a
        	3. 所以无论n为奇数还是偶数, ans都是最小
        */
        ans += abs(a[i] - a[t/2]);
    cout << ans << endl;
    /*
    1 2 3 4 5 6
    4 - 1 + 4 - 2 + 4 - 3 = 6
    */
    

    推公式-耍杂技的牛

    题目描述: 农民约翰N头奶牛(编号为1..N)计划逃跑并加入马戏团,为此它们决定练习表演杂技。奶牛们不是非常有创意,只提出了一个杂技表演:
    叠罗汉,表演时,奶牛们站在彼此的身上,形成一个高高的垂直堆叠。奶牛们正在试图找到自己在这个堆叠中应该所处的位置顺序。
    这N头奶牛中的每一头都有着自己的重量Wi以及自己的强壮程度Si。一头牛支撑不住的可能性取决于它头上所有牛的总重量(不包括它自己)减去它的身体强壮程度的值,现在称该数值为风险值,风险值越大,这只牛撑不住的可能性越高。您的任务是确定奶牛的排序,使得所有奶牛的风险值中的最大值尽可能的小。

    贪心思路: 按照wi+si从小到大的顺序排,最大的危险系数一定是最小的。

    typedef pair<int, int> PII;
    const int N = 50010;
    int n;
    PII cow[N];
    int main(){
        scanf("%d", &n);
        for (int i = 0; i < n; i ++ ){
            int s, w;
            scanf("%d%d", &w, &s);
            cow[i] = {w + s, w};
        }
        sort(cow, cow + n);
        int res = -2e9, sum = 0;
        for (int i = 0; i < n; i ++ ){
            int s = cow[i].first - cow[i].second, w = cow[i].second;
            res = max(res, sum - s);
            sum += w;
        }
        printf("%d\n", res);
        return 0;
    }
    
  • 相关阅读:
    好莱坞罢工事件!再次警醒人类重视AI监管,人工智能矛盾一触即发!
    强化学习实践(二)Gym(安装、环境搭建、运行倒立摆(代码可运行))
    Python 读取 Word 详解(python-docx)
    如何理解“构造函数是类公共标识,但原型是唯一的标识“
    HDFS编程实践-从HDFS中下载指定文件到本地
    【LeetCode】挑战100天 Day11(热题+面试经典150题)
    人工智能第五讲笔记
    左程云老师算法课笔记(一)
    H3C S7000/S7500E/10500系列交换机Console密码忘记处理方法
    工程师常用的6种最佳实践
  • 原文地址:https://www.cnblogs.com/sxy666666/p/17137006.html