dp问题——最大子段和
1. 基础
(1)最大子段和
题目连接:最大子段和
解法
//状态表示及转移
用f[i] 表示 以a[i]结尾的连续子序列的集合。
根据最后一个元素的不同,可将集合如此划分:
1. 子序列长度为1 ————> f[i] = a[i].
2. 子序列长度大于1 ————> f[i] = f[i - 1] + a[i].
(因为f[i] 表示以a[i]结尾的连续子序列,当长度大于1时
a[i - 1]必定存在。)
根据题目要求,求的是集合最大值
所以状态转移方程:
f[i] = max(f[i - 1] + a[i],a[i])
= max(f[i - 1],0) + a[i];
//边界处理
f[1] = ?
根据f[i]的定义,f[1]表示以a[1]结尾的连续子序列的集合,
此时子序列长度为1。所以f[1] = a[1]。
所以在状态转移时要确保f[1] = a[1]。
1.可以设定f[1] = a[1],然后i从2开始循环。
2.若i从1开始循环,根据状态转移方程
f[1] = max(f[0],0) + a[i].
要确保f[1] = a[1].
所以f[0] <= 0,一般设f[0] = 0.
// 输出的答案
根据f[i] 的定义。
由于不确定最后答案(也就是最大值)所在的子序列是不是以
a[n]结尾,所以f[n]并不是最终答案。
最终答案需要遍历f[i],找到所有f[i]中的最大值。
// 优化空间
根据状态转移方程:
f[i] = max(f[i - 1],0) + a[i].
发现对于每一个f[i]只会用到它前一个状态f[i - 1],
所以只需要一个变量f保存前一个状态的值。
即f = max(f,0) + a[i];
执行max时,f值未被更新,所以此时f保存的是上一个循环
i - 1时的值,也就是f[i - 1].
// 代码
// 注释为优化
#include<iostream>
using namespace std;
typedef long long ll;
const int N = 50010;
ll f[N],a[N];
//ll a[N];
int main()
{
int n;
cin >> n;
for(int i = 1; i <= n; i ++) cin >> a[i];
ll ans = -0x3f3f3f3f;
f[0] = 0;
//ll f = 0;
for(int i = 1; i <= n; i ++)
{
f[i] = max(f[i - 1],0ll) + a[i];
ans = max(f[i],ans);
//f = max(f,0ll) + a[i];
//ans = max(f,ans);
}
cout << ans << endl;
}
2. 扩展
(1)循环数组最大子段和
题目链接:循环数组最大子段和
解法
// 区别
本题不同点以及难处理的点在于
a[i] + a[i + 1] + ... + a[n] +
a[1] + a[2] + ... + a[j](#)
这种情况.(j < i)
// 如何求这种类型的值呢
1. sum = # + a[j] + a[j + 1] + ... + a[i](*)
sum代表a[1] + a[2] + ... + a[n].
2. 求#式的最大值,这意味着只要求*式的最小值,然后用sum
减去该值即可。
3. *式是什么?是j ~ i这一段区间的和,要求是值最小。
就是求最小子段和
// 最小子段和的求法
和最大子段和思想相同。
// 代码
#include<iostream>
using namespace std;
typedef long long ll;
const int INF = 0x3f3f3f3f;
const int N = 500010;
ll a[N];
int main()
{
int n;
cin >> n;
for(int i = 1; i <= n; i ++) cin >> a[i];
ll maxm = -INF,minm = INF;
ll fmax = 0,fmin = 0;// 最大子段和,最小子段和
ll sum = 0; // 数组总和
for(int i = 1; i <= n; i ++)
{
fmax = max(fmax,0ll) + a[i];
fmin = min(fmin,0ll) + a[i];
maxm = max(fmax,maxm);
minm = min(fmin,minm);
sum += a[i];
}
cout << max(maxm,sum - minm) << endl;
}
(2)二维最大子段和
题目链接:二维最大子段和
解法
// 二维变一维
如何将二维转换为一维呢?
1.一维数组是高度为1的矩阵。
2.二维数组是高度>1的矩阵。
只需在不改变答案的前提下压缩高度即可。
就是将每一列的值累加。(换而言之就是在列方向上
快速求出一段区间和的值)
如:
1 2 3
1 2 3 ————————> 3 6 9
1 2 3
3.前缀和可以在O(1)的时间内求出任意一段区间和。
只需要对于每一列预处理出前缀和即可。
4.在对转化后的"一维数组"求最大子段和即可。
5.如何枚举任意子矩阵?
只需确定子矩阵的高度和长度即可。
高度如何确定?高度是行数的差值。(只需确定矩阵所在的
第一行和最后一行即可,)
如3 * 3 的矩阵。
高度为2的子矩阵有:(1,2),(2,3)。
[(x,y),x为矩阵所在的第一行,y为矩阵所在的最后一行]。
#include<iostream>
using namespace std;
typedef long long ll;
const int N = 510;
const int INF = 0x3f3f3f3f;
ll s[N][N];
int main()
{
int n,m;
cin >> m >> n;
for(int i = 1; i <= n; i ++)
for(int j = 1; j <= m; j ++)
{
int x;
cin >> x;
s[i][j] = s[i - 1][j] + x;
// 预处理每一列的前缀和
}
ll ans = -INF;
for(int i = 1; i <= n; i ++)// 枚举矩阵所在的第一行
for(int j = i; j <= n; j ++)// 枚举矩阵所在的最后一行
{
ll f = 0;
for(int k = 1; k <= m; k ++)
{
f = max(f,0ll) + s[j][k] - s[i - 1][k];
// s[j][k] - s[i - 1][k] 表示将第k列i ~ j这段压缩
ans = max(f,ans);
}
}
cout << max(ans,0ll) << endl; // 全为负数要输出0
}
(3)最大M子段和
题目链接:最大m子段和
解法
// 状态表示及转移
f[i][j] 表示 分成i段且最后一段必须包含a[j]的连续子序列
的集合。
根据a[j],可以如此划分集合:
1.a[j]不是单独成段。又因为最后一段必须包含a[j]且序列
连续,所以最后一段必包含a[j - 1]。
表示为f[i][j - 1] + a[j].
2.a[j]单独成段。所以剔除a[j]后,还剩i-1段,此时最后一段
也就是第i- 1段以谁结尾?是不确定的。
假设为a[k](i - 1<= k <= j - 1)。
表示为f[i - 1][k] + a[j].
3.所以f[i][j] = max(f[i][j - 1],f[i - 1][k]) + a[j].
// 边界处理
因为分成i段至少要有i个数,所以f[i][j]当i大于j时为不合法
所以f[i][i] 只能由第二种情况转移。
f[i][i] = f[i - 1][k] + a[i].k>= i - 1&&k <= i - 1
所以f[i][i] = f[i - 1][i - 1] + a[i]
//优化时间
发现转移时需要枚举k,大大增加了时间复杂度,对此优化。
因为不确定a[j - 1]是否被包含在第i-1段,所以需要枚举k
那能不能根据最后一个数是a[j - 1]来完成优化呢?
// 引入辅助数组 p[i][j].
p[i][j] 表示分为i段的连续子序列且最后一个数为a[j]的集合
注意:这里最后一段不一定需要包含a[j].
// p[i][j]的转移
1.最后一段必须包含a[j].此时p[i][j]的定义等同于f[i][j]
表示为f[i][j].
2.最后一段不包含a[j].表示为p[i][j - 1]
3. p[i][j] = max(p[i][j - 1],f[i][j]).
// 有了p[i][j]的定义后,发现f[i][k] == p[i][j - 1].
故f[i][j] = max(f[i][j - 1],p[i - 1][j - 1])
+ a[j].
转移的时间复杂度由O(n)变为O(1)
优化时间空间:参考
//代码
#include<iostream>
using namespace std;
typedef long long ll;
const int INF = 0x3f3f3f3f;
const int N = 5010;
ll f[N],p[N],a[N];
int main()
{
int n,m;
cin >> n >> m;
for(int i = 1; i <= n; i ++) cin >> a[i];
ll cur;
for(int i = 1; i <= m; i ++)
{
f[i] = f[i - 1] + a[i];
cur = f[i];
for(int j = i + 1; j <= n - m + i; j ++)
{
f[j] = max(f[j - 1],p[j - 1]) + a[j];
p[j - 1] = cur;
if(f[j] > cur) cur = f[j];
}
p[n - m + i] = cur;
}
for(int i = 1; i <= m; i ++) cur = max(cur,f[i]);
cout << cur << endl;
}
(4)最大子段和v2
题目链接:最大子段和v2
(5)最大m子段和v2
题目链接:最大子段和v2
(6)最大m子段和v3
题目链接:最大子段和v3