本系列文章我将总结我在刷算法题所用到的知识,如果你也在刷算法并且是新手,我相信这系列文章会很适合你。

【洛谷刷题 | 第四天】
今日题目:
1.最大子段和(前缀和,贪心)
链接:P1115 最大子段和
给出一个长度为 n 的序列 a,选出其中连续且非空的一段使得这段和最大。
案例:
cpp
输入: 输出
7 4
2 -4 3 -1 2 -4 3
通过前缀和将 "连续子数组和" 转化为差值问题后,核心是贪心策略:对每个位置,尽可能用最小的前缀和去减当前前缀和,以得到最大的差值(即最大子数组和),避免了暴力枚举所有子数组的高复杂度。
题解:
cpp
#include<bits/stdc++.h>
using namespace std;
int a[200009] = {0},b[2000009] = {0},c[2000009] = {0};
int main()
{
int n;
cin>>n;
for(int i=0;i<n;i++)
{
cin>>a[i];
}
b[0] = a[0];
for(int i=1;i<n;i++){
b[i] = a[i] + b[i-1];
}
int minn = min(0,b[0]);c[0] = b[0];
for(int i=1;i<n;i++){
c[i] = b[i] - minn;
minn = min(b[i],minn);
}
int maxx = -1e5;
for(int i=0;i<n;i++){
maxx = max(c[i],maxx);
}
cout<<maxx;
}
知识点:前缀和,贪心
前缀和就是从数组开头到当前位置的所有元素的累加和。
cpp
假设有数组 a = [a₀, a₁, a₂, ..., aₙ₋₁]
定义前缀和数组 b,其中:
b[0] = a[0](第一个元素的前缀和就是它自己)
b[1] = a[0] + a[1](前 2 个元素的和)
b[2] = a[0] + a[1] + a[2](前 3 个元素的和)
...
b[i] = a[0] + a[1] + ... + a[i](前 i+1 个元素的和)
前缀和的核心价值:快速计算任意连续子数组的和。
cpp
比如想算数组中从第 j 个元素到第 i 个元素的和(j ≤ i):
不用重新遍历 j 到 i 累加;
直接用公式:a[j] + a[j+1] + ... + a[i] = b[i] - b[j-1];
特殊情况:如果 j=0(从第一个元素开始),则和为 b[i](因为 b[-1] 视为 0)。
贪心就是:
每一步都做出当前看起来最优的选择,不考虑全局后果,最终希望通过每一步的局部最优,得到全局最优的结果。
2.领地选择(前缀和)
链接:P2004 领地选择
给定一个 N行M列 的二维地图(每个位置有对应的土地价值,可正可负),需要在地图上找到一个 C×C 的正方形区域,使得该区域内所有地块的价值总和最大。最终输出这个正方形区域左上角的坐标 (X,Y)(题目保证最优解唯一)。
案例:
cpp
输入 输出
3 4 2 1 2
1 2 3 1
-1 9 0 2
2 0 1 1
这道题是一维前缀和的提升版 - 二维前缀和。
先计算每行的一维前缀和,再基于行前缀和累加得到二维前缀和,d [i][j] 表示从地图左上角 (1,1) 到 (i,j) 的矩形区域总价值;再遍历所有能放下 C×C 正方形的左上角坐标 (i,j),通过二维前缀和公式计算该正方形的总价值;最后对比所有合法正方形的总价值,记录最大值对应的左上角坐标,最终输出该坐标。
题解:
cpp
#include<bits/stdc++.h>
using namespace std;
int a[1001][1001],b[1001][1001],d[1001][1001];
int main()
{
int n,m,c;
cin>>n>>m>>c;
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
cin>>a[i][j];
b[i][j] = a[i][j] + b[i][j-1];
d[i][j] = b[i][j] + d[i-1][j];
}
}
int maxx = INT_MIN;;int x =1,y=1;
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
if((i+c-1)>n||(j+c-1)>m) continue;
int q = d[i + c-1][j + c-1] + d[i - 1][j - 1] - d[i - 1][j + c-1] - d[i + c-1][j - 1];
if(maxx<q){
maxx=q;
x=i,y=j;
}
}
}
cout<<x<<" "<<y;
}
3.语文成绩(差分)
链接:P2367 语文成绩
给定 n 个学生的初始成绩,执行 p 次加分操作(每次给第 x 到第 y 个学生每人加 z 分),最终输出全班的最低成绩。
案例:
cpp
输入 输出
3 2 2
1 1 1
1 2 1
2 3 1
这道题我们可以利用差分数组 d 替代直接遍历区间加分(降低复杂度),对每次 x 到 y 加 z 的操作,仅需在 d[x] 加 z、d[y+1] 减 z;之后对差分数组 d 做前缀和计算,得到每个学生实际需要增加的分数;最后将每个学生的初始成绩 a[i] 加上对应加分 d[i],遍历过程中实时记录所有成绩的最小值,最终输出该最小值。
题解:
cpp
#include<bits/stdc++.h>
using namespace std;
int a[5000009];
int d[5000009];
int minn = INT_MAX;
int main()
{
int n,p;
cin>>n>>p;
for(int i=1;i<=n;i++)
{
cin>>a[i];
}
int x,y,z;
for(int i=0;i<p;i++)
{
cin>>x>>y>>z;
d[x]+=z;
d[y+1]-=z;
}
for(int i=1;i<=n;i++)
{
d[i]+=d[i-1];
}
for(int i=1;i<=n;i++)
{
a[i]+=d[i];
if(minn>a[i])
{
minn=a[i];
}
}
cout<<minn;
}
知识点:差分
差分是一种预处理数组的技巧,核心是用一个 "差分数组" 记录原数组中相邻元素的差值,目的是把 "区间批量修改" 转化为 "单点修改",降低操作复杂度。
例如:
cpp
原数组 a = [a₁, a₂, a₃, ..., aₙ]
定义差分数组 d:
d₁ = a₁(第一个元素和原数组一致)
dᵢ = aᵢ - aᵢ₋₁(i≥2,即当前元素减前一个元素的差值)
差分的核心价值:快速处理数组的区间加减操作。
比如想给原数组中从 x 到 y 的所有元素都加 z:
不用遍历 x 到 y 逐个加; 只需对差分数组做 2 次单点修改:d[x] += z、d[y+1] -= z;
最后对差分数组做前缀和,就能还原出修改后的原数组。
举一个例子:
cpp
假设原成绩a = [10, 20, 30, 40],现在要给第 2 到 3 个学生各加 5 分:
差分数组初始d = [0, 0, 0, 0, 0](多开一位避免越界);
区间加分:d[2] +=5 d[4] -=5
对 d 做前缀和:
d[1] = 0
d[2] = 0+5=5
d[3] = 5+0=5
d[4] = 5+(-5)=0
最终成绩:10+0=10、20+5=25、30+5=35、40+0=40,
最后:
如果我的内容对你有帮助,请点赞,评论,收藏。创作不易,大家的支持就是我坚持下去的动力!
