Featured image of post 洛谷专题记录(前缀和与差分)

洛谷专题记录(前缀和与差分)

板刷记录

最大子段和

题目描述

给出一个长度为 $n$ 的序列 $a$,选出其中连续且非空的一段使得这段和最大。

思路

简单的前缀和 + 递推,用$a[i]$记录到第$i$个位置时与$i$相连续的最大值,用$ans$记录字段和的最大值

转移方程a[i] = max(x, a[i - 1] + x)ans = max(a[i], ans)

代码

void solve(){
    int n;
    cin >> n;
    vector<int>a(n);
    int ans = -INF;
    int x;
    for (int i = 0; i < n; i++) {
        cin >> x;
        if (i == 0){
            a[i] = x;
        } else{
            a[i] = max(x, a[i - 1] + x);
            ans = max(a[i], ans);
        }
    }
    cout << ans << endl;
}

地毯

题目描述

在 $n\times n$ 的格子上有 $m$ 个地毯。

给出这些地毯的信息,问每个点被多少个地毯覆盖。

输入格式 第一行,两个正整数 $n,m$。意义如题所述。

接下来$m$ 行,每行两个坐标 $(x_1,y_1)$ 和 $(x_2,y_2)$,代表一块地毯,左上角是 $(x_1,y_1)$,右下角是 $(x_2,y_2)$。

输出格式 输出 $n$ 行,每行 $n$ 个正整数。

第 $i$ 行第 $j$ 列的正整数表示 $(i,j)$ 这个格子被多少个地毯覆盖。

$n, m \leqslant 1000$

思路

差分

复习

何为差分

假设我们现在要给[2,5]这个区间加一。

原来的序列是: 0 0 0 0 0 0 0 0

这时候我们在2上面打 +1 标记, 6 上面打 -1 标记。

那么现在的序列是: 0 +1 0 0 0 -1 0

这样的话,从左往右扫描这个数组,记录当前经过的标签之和,这个和就是那个数对应的答案

这样,对于每个区间加操作,只需要$O(1)$的时间打上标记, 最后扫描输出即可

现在把问题拓展到二维。假设我们要覆盖$[(2,2),(5,5)]$ ,那么标记便可以这样打:

0 +1 0 0 0 -1
0 +1 0 0 0 -1
0 +1 0 0 0 -1
0 +1 0 0 0 -1
0 0 0 0 0 0

即在每一行都按照一维的方式来操作:

for (int j = x1; j <= x2; j++) {
    cf[j][y1]++;
    cf[j][y2 + 1]--;
}

这样一来,打标记时候的复杂度为$O(n)$,总复杂度为$O(mn + n^2)$

代码

int cf[2000][2000];

void solve(){
    int n, m;
    cin >> n >> m;
    int x1, y1, x2, y2;
    for (int i = 0; i < m; i++) {
        cin >> x1 >> y1 >> x2 >> y2;
        for (int j = x1; j <= x2; j++) {
            cf[j][y1]++;
            cf[j][y2 + 1]--;
        }
    }
    for (int i = 1; i <= n; i++) {
        for (int j = 1; j <= n; j++) {
            cf[i][j] += cf[i][j - 1];
            cout << cf[i][j] << " ";
        }

        cout << endl;
    }
}

海底高铁

题目描述

一个铁路经过$N$个城市,每个城市一个站,但不能直达,只能相邻城市的一段铁路单独买票

对于一段铁路$i$,连接了城市$i$和城市$i+ 1$,有两种买票方式。

  1. 单独购买纸质票,价格为$A_i$

  2. 买卡,只需要花工本费$C_i$,每次通行花费$B_i(B_i < A_i )$

现在一个人要出差去$M$个城市,按照$P_i$的顺序访问,可能会多次访问一个城市,且相邻访问的城市位置不一定相邻,而且不会是同一个城市。

求总费用的最小值

$M,N \leqslant 10^5, A_i,B_i,C_i \leqslant 10 ^ 5$

思路

前缀和和差分数组结合, 对于其中一小段,我们要么全部买纸票,要么全部刷卡。

所以问题拆分成了统计每一小段经过的总次数,再在两种费用方案之间取最小值

后者不难,此题的考点在前者,直接暴力统计会绎演顶证,鉴定为纯纯的$TLE$

因此先用差分数组预处理每次要去的起点和终点,打上标记。

然后再求每一项的前缀和,这样最后前缀和数组就是每一段铁路的经过次数了

例:如果我们要经过$1-3$ 和 $2-4$

先打上标记

                  +1      -1          +1 +1   -1 -1
 - - - - -        --  - - -- -        -- -- - -- --
 1 2 3 4 5        1   2 3 4  5        1  2  3 4  5
    图1                图2                 图3

再进行前缀和

1 2 2 1 0
- - - - -
1 2 3 4 5
   图4

更多细节见代码

代码

void solve(){
   int n, m;
   cin >> n >> m;
   vector<ll>p(n + 2);//station
   vector<ll>cnt(n + 2);//count
   for (int i = 1; i <= m; i++) {
       cin >> p[i];
   }
   ll ma, mi;
   for (int i = 1; i <= m - 1; i++) {
       ma = max(p[i], p[i + 1]);
       mi = min(p[i], p[i + 1]);
       cnt[ma]--;//因为第i段铁路是从第i连到第i+1个城市,所以减去ma就是减去第ma-1 +1段铁路
       cnt[mi]++;
   }
   for (int i = 1; i <= n; i++) {//WA了一发,开始的range是[1,m]
       cnt[i] += cnt[i - 1];//这就是为什么前缀和处理的话数组下标要从1开始
   }
   int a, b, c;
   ll ans = 0;
   for (int i = 1; i <= n - 1; i++) {
       cin >> a >> b >> c;
       ans += min(a * cnt[i], b * cnt[i] + c);
   }
   cout << ans << endl;
}

