CF1648D 题解

CF1648D 题解

在这里分享两种做法,分别是 O(nlog2n)cdq 分治,和 O(nlogn) 的线段树,

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

题意

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

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

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

做法

(i,j) 处格子的权为 vi,j,我们记 sx,y=i=1yvx,i,al=s1,ls2,l1,br=s3,ns3,r1+s2,r,则:

问题等价于求 max1lrn(al+brcost(l,r)),其中 cost(l,r) 是将第二行中区间 [l,r] 染黑的最小代价。

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

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

转移式为:cost(l,i)=minp : i,j[Lp,Rp],ji(cost(l,j1)kp),即用第 p 个三元组拓宽已染黑的区间。

发现 cost(l,r) 在转移时其 l 不变,且转移仅与 r 有关,那我们记 f(r)=maxlr(alcost(l,r))

则此时 f 函数也能用 DP 的方式计算,即:f(r)=maxp : LplrRp(f(l1)kp)

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

那么最后的答案就等于 maxr=1n(br+f(r)),我们也就得到了一个 O(n2) 的做法,但其实可以继续优化。

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

我们先递归处理区间 [l,mid]f 值,若三元组 (Lp,Rp,kp) 满足 Lpmid<Rp

则对 max(l,Lp)imid<jmin(Rp,r),我们都可以花费 kp 的代价从 f(i1) 转移到 f(j)

那么,我们可以将所有满足上面条件的转移 i1j 拆成三步,即:

先从 f(i1) 转移到 f(Lp),再从 f(Lp) 转移到 f(Rp),最后从 f(Rp) 转移到 f(j)

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

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

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

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

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

而对分治区间 [l,r],若 p,q 作为两个三元组的编号,满足 [l,r][Lp,Rp],[l,r][Lq,Rq]

pqk 值较大的那个一定是无用的。

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

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

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

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

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

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

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

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

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

f(i)f(s1),q[1,m1],f(pq)f(pq+1),f(pm)f(j) 这些条件全部成立。

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

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

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

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

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

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

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

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

是因为:如果将位置 pt 调整成 ptp 序列仍合法,

pt 所能转移到的位置集,必须与 pt 所能转移到的位置集一致,否则一定能构造出不合法的例子,

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

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

这个集合的每个元素,分别为 i,p1,p2,,pm,j 这些位置中的每对相邻位置,提供了转移的条件,

故这个集合的大小是 m+1,而 pt 可以调整到的合法位置就是 [Lt,Rt][Lt+11,Rt+1]

再进一步发现,此时一定有 [Lt,Rt][Lt+11,Rt+1]=[Lt+11,Rt],原因是:

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

故这个有序三元组集一定满足:t[1,m],Lt<Lt+1,Rt<Rt+1,即可证明上面结论。

而由于我们想让每个元素尽量靠左,故此时对 t[1,m]pt 就会被调整到 Lt+11 位置。

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

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

而这个三元组 (Lq,Rq,kq) 也一定会被下一次转移所依赖,即下一次转移靠的就是这个三元组。

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

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

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

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

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

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

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

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

而最后一步,我们可以把统计的答案稍作修改,即:maxp : 1LplrRpn(f(l)+brkp)

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

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

而此时 Rp+1 一定在最后一个用来染色的三元组对应区间 [Lm+1,Rm+1] 内,

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

具体来说,我们算好 f 函数后,枚举三元组 p,则你需要查询 maxLplrRp(f(l)+br)

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

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

#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 @   GaryH  阅读(104)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】
点击右上角即可分享
微信分享提示