CF1648D 题解

CF1648D 题解

在这里分享两种做法,分别是 \(O(n\log^2n)\)cdq 分治,和 \(O(n\log n)\) 的线段树,

由于笔者学艺不精,故如有错误或笔误之处欢迎提出,笔者将不胜感激。

题意

给一条 \(3\)\(n\) 列的带权(可以为负)网格,最初时第二行的所有格子是黑色,其它格子是白色。

你有一些三元组 \((L_p,R_p,k_p)\),代表可以花费 \(k_p\) 的代价把第二行在区间 \([L_p,R_p]\) 内的格子染黑。

你需要找到一条从 \((1,1)\)\((n,m)\) 的纯黑色路径,并最大化经过格子的权值和减去染色的代价。

做法

\((i,j)\) 处格子的权为 \(v_{i,j}\),我们记 \(s_{x,y}=\sum\limits_{i=1}^yv_{x,i},a_l=s_{1,l}-s_{2,l-1},b_r=s_{3,n}-s_{3,r-1}+s_{2,r}\),则:

问题等价于求 \(\max\limits_{1\le l\le r\le n}(a_l+b_r-cost(l,r))\),其中 \(cost(l,r)\) 是将第二行中区间 \([l,r]\) 染黑的最小代价。

我们考虑如何计算 \(cost\) 函数的值,这时可以考虑一个暴力的 DP,即:

初始值是:\(\forall l\in[1,n],cost(l,l-1)=0\),即初始时第二行所有格子都不是黑色;

转移式为:\(cost(l,i)=\min\limits_{\exists p\space:\space i,j\in[L_p,R_p],j\le i}(cost(l,j-1)-k_p)\),即用第 \(p\) 个三元组拓宽已染黑的区间。

发现 \(cost(l,r)\) 在转移时其 \(l\) 不变,且转移仅与 \(r\) 有关,那我们记 \(f(r)=\max\limits_{l\le r}(a_l-cost(l,r))\)

则此时 \(f\) 函数也能用 DP 的方式计算,即:\(f(r)=\max\limits_{\exists p\space:\space L_p\le l\le r\le R_p}(f(l-1)-k_p)\)

这个式子是容易得到的,将 \(f\) 函数的定义式里的 \(cost\) 函数表示为其转移式就行。

那么最后的答案就等于 \(\max\limits_{r=1}^n(b_r+f(r))\),我们也就得到了一个 \(O(n^2)\) 的做法,但其实可以继续优化。

我们可以用 cdq 分治为 \(f\) 函数的计算加速,具体来说,我们对当前分治区间 \([l,r]\) 及区间中点 \(mid\)

我们先递归处理区间 \([l,mid]\)\(f\) 值,若三元组 \((L_p,R_p,k_p)\) 满足 \(L_p\le mid<R_p\)

则对 \(\forall \max(l,L_p)\le i\le mid<j\le\min(R_p,r)\),我们都可以花费 \(k_p\) 的代价从 \(f(i-1)\) 转移到 \(f(j)\)

那么,我们可以将所有满足上面条件的转移 \(i-1\rightarrow j\) 拆成三步,即:

先从 \(f(i-1)\) 转移到 \(f(L_p)\),再从 \(f(L_p)\) 转移到 \(f(R_p)\),最后从 \(f(R_p)\) 转移到 \(f(j)\)

容易发现,如果你用做后缀 \(\max\) 的方式去维护第一步和第三步,并暴力的维护第二步,

那么复杂度递推式就是:\(T(l,r)=T(l,mid)+T(mid+1,r)+O(r-l)+O(num(l,r))\)

其中 \(num(l,r)\) 为对 \([l,r]\) 能产生影响的三元组个数,故 \(T(l,r)=O(n\log^2n)\)

原因是三元组最多只会影响到 \(O(\log n)\) 个分治区间的转移,

这个可以用线段树区间操作的复杂度来证明,即可以将任意 \([l,r]\) 拆成 \(O(\log n)\) 个线段树上的区间,

而对分治区间 \([l,r]\),若 \(\exists p,q\) 作为两个三元组的编号,满足 \([l,r]\subseteq[L_p,R_p],[l,r]\subseteq[L_q,R_q]\)

\(p\)\(q\)\(k\) 值较大的那个一定是无用的。

以上只是介绍了一种对本题中 cdq 分治的复杂度的一种理解方式,而并不是更严谨的证明,

更严谨的证明可能还需要一些讨论,但由于笔者写的不是这种做法,故当时并没有做严谨的证明,

但相信强大的读者们一定能够自行完成这个证明。

事实上,上面的 DP 式还有一种优化方式,即减少无用转移以优化转移复杂度。

为了方便说明我们是如何优化转移的,在此我们先说明一些定义。

我们首先定义 “ \(f(i)\) 能转移一次后到 \(f(j)\) ” 为,\(f(j)\) 最终的最优解可以由 \(f(i)\) 的转移而取到,

\(f(i)\) 可以作为 \(f(j)\)DP 的最优转移的 DAG 中的前驱。

