题目描述:
给定一个具有N个数的序列,求M个不相交连续子序列的最大和,相当于是最大子段和的升级版。
思路:
首先定义状态:
- 考虑第j个元素如何选择呢?很显然,第j个元素可以和第j-1个元素并在同一个段上,也可以自己单独作为一个段的开头
- 定义f[i][j]表示考虑前j个元素,分成i段能取到的最大值,a[j]必选(第i段以a[j]为结尾)
- 状态转移:f[i][j]=max{ f[i][j-1]+a[j], max{ f[i-1][k]+a[j],k=i-1,i,i+1,...,j-1 } },前者是把a[j]直接并在a[j-1]所在段的后面,后者是把a[j]单独开一个段
- 边界条件:i<=0,返回0;j<=0,返回0
- 答案:max{ f[M][K],K=M,M+1,M+2,...,N }(因为分成M段至少要M个元素)
记忆化搜索+二维数组的直接实现(容易理解)
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 int dp(int i, int j) 2 { 3 if (i <= 0 || j <= 0) return 0; 4 if (vis[i][j]) return f[i][j]; 5 vis[i][j] = 1; 6 f[i][j] = dp(i,j-1) + a[j]; 7 for (int k = i - 1; k <= j - 1; k++) 8 f[i][j] = max(f[i][j], dp(i - 1, k) + a[j]); 9 return f[i][j]; 10 } 11 //输出结果 12 int ans = -INT_MAX; 13 for (int i = M; i <= N; i++) 14 ans = max(ans, dp(M,i)); 15 cout << ans << endl;
不难发现,上述的时间复杂度是O(M*N*N),空间复杂度是O(M*N),不满足题目要求,应该进一步探索
考虑状态转移中的后者,即f[i-1][k]是不受当前状态f[i][j]的影响的,即f[i-1][k]可以在f[i][j]之前计算,那么我们就可以把f[i-1][k]的最大值在计算的时候记录下来,等到f[i][j]用的时候直接拿来,不用再去搜索,但是这样就不能用递归了,因为计算顺序有了要求
定义新数组maxF[i][j],表示考虑前j个元素,分成i段能得到的最大值,a[j]未必选(第i段未必以a[j]结尾)
则maxF[i][j]=max{ f[i][k],k=i,i+1,i+2,...,j },边界条件:i<=0或j<=0时,返回0,其他位置初始化为 -INF
新的F的状态转移:f[i][j]=max{ f[i][j-1]+a[j] , maxF[i-1][j-1]+a[j] }
但是随之而来的新问题是:当f[i][j]更新时,f[i][j]可以用来更新maxF[i][j],maxF[i][j+1],maxF[i][j+2].....,这样时间复杂度又变成了O(M*N*N),因为我们不过是从原来的向前搜求最值变成了向后搜更新最大值
以下是代码,从下述代码中,其实我们可以分析优化maxF
递推+maxF+二维数组
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 for (int i = 1; i <= MAXN - 1; i++) 2 for (int j = 1; j <= MAXN - 1; j++) 3 maxF[i][j] = -INT_MAX; 4 for (int i = 0; i <= MAXN - 1; i++) 5 maxF[i][0] = 0, maxF[0][i] = 0; 6 for (int i = 1; i <= M; i++) //M段 7 for (int j = i; j <= N; j++) 8 { 9 f[i][j] = max(f[i][j - 1] + a[j], maxF[i - 1][j - 1] + a[j]); 10 for (int k = j; k <= N; k++) 11 maxF[i][k] = max(maxF[i][k], f[i][j]); 12 } 13 cout << maxF[M][N] << endl;
降维maxF
必须延时更新,否则会覆盖需要用到的结果,而且正序枚举j
降维maxF代码
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 //初始化maxF[j]=0,因为把前j个元素分成0段,很显然最大值是0 2 memset(maxF, 0, sizeof(maxF)); 3 for (int i = 1; i <= M; i++) //M段 4 { 5 int nowMax = -INT_MAX; 6 for (int j = i; j <=N; j++) 7 { 8 f[i][j] = max(f[i][j - 1] + a[j], maxF[j - 1] + a[j]); 9 maxF[j - 1] = nowMax; //用上一轮的最大值更新上一轮应该更新的maxF[j] 10 nowMax = max(maxF[j - 1], f[i][j]); 11 } 12 maxF[N] = nowMax; //当前i,没有下一轮j了,不用延时更新,直接更新 13 } 14 cout << maxF[N] << endl;
降维f
考虑最初的状态转移方程中的前者,即f[i][j-1],发现i就是当前的i,所以第i轮的f数组f[i]和第i-1轮的数组f[i-1]是没有直接更新的联系的,所以可以直接用一维的f
最终代码
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 #define _CRT_SECURE_NO_WARNINGS 1 2 #include <cstdio> 3 #include <iostream> 4 #include <algorithm> 5 using namespace std; 6 const int MAXN = 1e6 + 5; 7 int f[MAXN], maxF[MAXN], a[MAXN]; 8 9 int main() 10 { 11 int N, M; 12 while (scanf("%d %d", &M, &N) == 2) 13 { 14 memset(f, 0, sizeof(f)); 15 16 for (int i = 1; i <= N; i++) 17 scanf("%d", a + i); 18 //初始化maxF[j]=0,因为把前j个元素分成0段,很显然最大值是0 19 memset(maxF, 0, sizeof(maxF)); 20 for (int i = 1; i <= M; i++) //M段 21 { 22 int nowMax = -INT_MAX; 23 for (int j = i; j <=N; j++) 24 { 25 f[j] = max(f[j - 1] + a[j], maxF[j - 1] + a[j]); 26 maxF[j - 1] = nowMax; //用上一轮的最大值更新上一轮应该更新的maxF[j] 27 nowMax = max(maxF[j - 1],f[j]); 28 } 29 maxF[N] = nowMax; //当前i,没有下一轮j了,不用延时更新,直接更新 30 } 31 cout << maxF[N] << endl; 32 } 33 return 0; 34 }
总结
- 感想:动态规划是真的难,这题想了快三天,不过最后貌似搞懂了,但是我感觉,还有更容易理解的思路过程,至少现在明白这题这样做的原理了,还要继续加油,真的太菜了
- 其他:递推其实很难写,可以定义好状态转移之后,先写记忆化搜索理解过程,再进一步变成递推;如果更新数组时,用到的以前的状态不多,可以考虑降维(滚动数组),有时候,题目的数据范围限定了必须使用一维数组