P1121环状最大两段子段和
题目
假定一个小白看这道题,一步步了解相关知识
最大子段和(DP,O(n))
状态dp[i]
表示以A[i]
作结尾的最大连续子序列和,求出dp
数组的最大值即题解
dp[1] = a[1]; //边界条件
for(int i = 2; i <= n; ++i)
{
dp[i] = max(dp[i - 1] + a[i], a[i]); //状态转移方程
}
sort(dp + 1, dp + n + 1); //找到dp数组的最大值
ans1 = dp[n];
环状最大子段和
即a[n]
和a[1]
相邻,取得的最大子段分两种情况
- 最大子段没有跨过
a[n]
和a[1]
,即和非环状最大子段和的结果是一样的,直接使用DP可解,得ans1
- 最大子段跨过了
a[n]
和a[1]
,此时,问题转化为求最小子段和min1
,然后我们求整个序列的和sum
,得ans2 = sum - min1
ans1
和ans2
的最大值就是题解
有些初学者难以理解ans2 = sum - min1
,它的前提条件是序列是一个环状,这个环上的数字的总和是确定的,我们从这个环上剔除和最小的子段,剩下子段的和自然就是最大子段和(这里,我们需要清楚,最大子段和最小子段只有一个可以跨过序列首和尾的,因为这个转折点只有一个),举个栗子
这个序列的和sum = 9
,最小序列(未跨)和是min = a[2] + a[3] = -7
,那么最大序列(跨过)和就是ans2 = sum - min = 16 = a[4] + a[5] + a[1]
最小(负数)子段和(DP,O(n))
和求最大子段和类似,状态dp[i]
表示以a[i]
结尾的最小子段和,分两种情况求dp[i]
-
如果
dp[i - 1] > 0
那么a[i]
就自立门户,dp[i] = a[i]
-
否则,就将
a[i]
接在a[i - 1]
后面,dp[i] = a[i] + dp[i - 1]
下面是利用最小子段和求环状最大子段和
dp[1] = a[1]; //边界条件
sum = a[1];
for(int i = 2; i <= n; ++i)
{
dp[i] = min(a[i], dp[i - 1] + a[i]); //状态转移方程
sum += a[i];
}
sort(dp + 1, dp + n + 1);
if(dp[1] < 0)
min1 = dp[1]; //找到dp数组最小值
else
min1 = 0;
ans2 = sum - min1; //求得ans2
cout << max(ans1, ans2);
最大双子段和P2642
维护两个数组dp1
与dp2
,dp1[i]
表示以a[i]
为结尾的最大和子序列,dp2[j]
表示以a[j]
为首的最大和子序列,显然,它们的边界分别是dp1[1] = a[1]
和dp2[n] = a[n]
,状态转移方程套用最大子段和的即可
求出这两个数组后,我们枚举求出dp1[i] + dp2[j]
的最大值就是我们找的最大双子段和,注意,要确保j > i + 1
,因为这两个子段是不能连在一起的
结果是,部分超时,因为枚举的复杂度高达O(n2),这道题的规模1e6 > 1e4,我们要加以改进
一种方法是,我们将dp[i]
表示以a[i]
为结尾的最大子序列和改为a[1]
~a[i]
范围内的最大子序列和,将求得的dp[i]
经过dp[i] = max(dp[i - 1], dp[i])
处理即可,以此求得的dp
必是单调递增,记录一轮**O(n)**的遍历就可求出结果
dp1[1] = a[1];
for(int i = 2; i <= n; ++i)
{
dp1[i] = max(a[i], dp1[i - 1] + a[i]);
//加一步处理
dp1[i] = max(dp1[i], dp1[i - 1]);
}
dp2[n] = a[n];
for(int i = n - 1; i >= 1; --i)
{
dp2[i] = max(a[i], dp2[i + 1] + a[i]);
//加一步处理
dp2[i] = max(dp2[i], dp2[i + 1]);
}
然后WA了五个点,怀疑人生
错在求解dp
的两步不能同时在一趟遍历中走完,因为如果我们求解dp[i]
表示以a[i]
结尾的最大子序列和时,前面的dp[j]
(j < i
)已经处理成a[1]
~a[i]
范围内的最大子序列和,那么求得的dp[i]
就有可能是错误的(dp[i - 1] + a[i]
,dp[i - 1]
代表的最大子序列和可能和a[i]
不连续)
AC代码
#include <bits/stdc++.h>
using namespace::std;
const int maxn = 1e6 + 5;
int a[maxn], dp1[maxn], dp2[maxn], ans;
int main()
{
int n;
cin >> n;
for(int i = 1; i <= n; ++i)
cin >> a[i];
dp1[1] = a[1];
for(int i = 2; i <= n; ++i)
dp1[i] = max(a[i], dp1[i - 1] + a[i]);
for(int i = 2; i <= n; ++i)
dp1[i] = max(dp1[i], dp1[i - 1]);
dp2[n] = a[n];
for(int i = n - 1; i >= 1; --i)
dp2[i] = max(a[i], dp2[i + 1] + a[i]);
for(int i = n - 1; i >= 1; --i)
dp2[i] = max(dp2[i], dp2[i + 1]);
ans = dp1[1] + dp2[3]; //相当于i = 2的情况
for(int i = 3; i <= n - 1; ++i)
{
ans = max(ans, dp1[i - 1] + dp2[i + 1]);
}
cout << ans;
return 0;
}
主问题(环状+两子段)
解决了上面几个子问题,这题的解决方法就呼之欲出了
注意题意,这两个子段可以相邻,两个子段的长度和必须大于2
#include <bits/stdc++.h>
using namespace::std;
const int maxn = 2e5 + 5;
int dp1[maxn], dp2[maxn], a[maxn], sum;
int mdp1[maxn], mdp2[maxn], mmin;
int main()
{
int n, ans1, ans2;
cin >> n;
for(int i = 1; i <= n; ++i)
{
cin >> a[i];
sum += a[i]; //顺便求和
}
//如果最大双子段都没有跨过分界
//按照最大双子段和去求即可
//注意本题这两个子段可以相邻,注意枚举时的改动
dp1[1] = a[1];
for(int i = 2; i <= n; ++i)
dp1[i] = max(a[i], dp1[i - 1] + a[i]);
for(int i = 2; i <= n; ++i)
dp1[i] = max(dp1[i], dp1[i - 1]);
dp2[n] = a[n];
for(int i = n - 1; i >= 1; --i)
dp2[i] = max(a[i], dp2[i + 1] + a[i]);
for(int i = n - 1; i >= 1; --i)
dp2[i] = max(dp2[i], dp2[i + 1]);
ans1 = dp1[1] + dp2[n];
for(int i = 2; i <= n - 1; ++i)
ans1 = max(ans1, dp1[i] + dp2[i + 1]);
//如果最大双子段跨过分界
//我们求出最小双子段和min,用sum - min即可
mdp1[1] = a[1];
for(int i = 2; i <= n; ++i)
mdp1[i] = min(min(a[i], 0), mdp1[i - 1] + a[i]);
for(int i = 2; i <= n; ++i)
mdp1[i] = min(mdp1[i], mdp1[i - 1]);
mdp2[n] = a[n];
for(int i = n - 1; i >= 1; --i)
mdp2[i] = min(min(a[i], 0), a[i] + mdp2[i + 1]);
for(int i = n - 1; i >= 1; --i)
mdp2[i] = min(mdp2[i], mdp2[i + 1]);
mmin = mdp1[1] + mdp2[2];
for(int i = 2; i <= n - 1; ++i)
mmin = min(mmin, mdp1[i] + mdp2[i + 1]);
ans2 = sum - mmin;
//确保全为非正数的情况下两个子序列至少共有两个元素
sort(a + 1, a + n + 1);
if(a[n] <= 0)
cout << a[n - 1] + a[n];
else
cout << max(ans1, ans2);
return 0;
}