为了使叙述更便捷,我们用 \(f(i)\rightarrow f(j)\) 来代替 “ \(f(i)\) 能转移一次后到 \(f(j)\) ” 。

自然的,对于 \(\forall m>1\),我们可以定义 “ \(f(i)\) 能转移 \(m\) 次后到 \(f(j)\) ” 为,存在序列 \(p[1\cdots m]\),满足:

\(f(i)\rightarrow f(s_1),\forall q\in [1,m-1], f(p_q)\rightarrow f(p_{q+1}),f(p_m)\rightarrow f(j)\) 这些条件全部成立。

那么,考虑这样的序列可能会有很多种,相应的,我们也就做了很多次无用转移,

因为我们只需要所有合法序列中的一个就可以从 \(i\) 转移到 \(j\)

那么,我们可以考虑适当并合理地减少转移量,使对 \(\forall i,j,m(i<j)\),合法的序列 $ p[1\cdots m]$ 唯一。

这等价于给出一些限制并按照限制来调整 \(p\) 序列,最后再给出一种新的转移方式,

使得这个转移方式的实际效果与调整后的 \(p\) 序列是一致的,且这个新转移的复杂度更低或可以优化。

那我们先考虑如何合理的调整 \(p\) 序列。

一个想法是,我们在保证序列合法的前提下,将 \(p\) 中每一个位置调整地尽量靠左,

以达到类似去重的目的。注意到为什么这里的 ” 尽量靠左 “ 的限制没有给出一个调整的先后顺序,

