P11331 题解

blog。写了好几天,人都要死了。提供一个不同的切入口,方便大家理解这个分段是在干嘛,以及一个更容易的线段树 DS。题解一堆废话,大家看看就行(

\(O(N^3)\)

先把 \(a_i\ne-1\) 且无论如何无法成为前缀 max 的位置 ban 掉。

由于答案只与 premax 的位置有关,于是\(dp_{i,j}\) 表示确定完 \(a_1,a_2,\cdots,a_i\) 且 premax 的位置为 \(j\) 的答案。

考虑转移 \(dp_{i,j}\)

  • \(a_i=-1\)
    • [1-1] 随便放个以后不用的小的:\(dp_{i,j}\gets dp_{i-1,j}\)
    • [1-2] 若上一个 premax 比 \(j\) 小,现在必须贪心地放 \(j\)(要求 \(vis_j=\textbf{true}\)):\(dp_{i,j}\gets\max\limits_{w=0}^{j-1}dp_{i-1,w}+c_j\)
  • \(a_i\ne-1\):可以发现,只需要考虑 \(j\ge a_i\) 的情况。
    • [2-1] 我要我要!这个时候强制 \(j=a_i\)\(dp_{i,a[i]}\gets\max\limits_{w=0}^{a[i]-1}dp_{i-1,w}+c_{a[i]}\)
    • [2-2] 不要不要!这需要保证前面的 premax 比 \(a_i\) 大:\(dp_{i,j}\gets dp_{i-1,j}\ \ (a_i<j\le n)\)

初始化 \(-\infty\)\(dp_{0,0}=0\);第 \(K\) 个答案即为 \(\max\limits_{i=0}^n dp_{K,i}\)

const int N = 4e5 + 5;
int n, a[N], c[N]; bool vis[N]; ll dp[2005][2005];
int main() {
	scanf("%d", &n);
	for (int i = 1; i <= n; i++) scanf("%d", &a[i]), ~a[i] ? (vis[a[i]] = true) : 0;
	for (int i = 1; i <= n; i++) scanf("%d", &c[i]);
  
	for (int i = 1, p = 0; i <= n; i++)
		if (~a[i]) {if (p > a[i]) c[a[i]] = 0;} else {while (vis[++p]);}

	mems(dp, -0x3f), dp[0][0] = 0;
	for (int i = 1; i <= n; i++) {
		if (~a[i]) {
			for (int j = 0; j < a[i]; j++) dp[i][a[i]] = max(dp[i][a[i]], dp[i - 1][j] + c[a[i]]);
			for (int j = a[i] + 1; j <= n; j++) dp[i][j] = dp[i - 1][j];
		} else {
			for (int j = 1; j <= n; j++) {
				dp[i][j] = dp[i - 1][j];
				if (!vis[j]) {for (int k = 0; k < j; k++) dp[i][j] = max(dp[i][j], dp[i - 1][k] + c[j]);}
			}
		}
		printf("%lld ", *max_element(dp[i], dp[i] + n + 1));
	}
	return 0;
}

\(O(N^2)\)

前缀 max 容易优化至 \(O(n^2)\)。由于过一会要上 DS,我们先把 code 变好看一点:

  1. 钦定 \(vis_j=\textbf{false}\)\(c^{\prime}_j=-\infty\) 否则 \(c^{\prime}_j=c_j\)。于是可以删个 [1-2] 的判断条件。

  2. \(f_{i,j}\) 表示确定完 \(a_1,a_2,\cdots,a_i\) 且 premax 的位置为 \(\le j\) 的答案。即 \(f_{i,j}=\max\limits_{w=0}^j dp_{i,w}\)。注意到可以全程依赖 \(f\) 进行转移!只要在每次结束后进行 [3-1] 前缀 chkmax 操作就行。

const int N = 4e5 + 5;
int n, a[N], c[N]; bool vis[N]; ll ccc[N], f[2005][2005];
int main() {
	scanf("%d", &n);
	for (int i = 1; i <= n; i++) scanf("%d", &a[i]), ~a[i] ? (vis[a[i]] = true) : 0;
	for (int i = 1; i <= n; i++) scanf("%d", &c[i]);
  
	for (int i = 1, p = 0; i <= n; i++) if (~a[i]) {if (p > a[i]) c[a[i]] = 0;} else {while (vis[++p]);}
	for (int i = 1; i <= n; i++) ccc[i] = (!vis[i] ? c[i] : -0x3f3f3f3f3f3f3f3f);

	mems(f, -0x3f), mems(f[0], 0);
	for (int i = 1; i <= n; i++) {
		if (~a[i]) {
			f[i][a[i]] = f[i - 1][a[i] - 1] + c[a[i]];
			for (int j = a[i] + 1; j <= n; j++) f[i][j] = f[i - 1][j];
		} else {
			for (int j = 1; j <= n; j++) f[i][j] = max(f[i - 1][j], f[i - 1][j - 1] + ccc[j]);
		}
		for (int j = 1; j <= n; j++) f[i][j] = max(f[i][j - 1], f[i][j]); printf("%lld ", f[i][n]);
	}
	return 0;
}

进而把代码改成 1D 形式。可以直接 DS......吗?

const int N = 4e5 + 5; const ll INF = 0x3f3f3f3f3f3f3f3f;
int n, a[N], c[N]; bool vis[N]; ll ccc[N], f[N], g[N];
int main() {
	scanf("%d", &n);
	for (int i = 1; i <= n; i++) scanf("%d", &a[i]), ~a[i] ? (vis[a[i]] = true) : 0;
	for (int i = 1; i <= n; i++) scanf("%d", &c[i]);

	for (int i = 1, p = 0; i <= n; i++) if (~a[i]) {if (p > a[i]) c[a[i]] = 0;} else {while (vis[++p]);}
	for (int i = 1; i <= n; i++) ccc[i] = (!vis[i] ? c[i] : -INF);

	for (int i = 1; i <= n; i++) {
		if (~a[i]) {
			f[a[i]] = f[a[i] - 1] + c[a[i]];
			for (int j = 0; j < a[i]; j++) f[j] = -INF;
		} else {
			for (int j = n; j; j--) f[j] = max(f[j], f[j - 1] + ccc[j]);
			f[0] = -INF;
		}
		for (int j = 1; j <= n; j++) f[j] = max(f[j - 1], f[j]); printf("%lld ", f[n]);
	}
	return 0;
}

Optimize1

其中一个大问题是 \(\forall(j:n\to1)\ f_j=\max(f_j,f_{j-1}+c^{\prime}_j)\) 这一句?这个结构是线段树无法维护的。这个是本题的最大难点。但是打表发现,只要 \(f_{j-1}\ne-\infty\),那么这个 chkmax 一定是后面成功!

(这是因为当 \(f_{j-1}\ne-\infty\),说明这个值及其后缀的值都是有效的。那么,如果 \(f_j>f_{j-1}+c^{\prime}_j\)由于 \(c\) 单调不降,那么我们将 \(f_j\) 处最大值的贡献更换为任意一个数,必定仍然有 \(f_j>f_{j-1}\),这说明 \(f_{j-1}\) 可以取更大,矛盾!)

进一步地,由于 \(f\) 单调不降,所以 \(f_{j}\ne-\infty\) 的一定是一段后缀。我们可以维护指针 \(up\) 表示这个后缀。

const int N = 4e5 + 5; const ll INF = 0x3f3f3f3f3f3f3f3f;
int n, a[N], c[N]; bool vis[N]; ll ccc[N], f[N], g[N];
int main() {
	scanf("%d", &n);
	for (int i = 1; i <= n; i++) scanf("%d", &a[i]), ~a[i] ? (vis[a[i]] = true) : 0;
	for (int i = 1; i <= n; i++) scanf("%d", &c[i]);
	
	for (int i = 1, p = 0; i <= n; i++) if (~a[i]) {if (p > a[i]) c[a[i]] = 0;} else {while (vis[++p]);}
	for (int i = 1; i <= n; i++) ccc[i] = (!vis[i] ? c[i] : -INF);

	int up = 0;
	for (int i = 1; i <= n; i++) {
		if (~a[i]) {
			f[a[i]] = max(f[a[i]], f[a[i] - 1] + c[a[i]]);
			for (int j = 0; j < a[i]; j++) f[j] = -INF;
		} else {
			for (int j = n; j > up; j--) f[j] = f[j - 1] + ccc[j];
			f[0] = -INF;
		}
		for (int j = 1; j <= n; j++) f[j] = max(f[j - 1], f[j]);
		for (int j = 1; j <= n; j++) if (f[j] >= 0) {up = j; break;}
		printf("%lld ", f[n]);
	}
	return 0;
}

Optimize2

另一个问题是前缀 chkmax。这个反而不困难,我们挖掘代码中的单调性质就能解决。

先看看 \(up\)。若 \(a_i\ne-1\),那么每次等价于 \(up\gets\max(up,a_i)\);否则, \(a_i=1\) 时除了边界情况,\(up\) 都应该不变。这说明 \(up\) 单调不降

  • 对于 \(a_i\ne-1\) 的情况,直接写成主动转移的形式(\(\forall j\in(up,n],f_j\gets\max(f_j,f_{a_i-1}+c_{a_i})\)),发现此时前缀 chkmax 操作直接丢掉就行。
  • 对于 \(a_i=-1\) 的情况,发现若 \(\forall c^{\prime}_j=c_j\),那么 \(f_{j-1}+c^{\prime}_j\) 是单调的(因为 \(f,c\) 都单调)!也就是说,所有 \(c^{\prime}_j=-\infty\) 的位置的值,都可以变成前面的第一个 \(c^{\prime}_j\ne-\infty\) 的位置的值。

自然地,考虑能否将 \(c^{\prime}_j=-\infty\) 的位置直接丢掉。

这显然是可以的!只要将所有 \(c^{\prime}_j=-\infty\)\(vis_j=\textbf{false}\) 的位置与其后一个 \(vis_j=\textbf{true}\) 的位置划分成一段,记录 \(up\) 的时候记录两个(一个是真实值,一个是对应到块的值)就行,每个块除了 \(-\infty\) 的情况,全部元素都应当是相同的。

const int N = 4e5 + 5; const ll INF = 0x3f3f3f3f3f3f3f3f;
int n, m, up, ups, a[N], c[N], bel[N]; bool vis[N]; ll f[N], ccc[N];
int main() {
	scanf("%d", &n);
	for (int i = 1; i <= n; i++) scanf("%d", &a[i]), ~a[i] ? (vis[a[i]] = true) : 0;
	for (int i = 1; i <= n; i++) scanf("%d", &c[i]);
  
	for (int i = 1, p = 0; i <= n; i++) if (~a[i]) {if (p > a[i]) c[a[i]] = 0;} else {while (vis[++p]);}
	for (int i = 1; i <= n; i++) {if (!vis[i]) ccc[++m] = c[i]; bel[i] = m;}

	for (int i = 1; i <= n; i++) {
		if (~a[i]) {
			if (ups < a[i]) {
				int p = bel[a[i]]; ll x = f[p] + c[a[i]];
				for (int j = 0; j < p; j++) f[j] = -INF;
				for (int j = p; j <= m; j++) f[j] = max(f[j], x);
				ups = a[i], up = p;
			}
		} else {
			for (int j = m; j > up; j--) f[j] = f[j - 1] + ccc[j];
			f[0] = -INF;
			if (!up) {ups = 1; for (int j = 1; j <= m; j++) if (f[j] >= 0) {up = j; break;}}
		}
		printf("%lld ", f[m]);
	}
	return 0;
}

ShiftTag + DS

终于,启动 DS!最麻烦的一个部分是"区间平移 + 区间加对应位置的数"。前者可以打 Shift Tag,后缀可以维护 \(f_i=v_i+\sum\limits_{l_i}^{r_i} c_i\) 转化为 \(r_i\) 的区间加。

有点问题的是区间 chkmax 操作,但由于 \(f\) 有单调性,二分分界点后就能转为区间覆盖。

const int N = 8e5 + 5, X = 4e5; const ll INF = 0x3f3f3f3f3f3f3f3f;
int n, m, up, ups, a[N], c[N], bel[N]; bool vis[N]; ll f[N], l[N], r[N], ccc[N], s[N];
inline ll cal(int x) {return f[x] + s[r[x]] - s[l[x] - 1];}
int main() {
	scanf("%d", &n);
	for (int i = 1; i <= n; i++) scanf("%d", &a[i]), ~a[i] ? (vis[a[i]] = true) : 0;
	for (int i = 1; i <= n; i++) scanf("%d", &c[i]);

	for (int i = 1, p = 0; i <= n; i++) if (~a[i]) {if (p > a[i]) c[a[i]] = 0;} else {while (vis[++p]);}
	for (int i = 1; i <= n; i++) {if (!vis[i]) ccc[++m] = c[i]; bel[i] = m;}
	for (int i = 1; i <= m; i++) s[i] = s[i - 1] + ccc[i];

	int S = 0;
	for (int i = 0; i <= 4e5; i++) l[i + X - S] = i + 1, r[i + X - S] = i;
	for (int i = 1; i <= n; i++) {
		if (~a[i]) {
			if (ups < a[i]) {
				int p = bel[a[i]]; ll x = cal(p + X - S) + c[a[i]];
				for (int j = 0; j < p; j++) f[j + X - S] = -INF;
				for (int j = p; j <= m; j++) if (x > cal(j + X - S)) f[j + X - S] = x, l[j + X - S] = j + 1, r[j + X - S] = j;
				ups = a[i], up = p;
			}
		} else {
			f[up - 1 + X - S] = f[up + X - S], l[up - 1 + X - S] = l[up + X - S], r[up - 1 + X - S] = r[up + X - S];
			for (int j = up; j < m; j++) r[j + X - S]++;
			S++; f[0 + X - S] = -INF, l[0 + X - S] = 1, r[0 + X - S] = 0; if (!up) up = ups = 1;
		}
		printf("%lld ", cal(m + X - S));
	}
	return 0;
}

这样再怎么样你都会做了吧?再维护一个 \(l^{\prime}_i=l_i+i,r^{\prime}_i=r_i+i\) 消除 \(\Delta\),然后对 \(v,l^{\prime},r^{\prime}\) 各维护一棵线段树就行。二分部分在三棵线段树上并行二分就能做到单 log。其余部分只需要区间加、区间覆盖、单点查。

综上,我们用大常数 \(O(N\log N)\) 通过了此题,被 deque 做法吊打。

Code

没啥参考价值的。

#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <vector>
#define mems(x, v) memset(x, v, sizeof x)
#define mcpy(x, y) memcpy(x, y, sizeof x)
using namespace std;
typedef pair <int, int> pii;
typedef long long ll;
typedef unsigned long long ull;
typedef long double wisdom;

const int N = 8e5 + 1005, X = 4e5 + 100; const ll INF = 0x3f3f3f3f3f3f3f3f, NOTVIS = -INF - 114514;
int n, m, up, ups, a[N], c[N], bel[N]; bool vis[N]; ll ccc[N], s[N];

#define ls pos << 1
#define rs pos << 1 | 1
struct SGT {
	// ll a[N];
	// void ADD(int l, int r, ll k) {for (int i = l; i <= r; i++) a[i] += k;} void COV(int l, int r, ll k) {for (int i = l; i <= r; i++) a[i] = k;} ll Q(int x) {return a[x];}
	ll LV[N << 2], add[N << 2], cov[N << 2]; void pushup(int pos) {LV[pos] = LV[ls];} void lcov(int pos, ll k) {cov[pos] = LV[pos] = k, add[pos] = 0;} void ladd(int pos, ll k) {add[pos] += k, LV[pos] += k;}
	void C(int pos) {if (cov[pos] != NOTVIS) lcov(ls, cov[pos]), lcov(rs, cov[pos]), cov[pos] = NOTVIS;} void A(int pos) {if (add[pos]) ladd(ls, add[pos]), ladd(rs, add[pos]), add[pos] = 0;} void pushdown(int pos) {C(pos), A(pos);}
	void update(int l, int r, int pos, int L, int R, ll k) {if (L <= l && r <= R) return (l == r ? void() : C(pos)), ladd(pos, k); int mid = (l + r) >> 1; pushdown(pos); if (L <= mid) update(l, mid, ls, L, R, k); if (mid < R) update(mid + 1, r, rs, L, R, k); pushup(pos);}
	void modify(int l, int r, int pos, int L, int R, ll k) {if (L <= l && r <= R) return lcov(pos, k); int mid = (l + r) >> 1; pushdown(pos); if (L <= mid) modify(l, mid, ls, L, R, k); if (mid < R) modify(mid + 1, r, rs, L, R, k); pushup(pos);}
	ll query(int l, int r, int pos, int id) {if (l == r) return LV[pos]; int mid = (l + r) >> 1; pushdown(pos); return id <= mid ? query(l, mid, ls, id) : query(mid + 1, r, rs, id);}
	inline void ADD(int l, int r, ll k) {if (l <= r) update(1, 801000, 1, l, r, k);} inline void COV(int l, int r, ll k) {if (l <= r) modify(1, 801000, 1, l, r, k);} ll Q(int x) {return query(1, 801000, 1, x);}
	inline void ADD(int x, ll k) {ADD(x, x, k);} inline void COV(int x, ll k) {COV(x, x, k);}
} F, L, R;

inline ll cal(int x) {return F.Q(x) + s[R.Q(x) + x] - s[L.Q(x) + x - 1];}
int fnd(int l, int r, int pos, int pl, int pr, ll x) {
	if (l == r) return l; int mid = (l + r) >> 1; F.pushdown(pos), L.pushdown(pos), R.pushdown(pos);
	if (mid >= pr) return fnd(l, mid, ls, pl, pr, x); if (pl > mid) return fnd(mid + 1, r, rs, pl, pr, x);
	return F.LV[rs] + s[R.LV[rs] + (mid + 1)] - s[L.LV[rs] + (mid + 1) - 1] >= x ? fnd(l, mid, ls, pl, pr, x) : fnd(mid + 1, r, rs, pl, pr, x);
}
int main() {
	scanf("%d", &n);
	for (int i = 1; i <= n; i++) scanf("%d", &a[i]), ~a[i] ? (vis[a[i]] = true) : 0;
	for (int i = 1; i <= n; i++) scanf("%d", &c[i]);

	for (int i = 1, p = 0; i <= n; i++) if (~a[i]) {if (p > a[i]) c[a[i]] = 0;} else {while (vis[++p]);}
	for (int i = 1; i <= n; i++) {if (!vis[i]) ccc[++m] = c[i]; bel[i] = m;}
	for (int i = 1; i <= m; i++) s[i] = s[i - 1] + ccc[i];

	for (int i = 0; i < (N << 2); i++) F.cov[i] = L.cov[i] = R.cov[i] = NOTVIS;
	int S = 0;
	for (int i = 0; i <= 4e5 + 100; i++) L.COV(i + X - S, S - X + 1), R.COV(i + X - S, S - X);
	for (int i = 1; i <= n; i++) {
		if (~a[i]) {
			if (ups < a[i]) {
				int p = bel[a[i]]; ll x = cal(p + X - S) + c[a[i]];
				//int lo = p + X - S, hi = m + X - S; while (lo < hi) {int md = (lo + hi + 1) >> 1; cal(md) < x ? lo = md : hi = md - 1;}
				int lo = fnd(1, 801000, 1, p + X - S, m + X - S, x);
				F.COV(p + X - S, lo, x), L.COV(p + X - S, lo, S - X + 1), R.COV(p + X - S, lo, S - X);
				ups = a[i], up = p;
			}
		} else {
			F.COV(up - 1 + X - S, F.Q(up + X - S)), L.COV(up - 1 + X - S, L.Q(up + X - S) + 1), R.COV(up - 1 + X - S, R.Q(up + X - S) + 1);
			R.ADD(up + X - S, m + X - S, 1);
			S++; F.COV(0 + X - S, -INF), L.COV(0 + X - S, 1 - (0 + X - S)), R.COV(0 + X - S, 0 - (0 + X - S)); if (!up) up = ups = 1;
		}
		printf("%lld ", cal(m + X - S));
	}
	return 0;
}
posted @ 2024-12-25 22:29  liangbowen  阅读(6)  评论(0编辑  收藏  举报