目录
题目描述
给定一个数字序列,求i, j (1 ≤ i ≤ j ≤ n),使得最大,输出这个最大和。
题目分析
虽然这篇博客旨在讲解动态规划解法,但我们还是先来看看各种解法都有哪些:
图一乐之超级遍历法
#include <iostream>
using namespace std;
const int N = 1010;
int num[N];
int MaxSubSequenceSum(const int A[], int N){
int ThisSum, MaxSum = 0;
for(int i = 0;i < N; i ++){
for(int j = i;j < N;j ++){
ThisSum = 0;
for(int k = i;k <= j;k ++){
ThisSum += A[k];
if(ThisSum > MaxSum){
MaxSum = ThisSum;
}
}
}
}
return MaxSum;
}
int main(){
int n;
cin >> n;
for(int i = 0; i < n; i ++)
cin >> num[i];
cout << MaxSubSequenceSum(num, n) << endl;
return 0;
}
最基础的想法,就是穷举所有可能,时间复杂度为
聪明了一些之还是遍历法
第一种方法涉及了太多的重复运算,笨到令人发指的程度。因此我们可以考虑撤掉一个for循环进行优化。
#include <iostream>
using namespace std;
const int N = 1010;
int num[N];
int MaxSubSequenceSum(const int A[], int N){
int ThisSum, MaxSum = 0;
for(int i = 0;i < N; i ++){
ThisSum = 0;
for(int j = i; j <N; j ++){
ThisSum += A[j];
if(ThisSum > MaxSum)
MaxSum = ThisSum;
}
}
return MaxSum;
}
int main(){
int n;
cin >> n;
for(int i = 0; i < n; i ++)
cin >> num[i];
cout << MaxSubSequenceSum(num, n) << endl;
return 0;
}
现在,时间复杂度变成了。
递归分治法
在问题中,最大连续子序列无非出现在三个地方:
- 全部出现在序列的左半边;
- 全部出现在序列的右半边;
- 出现在中部,跨越左右半边
对于前两种情况,我们可以采用递归求解的方式,而第三种情况的最大和可以通过求出左半边的最大和(包含左半边的最后一个元素)加上右半边的最大和(包含右半边的第一个元素)而得到。易知分治法的复杂度为,又因为for循环最多只有一层,所以该算法的复杂度为。
#include <iostream>
using namespace std;
const int N = 1010;
int num[N];
int MaxSubSequenceSum(const int A[], int Left, int Right){
int MaxLeftSum = 0, MaxRightSum = 0;
int MaxLeftBorderSum = 0, MaxRightBorderSum = 0;
int LeftBorderSum = 0, RightBorderSum = 0;
int Center;
if(Left == Right){
if(A[Left] > 0) return A[Left];
else return 0;
}
Center = (Right + Left) / 2;
MaxLeftSum = MaxSubSequenceSum(A, Left, Center);
MaxRightSum = MaxSubSequenceSum(A, Center + 1, Right);
for(int i = Center; i >= Left; i --){
LeftBorderSum += A[i];
if(LeftBorderSum > MaxLeftBorderSum)
MaxLeftBorderSum = LeftBorderSum;
}
for(int i = Center + 1; i <= Right; i ++){
RightBorderSum += A[i];
if(RightBorderSum > MaxRightBorderSum)
MaxRightBorderSum = RightBorderSum;
}
return max(max(MaxLeftSum, MaxRightSum), MaxRightBorderSum + MaxLeftBorderSum);
}
int main(){
int n;
cin >> n;
for(int i = 0; i < n; i ++)
cin >> num[i];
cout << MaxSubSequenceSum(num, 0, n - 1) << endl;
return 0;
} //go to hell
动态规划解法
上面的分治递归求解是不是感觉很高大上,把C++写出了Java的感觉?
事实上费力不讨好,还不是被动态规划一棒子撂倒。
下面,我们考虑用标准的动态规划来求解。
现进行如下思考:
1.令状态dp[i]表示以A[i]作为末尾的连续序列的最大和,所求的东西就是dp数组中的最大值。
2.做如下考虑:因为dp[i]要求是必须以A[i]结尾的连续序列,只有两种情况:
- 这个最大和的连续序列只有一个元素,即以A[i]开始,以A[i]结尾;
- 这个最大和的连续序列有多个元素
于是得到状态转移方程:
这个式子只和i与i之前的元素有关,边界条件是dp[1] = A[1],dp数组的最大值就是答案。
算法的时间复杂度为
代码如下:
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 10010;
int A[N], dp[N];
int main(){
ios::sync_with_stdio(false);
int n;
cin >> n;
for(int i = 1; i <= n; i ++) {
cin >> A[i];
}
dp[1] = A[1];
for(int i = 2; i <= n; i ++) {
dp[i] = max(A[i], dp[i - 1] + A[i]);
}
int ans = 0;
for(int i = 1; i <= n; i ++) {
ans = max(dp[i], ans);
}
cout << ans << '\n';
return 0;
}
联机算法
个人认为最简单也最好想,本质上是动态规划的思想。
该算法对所有数据遍历一遍,一旦A[ i ]被读入并被处理,它就不再需要被记忆。由于该问题不需要最大序列和的坐标输出,所以我们不需要记忆序列位置,仅仅记忆当前最大序列和的值即可。
算法的时间复杂度为
代码如下:
#include <iostream>
using namespace std;
const int N = 1010;
int num[N];
int MaxSubSequenceSum(const int A[ ], int N){
int ThisSum, MaxSum, j;
ThisSum = MaxSum = 0;
for (j= 0; j < N; j++) {
ThisSum += A[j];
if (ThisSum > MaxSum)
MaxSum = ThisSum;
if(ThisSum < 0)
ThisSum = 0;
}
return MaxSum;
}
int main(){
int n;
cin >> n;
for(int i = 0; i < n; i ++)
cin >> num[i];
cout << MaxSubSequenceSum(num, n - 1) << endl;
return 0;
}
写在最后
最后...如果你有兴趣的话,欢迎来挑战这个问题的加强版本!