最大加权矩形

题目描述

一个$N \times N$的矩阵,矩阵里每个点有对应的权值,求子矩阵权值和的最大值

$N \leqslant 120$

思路

一眼二维前缀和,数据范围很小,直接用$O(n^4)$的复杂度莽过去了。

但二维前缀和的公式还是不熟

二维矩阵的前缀和(到0,0点的):

$$f[i][j] = f[i][j - 1] + f[i -1][j] - f[i - 1][j - 1] + a[i][j]$$ 子矩阵的和从$(i,j)$到$(k,l)$ $O(n^4)$: $$f[k][l] - f[k][j - 1] - f[i - 1][l] + f[i - 1][j - 1]$$

代码 ($O(n^4)$)

int a[125][125], f[125][125];

void solve(){
   int n;
   cin >> n;
   for (int i = 1; i <= n; i++) {
       for (int j = 1; j <= n; j++) {
           cin >> a[i][j];
           f[i][j] = f[i][j - 1] + f[i -1][j] - f[i - 1][j - 1] + a[i][j];
       }
   }
   int ans = -INF;
   for (int i = 1; i <= n; i++) {
       for (int j = 1; j <= n; j++) {
           for (int l = i; l <= n; l++) {
               for (int i1 = j; i1 <= n; i1++) {
                   ans = max(f[l][i1] - f[l][j - 1] - f[i - 1][i1] + f[i - 1][j - 1], ans);
               }
           }
       }
   }
   cout << ans << endl;
}

优化版

思路

我们可以考虑将矩形压缩成一维,比如处理一个2行的矩形时,将a [ i ][ j ]与a [ i - 1 ][ j ]相加,成为一个新的数组f [ n ],再使用上述代码进行动态规划,找出局部最优解。

首先进行前缀和,这样可以用减法来快速表示压缩的矩形。

可以模拟一下,a[ i ][ j ] - a[ i - k ][ j ]正好是以i为最下面一行,往上k行的压缩结果,这就很方便地表示了压缩后的矩形。

那再次循环,运用第一个代码的简单变形,可以求出以i为最下一行,向上k行的矩形最大值,多次更新ans就行

代码

void solve2(){
   int n;
   cin >> n;
   for (int i = 1; i <= n; i++) {
       for (int j = 1; j <= n; j++) {
           cin >> a[i][j];
           a[i][j] += a[i - 1][j]; // 前缀和处理
       }
   }
   int ans = -INF;
   int f1[150] = {0}, dp[150] = {0};
   for (int i = 1; i <= n; i++) {
       for (int k = 1; k <= i; k++) {

           for (int j = 1; j <= n; j++) {
               f1[j] = a[i][j] - a[i - k][j];
               dp[j] = max(dp[j - 1] + f1[j], f1[j]);
               ans = max(ans, dp[j]);
           }
       }
   }
   cout << ans << endl;
}

这是前一种方法的速度

这是后一种的

可以看出来优化还是十分明显的


领地选择

题目描述

首都被认为是一个占地 $C \times C$ 的正方形。小 Z 希望你寻找到一个合适的位置,使得首都所占领的位置的土地价值和最高。

思路

与上一题的第一种方法类似,使用二位前缀和预处理,然后$O(n^2)$扫一遍即可

代码

int a[1005][1005];
int f[1005][1005];

void solve(){
   int n, m, c;
   cin >> n >> m >> c;
   int ma = -INF;
   for (int i = 1; i <= n; i++) {
       for (int j = 1; j <= m; j++) {
           cin >> a[i][j];
           f[i][j] = f[i][j - 1] + f[i - 1][j] - f[i - 1][j - 1] + a[i][j];
       }
   }
   int x, y;
   for (int i = c; i <= n; i++) {
       for (int j = c; j <= m; j++) {
           if (f[i][j] - f[i][j - c] - f[i - c][j] + f[i - c][j - c] > ma){
               ma = f[i][j] - f[i][j - c] - f[i - c][j] + f[i - c][j - c];
               x = i - c + 1;
               y = j - c + 1;
           }
       }
   }
   cout << x << " " << y << endl;
}

光骓者的荣耀

题目描述

小 K 为了关心人民生活,决定定期进行走访。他每一次会从 $1$ 号城市到 $n$ 号城市并在经过的城市进行访问。其中终点必须为城市 $n$。

不仅如此,他还有一个传送器,传送半径为 $k$,也就是可以传送到 $i-k$ 和 $i+k$。如果目标城市编号小于 $1$ 则为 $1$,大于 $n$ 则为 $n$。

但是他的传送器电量不足,只能传送一次,况且由于一些原因,他想尽量快的完成访问,于是就想问交通部部长您最快的时间是多少。

思路

先前缀和预处理,然后以区间大小为$k$扫一遍求传送的最大值即可

(92分坑):在求区间和最大值的时候,下标不要从1开始,因为cnt[i + k] - cnt[i]如果从1开始的话会忽略cnt[1]本身的值

代码

void solve(){
    int n, k;
    cin >> n >> k;
    vector<ll>a(n);
    vector<ll>cnt(n);
    for (int i = 1; i < n; i++) {
        cin >> a[i];
    }
    partial_sum(all(a),cnt.begin());
    ll ans = -1;
    for (int i = 0; i < n - k; i++) {
        ans = max(ans, cnt[i + k] - cnt[i]);
    }
    cout << cnt[n - 1] - ans << endl;
}
Licensed under CC BY-NC-SA 4.0
最后更新于 Oct 21, 2022 12:47 CST