0
点赞
收藏
分享

微信扫一扫

第一章 前缀和与差分

扶摇_hyber 2022-02-10 阅读 27

文章目录


数据结构相关知识点

在这里插入图片描述

第一章 前缀和与差分

笔记

  • 前缀和sum[i] = sum[i - 1] + a[i],故 a[l] + ... + a[r] = sum[r] - sum[l - 1]

    • 适用
      1. 乘法:例题A:智乃酱的区间乘积(注意:除法要用逆元)
      2. 其他操作:连续若干操作产生的叠加影响可通过某种反向操作”撤销“
        • 整体转移:例题F:牛牛的猜球游戏
        • 满秩矩阵的链乘法:例题E:智乃酱的双塔问题
        • 卷积
  • 差分d[i] = a[i] - a[i - 1],故 a[i] = a[i - 1] + d[i]

    • 差分数组:操作 d l + x , d r + 1 − x d_l+x,d_{r+1}-x dl+x,dr+1x,前缀和后原数组在 l ∼ r l\sim r lr + x +x +x
      • 变形:加多项式

        前提:最高次项为 n n n n n n 阶多项式做 n + 1 n+1 n+1 阶差分后余项为常数

  • 高阶前缀和:组合数贡献

  • 二维前缀和sum[i][j] = sum[i - 1][j] + sum[i][j - 1] - sum[i - 1][j - 1] + a[i][j]

    • 高维(k维)前缀和 SOSDP (Sum over Subsets):子集前缀和,例题B:智乃酱的子集与超集

      1. 容斥 O ( 2 k n ) O(2^kn) O(2kn)

        sum[i][j] = sum[i - 1][j] + sum[i][j - 1] - sum[i - 1][j - 1] + a[i][j]
        
      2. SOSDP O ( ( k + 1 ) n ) O((k+1)n) O((k+1)n)

        rep(i, 1, n): sum[i][j] = a[i][j];
        rep(i, 1, n): sum[i][j] += sum[i - 1][j];
        rep(i, 1, n): sum[i][j] += sum[i][j - 1];
        
  • 总结:
    在这里插入图片描述

习题

地址:第一章习题

A. 智乃酱的区间乘积

  • 题目链接:智乃酱的区间乘积

  • 思路:求区间连续乘积,注意 − p r e m u l [ l − 1 ] -premul[l - 1] premul[l1] 时要用逆元

    #include<bits/stdc++.h>
    using namespace std;
    #define rep(i, a, b) for(int i = (a); i <= (b); i++)
    #define per(i, a, b) for(int i = (a); i >= (b); i--)
    #define ll long long
    #define db double
    #define VI vector<int>
    #define PII pair<int, int>
    const db Pi = 3.141592653589793;
    const int INF = 0x7fffffff;
    const int N = 1e5 + 5;
    const db eps = 1e-10;
    const int mod = 1e9 + 7;
    int cas, n, m, l, r;
    ll a[N], premul[N];
    ll inv(ll i){
        if(i == 1) return 1;
        return (mod - mod / i) * inv(mod % i) % mod;
    }
    int main(){
        cin >> n >> m;
        premul[0] = 1;
        rep(i, 1, n){
            cin >> a[i];
            premul[i] = (premul[i - 1] * a[i]) % mod;
        }
        while(m--){
            cin >> l >> r;
            cout << (premul[r] * inv(premul[l - 1])) % mod << endl;
        }
    }
    /*
    5 3
    5 2 3 10 6
    1 5
    2 3
    2 5
    */
    

