P1121 环状最大两段子段和
Link:Luogu - P1121
题目描述
给出一段长度为 n n n 的环状序列 a a a,即认为 a 1 a_1 a1 和 a n a_n an 是相邻的,选出其中连续不重叠且非空的两段使得这两段和最大。
输入格式
第一行是一个整数 n n n,表示序列的长度。
第二行有 n n n 个整数,描述序列 a a a,第 i i i 个数字表示 a i a_i ai。
输出格式
一行一个整数,为最大的两段子段和是多少。
样例 #1
样例输入 #1
7
2 -4 3 -1 2 -4 3
样例输出 #1
9
提示
数据规模与约定
对于全部的测试点,保证 2 ≤ n ≤ 2 × 1 0 5 2 \leq n \leq 2 \times 10^5 2≤n≤2×105, − 1 0 4 ≤ a i ≤ 1 0 4 -10^4 \leq a_i \leq 10^4 −104≤ai≤104。
解题思路
发现最后选的策略就两种情况:(0表示不选,+表示选)
一、00++++00+++++000
二、++00+++0000++++
对于情况一,可以求两段不相交的最大子序列和(+的部分),再加起来;
对于情况二,可以求两段不相交的最小子序列和(0的部分),再用总和减去,就是答案。
现在问题来了:如何求一个序列中两段不相交的最大/最小子序列和?
以求最大值为例:可以想象成把原序列砍成左右两部分,然后分别求左、右两部分的最大子段和,最后相加。
so,设
f
[
i
]
f[i]
f[i] 表示原序列
a
[
1
∼
i
]
a[1 \sim i]
a[1∼i] 的最大子段和,
g
[
i
]
g[i]
g[i] 表示
a
[
n
∼
i
]
a[n \sim i]
a[n∼i] 的最大子段和(注意
g
g
g 数组是倒着的),
然后枚举原序列中间的分割点
k
k
k,对得到的所有答案取 max 即可:
a
n
s
=
m
a
x
{
f
[
i
]
+
g
[
i
+
1
]
}
,
1
<
=
i
<
=
n
−
1
ans = max\{f[i] + g[i + 1]\},1<=i<=n-1
ans=max{f[i]+g[i+1]},1<=i<=n−1(值域很好理解,要是够到
n
n
n,那么就会访问到
g
[
n
+
1
]
g[n+1]
g[n+1],这个地方未被赋值,会出现错误)
这样思路就很明确了。
n
<
=
2
×
1
0
5
,
a
[
i
]
<
=
1
0
4
n<=2 \times 10^5,a[i]<=10^4
n<=2×105,a[i]<=104,所以
n
×
a
[
i
]
<
=
2
×
1
0
9
n \times a[i]<=2 \times 10^9
n×a[i]<=2×109,用 int 足够。
实现过程可以用函数模板,达到一函数两用的效果。复杂度稳定
O
(
n
)
O(n)
O(n),但常数略大。
注意两种特殊的情况,需要特判:
- 数列里只有一个正数(其实是 0 0 0 也可以),其余全是负数。这样的话我们找不到两个连续的正数,所以答案应该是正数+最大的负数。
- 数列里全是负数,此时计算情况二时,会出现用 总和-所有负数和=总和+|所有负数和|=0 的情况。也就是说只要输入的全是负数,其输出永远是 0 0 0,答案显然不对。
Code
函数模板闪亮登场!!!这样可以实现一个函数,求最大值、最小值通用的效果,优雅且简单!更能装13!
具体详见代码注释。
#include <bits/stdc++.h>
using namespace std;
const int maxn = 2e5 + 7;
int n, a[maxn];
struct Max{ int operator() (int x, int y){ return (x > y? x : y); } };
struct Min{ int operator() (int x, int y){ return (x < y? x : y); } };
template <typename _Compare>
int work(_Compare cmp){
vector <int> f(n + 1), g(n + 1);
f[1] = a[1]; // 初始化,避免比较时和f[0]=0出现比较出现错误。下面的循环直接从2开始即可
for(int i = 2; i <= n; i ++) f[i] = cmp(f[i - 1] + a[i], a[i]); // 此时的f[i]表示1~i中以i结尾的最大(或最小)子段和
for(int i = 2; i <= n; i ++) f[i] = cmp(f[i], f[i - 1]); // 此时的f[i]表示1~i中的最大(或最小)子段和
g[n] = a[n]; // 初始化,避免比较时和g[n+1]=0出现比较出现错误。下面的循环直接从n-1开始即可
for(int i = n-1; i >= 1; i --) g[i] = cmp(g[i + 1] + a[i], a[i]); // 此时的g[i]表示n~i中以i结尾的最大(或最小)子段和
for(int i = n-1; i >= 1; i --) g[i] = cmp(g[i + 1], g[i]); // 此时g[i]表示n~i中的最大(或最小)子段和
int ans = cmp(0, -1) + INT_MIN;
/*上面是一个骚操作:当cmp为max函数时,ans=INT_MIN+0,正好是求最大值时的初始最小值
当cmp为min函数时,ans=INT_MIN-1,由于int自然溢出的特性,此时ans的值应为INT_MAX,正好是求最小值时的初始最大值*/
for(int i = 1; i <= n - 1; i ++) ans = cmp(ans, f[i] + g[i + 1]);
return ans;
}
void solve()
{
cin >> n;
int sum = 0, positive = 0; // sum记录Σa[i],positive记录其中非负数的个数(包括0和正整数)
for(int i = 1; i <= n; i ++){
cin >> a[i], sum += a[i];
positive += (a[i] >= 0);
}
if(positive == 0){ // 全是负数,答案为两个最大的负数和
sort(a + 1, a + n + 1, greater<int>()); // 从大到小排序
cout << a[1] + a[2] << '\n'; // 题目保证n>=2
}
else if(positive == 1){ // 只有一个非负数:答案为这个非负数+最大的负数
sort(a + 1, a + n + 1, greater<int>()); // 从大到小排序
// 排序完后,a[1]一定是那个非负数,a[2]一定是那个最大的负数
cout << a[1] + a[2] << '\n';
}
else{ // 正常情况
cout << max(work(Max()), sum - work(Min())) << '\n';
}
}
signed main()
{
ios :: sync_with_stdio(false), cin.tie(nullptr), cout.tie(nullptr);
solve();
return 0;
}
用函数作为参数的方法详见下面几篇文章:
- https://blog.csdn.net/zj1131190425/article/details/88528948 - C语言风格的函数指针传参
- https://blog.csdn.net/p942005405/article/details/84755007 - C++11起,可以用 function 类模板实现函数传参
- https://www.cnblogs.com/sherlock-lin/p/16200741.html - 拓展:介绍可变参数模板及函数传参(C++11起)
End
这里是 YLCHUP,谢谢大家!