HDUOJ_1003: 最大子段和解法深入解读
来源:互联网 发布:温州淘宝打包招聘 编辑:程序博客网 时间:2024/05/17 22:10
1. 题目
- Max Sum 最大子段和 (Click Me)
2. 题干分析
- 输入分析: 输入的数组的下标是1到n。但是在大部分程序设计语言中,数组下标都是0到n-1,所以在程序实现的时候,注意下标+1.
- 输入分析: 序列的最大长度为100000 (10万), 序列中的值的取值范围是
[−1000,1000] 。 为什么会如此限制序列的范围呢?原因是要使得序列和满足如下不等式:int32.MIN≤∑i=1nai≤int32.MAX
很明显,100000∗(−1000)=−108≥int32.MIN 100000∗1000=108≤int32.MAX
所以,说在程序中使用 32位的有符号整型存储最大和值就足够了。 - 输出分析: 从Sample Output中,可以看出第二个输出结果还是比较奇怪的。输出是(7, 1, 6),意思是最大子序列和是7,起始下标是1,结束下标是6。就是下图中的数组下方指出的解。但是,(7, 6, 6)也是一种可能的解,如下图数组右上方所示。而且(我认为)二者之间并没有顺序关系。题目要求输出第一个解,但是不能区分开这两种解。所以在程序实现的时候,要注意这个细节。
- 题目没有明确说明的问题: 如果序列全为负数的时候,解是不是可以认为是0。也就是说,不选任何元素也看做是一种特殊的子序列。从输出的格式上看,因为要输出最优解的起始和结束下标,考虑到如果将全负数序列的最优解看做零,是无法输出的,所以,将全负数序列的最优解看做零的可能性不大。
3. 题目形式化描述
经过上面的分析,我们给出相关概念与题目的形式化描述,
3.1子序列
3.2 子序列和
3.3 题目的形式化描述
对于输入的整数序列:
求得
maxSum 满足:
- 对
iMax,jMax , 我们先定义一个解集合(很明显,可能会有多各解)solutionSet , 如下:solutionSet={(i,j)|∑k=ijak=maxSum}
那么, 根据题目中的"If there are more than one result, output the first one.",我们的解要满足:对于 ∀(i,j)∈solutionSet,都有 iMax≤i
所以,对于题目中的第二个示例的解,我们就没那么困惑了。其实这个地方我们是定义了解之间的序关系,它是由起始下标决定的。
4. 解题思路与分析
比较常见的解题方法有三种,在王晓东编著的《计算机算法设计与分析》有一节最大子段和一节讲了这三个方法:全部遍历的常规方法、分治算法和动态规划。三者的时间复杂度分别是
其实,在我看来这个题目并不是动态规划的典型例子。它只是部分过程用到了动态规划的技术。首先,我们将这个问题做一个转换,如下:
如果将
现在问题就转化为从
- 看最优值是否能从子问题的最优值中得来
- 递归地定义最优值
- 根据递归的定义,以自底向上的方式计算出最优值
- 在计算最优值的过程中,记录下最优解
那么,我们就按照以上四步,来分析和计算
为解决最终问题,我们要计算出
, 我们写出如下递归方法:
public class Main{ public static int getBj(int[] a,int j){ if( j==0 ){ return a[0]; } int bjminusone=getBj(a, j-1) if( bjminusone < 0 ){ return a[j] }else{ return bjminusone+a[j]; } } public static void main(final String args[]){ int[] a = ...; int bn=getBj(a,a.length-1) }}
再将这个递归算法转换为自底向上的迭代算法。注意代码中用b[j]来表示
//用b来存储b[j]的值int b =-1;for( int i=0; i < a.length; i++ ){ if( b < 0 ) { //The b[i] is a[i] b = a[i]; }else{ b = b+a[i]; }}//循环结束后,b中存储了b[n]。但是在每次迭代中,我们都计算了b[i]。//每次迭代,我们也重用了上一次子问题的解。这也正是动态规划的特色。
很明显,我们离最终答案已经很近了。因为我们每一步已经计算出了
5. 测试用例
这些测试用例,我还没有测过,但是在实现程序的时候,总要考虑这些或典型或特殊的情况。
6. 我的解答
import java.io.*;import java.util.Scanner;public class Main { /** * @param a 包含输入序列的Java整形数组 * @return 大小为3的数组,[0]为最优值,[1],[2]分别为最优值得 * 起始和结束下标 */ public static int[] maxSubSequence(int[] a) { int[] ret = new int[3]; int n = a.length; //由题目可知,最大子段和一定大于-1001 //所以将初始的最大子段和设置为-1001 int maxSum = -1001; //b为初始的记录b[j]的变量 //将它设置为-1,由递归式可知,第一次迭代就会被舍弃 int b = -1; //maxStart和maxEnd记录最优解 //初始值设为-1,在程序出错时,更容易debug int maxStart = -1; int maxEnd = -1; //start和end记录b[j]的开始和结束下标 //maxStart和maxEnd就是从这些start和end选出的。 int start = -1; int end = -1; //开始循环求解 for (int i = 0; i < n; i++) { if (b < 0) { //b[i-1]小于零,所以b[i]为a[i], //那么,b[i]的起始和结束下标都为i,记录下来 b = a[i]; start = i; end = i; } else { //否者, b[i]就为b[i-1]+a[i] //起始下标保持不变,更新结束下标到i b = b + a[i]; end = i; } //更新题目的最终的最优解 if (b > maxSum) { maxSum = b; maxStart = start; maxEnd = end; } } ret[0] = maxSum; //因为题目的输入时1到n,所以注意+1 ret[1] = maxStart + 1; ret[2] = maxEnd + 1; return ret; } public static void main(final String[] args) { Scanner in = new Scanner(new BufferedReader(new InputStreamReader(System.in))); PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out)); int caseNumber = 0; if (in.hasNextInt()) { caseNumber = in.nextInt(); } for (int i = 0; i < caseNumber; i++) { int arraySize = 0; if (in.hasNextInt()) { arraySize = in.nextInt(); } int[] array = new int[arraySize]; for (int j = 0; j < arraySize; j++) { array[j] = in.nextInt(); } int[] result = maxSubSequence(array); out.printf("Case %s:\r\n", i + 1); if (i == caseNumber - 1) { out.printf("%s %s %s\r\n", result[0], result[1], result[2]); } else { out.printf("%s %s %s\r\n\r\n", result[0], result[1], result[2]); } } out.flush(); }}
7. 正确性证明与分析
7.1 第一次循环能够正确执行
由于b<0,所以一次循环更新 b=a[0], start=0, end=0。
对应地,由于maxSum初始值为-1001一定小于a[0], 所以,会更新: maxSum=a[0],maxStart=0,maxEnd=0。
7.2 如果第i-1次循环能够正确完成,第i次循环也能够正确完成
此处分析略。基本上就是上面递归式的重复。
7.3 循环可以正确结束
循环结束是在i=n-1计算完成后。这个时候,我们整个循环就已经计算出了
8. 复杂度分析
很显然,时间复杂度
9. 运行结果与评价
10 典型测试用例的运行过程
为了加强对算法的理解,可以用纸和笔手工按照这个算法运行一下。
输入: -1,6,-3,8,-12,5 期待最优值:11
10.1 动态规划
用表格的方法实施动态规划的方法如下,
基本评价:
- 时间复杂度: O(n)
- 加法运算次数:3 次
- 空间复杂度: O(1)
- 程序复杂度: 简单
动态规划算法有点像是一种贪婪算法,又可以得到最优解。
10.2 枚举法(最简单直观的方法)
用表格的方法实施最简单的枚举法,如下:
基本评价:
- 时间复杂度: O(n^2),
- 加法预算次数: 15次
- 空间复杂度: O(1)
- 程序复杂度: 简单
- 分析: 没有避免计算无用的解,例如 (1,2)。
这个算法可以作为衡量正确性的标准或是生成测试用例。而且我们很容易想到的就是这种算法,我们在具体解决问题是可以先用这个算法,以加深对问题的理解,并分析该算法的优缺点,从而,可以发散出优化的思路。
10.3 分治法
分治法是递归实现的。具体方法可见王晓东编著的《计算机算法设计与分析》最大子段和一节。所以,我要画个树去实施该算法,如下:
基本评价:
- 时间复杂度:O(nlgn)
- 加法运算次数: 11 次
- 空间复杂度:O(lgn)
- 程序复杂度: 较复杂
就从树的结构来看,该算法就比其他两种算法要复杂的多。而且有些东西还需要额外的解释才能看得懂。上面的蓝色方框是分解子问题,黄色方框是开始求解,也就是递归调用的一层层返回。最下面的蓝色,就是递归调用完成得到的最优解。下面解释一下红色方框是什么意思。因为分治是分成的两个子问题,但是子问题中更好的最优解并不一定是父问题的最优解,还要看有没有横跨两个子问题序列的最优解更好。而红色方框计算的就是这个问题。红色方框旁边是加法次数。
11. 遇到的问题,分析与经验:
11.1 解题顺序
- 理解题目,写形式化表达和典型测试用例。
- 寻找解题方法,先尝试暴力的枚举,逐步优化;或者分析问题结构与特点
- 写伪代码,如果需要,划分一下模块。如果,问题比较大,可以先考虑划分模块
- 实现代码
- 代码走读、分析、证明和复查。注意边界条件
- 运行测试用例
11.2 分析出问题所在,而不是针对某个失败测试尝试修修补补。
对于题目中的第二例子(0 6 -1 1 -6 7 -5),我的程序开始的输出是(7,6,6)。对于这个失败的测试用例,我就加了如下一句:
if( iMax == jMax ){ iMax = 0;}
然后,再去试一下能不能通过在线评判。这明显是修修补补而不是找到问题的所在,问题的症结是在于如下的判断条件:
if( b <= 0 ){ ...}else{ ...}
改为b<0才能真正修掉程序的问题。
12.参考
王晓东 《计算机算法设计与分析》
- HDUOJ_1003: 最大子段和解法深入解读
- 最大子段和:线性序列的最大子段和的三种解法
- 最大子段和详解(N种解法汇总)
- 最大子段和的动态规划解法
- 最大子段和详解(N种解法汇总)
- 最大子段和问题的动态规划解法
- 最大子段和解法及python实现
- 最大子段分治法解法
- 最大子段和
- 最大子段和
- 最大子段和
- 最大子段和
- “最大子段和”
- 最大子段和
- 最大子段和
- 最大子段和
- 最大子段和
- 最大子段和
- 【OpenCV】有关内存释放的一些问题
- 113 鳴谷 李山甫 跌足折齒
- 跨移动终端平台实现
- 【基础练习】【模拟】Uva133 - The Dole Queue题解
- Sort Colors
- HDUOJ_1003: 最大子段和解法深入解读
- 我的学习之旅(34) sched.c
- [Jsp]防止页面表单重复提交的解决方法
- 在Amazon Tech(o) 上的主题演讲--软件致简之道
- 游戏中常见(或必须)的42种要素
- 寒假项目4-点与距离
- java图形验证码生成工具类
- NetBeans的远程Linux C开发实践
- -command 用的是函数的引用