动态规划: 最大m子段和问题的详细解题思路(JAVA实现)
这道最大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实现)的更多相关文章
- Burst Balloons(leetcode戳气球,困难)从指数级时间复杂度到多项式级时间复杂度的超详细优化思路(回溯到分治到动态规划)
这道题目做了两个晚上,发现解题思路的优化过程非常有代表性.文章详细说明了如何从回溯解法改造为分治解法,以及如何由分治解法过渡到动态规划解法.解法的用时从 超时 到 超过 95.6% 提交者,到超过 9 ...
- 动态规划——A 最大子段和
A - 最大子段和 Time Limit:1000MS Memory Limit:32768KB 64bit IO Format:%I64d & %I64u Submit St ...
- 第十节:详细讲解一下Java多线程,随机文件
前言 大家好,给大家带来第十节:详细讲解一下Java多线程,随机文件的概述,希望你们喜欢 多线程的概念 线程的生命周期 多线程程序的设计 多线程的概念 多线程的概念:程序是静态的,进程是动态的.多进程 ...
- DP 状态 DP 转移方程 动态规划解题思路
如何学好动态规划(2) 原创 Gene_Liu LeetCode力扣 今天 算法萌新如何学好动态规划(1) https://mp.weixin.qq.com/s/rhyUb7d8IL8UW1IosoE ...
- 【动态规划】最大子段和问题,最大子矩阵和问题,最大m子段和问题
http://blog.csdn.net/liufeng_king/article/details/8632430 1.最大子段和问题 问题定义:对于给定序列a1,a2,a3……an,寻找它 ...
- 51nod 1118 机器人走方格 解题思路:动态规划 & 1119 机器人走方格 V2 解题思路:根据杨辉三角转化问题为组合数和求逆元问题
51nod 1118 机器人走方格: 思路:这是一道简单题,很容易就看出用动态规划扫一遍就可以得到结果, 时间复杂度O(m*n).运算量1000*1000 = 1000000,很明显不会超时. 递推式 ...
- 开发环境配置:jdk8的详细安装教程&&tomact的详细安装教程&&java环境变量的配置&&tomcat启动总失败原因
1.下载 链接: http://pan.baidu.com/s/1i57HZKx 密码: cnb4 2.详细安装过程 3.下载地址 链接: http://pan.baidu.com/s/1mi6VUp ...
- 【leetcode198 解题思路】动态规划
动态规划 https://blog.csdn.net/so_geili/article/details/53639920 最长公共子序列 https://blog.csdn.net/so_geili/ ...
- Moscow Pre-Finals Workshop 2016. Japanese School OI Team Selection. 套题详细解题报告
写在前面 谨以此篇题解致敬出题人! 真的期盼国内也能多出现一些这样质量的比赛啊.9道题中,没有一道凑数的题目,更没有码农题,任何一题拿出来都是为数不多的好题.可以说是这一年打过的题目质量最棒的五场比赛 ...
随机推荐
- eclipse出错
程序初次build project没有问题,代码没有做任何修改再次build project却出现了make[1]: ***这样的错误,这是为什么?尝试过修改一点代码后重新编译也可能出现make[1] ...
- VIM键盘映射 (Map)
http://www.pythonclub.org/linux/vim/map VIM键盘映射 (Map) 设置键盘映射 使用:map命令,可以将键盘上的某个按键与Vim的命令绑定起来.例如使用以下命 ...
- 如何做好 Android 端音视频测试?
在用户眼中,优秀的音视频产品应该具有清晰.低延时.流畅.秒开.抗丢包.高音效等特征.为了满足用户以上要求,网易云信的工程师通过自建源站,在SDK端为了适应网络优化进行QoS优化,对视频编码器进行优化, ...
- Port 3000 is already in use
cmd输入:netstat -ano | findstr :3000//查看是谁占用了3000号端口 显示如下 TCP 0.0.0.0:3000 0.0.0.0:0 LISTENING 18412 T ...
- 题解【2.23考试T1】div
1. div[题目描述] 这是一道传统题,源代码的文件名为 div.cpp/c/pas. 给定一棵树,你要判断是否存在一条边,使得割掉这条边后,这棵树被分成了点数相等的两部分,并且如果存在,请你找到这 ...
- 新手学习PHP的避雷针,这些坑在PHP开发中就别跳了
不要!用记事本编辑php文件 早些年能用记事本编程是一些人自我吹嘘的资本,能用记事本编程就是牛逼的代名词.但是这里要告诫大家的是,千万不要使用Windows自带的记事本编辑任何文本文件.用Window ...
- Qt核心剖析:信息隐藏
原文 http://devbean.blog.51cto.com/448512/326686 (一) 如果你阅读了 Qt 的源代码,你会看到一堆奇奇怪怪的宏,例如 Q_D,Q_Q.我们的Qt源码之旅就 ...
- Winform遍历窗口的所有控件(几种方式实现)
本文链接:https://blog.csdn.net/u014453443/article/details/85088733 扣扣技术交流群:460189483 C#遍历窗体所有控件或某类型所有控件 ...
- Django之form表单操作
小白必会三板斧 from django.shortcuts import render,HttpResponse,redirect HttpRespone:返回字符串 render:返回html页面 ...
- NABCD model作业
1)N(Need需求) 随着人类生活的快速发展,给人们带来了许多的便利,同时也给我们带来了一些麻烦,而我的拼图这个小游戏可以在人们在无聊时玩一玩,也可以给小孩子开发智力. 2)A(Approach做法 ...