这道最大m子段问题我是在课本《计算机算法分析与设计》上看到,课本也给出了相应的算法,也有解这题的算法的逻辑。但是,看完之后,我知道这样做可以解出正确答案,但是我如何能想到要这样做呢? 课本和网上的某些答案都讲得比较晦涩,有些关键的步骤不是一般人可以想得到的。不仅要知其然,还要知其所以然。否则以后我们遇到类似的问题还是不会解。
下面是我解这道题的思考过程。我按照自己的想法做,做到最后发现和课本的思想差不多,也有一点差别。如果对这道题有些不明白,可以仔细看看,相信看完之后你会豁然开朗。
问题: 给定n个整数(可能为负数)组成的序列 以及一个正整数m,要求确定序列的m个不相交子段,使这m个子段的总和达到最大。
0 首先举个例子方便理解题目 如果 = {1,-2,3,4,-5,-6,7,8,-9} m=2 明显所求两个子段为{3,4}{7,8} 最大m子段和为26。
1 先想如何求得最大子段和。
1.1最容易想到的方法是穷举法。列出所有的子段组合,求出每个组合的子段和,所有组合中最大者即为所求。
仔细分析后发现:计算量巨大且难以实现。果断放弃。
1.2 分析:用数组a[1…n]来表示一个序列,用二维数组SUM[n][m]表示由数组a的前n个数字组成的子序列的最大m子段。(可知 n>=m)
SUM[n][m]即为所求.
分析最后一个数字a[n]所有可能的情况
1) a[n] 不属于构成最大m子段和的一部分, SUM [n][m] = SUM [n-1][m]
2) a[n] 属于构成最大m子段和的一部分, 且a[n]单独作为一个子段。
此时SUM[n][m] = SUM[n-1][m-1]+a[n];
3) a[n] 属于构成最大m子段和的一部分, 且a[n]作为最后一个子段的一部分。
此时比较复杂, a[n]只是作为最后一个子段的一部分, 所以a[n-1]也一定在最后一个子段之中,否则a[n]便是一个单独的子段,前后矛盾.
所以SUM[n][m] = (包含a[n-1]的由前n-1个数字组成的子序列的最大m子段和) + a[n]
若用 b[n][m] 表示包含a[n]的、由前n个数字组成的子序列 的最大m子段和。
则 SUM[n][m] = b[n-1][m] + a[n]
1.3 我们仔细观察第三种情况里面定义的b[n][m]: 包含a[n]的、由前n个数字组成的子序列 的最大m子段和。
假设a[k] (k∈[m,n])是 原问题的解中的最后一个子段的最后一个元素, 则b[k][m]即为原问题的最大子段和。(若不明白再多看b的定义几次~)
所以原问题所求的最大子段和SUM[n][m]为
1.4 现在就好办了, 分析b[n][m]的值如何计算。
回顾我们刚刚对a[n]的分析的三种情况中的后面两种
1) a[n]单独作为一个子段,
则 b [n][m] = SUM[n-1][m-1] + a[n]
(而SUM[n-1][m-1]= )
2)
a[n]作为末尾子段的一部分
则 b[n][m] = b[n-1][m]+a[n]
分别计算情况1) 和情况2) 下的b[n][m], 比较, 取较大者.
a)
特殊情况,
若m=n 则a[n]为单独子段 按情况1)计算
若n=1 则SUM[n][m] =
a[n]
1.5 到这里很明显可以看出这是一个动态规划的问题,还不太懂动态规划也没关系,你只要记得,要计算b[i][j], 需要有:SUM[i-1][j-1]、b[i-1][j] 。
而 SUM[i-1][j-1]由数组b算出。需要先算出 b[k][j-1] (j-1<=k <=i-1 )。参见前面SUM的推导.
所以我需要先知道 b[k][j-1] (j-1<=k <=i-1 ) 以及 b[i-1][j]
所以,数组b 如何填写?不明白可以画个表看看
比如上表:在求SUM[8][4]时,我们需要先求的为图中黄色区域.
黑色部分不可求(无意义), 白色部分在求解的时候不需要用到.
可以看出 我们只需要求 当 1<=j<=m 且 j<=i<=n-m+j 部分的b[i][j]就可以得出解.(此处我用画图 有谁可以有更方便的方法来理解欢迎讨论)
至此 我们大概知道此算法如何填表了,以下为框架.
for(int j=1; j<=m ; j ++)
for(int i= j ;i <= n-m + i ; j++)
1.6 开始写算法(我用java 实现)
1 package com.cpc.dp; 2 3 public class NMSum { 4 5 public static void Sum(int[] a ,int m ) { 6 7 int n = a.length; // n为数组中的个数 8 int[][] b = new int[n+1][m+1]; 9 int[][] SUM = new int[n+1][m+1]; 10 11 for(int p=0;p<=n;p++) { // 一个子段获数字都不取时 // 12 b[p][0] = 0; 13 SUM[p][0] = 0; 14 } 15 // for(int p=0;p<=m;p++) { // 当p > 0 时 并无意义, 此部分不会被用到,注释掉 16 // b[0][p] = 0; 17 // SUM[0][p] = 0; 18 // } 19 for(int j=1;j<=m;j++){ 20 for (int i = j;i<=n-m+j;i++){ 21 22 // n=1 m=1 此时最大1子段为 a[0] java 数组为从0开始的 需要注意 后面所有的第i个数为a[i-1]; 23 if(i==1){ 24 b[i][j] = a[i-1]; 25 SUM[i][j] = a[i-1]; 26 }else 27 { 28 //先假设 第i个数作为最后一个子段的一部分 29 b[i][j] = b[i-1][j] + a[i-1]; 30 31 // 若第i个数作为单独子段时 b[i][j]更大 则把a[i-1] 作为单独子段 32 // 考虑特殊情况 若第一个数字为负数 b[1][1]为负数 在求b[2][1] SUM[1][0]=0>b[1][1] 则舍去第一个数字 此处合理 33 if(SUM[i-1][j-1]+a[i-1] > b[i][j]) b[i][j] = SUM[i-1][j-1] + a[i-1]; 34 35 //填写SUM[i][j]供以后使用 36 if(j<i){ // i 比j 大时 37 if(b[i][j]>SUM[i-1][j]){ // 用b[i][j] 与之前求的比较 40 SUM[i][j] = b[i][j]; 41 }else { 42 SUM[i][j] = SUM[i-1][j]; 43 } 44 }else // i = j 45 { 46 SUM[i][j] = SUM[i-1][j-1] + a[i-1]; 47 } 48 } 49 }//end for 50 }// end for 51 System.out.println(SUM[n][m]); // 输出结果 52 }// end of method 53 54 public static void main(String[] args) { 55 int[] a = new int[]{1,-2,3,4,-5,-6,7,18,-9}; 56 Sum(a, 3); 57 } 58 }
output : 33
测试通过
/************** 4.22 更新***************************/
2 算法的优化
2.1 分析 算法的空间复杂度 为O(mn).我们观察一下,在计算b[i][j]时 我们用到b[i-1][j] 和 SUM[i-1][j-1],也就是说,每次运算的时候 我们只需要用到数组b的这一行以及数组SUM的上一行.
我们观察一下算法的框架
for(int j=1; j<=m ; j ++)
for(int i= j ;i <= n-m + i ; j++)
// 计算b[i][j] 需要 SUM[i-1][j-1] 和 b[i-1][j]
// 计算SUM[i][j] 需要 SUM[i-1][j] b[i][j] SUM[i-1][j-1]
假设在 j=m 时(即最外面的for循环计算到最后一轮时)
要计算b[*][j] *∈[m,n]
我只需要知道 SUM[*-1][j-1] b[*-1][j] (即需要上一轮计算的数组SUM以及这一轮计算的数组b)
而之前所求的数组SUM和数组b其他部分的信息已经无效,
我们只关心最后一轮计算的结果,而最后一轮计算需要倒数第二轮计算的结果.
倒数第二轮计算需要再倒数第三结果.以此循环
因此我们可以考虑重复利用空间,
在一个位置所存储的信息已经无效的时候,可以覆盖这个位置,让它存储新的信息.
举个例子: 老师在黑板上推导某一个公式的时候, 黑板的面积有限,而有时候推导的过程十分长,很快黑板不够用了,这个老师通常会擦掉前面推导的过程,留下推导下一步要用的一部分,在擦掉的地方继续写.
但是如何安全地覆盖前面失效的内容而不会导致覆盖掉仍然需要使用的内容呢?
分析后可以得知一下约束:
1) 求b[i][j] 需要用到SUM[i-1][j-1] 所以SUM[i-1][j-1]必须在b[i][j]的值求完后才可以被覆盖
2) 求b[i][j] 需要用到 b[i-1][j] (j 相等)
3) 求SUM[i][j] 需要用到 SUM[i-1][j] (j 相等)
4) 求SUM[i][j] 需要用到 b[i][j] (j 相等)
5) 求SUM[i][j] 需要用到SUM[i-1][j-1] (i的位置错开)
对于最外面的for循环
我们只关心最后一轮(也就是第(j=m)轮)的结果,所以考虑把两个二维数组变成一维数组b[1...n] 、SUM[1..n]
假设在第j轮计算后:
b[i] 表示的意义与原来的 b[i][j]相同 ( 也就是原来的b[i][j]会覆盖b[i][j-1] )
SUM[i] 表示什么呢
我们观察约束1)知道,在第j 轮计算 b[i] (即原来的b[i][j])时,仍然会用到原来SUM[i-1][j-1],
也就是说 , 在计算b[i]时,SUM[i-1] 需要存储的是原来的SUM[i-1][j-1]
对于里面的for 循环
由于计算 b[i]需要SUM[i-1]
所以在计算完b[i] 后才计算新的SUM[i-1]
即在b[i]计算完后,可以覆盖掉SUM[i-1] 使之表示原来的SUM[i-1][j]
也就是说, 在第j轮计算完毕后, SUM[i] 表示的意义与原来的SUM[i][j]相同
2.2 分析得差不多了, 废话少说,开始优化代码
1 package com.cpc.dp; 2 3 public class NMSUM2 { 4 5 public static void Sum(int[] a ,int m ) { 6 7 int n = a.length; // n为数组中的个数 8 int[] b = new int[n+1]; 9 int[] SUM = new int[n+1]; 10 11 b[0] = 0;// 一个子段获或者数字都不取时 ,也可以不设置,因为 java默认int数组中元素的初始值为0 12 SUM[1] = a[0]; 13 14 for(int j=1;j<=m;j++){ 15 b[j] = b[j-1] + a[j-1]; // i=j 时 16 SUM[j-1] = -1; // 第j 轮 SUM[j-1]表示原来的 SUM[j-1][j] 无意义 设置为-1 17 int temp = b[j]; 18 for (int i = j+1;i<=n-m+j;i++){ 19 20 //先假设 第i个数作为最后一个子段的一部分 21 b[i] = b[i-1] + a[i-1]; 22 // 若第i个数作为单独子段时 b[i][j]更大 则把a[i-1] 作为单独子段 23 if(SUM[i-1]+a[i-1] > b[i]) b[i] = SUM[i-1] + a[i-1]; 24 25 //下面原来计算的是原来的SUM[i][j] ,但是现在要修改的应该是原来的SUM[i][j-1] ,如何把SUM[i][j]保存 下来? 26 // 可以在循环外面定义一个变量temp来暂存 等下一次循环再写入 27 SUM[i-1] = temp; 28 if(b[i]>temp){ 29 temp = b[i]; //temp 记录SUM[i][j] 30 } 31 }//end for 32 SUM[j+n-m] = temp; 33 }// end for 34 System.out.println(SUM[n]); // 输出结果 35 }// end of method 36 37 public static void main(String[] args) { 38 int[] a = new int[]{1,-2,3,4,-5,-6,7,18,-9}; 39 Sum(a, 1); 40 } 41 }
output: 25
算法的空间复杂度变为o(n)~ 优化完毕!
3 如何记录具体每个子段对应,下次再做。有谁做出了也可以直接评论。
可以转载 注明出处 http://www.cnblogs.com/chuckcpc/
--by陈培城