比赛链接:
https://codeforces.com/contest/1667
B. Optimal Partition
题目大意:
长为 \(n\) 的序列 \(a\),将它分成若干段,对于 \(a_l, a_{l + 1}, ..., a_r\) 这一段,记 \(s = \sum_{i = l}^r a_i\),
若 \(s > 0\), 则这一段的值为 \((r - l + 1)\);
若 \(s = 0\), 则值为 0;
若 \(s < 0\), 则值为 \(-(r - l + 1)\)。
求出分段后能取得的最大价值。
思路:
先考虑 \(O(n^2)\) 的暴力做法。
定义 \(f[i]\) 为从 1 到 \(i\) 这一段划分后的最大价值,\(s[i]\) 为从 1 到 \(i\) 这一段的前缀和。
定义 \(k\) 如下:
当 \(s[i] > s[j]\),\(k = 1\)。
当 \(s[i] = s[j]\),\(k = 0\)。
当 \(s[i] < s[j]\),\(k = -1\)。
容易得到 \(f[i] = max( \sum_{j = 1}^{i - 1} f[j] + k * (i - j) )\) 的转移方程。
显然,分三种情况看,
当 \(k = 1\) 时,\(f[i] = max(\sum_{j = 1}^{i - 1} f[j] - j + i)\),即需要找到在 \(i\) 之前最大的 \(f[j] - j\),这步操作可以通过线段树或者树状数组去快速查询和维护。
当 \(k = 0\) 时,\(f[i] = max(\sum_{j = 1}^{i - 1} f[j]\),即需要找到在 \(i\) 之前最大的 \(f[j]\),同样通过上述数据结构去操作。
当 \(k = -1\) 时,值是减少的,对于一段总和为负数的区间,因为它的子区间可能是正的,所以最优的策略肯定是继续划分,直到不能划分为止,即最后会剩下一个长为 1 的负数区间。所以这种情况就是之前最大的值 -1。
代码:
#include <bits/stdc++.h>
using namespace std;
#define LL long long
const int N = 5e5 + 10;
LL T, n;
struct fwt{
LL a[N], n;
LL lowbit(LL k){
return k & -k;
}
void update(LL x, LL k){
while (x <= n){
a[x] = max(a[x], k);
x += lowbit(x);
}
}
LL query(LL x){
LL t = -1e18;
while (x != 0){
t = max(t, a[x]);
x -= lowbit(x);
}
return t;
}
void init(LL m){
n = m;
for (int i = 0; i <= n + 1; i ++ )
a[i] = -1e18;
}
}f1, f2;
void solve(){
cin >> n;
vector <LL> f(n + 1), t, s(n + 1);
for (int i = 1; i <= n; i ++ ){
cin >> s[i];
s[i] += s[i - 1];
t.push_back(s[i]);
}
t.push_back(0);
sort(t.begin(), t.end());
t.erase(unique(t.begin(), t.end()), t.end());
for (int i = 0; i <= n; i ++ )
s[i] = lower_bound(t.begin(), t.end(), s[i]) - t.begin() + 1;
f1.init(n);
f2.init(n);
f1.update(s[0], 0);
f2.update(s[0], 0);
for (int i = 1; i <= n; i ++ ){
f[i] = f[i - 1] - 1;
f[i] = max( f[i], i + f1.query(s[i] - 1) );
f[i] = max( f[i], f2.query(s[i]) );
f1.update(s[i], f[i] - i);
f2.update(s[i], f[i]);
}
cout << f[n] << "\n";
}
int main(){
ios::sync_with_stdio(false);cin.tie(0);
cin >> T;
while (T--)
solve();
return 0;
}