CF1648D 题解
CF1648D 题解
在这里分享两种做法,分别是 的 cdq
分治,和 的线段树,
由于笔者学艺不精,故如有错误或笔误之处欢迎提出,笔者将不胜感激。
题意:
给一条 行 列的带权(可以为负)网格,最初时第二行的所有格子是黑色,其它格子是白色。
你有一些三元组 ,代表可以花费 的代价把第二行在区间 内的格子染黑。
你需要找到一条从 到 的纯黑色路径,并最大化经过格子的权值和减去染色的代价。
做法:
设 处格子的权为 ,我们记 ,则:
问题等价于求 ,其中 是将第二行中区间 染黑的最小代价。
我们考虑如何计算 函数的值,这时可以考虑一个暴力的 DP
,即:
初始值是:,即初始时第二行所有格子都不是黑色;
转移式为:,即用第 个三元组拓宽已染黑的区间。
发现 在转移时其 不变,且转移仅与 有关,那我们记 ,
则此时 函数也能用 DP
的方式计算,即:。
这个式子是容易得到的,将 函数的定义式里的 函数表示为其转移式就行。
那么最后的答案就等于 ,我们也就得到了一个 的做法,但其实可以继续优化。
我们可以用 cdq
分治为 函数的计算加速,具体来说,我们对当前分治区间 及区间中点 ,
我们先递归处理区间 的 值,若三元组 满足 ,
则对 ,我们都可以花费 的代价从 转移到 。
那么,我们可以将所有满足上面条件的转移 拆成三步,即:
先从 转移到 ,再从 转移到 ,最后从 转移到 。
容易发现,如果你用做后缀 的方式去维护第一步和第三步,并暴力的维护第二步,
那么复杂度递推式就是:,
其中 为对 能产生影响的三元组个数,故 ,
原因是三元组最多只会影响到 个分治区间的转移,
这个可以用线段树区间操作的复杂度来证明,即可以将任意 拆成 个线段树上的区间,
而对分治区间 ,若 作为两个三元组的编号,满足 ,
则 和 中 值较大的那个一定是无用的。
以上只是介绍了一种对本题中 cdq
分治的复杂度的一种理解方式,而并不是更严谨的证明,
更严谨的证明可能还需要一些讨论,但由于笔者写的不是这种做法,故当时并没有做严谨的证明,
但相信强大的读者们一定能够自行完成这个证明。
事实上,上面的 DP
式还有一种优化方式,即减少无用转移以优化转移复杂度。
为了方便说明我们是如何优化转移的,在此我们先说明一些定义。
我们首先定义 “ 能转移一次后到 ” 为, 最终的最优解可以由 的转移而取到,
即 可以作为 在 DP
的最优转移的 DAG
中的前驱。
为了使叙述更便捷,我们用 来代替 “ 能转移一次后到 ” 。
自然的,对于 ,我们可以定义 “ 能转移 次后到 ” 为,存在序列 ,满足:
这些条件全部成立。
那么,考虑这样的序列可能会有很多种,相应的,我们也就做了很多次无用转移,
因为我们只需要所有合法序列中的一个就可以从 转移到 。
那么,我们可以考虑适当并合理地减少转移量,使对 ,合法的序列 唯一。
这等价于给出一些限制并按照限制来调整 序列,最后再给出一种新的转移方式,
使得这个转移方式的实际效果与调整后的 序列是一致的,且这个新转移的复杂度更低或可以优化。
那我们先考虑如何合理的调整 序列。
一个想法是,我们在保证序列合法的前提下,将 中每一个位置调整地尽量靠左,
以达到类似去重的目的。注意到为什么这里的 ” 尽量靠左 “ 的限制没有给出一个调整的先后顺序,
是因为:如果将位置 调整成 后 序列仍合法,
则 所能转移到的位置集,必须与 所能转移到的位置集一致,否则一定能构造出不合法的例子,
故此时确定调整顺序是不必要的,且按任意一种调整顺序去调整到尽量靠左,最后结果都是一致的。
一种更直观的的证明,是考虑原序列 一定有一个对应的有序三元组集合,
这个集合的每个元素,分别为 这些位置中的每对相邻位置,提供了转移的条件,
故这个集合的大小是 ,而 可以调整到的合法位置就是 ,
再进一步发现,此时一定有 ,原因是:
这些区间显然不会有包含或不交的情况,而因为 DP
转移时下标单增,
故这个有序三元组集一定满足:,即可证明上面结论。
而由于我们想让每个元素尽量靠左,故此时对 , 就会被调整到 位置。
那么我们考虑应该如何转移,才能使转移的实际效果,与按照上面方式来调整后得到的序列 一致。
发现新的转移方式等价于,每次转移到的新点,都是某个三元组对应区间 的左端点减一,
而这个三元组 也一定会被下一次转移所依赖,即下一次转移靠的就是这个三元组。
也就是说,我们除了在第一次转移时会从任意位置出发,
以后的每次转移的起点都是某个三元组对应区间 的左端点,终点是这个区间内的所有点。
这时我们需要支持区间与某数取 和单点查询的数据结构,而线段树已经足以支持这一点了,
但实际上我们没必要写区间取 数据结构 ,而是考虑将第二行整体翻转后,转移的形式。
发现此时转移等价于:除了最后一次转移的终点可以是任意点,
其余每次转移的起点是某个三元组对应区间 内所有点,终点是 ,
原因也是好理解的,即我们将第二行翻转后,三元组对应区间的左端点减一,就变成了右端点加一。
那么,除最后一步以外的转移,我们只需要区间查询 值,单点修改的数据结构就能快速维护。
而最后一步,我们可以把统计的答案稍作修改,即:,
如果我们统计的是这个答案,那么我们对最后一步的转移,就也可以与其它转移一样了,
因为此时,最后一步只转移到了倒数第二个用来染色的三元组对应区间 的右端点加一,
而此时 一定在最后一个用来染色的三元组对应区间 内,
故上面的答案统计一定是正确的,而快速维护答案的方法,我们也只需要一棵线段树即可,
具体来说,我们算好 函数后,枚举三元组 ,则你需要查询 ,
而这个式子放在线段树上是具有结合律的信息,我们可以用类似于维护区间最大子段和的方法做到。
这样的时间复杂度就是 的,下面放个代码,代码里的实现可能与题解中所讲略有不同。
#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;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】