Week12 作业 C - HDU 1024

题目描述:

给定一个具有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个元素)

记忆化搜索+二维数组的直接实现(容易理解)

 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;
View Code

不难发现,上述的时间复杂度是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+二维数组

 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;
View Code

降维maxF

必须延时更新,否则会覆盖需要用到的结果,而且正序枚举j

 降维maxF代码

 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;
View Code

降维f

考虑最初的状态转移方程中的前者,即f[i][j-1],发现i就是当前的i,所以第i轮的f数组f[i]和第i-1轮的数组f[i-1]是没有直接更新的联系的,所以可以直接用一维的f

最终代码

 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 }
View Code

总结

  • 感想:动态规划是真的难,这题想了快三天,不过最后貌似搞懂了,但是我感觉,还有更容易理解的思路过程,至少现在明白这题这样做的原理了,还要继续加油,真的太菜了
  • 其他:递推其实很难写,可以定义好状态转移之后,先写记忆化搜索理解过程,再进一步变成递推;如果更新数组时,用到的以前的状态不多,可以考虑降维(滚动数组),有时候,题目的数据范围限定了必须使用一维数组
posted @ 2020-05-30 18:37  菜鸡今天学习了吗  阅读(159)  评论(0编辑  收藏  举报