动态规划-最大子段和

合集下载
  1. 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
  2. 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
  3. 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。

动态规划-最⼤⼦段和
2018-01-14 21:14:58
⼀、最⼤⼦段和问题
问题描述:给定n个整数(可能有负数)组成的序列a1,a2,...,an,求该序列的最⼤⼦段和。

如果所有整数都是负数,那么定义其最⼤⼦段和为0。

⽅法⼀、最⼤⼦段和的简单算法
显然可以在O(n^2)的时间复杂度上完成这个问题。

但是是否可以对算法进⾏优化呢?答案是肯定的。

⽅法⼆、分治算法
朴素的分法是⼆分,问题就是如何merge,在merge的时候,因为已经确定了会包含边界点,所以可以在O(n)的时间复杂度上完成merge 时的最⼤⼦段和。

因此分治公式是:T(n) = 2T(n/2)+O(n)
根据主定理可以算得,分治算法的时间复杂度为O(nlgn)。

int maxSubSum(int[] a,int L,int R){
if(L == R) return a[L] > 0 ? a[L] : 0;
else {
int mid = L + (R - L) / 2;
int sumL = maxSubSum(a, L, mid);
int sumR = maxSubSum(a, mid + 1, R);
int tmpL = 0;
int tmpR = 0;
int sum = 0;
for (int i = mid; i >=0 ; i--) {
sum += a[i];
if(sum>tmpL) tmpL = sum;
}
sum = 0;
for (int i = mid+1; i <=R ; i++) {
sum += a[i];
if(sum>tmpR) tmpR = sum;
}
return Math.max(sumL,Math.max(sumR,tmpL+tmpR));
}
}
⽅法三、动态规划
这种两端都是变化的问题是很难优化的,最好可以让⼀端固定,这样就会⼤⼤简化分析难度。

于是将j暂时从原式⼦中提取出来,将剩下的命名为b[j]。

所以原问题就变成了这样。

根据b[j]的定义,b[j]是指以a[j]结尾的最⼤⼦段和。

因此有如下公式:
b[j] = max{b[j - 1] + a[j] , a[j]} 1=<j<=n
有了b[j],再对他取个极值,就可以得到原问题的解。

时间复杂度为O(n)
int maxSum(int[] a) {
int res = 0;
int b = 0;
for (int i = 0; i < a.length; i++) {
b = Math.max(b + a[i], a[i]);
if (b > res) res = b;
}
return res;
}
⼆、推⼴问题
最⼤⼦矩阵和问题
问题描述:给定⼀个m*n的整数矩阵A,试求矩阵A的⼀个⼦矩阵,使其各个元素之和最⼤。

问题分析:事实上,只需要将矩阵“压扁”就可以规约到最⼤⼦段和问题,具体来说就是将多⾏进⾏求和变为⼀⾏,这样就可以直接使⽤上述问题的解法。

将多⾏“压缩”成⼀⾏有多种可⾏⽅案,需要遍历⼀下,花费O(m^2),最⼤⼦段和动态规划算法花费O(n),所以总的时间消耗是O(m^2*n)。

最⼤m⼦段和问题
问题描述:给定n个整数(可能有负数)组成的序列a1,a2,...,an,以及⼀个正整数m,要求确定该序列的m个不相交⼦段,使这m个⼦段的总和最⼤。

问题分析:设b(i, j)表⽰数组a的前j项中i个⼦段和的最⼤值,且第i个⼦段含a[j](1<=i<=m,i<=j<=n),则所求的最优值显然为max b(m, j),其中 m <= j <= n。

与最⼤⼦段和类似。

计算b(i,j)的递归式⼦为:
b(i, j) = max{ b(i, j-1) + a[j] , max{ b(i - 1, t) + a[j] 其中t = i - 1 ~ j - 1} }
初始时,b(0, j) = 0; b(i, 0) = 0。

