这道最大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 实现)

 package com.cpc.dp;

 public class NMSum {

     public static void Sum(int[] a  ,int m ) {

         int n = a.length;        // n为数组中的个数
int[][] b = new int[n+1][m+1];
int[][] SUM = new int[n+1][m+1]; for(int p=0;p<=n;p++) { // 一个子段获数字都不取时 //
b[p][0] = 0;
SUM[p][0] = 0;
}
// for(int p=0;p<=m;p++) { // 当p > 0 时 并无意义, 此部分不会被用到,注释掉
// b[0][p] = 0;
// SUM[0][p] = 0;
// }
for(int j=1;j<=m;j++){
for (int i = j;i<=n-m+j;i++){ // n=1 m=1 此时最大1子段为 a[0] java 数组为从0开始的 需要注意 后面所有的第i个数为a[i-1];
if(i==1){
b[i][j] = a[i-1];
SUM[i][j] = a[i-1];
}else
{
//先假设 第i个数作为最后一个子段的一部分
b[i][j] = b[i-1][j] + a[i-1]; // 若第i个数作为单独子段时 b[i][j]更大 则把a[i-1] 作为单独子段
// 考虑特殊情况 若第一个数字为负数 b[1][1]为负数 在求b[2][1] SUM[1][0]=0>b[1][1] 则舍去第一个数字 此处合理
if(SUM[i-1][j-1]+a[i-1] > b[i][j]) b[i][j] = SUM[i-1][j-1] + a[i-1]; //填写SUM[i][j]供以后使用
if(j<i){ // i 比j 大时
if(b[i][j]>SUM[i-1][j]){ // 用b[i][j] 与之前求的比较
SUM[i][j] = b[i][j];
}else {
SUM[i][j] = SUM[i-1][j];
}
}else // i = j
{
SUM[i][j] = SUM[i-1][j-1] + a[i-1];
}
}
}//end for
}// end for
System.out.println(SUM[n][m]); // 输出结果
}// end of method public static void main(String[] args) {
int[] a = new int[]{1,-2,3,4,-5,-6,7,18,-9};
Sum(a, 3);
}
}

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 分析得差不多了, 废话少说,开始优化代码

 package com.cpc.dp;

 public class NMSUM2 {

     public static void Sum(int[] a  ,int m ) {

         int n = a.length;        // n为数组中的个数
int[] b = new int[n+1];
int[] SUM = new int[n+1]; b[0] = 0;// 一个子段获或者数字都不取时 ,也可以不设置,因为 java默认int数组中元素的初始值为0
SUM[1] = a[0]; for(int j=1;j<=m;j++){
b[j] = b[j-1] + a[j-1]; // i=j 时
SUM[j-1] = -1; // 第j 轮 SUM[j-1]表示原来的 SUM[j-1][j] 无意义 设置为-1
int temp = b[j];
for (int i = j+1;i<=n-m+j;i++){ //先假设 第i个数作为最后一个子段的一部分
b[i] = b[i-1] + a[i-1];
// 若第i个数作为单独子段时 b[i][j]更大 则把a[i-1] 作为单独子段
if(SUM[i-1]+a[i-1] > b[i]) b[i] = SUM[i-1] + a[i-1]; //下面原来计算的是原来的SUM[i][j] ,但是现在要修改的应该是原来的SUM[i][j-1] ,如何把SUM[i][j]保存 下来?
// 可以在循环外面定义一个变量temp来暂存 等下一次循环再写入
SUM[i-1] = temp;
if(b[i]>temp){
temp = b[i]; //temp 记录SUM[i][j]
}
}//end for
SUM[j+n-m] = temp;
}// end for
System.out.println(SUM[n]); // 输出结果
}// end of method public static void main(String[] args) {
int[] a = new int[]{1,-2,3,4,-5,-6,7,18,-9};
Sum(a, 1);
}
}

output: 25

算法的空间复杂度变为o(n)~  优化完毕!

3 如何记录具体每个子段对应,下次再做。有谁做出了也可以直接评论。

可以转载 注明出处 http://www.cnblogs.com/chuckcpc/

--by陈培城

