[NOISG 2022 Finals] Fruits
简要题意:给定一个递增数组的权值数组 \(c\),和一个已经确定了部分位置的排列,你需要补全这个排列,使得长度为 \(k\) 的所有前缀最大值权值和最大化,对于每个 \(k\) 求出答案。
牛牛牛题,zhy 一个上午之内拿下了一个做法 orz,然后我需要一个上午+一个下午才会这个题!
这个题第一步便是需要给出一个可以做到 \(O(n^2)\) 的想法,一个想法是考虑对于所有已经确定位置的前缀最大值,你 DP 它的一个子集,钦定这个子集一定产生贡献,这个可以导向 zhy 的想法。
假设你只是想拿 \(O(n^2)\) 的分跑路,你可以考虑一些更加暴力的 DP,比如设 \(dp_{i,j}\) 表示前 \(i\) 个数的最大值为 \(j\),最大的权值和。利用前缀 \(\max\) 转移是简单的。为了保证这个状态是合法的,也就是说必须要存在一个合法排列前 \(i\) 个数最大值为 \(j\),要求的条件是 \(s_i\le t_j\)。其中 \(s_i\) 表示前 \(i\) 个位置中有多少个没确定的元素,\(t_j\) 表示 \(1\sim j\) 中有多少个还没被填入排列。你只需要将不满足 \(s_i\le t_j\) 的位置清空就行了。
优化这种形式简单的二维 DP 的方向往往是进行某种整体 DP。为了更好地向整体 DP 的形式进发,你需要对 DP 形式进行一点简化。原 DP 状态一个很不舒服的点在于假设你关注到一个固定的 \(i\) 所有 \(j\) 对应的 DP 值,发现除了一段前缀是 \(-\infin\) 之外,其中还有很多零散的 \(-\infin\) 的位置。这些位置产生的原因在于在后 \(n-i\) 个数中有一些已经被填入的位置不能填到前面去,所以前面 \(i\) 个数的最大值不能是这些。
解决这个问题的想法是我们将所有还未填入排列中的数排序形成一个数组 \(b\),我们考虑到对于一个前缀 \(i\) 其可能的最大值,要么是已经填入的数的前缀最大值 \(mx\),要么就是 \(b\) 中比 \(mx\) 大且位置 \(\ge s_i\) 的那些元素。重新设计状态 \(f_{i,j}\) 表示前 \(i\) 个数最大值为 \(b_j\) 的最大权值,\(g_i\) 表示前 \(i\) 个数最大值为 \(mx\) 的最大权值。
将 DP 改写成如下形式之后,就可以马上发现一个重要性质:\(f_i\) 恰好是一个后缀有值,且有值的部分单调不降,但是这并不意味着 \(g_i\) 一定比 \(f_i\) 的最小值小。证明是显然的,因为对于 \(x<y\) 你可以把 \(f_{i,x}\) 中的一个 \(x\) 直接修改成 \(y\),使得权值变大。我们先写出一份 \(O(n^2)\) 代码(cur
就是 \(g_i\) 啊):
#include <algorithm>
#include <cstdio>
using namespace std;
const int N = 400003;
typedef long long ll;
const ll INF = 0x3f3f3f3f3f3f3f3f;
int read() {/* fast read */}
int n, m;
int a[N], b[N], c[N], s[N], t[N];
bool vis[N];
ll f[N];
inline void chmx(ll &x, ll v) {/* check max */}
int main() {
n = read();
for (int i = 1; i <= n; ++i) {
a[i] = read();
if (~a[i])
vis[a[i]] = 1;
t[i] = t[i - 1] + (a[i] < 0);
}
for (int i = 1; i <= n; ++i) {
c[i] = read();
if (!vis[i]) {
s[++m] = c[i];
b[m] = i;
}
}
int mx = 0, lim = 1, cnt = 0;
for (int i = 1; i <= m; ++i)
f[i] = -INF;
ll cur = 0;
for (int i = 1; i <= n; ++i) {
ll now = -INF;
if (~a[i]) {
if (mx < a[i]) {
chmx(now, cur);
while (lim <= m and b[lim] < a[i])
chmx(now, f[lim++]);
if (lim > cnt)
cur = now + c[a[i]];
else
cur = -INF;
mx = a[i];
}
} else {
for (int j = m; j >= lim; --j) {
if (j > lim) chmx(f[j], f[j - 1] + s[j]);
chmx(f[j], cur + s[j]);
}
++cnt;
if (cnt > lim)
++lim;
}
printf("%lld ", max(cur, f[m]));
}
putchar('\n');
return 0;
}
然后我的思想陷入到了瓶颈中,我当时以为 chmx(f[j], f[j - 1] + s[j])
这种形式的更新可以通过某种数据结构快速维护,所以一直在想如何用线段树维护这个形式。这些想法不出意外地都假了。
下午回来换了换🧠,听说到了 zhy 的做法是关于某种决策单调性的,于是开始意识到这个 DP 转移式子中有些特殊性质我没有发现。
我们注意到 \(f_{i,j}\) 如果去掉最大值的贡献,也就是减去一个 \(c_{b_j}\) (代码中写成 \(s_j\) 了),其依然满足单调不降的性质,因为就算最大值没贡献,依然是越大的最大值给前面的限制越松。
这也就意味着代码中的 chmx(f[j], cur + s[j]);
很诈骗,它实际上正好是对一个前缀赋值。此时已经有直接用数据结构优化的可能了,但是还可以继续思考性质。
对于代码中的 chmx(f[j], f[j - 1] + s[j]);
我们通过“IOI 赛制中的改一改再交上去法”验证注意到这其实是一定会 checkmax 成功的。
思考一下为什么?因为 \(f_{i,j}\) 和 \(f_{i,j-1}\) 之间不可能差了一整个 \(c_{b_j}\),否则将 \(f_{i,j}\) 方案中的 \(b_j\) 去掉换任意一个数填上来它的贡献都不劣于 \(f_{i,j-1}\)。所以这条语句写成 f[j] = f[j - 1] + s[i]
也是对的。
接下来的工作就很简单了!轻松套路整体 DP!我们对于固定的 \(i\) 维护一整个 \(f'_{i,j} = f_{i,j}-c_{b_j}\) 数组,我们需要支持哪些操作:
- 查询头尾。
- 删除一段前缀。
- 全局与某个数取 \(\max\),由于 \(f'_i\) 递增,实际上就是前缀赋值。
- 从有值第二个数开始,每个数等于原来的前一个数加上 \(c_{b_{j-1}}\)。
可以使用 deque
+ 懒标记来维护这个数组。懒标记的含义形如加上从当前位置开始往前一段区间中所有 \(c_{b_{j-1}}\) 的和。记录这个区间的长度作为懒标记就行了。
还有前缀赋值这个操作,这个可以直接考虑颜色段均摊。二分出需要赋值的前缀长度之后,删除一段前缀再往前 emplace_front
一段新的区间就可以了。时间复杂度为常数极小无比的 \(O(n\log n)\)(因为二分你只需要在某一个颜色段里二分就行了)。
#include <algorithm>
#include <cstdio>
#include <deque>
using namespace std;
const int N = 400003;
typedef long long ll;
const ll INF = 0x3f3f3f3f3f3f3f3f;
int read() {
char c = getchar();
int x = 0;
bool f = 0;
while (c < 48 or c > 57)
f |= (c == '-'), c = getchar();
do
x = (x * 10) + (c ^ 48), c = getchar();
while (c >= 48 and c <= 57);
return f ? -x : x;
}
int n, m, cnt, lim;
int a[N], b[N], c[N], s[N];
bool vis[N];
ll w[N];
inline void chmx(ll &x, ll v) {
if (x < v)
x = v;
}
struct Info {
int len, tag;
ll val;
Info(int L, int T, ll V) : len(L), tag(T), val(V) {
}
};
deque<Info> dq;
ll get_front() {
if (dq.empty())
return -INF;
ll res = dq.front().val;
int t = cnt - dq.front().tag;
res += w[lim - t] - w[lim];
return res;
}
ll get_back() {
if (dq.empty())
return -INF;
ll res = dq.back().val;
int t = cnt - dq.back().tag;
res += w[m - t] - w[m];
return res;
}
void del_front() {
if (!--dq.front().len)
dq.pop_front();
}
void del_back() {
if (!--dq.back().len)
dq.pop_back();
}
int main() {
n = read();
for (int i = 1; i <= n; ++i) {
a[i] = read();
if (~a[i])
vis[a[i]] = 1;
}
for (int i = 1; i <= n; ++i) {
c[i] = read();
if (!vis[i]) {
s[++m] = c[i];
b[m] = i;
}
}
int mx = 0;
for (int i = m; ~i; --i)
w[i] = w[i + 1] + s[i];
lim = 1;
dq.emplace_back(m, 0, -INF);
ll cur = 0;
for (int i = 1; i <= n; ++i) {
if (~a[i]) {
if (mx < a[i]) {
ll now = cur;
while (lim <= m and b[lim] < a[i])
chmx(now, get_front() + s[lim]), del_front(), ++lim;
if (lim > cnt)
cur = now + c[a[i]];
else
cur = -INF;
mx = a[i];
}
} else {
ll now = get_front();
++cnt;
if (!dq.empty())
del_back(), dq.emplace_front(1, cnt, now);
int sz = 0;
while (!dq.empty()) {
int x = lim + dq.front().len - 1, t = cnt - dq.front().tag;
ll val = dq.front().val + w[x - t] - w[x];
if (val > cur)
break;
sz += dq.front().len;
lim += dq.front().len;
dq.pop_front();
}
if (!dq.empty()) {
int l = lim, r = lim + dq.front().len,
tt = cnt - dq.front().tag;
while (l < r) {
int mid = (l + r) >> 1;
ll now = dq.front().val + w[mid - tt] - w[mid];
if (now <= cur)
l = mid + 1;
else
r = mid;
}
sz += r - lim;
dq.front().len -= r - lim;
lim = r;
}
if (sz)
dq.emplace_front(sz, cnt, cur), lim -= sz;
if (cnt > lim)
del_front(), ++lim;
}
printf("%lld ", max(cur, get_back() + s[m]));
}
putchar('\n');
return 0;
}