CF1420E Battle Lemmings 斜率优化
题目来源:Codeforces,Codeforces Round #672 (Div. 2),CF1420,E 题,Battle Lemmings。
题目大意
有一排 \(n\) 个[林檎实],有些[林檎实]拿着盾牌,有些没有。我们称一对[林檎实]是被保护的,当且仅当它们都没有拿盾牌,且在他们之间有人拿着盾牌。
你可以进行若干次操作,每次操作是如下两种之一:
- 选择一个拿着盾牌,且他左边的人没盾牌的[林檎实] \(i\) (\(1< i\leq n\)),将第 \(i\) 个[林檎实]的盾牌给他左边的人。
- 选择一个拿着盾牌,且他右边的人没盾牌的[林檎实] \(j\) (\(1\leq j<n\)),将第 \(j\) 个[林檎实]的盾牌给他右边的人。
现在,给你一个长度为 \(n\) 的 \(01\) 序列,表示初始时每个[林檎实]是否拿着盾牌。
[林檎实]的妹子随时会过来找他约会,所以他也不知道他还能进行几次操作。因此,对于所有 \(k\in[0,\frac{n(n-1)}{2}]\),请算出做不超过 \(k\) 次操作时,最多能有多少对被保护的[林檎实]。也就是说,你需要输出 \(\frac{n(n-1)}{2}\) 个数。
数据范围:\(1\leq n\leq 80\)。
本题题解
设没拿盾牌的点数量为 \(c_0\),拿着盾牌的点数量为 \(c_1\)。
初步转化 1:考虑两个 \(0\) 不是被保护的,当且仅当它们之间没有 \(1\)。也就是说,被保护的对数 = 总对数 - 在同一段里的 \(0\) 的对数。具体来说,设有 \(k\) 段极长、连续的 \(0\),长度分别为 \(l_1,l_2,\dots,l_k\),则被保护的对数 \(=\frac{c_0(c_0-1)}{2}-\sum_{i=1}^{k}\frac{l_i(l_i-1)}{2}\)。于是我们只要最小化 \(\sum_{i=1}^{k}\frac{l_i(l_i-1)}{2}\) 即可。
初步转化 2:考虑已知一个目标局面(也就是一个 \(01\) 序列),那么从初始局面到这个目标局面的最少操作次数,显然就是两个局面里 第一个 \(1\)、第二个 \(1\)、...、第 \(c_1\) 个 \(1\) 分别的坐标差之和。也就是说,初始局面的每个 \(1\) 和目标局面的每个 \(1\),按顺序依次对应,可以证明这样操作是最优的(调整法)。记初始局面里每个 \(1\) 的位置分别为 \(p_1,p_2,\dots,p_{c_1}\)。
有了上述两个结论,我们可以通过 DP 来确定目标序列,顺便求出未被保护的对数(\(\sum_{i=1}^{k}\frac{l_i(l_i-1)}{2}\))和操作次数。
设 \(dp[i][x][y][z]\) 表示考虑了前 \(i\) 位,用了 \(x\) 次操作,前 \(i\) 位里总共有 \(y\) 个 \(1\),从最后一个 \(1\) 到 \(i\) 之间这最后一段 \(0\) 的长度为 \(z\),这个局面下未被保护的对数的最小值。转移时考虑第 \(i+1\) 位是填 \(0\) 还是填 \(1\) 即可。时间、空间复杂度都是 \(O(n^5)\)(因为 \(x\) 这一维大小是 \(\frac{n(n-1)}{2}=O(n^2)\) 的,不要忘了)。可以用滚动数组优化空间,不过这种做法时间、空间常数都较大。
考虑简化状态定义。设 \(dp[i][x][y]\) 表示考虑了前 \(i\) 位,第 \(i\) 位上填 \(1\),用了 \(x\) 次操作,前 \(i\) 位里总共有 \(y\) 个 \(1\),这个局面下未被保护的对数的最小值。转移时枚举下一个 \(1\) 在哪里。这样时间复杂度依然是 \(O(n^5)\),但空间复杂度优化到了 \(O(n^4)\)。本做法的 AC 代码请见【参考代码1】。
继续优化。考虑先枚举 \(x,y\),再枚举 \(i\)。此时能转移到 \(dp[i][x][y]\) 的,一定是 \(x'=x-|p_y -i|\),\(y'=y-1\)。也就是说 \(x'\) 和 \(y'\) 都是确定的,我们只需要找到最优的 \(j\) (\(j<i\)),然后用 \(dp[j][x'][y']\) 更新 \(dp[i][x][y]\) 即可。\(O(n^5)\) 的做法相当于是枚举了 \(j\)。考虑如何不枚举,快速求出最优的 \(j\)。
具体来说,我们先观察一下转移式:
首先可以去掉“除以 \(2\)”,在求答案的时候一起除。另外,因为 \(x,y,x',y'\) 都是常数,不妨将它们写在前面。于是我们重新定义状态:\(f_{x,y}(i) = dp[i][x][y]\times2\)。然后把上式中的 \((i - j - 1)\cdot (i - j - 2)\) 拆开,得到:
其中 \(i^2\), \(-3i\), \(2\) 都是常数,可以提出来。关键的部分是 \(-2ij\),可以用斜率优化搞定它。具体来说,把每个 \(j\) 看成二维平面上一个坐标为 \((j,f_{x',y'}(j)+j^2+3j)\) 的点,转移看成一条斜率为 \(2i\) 的直线,我们要最小化直线的截距(也就是 \(f_{x,y}(i)\) 加上一堆关于 \(i\) 的常数)。写成式子就是:
对每个 \((x',y')\) 分别维护一个下凸壳,然后在凸壳上二分出第一段斜率大于 \(2i\) 的位置即可。
时间复杂度 \(O(n^4\log n)\)。空间复杂度 \(O(n^4)\)。本做法的 AC 代码请见【参考代码2】。
参考代码
参考代码1:时间复杂度 \(O(n^5)\),空间复杂度 \(O(n^4)\)。
// problem: CF1420E
#include <bits/stdc++.h>
using namespace std;
#define pb push_back
#define mk make_pair
#define lob lower_bound
#define upb upper_bound
#define fi first
#define se second
#define SZ(x) ((int)(x).size())
typedef unsigned int uint;
typedef long long ll;
typedef unsigned long long ull;
typedef pair<int, int> pii;
template<typename T> inline void ckmax(T& x, T y) { x = (y > x ? y : x); }
template<typename T> inline void ckmin(T& x, T y) { x = (y < x ? y : x); }
const int MAXN = 80;
const int INF = 0x3f3f3f3f;
int n, a[MAXN + 5], cnt[2], pos[MAXN + 5];
int total, max_op;
int dp[MAXN + 5][MAXN * (MAXN - 1) / 2 + 5][MAXN + 5];
int calc(int len) { return len * (len - 1) / 2; }
int main() {
cin >> n;
for(int i = 1; i <= n; ++i) {
cin >> a[i];
cnt[a[i]]++;
if(a[i] == 1) {
pos[cnt[1]] = i;
}
}
total = calc(cnt[0]);
max_op = calc(n);
if(!cnt[1]) {
for(int i = 0; i <= max_op; ++i) cout << 0 << " "; cout << endl;
return 0;
}
memset(dp, 0x3f, sizeof(dp));
for(int i = 1; i < n; ++i) {
dp[i][abs(pos[1] - i)][1] = calc(i - 1);
for(int j = 0; j <= max_op; ++j) {
for(int k = 1; k <= i && k < cnt[1]; ++k) if(dp[i][j][k] != INF) {
int rest = cnt[1] - k;
for(int l = i + 1; l <= n - rest + 1; ++l) {
int newj = j + abs(pos[k + 1] - l);
if(newj <= max_op)
ckmin(dp[l][newj][k + 1], dp[i][j][k] + calc(l - i - 1));
}
}
}
}
int res = total;
for(int j = 0; j <= max_op; ++j) {
for(int i = cnt[1]; i <= n; ++i) if(dp[i][j][cnt[1]] != INF) {
ckmin(res, dp[i][j][cnt[1]] + calc(n - i));
}
cout << total - res << " ";
}
cout << endl;
return 0;
}
参考代码2:时间复杂度 \(O(n^4\log n)\) 斜率优化做法。
// problem: CF1420E
#include <bits/stdc++.h>
using namespace std;
#define pb push_back
#define mk make_pair
#define lob lower_bound
#define upb upper_bound
#define fi first
#define se second
#define SZ(x) ((int)(x).size())
typedef unsigned int uint;
typedef long long ll;
typedef unsigned long long ull;
typedef pair<int, int> pii;
template<typename T> inline void ckmax(T& x, T y) { x = (y > x ? y : x); }
template<typename T> inline void ckmin(T& x, T y) { x = (y < x ? y : x); }
const int MAXN = 80, MAXOP = MAXN * (MAXN - 1) / 2;
const int INF = 0x3f3f3f3f;
int n, a[MAXN + 5], cnt[2], pos[MAXN + 5];
int total, max_op;
int dp[MAXOP + 5][MAXN + 5][MAXN + 5];
struct Queue {
int a[MAXN + 5];
int ql, qr;
int size() { return qr - ql + 1; }
bool empty() { return ql > qr; }
int front() { return a[ql]; }
int back() { return a[qr]; }
int back2() { return a[qr - 1]; }
void push_back(int e) { a[++qr] = e; }
void pop_back() { --qr; }
void init() { ql = 1; qr = 0; }
}que[MAXOP + 5][MAXN + 5];
int calc(int len) { return len * (len - 1) / 2; }
int calc2(int len) { return len * (len - 1); }
int main() {
cin >> n;
for(int i = 1; i <= n; ++i) {
cin >> a[i];
cnt[a[i]]++;
if(a[i] == 1) {
pos[cnt[1]] = i;
}
}
total = calc(cnt[0]);
max_op = calc(n);
if(!cnt[1]) {
for(int i = 0; i <= max_op; ++i) cout << 0 << " "; cout << endl;
return 0;
}
memset(dp, 0x3f, sizeof(dp));
for(int j = 0; j <= max_op; ++j) {
for(int k = 1; k <= cnt[1]; ++k) {
que[j][k].init();
}
}
for(int i = 1; i <= n; ++i) {
#define Q que[abs(pos[1] - i)][1]
#define F dp[abs(pos[1] - i)][1]
#define Y(p) (F[(p)] + (p) * (p) + 3 * (p))
F[i] = calc2(i - 1);
while(Q.size() >= 2 && (Y(Q.back()) - Y(Q.back2())) * (i - Q.back()) > (Y(i) - Y(Q.back())) * (Q.back() - Q.back2()))
Q.pop_back();
Q.push_back(i);
#undef Q
#undef F
#undef Y
}
for(int j = 0; j <= max_op; ++j) {
for(int k = 2; k <= cnt[1]; ++k) {
for(int i = k; i <= n - (cnt[1] - k); ++i) {
if(j < abs(pos[k] - i)) continue;
int last_j = j - abs(pos[k] - i);
#define Q que[last_j][k - 1]
#define F dp[last_j][k - 1]
#define Y(p) (F[(p)] + (p) * (p) + 3 * (p))
if(Q.empty()) continue;
if(Q.front() >= i) continue;
int l = Q.ql;
int r = Q.qr;
while(l < r) {
int mid = (l + r + 1) >> 1;
if(Q.a[mid] < i) {
l = mid;
} else {
r = mid - 1;
}
}
l = Q.ql;
while(l < r) {
int mid = (l + r) >> 1;
int p1 = Q.a[mid], p2 = Q.a[mid + 1];
if(Y(p2) - Y(p1) > 2 * i * (p2 - p1)) {
r = mid;
} else {
l = mid + 1;
}
}
ckmin(dp[j][k][i], F[Q.a[l]] + calc2(i - Q.a[l] - 1));
#undef Q
#undef F
#define Q que[j][k]
#define F dp[j][k]
while(Q.size() >= 2 && (Y(Q.back()) - Y(Q.back2())) * (i - Q.back()) > (Y(i) - Y(Q.back())) * (Q.back() - Q.back2()))
Q.pop_back();
Q.push_back(i);
#undef Q
#undef F
#undef Y
}
}
}
int res = total;
for(int j = 0; j <= max_op; ++j) {
for(int i = cnt[1]; i <= n; ++i) if(dp[j][cnt[1]][i] != INF) {
ckmin(res, (dp[j][cnt[1]][i] + calc2(n - i)) / 2);
}
cout << total - res << " ";
}
cout << endl;
return 0;
}