是因为:如果将位置 \(p_t\) 调整成 \(p'_t\)\(p\) 序列仍合法,

\(p_t\) 所能转移到的位置集,必须与 \(p'_t\) 所能转移到的位置集一致,否则一定能构造出不合法的例子,

故此时确定调整顺序是不必要的,且按任意一种调整顺序去调整到尽量靠左,最后结果都是一致的。

一种更直观的的证明,是考虑原序列 \(p\) 一定有一个对应的有序三元组集合,

这个集合的每个元素,分别为 \(i,p_1,p_2,\cdots,p_m,j\) 这些位置中的每对相邻位置,提供了转移的条件,

故这个集合的大小是 \(m+1\),而 \(p_t\) 可以调整到的合法位置就是 \([L_t,R_t]\cap[L_{t+1}-1,R_{t+1}]\)

再进一步发现,此时一定有 \([L_t,R_t]\cap[L_{t+1}-1,R_{t+1}]=[L_{t+1}-1,R_t]\),原因是:

这些区间显然不会有包含或不交的情况,而因为 DP 转移时下标单增,

故这个有序三元组集一定满足:\(\forall t\in[1,m],L_t<L_{t+1},R_t<R_{t+1}\),即可证明上面结论。

而由于我们想让每个元素尽量靠左,故此时对 \(\forall t\in[1,m]\)\(p_t\) 就会被调整到 \(L_{t+1}-1\) 位置。

那么我们考虑应该如何转移,才能使转移的实际效果,与按照上面方式来调整后得到的序列 \(p\) 一致。

发现新的转移方式等价于,每次转移到的新点,都是某个三元组对应区间 \([L_q,R_q]\) 的左端点减一,

而这个三元组 \((L_q,R_q,k_q)\) 也一定会被下一次转移所依赖,即下一次转移靠的就是这个三元组。

也就是说,我们除了在第一次转移时会从任意位置出发,

以后的每次转移的起点都是某个三元组对应区间 \([L_p,R_p]\) 的左端点,终点是这个区间内的所有点。

这时我们需要支持区间与某数取 \(\max\) 和单点查询的数据结构,而线段树已经足以支持这一点了,

但实际上我们没必要写区间取 \(\max\) 数据结构 ,而是考虑将第二行整体翻转后,转移的形式。

发现此时转移等价于:除了最后一次转移的终点可以是任意点,

其余每次转移的起点是某个三元组对应区间 \([L_q,R_q]\) 内所有点,终点是 \(R_q+1\)

原因也是好理解的,即我们将第二行翻转后,三元组对应区间的左端点减一,就变成了右端点加一。

那么,除最后一步以外的转移,我们只需要区间查询 \(\max\) 值,单点修改的数据结构就能快速维护。

而最后一步,我们可以把统计的答案稍作修改,即:\(\max\limits_{\exists p\space:\space1\le L_p\le l\le r\le R_p\le n}(f(l)+b_r-k_p)\)

如果我们统计的是这个答案,那么我们对最后一步的转移,就也可以与其它转移一样了,

因为此时,最后一步只转移到了倒数第二个用来染色的三元组对应区间 \([L_m,R_m]\) 的右端点加一,

而此时 \(R_p+1\) 一定在最后一个用来染色的三元组对应区间 \([L_{m+1},R_{m+1}]\) 内,

故上面的答案统计一定是正确的,而快速维护答案的方法,我们也只需要一棵线段树即可,

具体来说,我们算好 \(f\) 函数后,枚举三元组 \(p\),则你需要查询 \(\max\limits_{L_p\le l\le r\le R_p}(f(l)+b_r)\)

而这个式子放在线段树上是具有结合律的信息,我们可以用类似于维护区间最大子段和的方法做到。

这样的时间复杂度就是 \(O(n\log n)\) 的,下面放个代码,代码里的实现可能与题解中所讲略有不同。

#include <bits/stdc++.h>

#define fi first
#define se second
#define vi vector
#define db double
#define mp make_pair
#define pb push_back
#define LL long long
#define emp emplace_back
#define pii pair < int , int >
#define SZ(x) ((int)(x.size()))
#define all(x) x.begin(), x.end()
#define ckmax(a, b) ((a) = max((a), (b)))
#define ckmin(a, b) ((a) = min((a), (b)))
#define rep(i, a, b) for (int i = (a); i <= (b); i++)
#define per(i, a, b) for (int i = (a); i >= (b); i--)
#define edg(i, v, u) for (int i = head[u], v = e[i].to; i; i = e[i].nxt, v = e[i].to)
#define rt 1, 1, n
#define mid ((l + r) >> 1)
#define lsn p << 1, l, mid
#define rsn p << 1 | 1, mid + 1, r
using namespace std;
int read (char ch = 0, int x = 0, int f = 1) { 
	// 快读就不放了
}
void write (int x) { 
	// 快输也不放了
}

const int N (5e5 + 5);
const LL INF (4e18 + 5);

int n, q;
LL x[4][N];
LL s[4][N];
LL s2[2][N];
LL a[N];
LL b[N];

struct Node {
	int l, r; LL k;
} seg[N];
struct Data {
	LL lmx, rmx, val;
};
bool cmp (Node u, Node v) {
	return u.r < v.r;
}
Data mrg (Data u, Data v) { //查询最后答案时,合并区间信息
	Data r;
	r.lmx = max (u.lmx, v.lmx);
	r.rmx = max (u.rmx, v.rmx);
	r.val = max ({u.val, v.val, u.lmx + v.rmx});
	return r;
}
struct SGTlft { // 用来维护转移的线段树
	LL t[N << 2];
	void push_up (int p) {
		t[p] = max (t[p << 1], t[p << 1 | 1]);
	}
	void build (int p, int l, int r) {
		t[p] = -INF;
		if (l == r) t[p] = s[1][l] - s2[0][l - 1];
		else build (lsn), build (rsn), push_up (p);
	}
	LL qmax (int p, int l, int r, int ql, int qr, LL ret = -INF) {
		if (ql <= l && qr >= r) return t[p];
		if (ql <= mid) ckmax (ret, qmax (lsn, ql, qr));
		if (qr > mid)  ckmax (ret, qmax (rsn, ql, qr));
		return ret;
	}
	void upd (int p, int l, int r, int pos, LL val) {
		if (l == r) return ckmax (t[p], val), void();
		if (pos <= mid) upd (lsn, pos, val);
		else upd (rsn, pos, val);
		push_up (p); 
	}
} t1;

struct SGTmax { //最后用来查询答案的线段树
	Data t[N << 2];
	void push_up (int p) {
		t[p] = mrg (t[p << 1], t[p << 1 | 1]);
	}
	void build (int p, int l, int r) {
		t[p] = {-INF, -INF, -INF};
		if (l == r) t[p] = {a[l], b[l], a[l] + b[l]};
		else build (lsn), build (rsn), push_up (p);
	}
	Data qry (int p, int l, int r, int ql, int qr, LL ret = -INF) {
		if (ql <= l && qr >= r) return t[p];
		Data lft = {-INF, -INF, -INF}, rht = {-INF, -INF, -INF};
		if (ql <= mid) lft = qry (lsn, ql, qr);
		if (qr > mid) rht = qry (rsn, ql, qr);
		return mrg (lft, rht);
	}
} t2;

int main() {
	LL res = -INF;
	n = read(), q = read();
	rep (i, 1, 3) rep (j, 1, n) x[i][j] = read();
	rep (i, 1, n) s[1][i] = s[1][i - 1] + x[1][i], 
				  s2[0][i] = s2[0][i - 1] + x[2][i];
	per (i, n, 1) s[3][i] = s[3][i + 1] + x[3][i],
				  s2[1][i] = s2[1][i + 1] + x[2][i];
	rep (i, 1, q) 
	  seg[i].l = read(), seg[i].r = read(), seg[i].k = read();
	sort (seg + 1, seg + q + 1, cmp), t1.build (rt);
	rep (i, 1, q) {
		int l = seg[i].l, r = seg[i].r; LL k = seg[i].k;
		LL val = t1.qmax (rt, l, r) - k; t1.upd (rt, r + 1, val);
	}
	rep (i, 1, n) a[i] = t1.qmax (rt, i, i), b[i] = s[3][i] - s2[1][i + 1];
	t2.build (rt);
	rep (i, 1, q) {
		int l = seg[i].l, r = seg[i].r; LL k = seg[i].k;
		ckmax (res, t2.qry (rt, l, r).val - k);
	}
	printf ("%lld\n", res + s2[0][n]);
	return 0;
}
posted @ 2022-03-11 10:47  GaryH  阅读(94)  评论(0编辑  收藏  举报