最长子序列和问题

最长子序列和问题

本随笔写于20200121

问题描述:

算法1:枚举法

算法描述:以2 -3 -4 13 -5 2 -5 -3 12 -9 为例,分别计算以2,-3,-4…,-9开头的所有子序列和,如果新子序列的和大于当前最大值,则用新子序列的和替代当前最大值。最长序列的下标的更新就发生在最大值的更新过程中。

 

time cost : 枚举左端点和右端点,需要O(n2)的复杂度;根据计算新子序列之和的方式,总复杂度不一样:

    (a) 计算新子序列之和方式为 从新子序列头加到尾,计算复杂度为O(n),总时间复杂度为O(n3);

    (b) 计算新子序列之和方式为 新子序列和 = 旧子序列和 + 新增加的元素 - 新减少的元素,计算时间复杂度为O(1),总时间复杂度为O(n2)

 

代码实现:

#include<iostream>
using namespace std;
int maxm,a[100],sum;
int start,end;// record the index of the subsequence
int main(){
    int n;
    cin>>n;
    for(int i=0;i<n;i++) cin>>a[i];
    for(int i=0;i<n;i++){
        sum = a[i];
        if(sum > maxm) {
            maxm = sum;
            start = i;
            end = i;
        }
        for(int j=i+1;j<n;j++){
            sum += a[j];
            if(sum > maxm) {
                maxm = sum;
                start = i;
                end = j;
            }
        }
    }
    cout<<maxm<<endl;
    cout<<"from "<<start<<" to "<<end;
    return 0;
}
View Code

 

 

算法2:动态规划

算法描述:用变量sum记录当前子序列累加和,用maxm记录最大值。

从头开始进行累加,如果sum>max,则max = sum;如果sum<0,则将sum置0。

最长序列尾端下标的更新,发生在最大值更新过程中;

最长序列前端下标的更新,发生在第一个非负数时,因此下标为sum<0时的下标加一。

 

理解:最长子序列和的子序列肯定以正数开头,所以如果sum是负数,不妨将其设为0,等待下一个正数的到来。

 

time cost : O(n) (从头到尾只进行了一次扫描)

 

代码实现:

#include<iostream>
using namespace std;
int maxm,a[100],sum;
int start,end;// record the index of the subsequence
int main(){
    int n;
    cin>>n;
    for(int i=0;i<n;i++) cin>>a[i];
    for(int i=0;i<n;i++){
        sum += a[i];
        if(sum > maxm) {
            maxm = sum;
            end = i; 
        } 
        else if(sum < 0) {
            sum = 0;
            start = i+1;
        }
    }
    cout<<maxm<<endl;
    cout<<"from "<<start<<" to "<<end;
}
View Code

 

 另一种算法描述(更容易理解):见《算法笔记》P430;状态的无后效性 :P431

 

 

算法3:分治法(递归)

分治算法的核心是把问题分成两个大致相等的子问题,然后递归对它们求解,这是“分”的部分,在“治”这一阶段将两个子问题的解合并到一起求解。

算法描述:把数组分割成两部分,左半部分和右半部分,最大子序列出现的位置可能在:左半部、右半部、跨越分割的位置占据左右半部。

对于横跨整个序列的情况,我们采用以下方法:从中间开始,分别向左、向右遍历,分别得到向左、向右遍历的最大值,将这两个最大值相加得到跨越分割的位置占据左右半部的最大值,再与左半部、右半部的最大值比较,找到三者中的最大值,即为整个序列的最大值。[2]

 

代码实现:

#include<iostream>
using namespace std;

int a[100];
int ans,start,end,state;
int maxm(int a,int b,int c){
    if(a>b && a>c)  return a;
    else if(b>a && b>c) return b;
    else if(c>a && c>b) return c;
}

int merge(int left,int right){
    if(left == right) return a[left];
    int mid = (left+right) / 2;
    int t1 = merge(left,mid);
    int t2 = merge(mid+1,right);
    int suml = 0; int sumr = 0;
    int maxl = 0; int maxr = 0;
    for(int i=mid;i>=left;i--){
        suml += a[i];
        if(suml > maxl) {
            maxl = suml;
            start = i;
        }
    }
    for(int i = mid+1;i<=right;i++){
        sumr += a[i];
        if(sumr > maxr) {
            maxr = sumr;
            end = i;
        }        
    }
    return maxm(t1,t2,maxl+maxr);
}

int main(){
    int n;
    cin>>n;
    for(int i =0;i<n;i++){
        cin>>a[i];
    }
    ans = merge(0,n-1);
    cout<<ans<<endl;
    cout<<"from "<<start<<" to "<<end;
    
    return 0;
}
View Code

 

 time cost : O(nlogn)

 

测试数据 :

//test1
10
2 -3 -4 13 -5 2 -5 -3 12 -9
//test2
6
-2 11 -4 13 -5 -2
//test3
8
2 -3 -4 13 -5 2 -5 12

 

 

如何理解递归?[3]

看递归程序不要老想着它是怎么执行的,没必要看成“不停调用函数”。基本原理同数学归纳法。

递归程序理解两点:

1.只要假设第一句求出了前半段的最大子序列和,第二句求出了后半段的,而下面的合并过程在上面假设的基础上能正确执行。

2.终止条件,并且终止处是满足上面假设的。

 

 

练习题目:

http://codeup.cn/problem.php?cid=100000626&pid=0

 

Reference:

[1]《算法笔记》胡凡 P430-431

[2] 分治法算法思想:https://www.jianshu.com/p/c4d68d0376ad

[3] 如何理解递归:https://bbs.csdn.net/topics/300115494?locationNum=8&fps=1

 

posted @ 2020-01-30 16:52  icodes  阅读(1088)  评论(0)    收藏  举报