B. 智乃酱的子集与超集

  • 题目链接:智乃酱的子集与超集

  • 类型:高阶前缀和

  • 思路:一共 20 20 20 个物品,显然需要状态压缩来表示。我们考虑每个组成集合的异或和,再处理出每个集合的子集,超集的异或和即可。

    • 对于一共 2 20 2^{20} 220 个集合,逐一计算出其集合价值,即异或和
    • 利用高阶前缀和求解(SOSDP做法):对于每个集合的子集,从低位到高位依次考虑每个集合的由来。通过对当前位已有物品的忽略,由缺少状态来计算当前状态。
    • 超集同理
    • 询问时,由物品添加为所求集合即可
    #include<bits/stdc++.h>
    using namespace std;
    #define rep(i, a, b) for(int i = (a); i <= (b); i++)
    #define per(i, a, b) for(int i = (a); i >= (b); i--)
    #define ll long long
    #define db double
    #define VI vector<int>
    #define PII pair<int, int>
    const db Pi = 3.141592653589793;
    const int INF = 0x7fffffff;
    const int N = 21;
    const db eps = 1e-10;
    int n, m, k, maxbit, p;
    ll a[N], Xor[1 << N], pre_sum[1 << N], suf_sum[1 << N];
    void init(){  //预处理
        maxbit = (1 << n) - 1;
        //状压计算各集合异或和结果
        rep(bit, 0, maxbit){
            rep(i, 0, n - 1){
                if(bit & (1 << i)) Xor[bit] ^= a[i];
            }
            pre_sum[bit] = suf_sum[bit] = Xor[bit];
        }
        rep(i, 0, n - 1){
            rep(bit, 0, maxbit){
                //二进制位为1,表示要选
                if(bit & (1 << i)) pre_sum[bit] += pre_sum[bit ^ (1 << i)];
                //二进制位为0,表示不选
                else suf_sum[bit] += suf_sum[bit ^ (1 << i)];
            }
        }
    }
    int main(){
        ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
        cin >> n >> m;
        rep(i, 0, n - 1) cin >> a[i];
        init();
        while(m--){
            cin >> k;
            int nowbit = 0;
            rep(i, 1, k){
                cin >> p;
                nowbit += (1 << (p - 1));
            }
            cout << pre_sum[nowbit] << " " << suf_sum[nowbit] << endl;
        }
    }
    /*
    3 5
    1 5 9
    1 1
    1 2
    2 1 2
    3 1 2 3
    0
    */
    

C. 智乃酱的前缀和与差分

  • 题目链接:智乃酱的前缀和与差分

  • 思路

