自打上次做了2084之后,尽管一直没再接触DP的题目,但是心里还是比较向往的。怎么说,尽管在小组中DP的部分没有被分给我,但是一点不知道也不好。这次接触了一个1003-Max Sum。
原题是给出一段序列,求其最大子序列的和以及起始和终止位置。下面是原题:
Problem Description
Given a sequence a[1],a[2],a[3]......a[n], your job is to calculate the max sum of a sub-sequence. For example, given (6,-1,5,4,-7),the max sum in this sequence is 6 + (-1) + 5 + 4 = 14.
Input
The first line of the input contains an integer T(1<=T<=20) which means the number of test cases. Then T lines follow,each line starts with a number N(1<=N<=100000), then N integersfollowed(all the integers are between -1000 and 1000).
Output
For each test case, you should output two lines. The first line is "Case #:", # means the number of the test case. The second line contains three integers, the Max Sum in the sequence, the start position of the sub-sequence, the end position of the sub-sequence. If thereare more than one result, output the first one. Output a blank line between two cases.
(样例略)
最初的时候我并不认为这是一个DP题目。于是我天真地写了一个最简单易懂的代码——简单地将每组子序列都计算出来再逐一比较:
#include<cstdio>
using namespacestd;
int t, n, ai, mem[100000], bestans=-1000000000, position[2], tempbest[3];
int main(){
scanf("%d", &t);
for(int i=0; i<t; i++){
scanf("%d", &n);
scanf("%d", &ai);
mem[0]=ai, bestans=ai, position[0]=1,position[1]=1;
for(int j=1; j<n; j++){
scanf("%d", &ai);
mem[j]=ai, tempbest[0]=ai,tempbest[1]=j+1, tempbest[2]=j+1;
for(int k=j-1; k>-1; k--){
if((mem[k]+ai) >= tempbest[0]){
tempbest[0]=mem[k]+ai;
tempbest[1]=k+1;
tempbest[2]=j+1;
}
mem[k]+=ai;
}
if(tempbest[0] > bestans){
bestans=tempbest[0];
position[0]=tempbest[1];
position[1]=tempbest[2];
}
}
printf("Case %d:\n%d %d %d\n", i+1, bestans, position[0], position[1]);
if(t != n-1)printf("\n");
}
return 0;
}
然而OJ无情的给了我一个TLE。我一看,这样做的复杂度就是O(n^2),不超时才怪……于是我又转回问题分析过程,然后……发现这个问题是可以被简化的。从第二个数据开始每次输入一个数据后,就会有两个新的子序列生成,然而生成的子序列的和一旦比其他同终点的子序列的和小,就会被淘汰掉。(注意:长度即数字个数,下同)
第一次,输入序列中第一个数后诞生了一个相对于部分序列(长度为1)的最大子序和;而第二次,在序列中第二个数输入后出现了两个新序列,它们其中一个是前一个部分序列的最大子序和与第二个数的和。这两个新的子序列和中必有一个被淘汰,而另一个和再与前一个部分序列的最大子序和比较,从而得到一个相对于长度为2的部分序列的最大子序和;
第三次和前面第二次的过程几乎一样,只是得到的结果是相对于长度为3的部分序列的最大子序和……
以此类推,当第n个(最后一个)数据读入后,经过常数次运算即可得到相对于长度为n的部分序列(整个序列)的最大子序和。
至此,应该不难看出,此题是由重叠的子问题构成的;每一个子问题相对于其他问题是独立的,因为对于每一个子问题,它们都不需要考虑其他子问题的求解过程;每一个子问题都有一个对于该问题的独一无二的最优解,所以具有最优子结构;当序列长度为1时,其最大子序和有唯一确定的答案,即边界。
于是我此时大悟,这是个DP。
关于备忘,每次只需记录下上一个最大部分子序列的值和它的开始/终止位置即可。
看一下时间。对于每组输入测试用例,所需的时间复杂度是O(n),在接受范围之内。
接下来是状态转移方程。
(注:mem记录前一个最大子序列和的值;tempbestpos数组存储每次的淘汰结果:[0]为值,[1]、[2]分别是起点、终点;bestans储存目前得到的最大子序和,position数组记录该最大子序和的起点和终点,mem == 0 和 ai == 0 是为配合代码输出“第一种可能”添加的条件)
if((mem+ai > ai) || (mem == 0)){
tempbestpos[0]=mem+=ai;
tempbestpos[2]=j+1;
}
else if((mem+ai < ai) || (ai == 0)){
tempbestpos[0]=mem=ai;
tempbestpos[1]=tempbestpos[2]=j+1;
}
if(tempbestpos[0] > bestans){
bestans=tempbestpos[0];
position[0]=tempbestpos[1];
position[1]=tempbestpos[2];
}
#include<cstdio>
using namespace std;
int t, n, ai, mem, bestans=-1000, position[2], temppos[3], tempbestpos[3];
int main(){
scanf("%d", &t);
for(int i=0; i<t; i++){
scanf("%d", &n);
scanf("%d", &ai);
mem=bestans=ai,tempbestpos[1]=position[0]=1, tempbestpos[2]=position[1]=1;
for(int j=1; j<n; j++){
scanf("%d", &ai);
if((mem+ai > ai) || (mem == 0)){
tempbestpos[0]=mem+=ai;
tempbestpos[2]=j+1;
}
else if((mem+ai < ai) || (ai ==0)){
tempbestpos[0]=mem=ai;
tempbestpos[1]=tempbestpos[2]=j+1;
}
if(tempbestpos[0] > bestans){
bestans=tempbestpos[0];
position[0]=tempbestpos[1];
position[1]=tempbestpos[2];
}
}
printf("Case %d:\n%d %d %d\n", i+1, bestans, position[0], position[1]);
if(i != t-1)printf("\n");
}
return 0;
}