动态规划: 最大m子段和问题的详细解题思路(JAVA实现)的更多相关文章

  1. Burst Balloons(leetcode戳气球,困难)从指数级时间复杂度到多项式级时间复杂度的超详细优化思路(回溯到分治到动态规划)

    这道题目做了两个晚上,发现解题思路的优化过程非常有代表性.文章详细说明了如何从回溯解法改造为分治解法,以及如何由分治解法过渡到动态规划解法.解法的用时从 超时 到 超过 95.6% 提交者,到超过 9 ...

  2. 动态规划——A 最大子段和

    A - 最大子段和 Time Limit:1000MS     Memory Limit:32768KB     64bit IO Format:%I64d & %I64u Submit St ...

  3. 第十节:详细讲解一下Java多线程,随机文件

    前言 大家好,给大家带来第十节:详细讲解一下Java多线程,随机文件的概述,希望你们喜欢 多线程的概念 线程的生命周期 多线程程序的设计 多线程的概念 多线程的概念:程序是静态的,进程是动态的.多进程 ...

  4. DP 状态 DP 转移方程 动态规划解题思路

    如何学好动态规划(2) 原创 Gene_Liu LeetCode力扣 今天 算法萌新如何学好动态规划(1) https://mp.weixin.qq.com/s/rhyUb7d8IL8UW1IosoE ...

  5. 【动态规划】最大子段和问题,最大子矩阵和问题,最大m子段和问题

    http://blog.csdn.net/liufeng_king/article/details/8632430 1.最大子段和问题      问题定义:对于给定序列a1,a2,a3……an,寻找它 ...

  6. 51nod 1118 机器人走方格 解题思路:动态规划 & 1119 机器人走方格 V2 解题思路:根据杨辉三角转化问题为组合数和求逆元问题

    51nod 1118 机器人走方格: 思路:这是一道简单题,很容易就看出用动态规划扫一遍就可以得到结果, 时间复杂度O(m*n).运算量1000*1000 = 1000000,很明显不会超时. 递推式 ...

  7. 开发环境配置:jdk8的详细安装教程&&tomact的详细安装教程&&java环境变量的配置&&tomcat启动总失败原因

    1.下载 链接: http://pan.baidu.com/s/1i57HZKx 密码: cnb4 2.详细安装过程 3.下载地址 链接: http://pan.baidu.com/s/1mi6VUp ...

  8. 【leetcode198 解题思路】动态规划

    动态规划 https://blog.csdn.net/so_geili/article/details/53639920 最长公共子序列 https://blog.csdn.net/so_geili/ ...

  9. Moscow Pre-Finals Workshop 2016. Japanese School OI Team Selection. 套题详细解题报告

    写在前面 谨以此篇题解致敬出题人! 真的期盼国内也能多出现一些这样质量的比赛啊.9道题中,没有一道凑数的题目,更没有码农题,任何一题拿出来都是为数不多的好题.可以说是这一年打过的题目质量最棒的五场比赛 ...

随机推荐

  1. 使用tableExport.js直接导出web页面上的table

    1,需要导入两个js文件,一个tableExport.js,另一个是jquery.base64.js,前一个文件是导出数据和核心类库,后一个是为了避免导出中文时乱码的js文件,如果你导出的数据没有中文 ...

  2. vue.js + element中el-select实现拼音匹配,分词、缩写、多音字匹配能力

    1.既然要用到拼音搜索,我们就需要一个拼音库,在这里我推荐一个第三方包:https://github.com/xmflswood/pinyin-match,在这里首先对这个包的开发者表示万分的感谢. ...

  3. break continue goto

    break:跳出本层循环:continue:结束本次循环,并不跳出循环:goto:转移到指定位置,无条件转移:

  4. ASP.NET + MVC5 入门完整教程七 -—-- MVC基本工具(下)

    https://blog.csdn.net/qq_21419015/article/details/80493633 Visual Stdio 的单元测试

  5. 复制到粘贴板 && 提示

    copyText(String); TipLayer.showTip(String,timeOut);

  6. 8.1.1 IO

    IO对象无拷贝或赋值.进行IO操作的函数通常以引用的方式传递和返回流,且该引用不能是const的 确定一个流对象是否处于良好状态的最简单的方法是将它作为一个条件来使用 while (cin >& ...

  7. Go之第三方库ini

    文章转自 快速开始 my.ini # possible values : production, development app_mode = development [paths] # Path t ...

  8. Spring Boot中快速操作Mongodb

    Spring Boot中快速操作Mongodb 在Spring Boot中集成Mongodb非常简单,只需要加入Mongodb的Starter包即可,代码如下: <dependency> ...

  9. 第二十八篇 玩转数据结构——堆(Heap)和有优先队列(Priority Queue)

          1.. 优先队列(Priority Queue) 优先队列与普通队列的区别:普通队列遵循先进先出的原则:优先队列的出队顺序与入队顺序无关,与优先级相关. 优先队列可以使用队列的接口,只是在 ...

  10. C#中DataSet、SqlDataAdapter的使用-关于数据库操作

    本文链接:https://blog.csdn.net/xubaifu1997/article/details/51816785 DataSet 表示数据在内存中的缓存. 我的理解是,在内存中的数据表, ...