Codeforces-1667 B: Optimal Partition
题目传送门:Codeforces-1667 B
题目
题目截图
样例描述
题目大意
给定一个长度为
n
n
n 的数组
a
a
a,希望将
a
a
a 切成几个连续的段。对于给定
l
⋯
r
l \cdots r
l⋯r 的一段连续子数组
a
l
,
a
l
+
1
,
⋯
,
a
r
a_l, a_{l+1},\cdots,a_r
al,al+1,⋯,ar,令
s
=
a
l
+
a
l
+
1
+
⋯
+
a
r
s=a_l+a_{l+1}+\cdots+a_r
s=al+al+1+⋯+ar,则该段的价值为:
v
l
⋯
r
=
{
(
r
−
l
+
1
)
i
f
s
>
0
,
0
i
f
s
=
0
−
(
r
−
l
+
1
)
i
f
s
<
0
v_{l\cdots r} = \left\{ \begin{array}{ll} (r-l+1) \; if \; s > 0, \\ 0 \; if \; s = 0 \\ -(r-l+1) \; if \; s < 0 \end{array} \right.
vl⋯r=⎩⎨⎧(r−l+1)ifs>0,0ifs=0−(r−l+1)ifs<0
问将该数组切成几个非空连续的段后,所能获得的最大价值总和是多少。
题目解析
这道题很有趣,首先根据题目,不难判断出这是一个
d
p
dp
dp 问题。设
d
p
i
dp_i
dpi 代表从
1
⋯
i
1\cdots i
1⋯i 能够得到的最大价值总和,那么
d
p
i
=
max
j
{
d
p
j
+
v
j
+
1
⋯
i
}
dp_i = \max_j \{dp_j + v_{j+1 \cdots i}\}
dpi=maxj{dpj+vj+1⋯i},尽管
v
v
v 值我们可以靠预处理前缀和
O
(
1
)
O(1)
O(1) 得到,但是找
i
i
i 的过程仍然是
O
(
n
2
)
O(n^2)
O(n2) 的,这样是过不了这道题的。
考虑如何更快地进行解题,首先从这个条件入手,看题解发现,当一段长度大于
1
1
1 的区间
s
>
0
s > 0
s>0 时,它加入答案才能获得更大的收益,否则,我们都可以通过拆的方式将它拆成长度为
1
1
1 的几个小段。因为
s
<
0
s < 0
s<0 时,我们将整个区间再进行拆分,那么得到的结果如果有
s
>
0
s > 0
s>0 的部分,则结果更优,否则结果也不会更差。如果
s
=
0
s = 0
s=0 的话,我们也可以对整个区间进行拆分,若长度是偶数,那么可以分成左右两部分,结果不会更差(
s
s
s 要么都是
0
0
0,要么一正一负;若长度为奇数,则暂时将该段分成左、中(长度为
1
1
1)、右三部分,若左或右的
s
<
0
s < 0
s<0,那么将其分成两段(左,中右)或(左中,右)两部分的话,结果会更好。其余情况可以直接拆成(左、中、右)三部分,结果不会更差。因此最后长度大于
1
1
1 的子段,我们只需要计算
s
>
0
s > 0
s>0 的,其余情况只计算长度为
1
1
1 的即可。
我们考虑如何选取子段
s
>
0
s > 0
s>0 的区间,首先设
p
i
p_i
pi 为代表
a
1
+
a
2
+
⋯
a
i
a_1+a_2+\cdots a_i
a1+a2+⋯ai 的前缀和。若我们可以对前缀和进行排序,当我们已知当前位置
i
i
i 的前缀和排位
o
r
d
i
ord_i
ordi 时,我们可以通过查找前缀和比其小的段
1
⋯
j
1\cdots j
1⋯j(
o
r
d
j
<
o
r
d
i
ord_j < ord_i
ordj<ordi),且
j
<
i
j<i
j<i 进行更新。这样我们可以保证
j
+
1
⋯
i
j+1 \cdots i
j+1⋯i 区间的
v
v
v 值是正的,我们可以直接使用
i
−
j
i-j
i−j 表示这一段的价值。
但尽管这样能够保证正值,但仍然需要查找
j
j
j,根源在于统计
j
+
1
⋯
i
j+1 \cdots i
j+1⋯i 的价值需要同时用到两次更新的坐标位置。但其实我们可以把
+
i
,
−
j
+i,-j
+i,−j 这两件事情分开做,若我们每次找最大的
d
p
j
−
j
dp_j - j
dpj−j,那么在更新
i
i
i 时,我们可以直接将该结果
+
i
+i
+i 便变成了
d
p
j
+
i
−
j
dp_j + i - j
dpj+i−j,这样我们就可以分开统计,直接使用树状数组找最大的
d
p
j
−
j
dp_j - j
dpj−j 就可以省去那一次找
j
j
j 的遍历,使得总体复杂度变为
O
(
n
log
n
)
O(n \log n)
O(nlogn)。
值得注意的是排序时,对于前缀和相等的情况,我们要把位置大的放前面,这样我们更新的时候就不会使用位置小的去更新位置大的了(此时该段的总价值为
0
0
0,我们更新的时候是直接
+
i
,
−
j
+i, -j
+i,−j 的操作,没有考虑
0
0
0 的情况,会导致错误答案,而实际上根据第二段的分析,价值为
0
0
0 我们是不该考虑的)。以及要考虑没有分段的情况,即当前位置
i
i
i 的前缀和大于
0
0
0,此时最大价值总和就是
i
i
i。
Code
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int maxn = 5e5 + 7;
int a[maxn], f[maxn], dp[maxn], ord[maxn], n;
LL p[maxn];
const int rinf = -0x7fffffff;
int lowbit(int x) { return (x & -x); }
void update(int x, int v) {
while(x <= n) {
f[x] = max(f[x], v);
x += lowbit(x);
}
}
int query(int x) {
int ans = rinf;
while(x > 0) {
ans = max(f[x], ans);
x -= lowbit(x);
}
return ans;
}
int main() {
int T;
cin >> T;
while(T--) {
cin >> n;
for(int i=1; i<=n; ++i) f[i] = rinf;
vector<pair<LL, int> > pre;
for(int i=1; i<=n; ++i) {
cin >> a[i]; p[i] = p[i-1] + a[i]; pre.push_back(make_pair(p[i], -i));
}
sort(pre.begin(), pre.end());
for(int i=1; i<=n; ++i) ord[-pre[i-1].second] = i;
for(int i=1; i<=n; ++i) {
dp[i] = dp[i-1] + (a[i] > 0?1:(a[i] < 0? -1: 0));
dp[i] = max(dp[i], query(ord[i]) + i);
if(p[i] > 0) dp[i] = i;
update(ord[i], dp[i] - i);
}
cout << dp[n] << endl;
p[0] = 0; dp[0] = 0;
}
return 0;
}