#include "stdafx.h"
#include <iostream>
using namespace std;
int MaxSum(int m,int n,int *a);
int main() {
int a[] = {0,2,3,-7,6,4,-5};//数组脚标从1开始
for(int i=1; i<=6; i++) {
cout<<a[i]<<" ";
}
cout<<endl;
cout<<"数组a的最⼤连续⼦段和为:"<<MaxSum(3,6,a)<<endl;
}
int MaxSum(int m,int n,int *a) {
if(n<m || m<1)
return 0;
int **b = new int *[m+1];
for(int i=0; i<=m; i++) {
b[i] = new int[n+1];
}
for(int i=0; i<=m; i++) {
b[i][0] = 0;
}
for(int j=1;j<=n; j++) {
b[0][j] = 0;
}
//枚举⼦段数⽬,从1开始,迭代到m,递推出b[i][j]的值
for(int i=1; i<=m; i++) {
//n-m+i限制避免多余运算,当i=m时,j最⼤为n,可据此递推所有情形
for(int j=i; j<=n-m+i; j++) {
if(j>i) {
b[i][j] = b[i][j-1] + a[j];//代表a[j]同a[j-1]⼀起,都在最后⼀⼦段中
for(int k=i-1; k<j; k++) {
if(b[i][j]<b[i-1][k]+a[j])
b[i][j] = b[i-1][k]+a[j];//代表最后⼀⼦段仅包含a[j]
}
else {
b[i][j] = b[i-1][j-1]+a[j];//当i=j时,每⼀项为⼀⼦段
}
}
}
int sum = 0;
for(int j=m; j<=n; j++) {
if(sum<b[m][j]) {
sum = b[m][j];
}
}
return sum;
}
上述算法显然需要O(m*n^2)计算时间和O(m*n)。

可以看⼀下具体矩阵是怎么填写的。

注意到上述算法中,计算b[i][j]时只⽤到了b的当前⾏的前⼀个数以及上⼀⾏的⼀个极值。

因此我们可以定义两个数组,⼀个数组来保存当前⾏,⼀个数组来保存上⼀⾏的极值。

并且使⽤数组来保存极值可以边⽣成当前⾏的数值边进⾏极值的判断并进⾏填充。

#include "stdafx.h"
#include <iostream>
using namespace std;
int MaxSum(int m,int n,int *a);
int main() {
int a[] = {0,2,3,-7,6,4,-5};//数组脚标从1开始
for(int i=1; i<=6; i++) {
cout<<a[i]<<" ";
}
cout<<endl;
cout<<"数组a的最⼤连续⼦段和为:"<<MaxSum(3,6,a)<<endl;
}
int MaxSum(int m,int n,int *a) {
if(n<m || m<1)
return 0;
// b数组记录第i⾏的最⼤i⼦段和
// c数组记录第i-1⾏的极值
int *b = new int[n+1];
int *c = new int[n+1];
// 当然可以全部初始化为0,但事实上,只需要将b[0]初始化为0即可,因为对于下⼀轮的b值
// 只会直接调⽤前⼀轮的b⾸个数值,其他的数值都是⾃⽣成
// 具体的可以看图就明⽩了
b[0] = 0;
for (int i = 0;i < n+1;i++){
c[i] = 0;
}
for(int i=1; i<=m; i++) {
// 对于 i == j的情况,单独⽣成
b[i] = b[i-1] + a[i];
// n-m+i限制避免多余运算,当i=m时,j最⼤为n,可据此递推所有情形
for(int j=i+1; j<=i+n-m;j++) {
b[j] = b[j-1]>c[j-1]?b[j-1]+a[j]:c[j-1]+a[j];
c[j-1] = max;
if(max<b[j]) {
max = b[j];
}
}
c[i+n-m] = max;
}
int sum = 0;
for(int j=m; j<=n; j++) {
if(sum<b[j]) {
sum = b[j];
}
}
return sum;
}
上述算法需要O(m(n-m))的时间复杂度和O(n)的空间。

当m或者(n-m)为常数的时候,上述算法只需要O(n)的时间复杂度和O(n)的空间。

相关文档
最新文档