动态规划系列专题讲义
专题三:最大连续子序列之和
/*
Name: 动态规划专题之最大连续子序列之和
Author: 巧若拙
Description: 最大连续子序列之和
给定K个整数的序列{ N1,N2, ..., NK },其任意连续子序列可表示为{Ni, Ni+1, ..., Nj },
其中 1 <= i<= j <= K。最大连续子序列是所有连续子序列中元素和最大的一个,
例如给定序列{ -2,11, -4, 13, -5, -2 },其最大连续子序列为{ 11, -4, 13 },最大和为20。
输入:
测试输入包含若干测试用例,每个测试用例占2行,第1行给出正整数K( < 10000 ),第2行给出K个整数,中间用空格分隔。
当K为0时,输入结束,该用例不被处理。
输出:
对每个测试用例,在1行里输出最大和。若所有K个元素都是负数,则定义其最大和为0。
输入示例:
6
-2 11 -4 13 -5-2
10
-10 1 2 3 4 -5-23 3 7 -21
6
5 -8 3 2 5 0
1
10
3
-1 -5 -2
3
-1 0 -2
0
输出示例:
20
10
10
10
0
0
*/
#include<iostream>
#include<string>
using namespace std;
const int INF = -100000;
const int MAX = 10000;
int B[MAX]; //B[i]用来存储包含A[i]的最大连续子序列之和
int MaxSubsequenceSum_1(const int A[], intn);//低效算法1
int MaxSubsequenceSum_2(const int A[], intn);//低效算法2
int MaxSubsequenceSum_3(const int A[], intn);//分治算法
int MaxSubSum(const int A[], int left, intright);//分治算法子程序
int MaxSubsequenceSum_4(const int A[], inti);//记忆化搜索(备忘录算法)
int MaxSubsequenceSum_5(const int A[], intn);//使用一维数组的动态规划算法
int MaxSubsequenceSum_6(const int A[], intn);//使用一个变量代替一维数组
int MaxSubsequenceSum_7(const int A[], intn);//使用一个变量代替一维数组的另一种写法
int MaxSubsequenceSum_8(const int A[], intn);//记录数组的同时,记录最优解和左右边界
int MaxSubsequenceSum_9(const int A[], intn);//使用一个变量代替一维数组,输出子序列
int main()
{
intA[MAX] = {0};
intn;
cin>> n;
while(n != 0)
{
for(int i=0; i<n; i++)
cin>> A[i];
cout<< MaxSubsequenceSum_1(A, n) << endl;
cout << MaxSubsequenceSum_2(A, n)<< endl;
cout << MaxSubsequenceSum_3(A, n)<< endl;
for(int i=0; i<n; i++)
B[i]= INF;
MaxSubsequenceSum_4(A, n-1);
intmaxSum = max(0, B[0]); //注意全是负数的情形
for (int i=1; i<n; i++)//存储各连续子序列的最大和
{
if (B[i] > maxSum)
maxSum = B[i];
}
cout << maxSum << endl;
cout << MaxSubsequenceSum_5(A, n)<< endl;
cout << MaxSubsequenceSum_6(A, n)<< endl;
cout << MaxSubsequenceSum_7(A, n)<< endl;
cout << MaxSubsequenceSum_8(A, n)<< endl;
cout << MaxSubsequenceSum_9(A, n)<< endl;
cin >> n;
}
return 0;
}
算法1:低效算法1:三重循环
int MaxSubsequenceSum_1(const int A[], intn)//低效算法1
{
int sum, maxSum = 0;
for (int i=0; i<n; i++)
{
for (int j=i; j<n; j++)
{
sum = 0;
for (int k=i; k<=j; k++) //语句1
{
sum += A[k];
}
if (sum > maxSum)
maxSum = sum;
}
}
return maxSum;
}
问题1:语句1所在循环体的作用计算连续子序列A[i..j]之和,但三重循环循环看上去还是有些低效,注意变量sum的作用,想想能否去掉语句1所在循环体,提高效率?
答案:
问题1:详见算法2。
算法2:低效算法2:二重循环
int MaxSubsequenceSum_2(const int A[], intn)//低效算法2
{
int sum, maxSum = 0;
for (int i=0; i<n; i++)
{
sum = //语句1
for (int j=i; j<n; j++)
{
sum += //语句2
if (sum > maxSum)
maxSum = //语句3
}
}
return maxSum;
}
问题1:将语句1,语句2和语句3补充完整。
答案:
问题1:语句1:sum = 0; 语句2:sum += A[j]; 语句3:maxSum = sum;
算法3::分治算法
int MaxSubsequenceSum_3(const int A[], intn)//分治算法
{
return MaxSubSum(A, 0, n-1);
}
int MaxSubSum(const int A[], int left, intright)//分治算法子程序
{
int maxLeftSum, maxRightSum;
int maxLeftBorderSum, maxRightBorderSum;
int leftBorderSum, rightBorderSum;
if (left == right)
return (A[left] > 0) ? A[left] : 0;
int mid = (left + right) / 2;
maxLeftSum = MaxSubSum(A, left, mid);
maxRightSum = MaxSubSum(A, mid+1, right);
maxLeftBorderSum = leftBorderSum = 0;
for (int i=mid; i>=left; i--) //语句1
{
leftBorderSum += A[i];
if (leftBorderSum > maxLeftBorderSum)
maxLeftBorderSum = leftBorderSum;
}
maxRightBorderSum = rightBorderSum = 0;
for (int i=mid+1; i<=right; i++)
{
rightBorderSum += A[i];
if (rightBorderSum > maxRightBorderSum)
maxRightBorderSum = rightBorderSum;
}
return max(max(maxLeftSum, maxRightSum), maxLeftBorderSum+maxRightBorderSum);
}
问题1:能否把语句1改为for (int i=left; i<=mid; i++)?为什么?
答案:
问题1:不能。因为语句1的作用是从中间开始向左计算包含A[mid]子序列的最大和。
算法4:记忆化搜索(备忘录算法),需要用到全局变量B[MAX]
int MaxSubsequenceSum_4(const int A[], inti)
{ //B[i]用来存储包含A[i]的最大连续子序列之和
if (B[i] != INF)
return B[i];
if (i == 0) //边界条件
{
B[i]= A[0];
}
else
{
if(MaxSubsequenceSum_4(A, i-1) > 0)
B[i]= //语句1
else
B[i]= A[i];
}
return B[i];
}
问题1:将语句1补充完整。
答案:
问题1:语句1:B[i] = MaxSubsequenceSum_4(A, i-1) + A[i];
算法5:使用备忘录数组的动态规划算法
int MaxSubsequenceSum_4(const int A[], intn)//使用备忘录数组的动态规划算法
{
int S[MAX] = {A[0]};//S[i]用来存储包含A[i]的最大连续子序列之和
for (int i=1; i<n; i++)
{
if (S[i-1] > 0)
S[i] = //语句1
else
S[i] = A[i];
}
int maxSum = max(0, S[0]); //语句2
for (int i=1; i<n; i++)
{
if (S[i] > maxSum)
maxSum = S[i];
}
return maxSum;
}
问题1:将语句1补充完整。
问题2:能否把语句1改为int maxSum = S[0];?为什么?
问题3:注意到算法4中S[i]的值只与S[i-1]有关,故无需把所有的S[i]都记录下来,可以进行降维优化,用变量sum代替S[i]实现相关功能。试着实现相关代码。
答案:
问题1:语句1:S[i] = S[i-1] + A[i];
问题2:不能。要考虑到数组A的元素全是负数的情形。
问题:3:详见算法6。
算法6:使用一个变量代替备忘录数组
int MaxSubsequenceSum_5(const int A[], intn)//使用一个变量代替备忘录数组
{
int sum = A[0];
int maxSum = max(0, sum);
for (int i=1; i<n; i++)
{
if (sum > 0)
{
sum += A[i];
if (sum > maxSum)
maxSum = sum;
}
else
{
sum = A[i];
}
}
return maxSum;
}
算法7:使用一个变量代替备忘录数组的另一种算法
int MaxSubsequenceSum_7(const int A[], intn)//使用一个变量代替一维数组的另一种写法
{
int sum = 0;
int maxSum = max(0, sum); //注意全是负数的情形
for (int i=0; i<n; i++)//存储各连续子序列的最大和
{
sum+= A[i];
if (sum > maxSum) //更新最大值
{
maxSum = //语句1
}
else if (sum < 0) //重新开始
{
sum = //语句2
}
}
return maxSum;
}
问题1:将语句1和语句2补充完整。
问题2:算法6和算法7的思路有哪些不同之处?
答案:
问题1:语句1:m = sum;
语句2:sum = 0;
问题2:算法6先判断sum 是否大于0,再根据情况更新sum的值;算法7先默认将A[i]的值加到sum上,再判断其是否大于0,然后根据情况更新maxSum 或sum的值。即算法6是先判断再赋值,算法7是先给定一个默认值,再判断是否改变该默认值,两种思路都很常见,但算法6的逻辑更严谨,思路更清晰。
拓展练习:原题只要求计算最大连续子序列之和,而没有把对应的连续子序列输出来,现在要求:
1. 在算法5 MaxSubsequenceSum _5()的基础上,编写函数int MaxSubsequenceSum_8(const int A[], int n)//计算最大连续子序列之和,并输出对应的连续子序列。
2. 在算法6 MaxSubsequenceSum_6()的基础上,编写函数int MaxSubsequenceSum_9(constint A[], int n)//计算最大连续子序列之和,并输出对应的连续子序列。
参考答案:
int MaxSubsequenceSum_8(const int A[], intn)//记录数组的同时,记录最优解和左右边界
{
int S[MAX] = {A[0]};//S[i]用来存储包含A[i]的最大连续子序列之和
int maxSum = max(0, S[0]);
int left = 0, mLeft = 0, right = 0; //mLeft和right分别存储最大连续子序列的左右边界
for (int i=1; i<n; i++)//存储各连续子序列的最大和
{
if (S[i-1] > 0) //若之前的连续子序列之和大于0,则把A[i]累加上去
{
S[i] = S[i-1] + A[i];
if (S[i] > maxSum)
{
maxSum = S[i];
mLeft = left;
right = i;
}
}
else //否则重新开始
{
S[i] = A[i];
left = i;
}
}
if(maxSum > 0)
{
cout<< "A[" << mLeft << ":" << right<< "] : ";
for (int i=mLeft; i<=right; i++)//存储各连续子序列的最大和
{
cout << A[i] << "";
}
cout << "= ";
}
return maxSum;
}
int MaxSubsequenceSum_9(const int A[], intn)//使用一个变量代替一维数组,输出子序列
{
int sum = A[0];
int maxSum = max(0, sum);
int left = 0, mLeft = 0, right = 0; //mLeft和right分别存储最大连续子序列的左右边界
for (int i=1; i<n; i++)//存储各连续子序列的最大和
{
if (sum > 0) //若之前的连续子序列之和大于0,则把A[i]累加上去
{
sum += A[i];
if (sum > maxSum)
{
maxSum = sum;
mLeft = left;
right = i;
}
}
else //否则重新开始
{
sum = A[i];
left = i;
}
}
if(maxSum > 0)
{
cout<< "A[" << mLeft << ":" << right<< "] : ";
for (int i=mLeft; i<=right; i++)
{
cout << A[i] << "";
}
cout << "= ";
}
return maxSum;
}
课后练习:
练习1:1768_最大子矩阵
题目描述:已知矩阵的大小定义为矩阵中所有元素的和。给定一个矩阵,你的任务是找到最大的非空(大小至少是1 * 1)子矩阵。
比如,如下4 * 4的矩阵
0 -2 -7 0
9 2 -6 2
-4 1 -4 1
-1 8 0 -2
的最大子矩阵是
9 2
-4 1
-1 8
这个子矩阵的大小是15。
输入
输入是一个N * N的矩阵。输入的第一行给出N (0 < N <= 100)。再后面的若干行中,依次(首先从左到右给出第一行的N个整数,再从左到右给出第二行的N个整数……)给出矩阵中的N2个整数,整数之间由空白字符分隔(空格或者空行)。已知矩阵中整数的范围都在[-127,127]。
输出
输出最大子矩阵的大小。
样例输入
4
0 -2 -7 0
9 2 -6 2
-4 1 -4 1
-1 8 0 -2
样例输出
15