D. 智乃酱的静态数组维护问题多项式

  • 题目链接:智乃酱的静态数组维护问题多项式

  • 思路

    • 结论 k k k 次多项式做 k + 1 k+1 k+1 次差分后为余项为常数
    • 由于根据差分和前缀和均可以完全还原出原序列,则为了将所有多项式降为常数,我们可对所有序列进行 k + 1 k+1 k+1 次差分。(本题多项式最高 5 5 5 次,故 k + 1 = 5 + 1 = 6 k+1=5+1=6 k+1=5+1=6
    • 注意初始原序列进行 6 6 6 次差分
    • 计算多项式函数,和 r r r 后对应相减函数,进行 6 6 6 次差分
    • 统一加齐后,进行 6 6 6 次前缀和便可还原结果序列
    • 求区间和可再次前缀和处理。
    #include<bits/stdc++.h>
    using namespace std;
    #define rep(i, a, b) for(int i = (a); i <= (b); i++)
    #define per(i, a, b) for(int i = (a); i >= (b); i--)
    #define ll long long
    #define db double
    #define VI vector<int>
    #define PII pair<int, int>
    const db Pi = 3.141592653589793;
    const int INF = 0x7fffffff;
    const int N = 1e5 + 5;
    const db eps = 1e-10;
    const int mod = 1e9 + 7;
    int cas, n, m, q, l, r, k;
    ll c[10], f[10], a[N];
    void sum(ll a[], int n){
        rep(i, 1, n) a[i] = (a[i - 1] + a[i] + mod) % mod;
    }
    void diff(ll a[], int n){  //注意逆序!!!
        per(i, n, 1) a[i] = (a[i] - a[i - 1] + mod) % mod;
    }
    ll Pow(int x, int cnt){
        ll tmp = 1;
        rep(i, 1, cnt) (tmp *= (ll)x) %= mod;
        return tmp;
    }
    ll funct(int x){
        ll res = 0;
        rep(i, 0, 5)
            if(c[i]) (res += c[i] * Pow(x, i)) %= mod;
        return res;
    }
    int main(){
        ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
        cin >> n >> m >> q;
        rep(i, 1, n) cin >> a[i];
        //差分预处理
        rep(time, 1, 6) diff(a, n);
        rep(i, 1, m){
            cin >> l >> r >> k;
            memset(c, 0, sizeof(c));
            per(j, k, 0) cin >> c[j];
            //加f
            rep(i, 1, 10) f[i] = funct(i);
            rep(time, 1, 6) diff(f, 10);
            rep(i, 1, 10) (a[l - 1 + i] += f[i]) %= mod;
            //减f
            rep(i, 1, 10) f[i] = funct(i + r - l + 1);
            rep(time, 1, 6) diff(f, 10);
            rep(i, 1, 10) ((a[r + i] -= f[i]) += mod) %= mod;
        }
        //还原
        rep(time, 1, 6) sum(a, n);
        //准备输出答案
        sum(a, n);
        while(q--){
            cin >> l >> r;
            cout << (a[r] - a[l - 1] + mod) % mod << endl;
        }
    }
    /*
    10 2 11
    1000 1000 1000 100000 1000 1000 10000 10000 10000 100000
    1 10 0 100
    1 10 1 1 0
    1 1
    2 2
    3 3
    4 4
    5 5
    6 6
    7 7
    8 8
    9 9
    10 10
    1 10
    */
    

E. 智乃酱的双塔问题

  • 题目链接:智乃酱的双塔问题

  • 类型:矩阵前缀和

  • 思路:从地面到达第 i i i 层楼可记录 2 × 2 2\times 2 2×2 矩阵 p r e s u m [ i ] presum[i] presum[i]​,这个矩阵

    01
    0表示从左楼到左楼表示从左楼到右楼
    1表示从右楼到左楼表示从右楼到右楼
    • 左上方向楼梯表示 [ 1 1 0 1 ] \begin{bmatrix}1&1\\0&1 \end{bmatrix} [1011],右上方向楼梯表示 [ 1 0 1 1 ] \begin{bmatrix}1&0\\1&1 \end{bmatrix} [1101],求出前缀矩阵。

    • 对于需要楼层区间,只需要利用逆矩阵代替相减即可

    • 注意矩阵乘法逆矩阵的求解,见代码。

    //矩阵前缀和
    #include<bits/stdc++.h>
    using namespace std;
    #define rep(i, a, b) for(int i = (a); i <= (b); i++)
    #define per(i, a, b) for(int i = (a); i >= (b); i--)
    #define ll long long
    #define db double
    #define VI vector<int>
    #define PII pair<int, int>
    const db Pi = 3.141592653589793;
    const int INF = 0x7fffffff;
    const int N = 1e5 + 5;
    const db eps = 1e-10;
    const int mod = 1e9 + 7;
    int n, m, src, des, ps, pt;
    string s;
    ll A[5][5];
    struct Mat{
        ll a[2][2];
        Mat(int a1 = 0, int a2 = 0, int b1 = 0, int b2 = 0){
            a[0][0] = a1, a[0][1] = a2;
            a[1][0] = b1, a[1][1] = b2;
        }
    }presum[N];
    Mat left_to_up = Mat(1, 1, 0, 1);
    Mat right_to_up = Mat(1, 0, 1, 1);
    
    Mat operator * (Mat x, Mat y){
        Mat ans = Mat(0, 0, 0, 0);
        rep(i, 0, 1) rep(j, 0, 1) 
            rep(k, 0, 1){
                (ans.a[i][j] += x.a[i][k] * y.a[k][j] % mod) %= mod;
            }
        return ans;
    }
    ll inv(ll x){
        if(x == 1) return 1;
        return (mod - mod / x) * inv(mod % x) % mod;
    }
    Mat getinv(Mat x, int n){  //求逆矩阵
        //构造
        rep(i, 0, n - 1) rep(j, 0, n - 1){
            A[i][j] = x.a[i][j];
            A[i][j + n] = (j == i ? 1 : 0);
        }
        //化为对角矩阵
        rep(j, 0, n - 1){
            //选择主元
            if(!A[j][j]){
                rep(i, j + 1, n - 1){
                    if(!A[i][j]) continue;
                    //行交换
                    rep(k, 0, 2 * n - 1) swap(A[i][k], A[j][k]);
                    break;
                }
            }
            //主元化为1
            int inva = inv(A[j][j]);
            rep(k, 0, 2 * n - 1) (A[j][k] *= inva) %= mod;
            //消元
            rep(i, j + 1, n - 1){
                int mul = A[i][j];
                rep(k, 0, 2 * n - 1) A[i][k] = (A[i][k] - mul * A[j][k] % mod + mod) % mod;
            }
        }
        //左半部分化为单位阵
        per(j, n - 1, 0)
            per(i, j - 1, 0){
                int mul = A[i][j];
                rep(k, 0, 2 * n - 1) A[i][k] = (A[i][k] - mul * A[j][k] % mod + mod) % mod;
            }
        Mat res;
        rep(i, 0, n - 1) rep(k, 0, n - 1) res.a[i][k] = A[i][k + n];
        return res;
    }
    int main(){
        cin >> n >> m >> s;
        presum[1] = Mat(1, 0, 0, 1);
        rep(i, 2, n){
            if(s[i - 2] == '/') presum[i] = presum[i - 1] * left_to_up;
            else presum[i] = presum[i - 1] * right_to_up;
        }
        while(m--){
            cin >> src >> des >> ps >> pt;
            // -sum[l - 1] + sum[r]
            Mat ans = getinv(presum[src], 2) * presum[des];
            cout << ans.a[ps][pt] << endl;
        }
    }
    /*
    4 5
    //\
    1 4 0 0
    2 4 0 0
    2 4 0 1
    3 4 0 1
    3 4 1 0
    */
    

F. 牛牛的猜球游戏

  • 题目链接:牛牛的猜球游戏

  • 类型:序列排列变换前缀和

  • 思路:每次排列都是一种全部改变,且有连续性。故可作为前缀和统计,在查找时需要定义减法操作,逆向还原即可。

    //数组排列前缀和
    #include<bits/stdc++.h>
    using namespace std;
    #define rep(i, a, b) for(int i = (a); i <= (b); i++)
    #define per(i, a, b) for(int i = (a); i >= (b); i--)
    #define ll long long
    #define db double
    #define VI vector<int>
    #define PII pair<int, int>
    const db Pi = 3.141592653589793;
    const int INF = 0x7fffffff;
    const int N = 1e5 + 5;
    const db eps = 1e-10;
    int cas, n, m, a, b, l, r;
    struct AC{
        int presum[10];
    }matrix[N];
    AC operator - (AC a, AC b){  //a - b
        AC ans, tmp;
        rep(i, 0, 9) tmp.presum[b.presum[i]] = i;
        rep(i, 0, 9) ans.presum[i] = tmp.presum[a.presum[i]];
        return ans;
    }
    void Print(AC x){
        rep(i, 0, 9) printf("%d ", x.presum[i]);
        printf("\n");
    }
    int main(){
        cin >> n >> m;
        rep(i, 0, 9) matrix[0].presum[i] = i;
        rep(i, 1, n){
            scanf("%d%d", &a, &b);
            matrix[i] = matrix[i - 1];
            swap(matrix[i].presum[a], matrix[i].presum[b]);
        }
        while(m--){
            scanf("%d%d", &l, &r);
            Print(matrix[r] - matrix[l - 1]);
        }    
    }
    /*
    5 3
    0 1
    1 2
    2 3
    0 1
    9 0
    1 5
    5 5
    3 5
    */
    

G. 牛牛的Link Power I

  • 题目链接:牛牛的Link Power I

  • 思路:对于 i i i​ 处的节点对后面位置的影响为
    a :    i a :    0123456 d :    0111111 d d :    0100000 \begin{aligned} a:\;&i\\ a:\;&0123456\\ d:\;&0111111\\ dd:\;&0100000 \end{aligned} a:a:d:dd:i012345601111110100000
    即两次差分后化为常数序列,则对所有数字 1 1 1 的位置处理,最后两次前缀和还原原序列后计算对应 1 1 1 的位置即可。

    #include<bits/stdc++.h>
    using namespace std;
    #define rep(i, a, b) for(int i = (a); i <= (b); i++)
    #define per(i, a, b) for(int i = (a); i >= (b); i--)
    #define ll long long
    #define db double
    #define VI vector<int>
    #define PII pair<int, int>
    const db Pi = 3.141592653589793;
    const int INF = 0x7fffffff;
    const int N = 1e5 + 5;
    const db eps = 1e-10;
    const int mod = 1e9 + 7;
    ll n, a[N], effect[N], tmp[N], ans = 0;
    char ch;
    vector<int> store;
    int main(){
        cin >> n;
        store.clear();
        rep(i, 1, n){
            cin >> ch;
            a[i + 1] = (int)(ch - '0');
            if(ch == '1') store.push_back(i);
        }
        rep(i, 1, n) tmp[i] = (tmp[i - 1] + a[i]) % mod;
        rep(i, 1, n) effect[i] = (effect[i - 1] + tmp[i]) % mod;
        for(auto i : store) (ans += effect[i]) %= mod;
        cout << ans << endl;
    }
    /*
    5
    00110
    */
    

H. 小w的糖果

  • 题目链接:小w的糖果

  • 思路:对于加等差序列和平方序列,计算发现

    • 等差序列中差分两次为常数
      a :    1 , 2 , 3 , 4 , 5 , 6 d :    1 , 1 , 1 , 1 , 1 , 1 d d :    1 , 0 , 0 , 0 , 0 , 0 \begin{aligned} a:\;&1,2,3,4,5,6\\ d:\;&1,1,1,1,1,1\\ dd:\;&1,0,0,0,0,0 \end{aligned} a:d:dd:1,2,3,4,5,61,1,1,1,1,11,0,0,0,0,0

    • 平方序列中差分两次为常数
      a :    1 , 4 , 9 , 16 , 25 , 36 d :    1 , 3 , 5 ,      7 ,      9 , 11 d d :    1 , 2 , 2 ,      2 ,      2 ,      2 d d d :    1 , 1 , 0 ,      0 ,      0 ,      0 \begin{aligned} a:\;&1,4,9,16,25,36\\ d:\;&1,3,5,\;\;7,\;\;9,11\\ dd:\;&1,2,2,\;\;2,\;\;2,\;\;2\\ ddd:\;&1,1,0,\;\;0,\;\;0,\;\;0\\ \end{aligned} a:d:dd:ddd:1,4,9,16,25,361,3,5,7,9,111,2,2,2,2,21,1,0,0,0,0

    • 于是可对三种操作分别预处理,最后不同次数前缀和还原即可。

    #include<bits/stdc++.h>
    using namespace std;
    #define rep(i, a, b) for(int i = (a); i <= (b); i++)
    #define per(i, a, b) for(int i = (a); i >= (b); i--)
    #define ll long long
    #define db double
    #define VI vector<int>
    #define PII pair<int, int>
    const db Pi = 3.141592653589793;
    const int INF = 0x7fffffff;
    const int N = 1e5 + 5;
    const db eps = 1e-10;
    const int mod = 1e9 + 7;
    int cas, n, m;
    ll d1[N], d2[N], d3[N];
    void init(){
        rep(i, 1, n) d1[i] = d2[i] = d3[i] = 0;
    }
    void pre_sum(ll a[]){
        rep(i, 1, n) a[i] = (a[i - 1] + a[i]) % mod;
    }
    int main(){
        ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
        cin >> cas;
        while(cas--){
            cin >> n >> m;
            init();
            rep(i, 1, m){
                int type, pos; cin >> type >> pos;
                if(type == 1) d1[pos]++;
                if(type == 2) d2[pos]++;
                if(type == 3) d3[pos]++, d3[pos + 1]++;
            }
            pre_sum(d1);
            pre_sum(d2), pre_sum(d2);
            pre_sum(d3), pre_sum(d3), pre_sum(d3);
            rep(i, 1, n) cout << (d1[i] + d2[i] + d3[i]) % mod << " ";
            cout << endl;
        }
    }
    /*
    4
    10 1
    1 1
    10 1
    2 2
    10 1
    3 3
    10 3
    1 1
    2 2
    3 3
    */
    

I. 积木大赛

  • 题目链接:积木大赛(来源:NOIP2013)

  • 思路:差分处理计算正数即可

    #include<bits/stdc++.h>
    using namespace std;
    #define rep(i, a, b) for(int i = (a); i <= (b); i++)
    #define per(i, a, b) for(int i = (a); i >= (b); i--)
    #define ll long long
    #define db double
    #define VI vector<int>
    #define PII pair<int, int>
    const db Pi = 3.141592653589793;
    const int INF = 0x7fffffff;
    const int N = 1e5 + 5;
    const db eps = 1e-10;
    int n;
    ll a[N], d[N], ans;  //差分看正数
    int main(){
        cin >> n;
        rep(i, 1, n){
            cin >> a[i];
            d[i] = a[i] - a[i - 1];
            if(d[i] > 0) ans += d[i];
        }
        cout << ans << endl;
    }
    /*
    5
    2 3 4 1 2
    */
    

J. 道路铺设

  • 题目链接:道路铺设(来源:NOIP2018)

  • 思路:同上题

    #include<bits/stdc++.h>
    using namespace std;
    #define rep(i, a, b) for(int i = (a); i <= (b); i++)
    #define per(i, a, b) for(int i = (a); i >= (b); i--)
    #define ll long long
    #define db double
    #define VI vector<int>
    #define PII pair<int, int>
    const db Pi = 3.141592653589793;
    const int INF = 0x7fffffff;
    const int N = 1e5 + 5;
    const db eps = 1e-10;
    ll n, a[N], d[N], ans;
    int main(){
        cin >> n;
        rep(i, 1, n){
            cin >> a[i];
            d[i] = a[i] - a[i - 1];
            if(d[i] > 0) ans += d[i];
        }
        cout << ans << endl;
    }
    /*
    6
    4 3 2 5 3 5
    */
    
举报

相关推荐

第一章 绪论

第一章综述

第一章 起步

第一章.概论

docker 第一章

第一章介绍

第一章 引论

第一章作业

0 条评论