当前位置: 首页 > news >正文

CSP-S 2022 Solution

\[『山鸟与鱼不同路,从此山水不相逢。莫问故人长短痛,昨夜星辰皆随风』 \]

\[\LARGE CSP-S\ \ 2022 题解报告\\ Author : Floyd.Uranus \]

题目 假期计划 策略游戏 星战 数据传输
算法概览 \(贪心,bfs\) \(线段树, ST表\) \(哈希(和哈希),图论\) \(倍增,动态 dp ,矩阵乘法\)
难度 \(\color{skyblue} 『提高+/省选-』\) \(\color{lightgreen} 『普及+/提高』\) \(\color{violet} 『省选/NOI-』\) \(\color{violet} 『省选/NOI-』\)

T1 假期计划

大意

给你一张\(n\) 个点, \(m\) 条边的无向图,每个点 (除了1号点) 有一个点权 \(val_i\) , 给定一个数 \(k\)

我们选定四个不同点 \(a, b, c, d\) ,使得 \((1, a), (a, b), (b, c), (c, d), (d, 1)\) 五组点对内两点之间的最短路经过不超过\(k + 1\) 条边

求出\(val_a + val_b + val_c + val_d\) 的最大可能值

\(1\leq n\leq 2500, 1\leq m\leq 10000, 1\leq k \leq 100, 1\leq val_i\leq 10^{18}\)

Sol

首先不必多说,肯定要对每个点跑一遍 \(bfs\) 来统计哪些点和它能够在一次操作内到达

然后我们先想一个极其暴力的暴力

因为路径内关键点只有四个,所以我们可以往暴力枚举这边去想

我们可以 \(O(n^4)\) 枚举 \(a, b, c, d\) 都是谁,正确性显然

但是这显然是做不过去的,考虑优化枚举

假设我们现在已经枚举了 \(a, b, c\) 三个点了,那么我们如何依据这个来缩小\(d\) 的枚举范围呢?

显然一个点要作为 \(d\) 的必要条件就是它得满足同时和\(1\), \(c\) 联通,这些点我们可以 \(O(n)\) 查询出来,换成预处理就是 \(n ^ 2\)

显然,在枚举的时候我们枚举点权最大的三个就可以了,具体为什么请读者思考

这样整体复杂度就降低到了 \(O(n ^ 3)\),还是过不去

那么同理,我们考虑优化 \(a\) 的枚举,和 \(d\) 一样,也是可以 \(n ^ 2\) 做出来,同样也是枚举前三个

其实枚举前三个就是维护最大的三个,最后 \(n ^ 2\) 枚举 \(b, c\) , 此时枚举\(a, d\) 就是常数级别的,可以通过

Code

// Problem: P8817 [CSP-S 2022] 假期计划
// Contest: Luogu
// URL: https://www.luogu.com.cn/problem/P8817
// Memory Limit: 512 MB
// Time Limit: 1000 ms
// Author: Floyd.Uranus
// 
// Powered by CP Editor (https://cpeditor.org)#include <bits/stdc++.h>
using namespace std;
typedef long long ll;const int N = 2510;#define Floyd main
#define push_back emplace_backll inline read() {ll x = 0, f = 1;char ch = getchar ();while (ch < '0' || ch > '9') {if (ch == '-') f = -1;ch = getchar ();}while (ch >= '0' && ch <= '9') {x = x * 10 + ch - '0';ch = getchar ();}return x * f;
}void inline write (ll x) {if (x < 0) putchar ('-'), x = -x;if (x > 9) write (x / 10);putchar (x % 10 + '0');return void ();
}int n, m, k, fir[N][3];
bool flag[N][N], tag[N];
// flag[i][j] 表示我们从i可以一步走到j(反向相同)
// fir[i][0], fir[i][1], fir[i][2] 维护与i联通的前三大编号ll v[N], ans = 0;vector <int> e[N];void inline bfs (int st) {queue <int> x, step;x.push(st), step.push(0);while (x.size()) {int now = x.front(), a = step.front();x.pop(), step.pop();if (a == k + 1) continue;for (auto nxt : e[now]) {if (flag[st][nxt]) continue;flag[st][nxt] = 1, x.push(nxt), step.push(a + 1);}}return void();
}
//bfs标记void inline solve () {n = read(), m = read(), k = read();v[0] = -9e18;for (int i = 2; i <= n; ++i) v[i] = read();for (int i = 1; i <= m; ++i) {int a = read(), b = read();e[a].push_back(b), e[b].push_back(a);}for (int i = 2; i <= n; ++i) bfs (i);for (int i = 2; i <= n; ++i) {if (!flag[i][1]) continue;for (int j = 2; j <= n; ++j) {if (i == j || ! flag[i][j]) continue;if (v[i] > v[fir[j][0]]) fir[j][2] = fir[j][1], fir[j][1] = fir[j][0], fir[j][0] = i;else if (v[i] > v[fir[j][1]])fir[j][2] = fir[j][1], fir[j][1] = i;else if (v[i] > v[fir[j][2]])fir[j][2] = i;}}//预处理前三大for (int i = 2; i <= n; ++i)for (int j = 2; j <= n; ++j) {bool tf = 0;if (i == j || !flag[i][j]) continue;for (int i1 = 0; i1 < 3; ++i1) {if (fir[i][i1] == j) continue;if (fir[i][i1] == 0) break; //小优化,如果与i联通的不足3个点就break掉,下面f[j][j1] == 0 同理for (int j1 = 0; j1 < 3; ++j1) {if (fir[j][j1] == fir[i][i1] || fir[j][j1] == i) continue;if (fir[j][j1] == 0) {tf = 1;break;}ans = max (ans, v[i] + v[j] + v[fir[i][i1]] + v[fir[j][j1]]);tf = 1;//如果匹配过了就break,因为我们从大往小枚举虽然不一定最优,但是我们后面相同的方案还会反方向统计到,正好弥补了这种小概率错误break;}if (tf) break;}}write (ans);//暴力枚举b, c统计答案return void ();
}signed Floyd() {int TIMES = 1;while (TIMES --) solve ();return 0;
}

这道题比较考验思路的灵活性,说实话有点难

但是如果你刚开始没有想偏的话还是比较显然的

T2 策略游戏

大意

给你长度为 \(n\)\(a\) 数组,长度为 \(m\)\(b\) 数组,\(q\) 次询问,每次给出 \(l_1, r_1, l_2, r_2\) ,有两个人\(A, B\)来分别选择 \(a_{l_1}, a_{l_1 + 1},......, a_{r_1}\)中的一个数和 \(b_{l_2}, b_{l_2 + 1},......,b_{r_2}\)中的一个数,\(A\) 先选,\(A\) 要使选出的两个数乘积尽可能大,\(B\) 则想尽可能小,问在两人都最优决策下,乘积为多少?

\(1\leq n, m\leq 10^5, -10^9\leq a_i, b_i\leq 10^9\)

Sol

这道题就不想那么多部分分了,我们直接分类讨论即可

先把 \(0\) 拉出来单独考虑:

如果 \(A\)\(0\) 的话,什么情况下他会用这个 \(0\) 作为答案呢?

显然,是当他没有其他数可选或者是无论选什么数 \(B\) 都能使乘积\(< 0\) 的时候

同理我们可以得到 \(B\) 的情况也是类似的,也就是 \(A\)存在一种选择使得 \(B\) 无论选什么数乘积都为正的时候

于是我们就有了以下式子

\[ans = \max (ans, 0)&\exist i\in [l_1,r_1], a_i = 0 \\ ans = \min (ans, 0)&\exist i\in [l_2,r_2], b_i = 0 \]

然后我们把这东西扔到最后去判断即可,先看其他情况(假设都没有\(0\))

我们现在定义 \(S_1, S_2\) 分别为 \(A, B\) 可选数集合,定义函数 \(f(S) = (0/1, 0/ 1)\) 表示\(S\) 内 有/没有 正数,有/没有 负数

然后开始大分讨

  1. \(f(S_2) = (1, 1)\)

    此时无论我们的 \(A\) 如何选择,答案都一定为负数,那么 \(A\) 一定是选择负数最大值或正数最小值,相应的, \(B\) 一定选正数最大值或负数最小值,我们将两种选择中较大的赋值给 \(ans\) 即可

  2. \(f(S_2) = (1, 0)\)

    此时我们发现 \(B\) 只能选正数,那么如果 \(A\) 有 正数的话,\(A\) 一定选择正数最大值,\(B\) 则选择正数最小值; 但如果 \(A\) 只有负数的话,\(A\) 一定选负数最大值,\(B\)一定选正数最大值。这两个结论显然

  3. \(f(S_2) = (0, 1)\)

    与第二种情况同理,这里不再赘述,请读者自证

  4. \(f(s_2) = (0,0)\)

    只能选 \(0\), 答案固定

然后我们就讨论完了,这里我们发现需要对 \(a, b\) 两个数组分别维护 正数最大值,负数最大值,正数最小值,负数最小值以及 \(0\) 的个数,区间询问, 这东西显然可以用 \(ST\) 表或者线段树做, 这里我用的是线段树写法(封装)

Code

// Problem: P8818 [CSP-S 2022] 策略游戏
// Contest: Luogu
// URL: https://www.luogu.com.cn/problem/P8818
// Memory Limit: 512 MB
// Time Limit: 1000 ms
// Author: Floyd.Uranus
// 
// Powered by CP Editor (https://cpeditor.org)#include <bits/stdc++.h>
using namespace std;
typedef long long ll;const int N = 1e5 + 10, mod = 1e9 + 7;#define Floyd mainint inline read() {int x = 0, f = 1;char ch = getchar ();while (ch < '0' || ch > '9') {if (ch == '-') f = -1;ch = getchar ();}while (ch >= '0' && ch <= '9') {x = x * 10 + ch - '0';ch = getchar ();}return x * f;
}void inline write (ll x) {if (x < 0) putchar ('-'), x = -x;if (x > 9) write (x / 10);putchar (x % 10 + '0');return void ();
}
//快读快写,⚠️注意long long!!!int n, m, q, a[N][2], l1, l2, r1, r2;
ll ans;
//为了写的稍稍简洁(其实是为了封装),这里直接把a,b合成了一个二维数组 _(-_-)_struct node {int minn1, minn2, maxx1, maxx2;bool tag1, tag2, tag3;//tag1, minn1, maxx1 ---> 正数//tag2, minn2, maxx2 ---> 负数//tag3               --->  0inline node () {minn1 = minn2 = 2e9, maxx1 = maxx2 = -2e9, tag1 = tag2 = tag3 = 0;}//初始化void merge (node a, node b) {minn1 = min (a.minn1, b.minn1);minn2 = min (a.minn2, b.minn2);maxx1 = max (a.maxx1, b.maxx1);maxx2 = max (a.maxx2, b.maxx2);tag1 = a.tag1 | b.tag1;tag2 = a.tag2 | b.tag2;tag3 = a.tag3 | b.tag3;return void();}//线段树两个节点的合并
};struct Segment_tree {#define mid ((l + r) >> 1)#define ls (x << 1)#define rs (x << 1 | 1)node t[N * 4];void inline build (int x, int l, int r, int fl) {if (l == r) {if (a[l][fl] > 0) t[x].tag1 = 1, t[x].maxx1 = a[l][fl], t[x].minn1 = a[l][fl];if (a[l][fl] < 0) t[x].tag2 = 1, t[x].maxx2 = a[l][fl], t[x].minn2 = a[l][fl];if (a[l][fl] == 0) t[x].tag3 = 1;return void();}build (ls, l, mid, fl);build (rs, mid + 1, r, fl);t[x].merge (t[ls], t[rs]);return void ();}node inline query (int x, int l, int r, int ql, int qr) {if (ql <= l && r <= qr) return t[x];node ret;if (ql <= mid) ret.merge (ret, query (ls, l, mid, ql, qr));if (qr > mid) ret.merge (ret, query (rs, mid + 1, r, ql, qr));return ret;}#undef mid#undef ls#undef rs
}A, B;
//封装, A, B分别对应 a, b数组void solve () {n = read(), m = read(), q = read();for (int i = 1; i <= n; ++i) a[i][0] = read();for (int i = 1; i <= m; ++i) a[i][1] = read();A.build (1, 1, n, 0), B.build (1, 1, m, 1);//建树for (int i = 1; i <= q; ++i) {l1 = read(), r1 = read(), l2 = read(), r2 = read();node ans1 = A.query (1, 1, n, l1, r1), ans2 = B.query (1, 1, m, l2, r2);// ans1, ans2 分别表示两个区间的具体数据if (ans1.tag2 && ans1.tag1) {if (ans2.tag1 && ans2.tag2)ans = max (1ll * ans1.minn1 * ans2.minn2, 1ll * ans1.maxx2 * ans2.maxx1);else if (!ans2.tag1)ans = (1ll * ans1.minn2 * ans2.maxx2);else if (!ans2.tag2)ans = (1ll * ans1.maxx1 * ans2.minn1);else ans = 0;}if (ans1.tag1 && !ans1.tag2) {if (ans2.tag2) ans = 1ll * ans1.minn1 * ans2.minn2;else if (ans2.tag1) ans = 1ll * ans1.maxx1 * ans2.minn1;else ans = 0;}if (!ans1.tag1 && ans1.tag1) {if (ans2.tag1) ans = 1ll * ans1.maxx2 * ans2.maxx1;else if (ans2.tag2) ans = 1ll * ans1.minn2 * ans2.maxx2;else ans = 0;}if (!ans1.tag1 && !ans1.tag2) ans = 0;if (ans1.tag3) ans = max (ans, 0ll);if (ans2.tag3) ans = min (ans, 0ll);/*Emmm....这里我是按照A的选择进行分类讨论的实质上和我上面说的一样读者可以自行思考😎*/write (ans), putchar ('\n');}return void ();
}signed Floyd() {int TIMES = 1;while (TIMES --) solve ();return 0;
} 

这道题倒是没什么思维难度,题面也十分简洁,就是要注意分类讨论的细节,而且维护的值比较多,稍微难写,一不注意就写了150多行

T3 星战

不可以,总司令!

没错就是这个梗的出处 (偷笑🤭)

大意

给你 \(n\) 个点, \(m\) 条有向边,\(q\) 次操作

操作共分为 \(4\)

  1. 删除边 \(u, v\)

  2. 删除以 \(u\) 为终点的所有边

  3. 添加边 \(u, v\)

  4. 将以 \(u\) 为终点的所有边全部加回来

其中每个边都一定是 \(m\) 条有向边里原有的

每个操作后,我们要判断以下条件,满足输出 YES, 不满足输出 NO

\[\forall i\in [1, n], i的出度 = 1 且从 i 走能进入一个环 \]

\(1\leq n, m, q\leq 5\times 10^5\)

Sol

所以说全部输出 NO 可以骗取大量分数是有道理的,在数据非刻意构造的情况下这条件还真难满足

当然暴力是好写的,分也是少的可怜的,这里我就不再说其他推导了,直接正解

\[Hash \]

我们考虑我们并不在意边的状况,我们只是在意 每个点的出度是否为 \(1\),而且我们甚至连是否能进入一个环都不用管,因为如果出度都为 \(1\) 的话,必定满足第二个条件(读者自证)

那么也就是说,最后每个点的出度状况是确定的(废话)

于是我们就有了暴力统计出度的做法, 复杂度 \(O(q\times (n + m))\)

对于题目中给出的 \(4\) 种操作, \(2, 4\) 我们直接统计出度的算法是很劣的,但是统计入度却是好做的,那么我们该如何把出度转移到入度上统计呢?

显然我们只能统计入度之和,如果只是单纯的 \(+1,-1\) ,正确性显然是错的

那么我们就来到了哈希部分

对于每一个数我们可以给它随机赋上一个映射值 \(val_i\) ,把操作入度统计时每次 \(now_v ++\) 改成 \(now_v += val_u\) 即可,记录所有点出度均为 \(1\) 时的和为 \(sum\)

我们容易想到,这样子的话我们每一个出度的映射值大概率是唯一的,统计时不同状况和也是大概率唯一的 (除非你运气真的太"好"了)

那么我们直接每次判断\(\sum\limits_{i = 1}^n now_i\) 是否等于 \(sum\) 即可

然后就做完了

Code

// Problem: P8819 [CSP-S 2022] 星战
// Contest: Luogu
// URL: https://www.luogu.com.cn/problem/P8819
// Memory Limit: 512 MB
// Time Limit: 2000 ms
// Author: Floyd.Uranus
// 
// Powered by CP Editor (https://cpeditor.org)#include <bits/stdc++.h>
using namespace std;
typedef long long ll;const int N =1e6 + 10;
const ll mod = 1e9 + 7;#define Floyd mainll inline read () {ll x = 0, f = 1;char ch = getchar ();while (ch < '0' || ch > '9') {if (ch == '-') f = -1;ch = getchar ();}while (ch >= '0' && ch <= '9') {x = x * 10 + ch - '0';ch = getchar ();}return x * f;
}void inline write (ll x) {if (x < 0) putchar ('-'), x = -x;if (x > 9) write (x / 10);putchar (x % 10 + '0');return void ();
}int n, m, q, ind[N];
ll sum = 0, ans = 0, to[N], g[N], now[N];void inline solve () {srand (time (0));n = read(), m = read();for (int i = 1; i <= n; ++i){to[i] = (rand() * 1ll * rand() * rand() * rand() % mod), sum += to[i];//这里我赋值的映射范围是1~1e9 + 7, 很多数都相同的概率较低,可以放心哈希}for (int i = 1; i <= m; ++i) {int a = read(), b = read();g[b] += to[a];//g[i] 表示原图上i点用入度统计出度的哈希值之和}for (int i = 1; i <= n; ++i) {ans += g[i], now[i] = g[i];}q = read();for (int i = 1; i <= q; ++i) {int id = read(), u, v;if (id == 1) {u = read(), v = read();now[v] -= to[u], ans -= to[u];}if (id == 2) {v = read();ans -= now[v];now[v] = 0;}if (id == 3) {u = read(), v = read();now[v] += to[u],  ans += to[u];}if (id == 4) {v = read();ans -= now[v], now[v] = g[v];ans += now[v];}if (ans == sum) puts("YES");else puts("NO");}return void ();
}signed Floyd() {int TIMES = 1;while (TIMES --) solve ();return 0;
}

不好想,如果考场上真做不出来的话, 这边建议您骗分呢亲

T4 数据传输

这也真是难写,反正赛场上是场切不了的

大意

给你一颗树,每个点都有点权 \(val_i\) ,现在给你 \(q\) 次询问,每次从点 \(s\) 出发,每次可以跳 \(1, 2, ..., k\) 步, 一直跳到 \(t\) ,问路径上你经过的点点权之和最少为多少?

这里先粘一下数据范围 QWQ

对于所有的测试数据,满足 \(1 \le n \le 2 \times {10}^5\)\(1 \le Q \le 2 \times {10}^5\)\(1 \le k \le 3\)\(1 \le a_i, b_i \le n\)\(1 \le s_i, t_i \le n\)\(s_i \ne t_i\)

测试点 \(n \le\) \(Q \le\) \(k =\) 特殊性质
\(1\) \(10\) \(10\) \(2\)
\(2\) \(10\) \(10\) \(3\)
\(3\) \(200\) \(200\) \(2\)
\(4 \sim 5\) \(200\) \(200\) \(3\)
\(6 \sim 7\) \(2000\) \(2000\) \(1\)
\(8 \sim 9\) \(2000\) \(2000\) \(2\)
\(10 \sim 11\) \(2000\) \(2000\) \(3\)
\(12 \sim 13\) \(2 \times {10}^5\) \(2 \times {10}^5\) \(1\)
\(14\) \(5 \times {10}^4\) \(5 \times {10}^4\) \(2\)
\(15 \sim 16\) \({10}^5\) \({10}^5\) \(2\)
\(17 \sim 19\) \(2 \times {10}^5\) \(2 \times {10}^5\) \(2\)
\(20\) \(5 \times {10}^4\) \(5 \times {10}^4\) \(3\)
\(21 \sim 22\) \({10}^5\) \({10}^5\) \(3\)
\(23 \sim 25\) \(2 \times {10}^5\) \(2 \times {10}^5\) \(3\)

特殊性质:保证 \(a_i = i + 1\),而 \(b_i\) 则从 \(1, 2, \ldots, i\) 中等概率选取。

Sol:

由于这道题难度较大且部分分和正解之间联系较大 (好题!),所以我将一个一个部分的讲解 (如果太简单除外)

Part 1 : k = 1 (16pts)

这里是显然的,在 \(k = 1\) 的情况下,我们的路径上每一个数都要选到,我们直接用倍增 \(LCA\) 维护即可

时间复杂度 \(O(q \log n)\)

Part 2 : k = 2 && \(n, q\leq 2000\) (32pts)

在这种情况下,我们可以考虑放弃 \(\log_2 (n)\)\(LCA\) 求法,转而选择把路径上所有点 \(O(n)\) 取下来做一遍\(dp\), 这样我们就是在一个序列上的问题了,\(dp\) 转移方程是好写的

\[dp_{i} = \min (dp_{i - 1}, dp_{i - 2}) + val_i \]

(这里要注意 \(i\) 不是原图中的编号,而是我们将路径取出后的节点编号)

时间复杂度 \(O(n\times q)\)

Part 3 : k = 3 && \(n, q\leq 2000\) (52pts)

和上面不同的就是,\(k\) 的取值变成了 \(3\)

那么我们还是像上面一样直接 \(dp\) 吗?

你大胆写了一发跟上面差不多的 \(dp\) , 然后就发现......你寄了

其实这两种情况是有本质差别的, 你可以考虑下面这张图

img

图中标注的线段为 \(k = 3\) 时的最优方案,而在 \(k = 2\) 时显然不会出现这种情况

也就是说,我们的最优路径可能不止经过两点之间简单路径上的点,还有可能包含某一个点的子节点

类似的,根据贪心,我们把路径上的点的点权最小的子节点纳入我们的考虑范围即可

我们可以设 \(f_{i, 0/1}\) 表示跳到了点 \(i\) 自己/它儿子的最小值,\(num_i\) 表示 \(i\) 号节点带来的需要考虑的子节点权值

聪明的你很快就想出了 \(dp\) 的状态转移方程

\[f_{i, 0} = \min (f_{i - 1, 0}, f_{i - 1, 1},f_{i - 2, 0},f_{i - 2, 1}, f_{i - 3, 0}) + val_i\\ \]

\[\begin{aligned} \ f_{i, 1} & = \min (f_{i, 0} + num_i, \min (f_{i - 1, 0},f_{i - 1, 1}, f_{i - 2, 0}) + num_i)\\&=\min (f_{i, 0}, f_{i - 1, 0}, f_{i - 1, 1},f_{i - 2, 0}) + num_i\\&=\min (f_{i - 1, 0}, f_{i - 1, 1}, f_{i - 2, 0}) + num_i \end{aligned} \]

时间复杂度 \(O(n\times q)\)

Part 4 : 特殊性质 (76pts)

前面的都做完了,我们来看一下特殊性质

这相当于是随机生成一颗树,期望树高为 \(\log n\) 级别

也就是说,我们的 \(LCA\) 路径上经过的点也是期望 \(\log n\) 级别

这和前面的不是差不多吗?

然后我们也是暴力提取路径即可

复杂度 \(O(q \log n)\)

Part 5 : k = 2 (88pts)

做到这里就非常接近正解了

因为 \(k = 2\) ,这个转移方程是相当简单的,于是我们就可以考虑上矩阵乘法来优化

我们回顾一下,\(dp\) 方程是

\[dp_{i} = \min (dp_{i - 1}, dp_{i - 2}) + val_i \]

我们可以转化为一个 \({min,+}\) 转移矩阵 \(A_i\), 初始矩阵为 \(st\), 答案矩阵为 \(ans\), 这些应该都是好推出来的

\[A_i = \begin{Bmatrix} val_i & val_i \\ 0 & \infty\end{Bmatrix} \\ st = \begin{Bmatrix} val_1 \\ \infty \end{Bmatrix}\\ ans = \begin{Bmatrix} dp_{len} \\ dp_{len - 1} \end{Bmatrix} \]

所以我们直接上矩阵乘法优化即可,用倍增 \(LCA\) 来维护

但是有一点要注意的我们的方程转移过程如下

\[ans = A_{len} \times A_{len - 1} \times......\times A_2\times st \]

也就是说,我们的倍增要维护两种,一种是从父节点往子节点乘,另一种反过来(拆成\(LCA\)两侧两段序列),毕竟矩阵乘法它不满足交换律,乘的时候必须有序

例如这张图,我们的询问是 \(4, 6\) ,矩阵乘法的转移如下

img

那么我们从 \(6\)\(1\) 的路径就应该维护从下往上的乘积,\(1\)\(4\) 反过来,而且要注意 \(4\) 的矩阵不能乘,这个由上面的式子可以推得

复杂度为 \(O(q\log n \times 2^3)\)

Part 6 : 正解 (100pts)

正解其实已经了然了

就是矩阵乘法倍增做一下 \(k = 3\) 的情况即可

但这样显然不行,复杂度 \(O(q\log n\times 5^3)\),单次转移都有 \(125\) 的常数,是我们接受不了的

怎么办呢?

考虑优化一下 \(dp\) 状态

我们敏锐的发现其实我们转移过程只牵扯到距离,于是我们就可以考虑用距离表示 \(dp\) 状态

考虑设 \(f_{i,0/1/2}\) 表示我们当前做到了 \(i\) 号点,转移到距离 \(i\) 号点距离为 \(0/1/2\) 的点的最小代价

显然最后的答案就是 \(f_{len,0}\)

转移方程其实并不难搞,分情况讨论可以得到

\[\begin{aligned}f_{i, 0} &= \min (f_{i - 1, 0}, f_{i - 1, 1}, f_{i - 1, 2}) + val_i \\&=\min (f_{i - 1, 0} + val_i, f_{i - 1, 1} + val_i, f_{i - 1, 2} + val_i)\end{aligned} \]

\[\begin{aligned} f_{i, 1} &= \min (f_{i - 1, 0}, \min (f_{i - 1, 0}, f_{i - 1, 1}, f_{i, 0}) + num_i)\\ &=\min ( f_{i - 1, 0},f_{i - 1, 1} + num_i,f_{i - 1, 2} + val_i + num_i)\end{aligned} \]

\[\begin{aligned}f_{i, 2} &= f_{i - 1, 1}\\ &= \min (f_{i - 1, 0}+\infty,f_{i - 1, 1}, f_{i - 1, 2} + \infty) \end{aligned} \]

根据上面三个式子我们不难看出转移矩阵 \(A_i\) 是什么

\[A_i=\begin{Bmatrix} val_i & val_i & val_i\\ 0 & num_i & val_i + num_i\\ \infty & 0 & \infty\end{Bmatrix} \]

初始矩阵也差不多,是 \(\begin{Bmatrix} val_i\\ \infty\\ \infty\end {Bmatrix}\)

细节和上面 \(k = 2\) 的情况差不多,整体复杂度为 \(O(q\log n\times 3^3)\)

码量比较大,细节较多,要想明白再写

Code

// Problem: P8820 [CSP-S 2022] 数据传输
// Contest: Luogu
// URL: https://www.luogu.com.cn/problem/P8820
// Memory Limit: 1024 MB
// Time Limit: 3000 ms
// Author: Floyd.Uranus
// 
// Powered by CP Editor (https://cpeditor.org)#include <bits/stdc++.h>
using namespace std;
typedef long long ll;#define Floyd mainconst ll inf = 1e18;
constexpr int N = 2e5 + 10;int inline read() {int x = 0, f = 1;char ch = getchar ();while (ch < '0' || ch > '9') {if (ch == '-') f = -1;ch = getchar ();}while (ch >= '0' && ch <= '9') {x = x * 10 + ch - '0';ch = getchar ();}return x * f;
}void inline write (ll x) {if (x < 0) putchar ('-'), x = -x;if (x > 9) write (x / 10);putchar ('0' + x % 10);return void ();
}//注意long long !int len;struct square {ll node[3][3];//能少开尽量少开,不然小心MLE!inline square () {for (int i = 0; i < 3; ++i) for (int j = 0; j < 3; ++j)node[i][j] = inf;}//初始化要全部赋值为 infvoid inline readin () {for (int i = 0; i < 3; ++i)for (int j = 0; j < 3; ++j)node[i][j] = read();}void inline print () {puts("--------------------");for (int i = 0; i < len; ++i) {for (int j = 0; j < len; ++j)write (node[i][j]), putchar (' ');putchar ('\n');}puts("--------------------");}friend square operator *(square a, square b) {square c;for (int i = 0; i < len; ++i)for (int j = 0; j < len; ++j)for (int k = 0; k < len; ++k)c.node[i][j] = min (c.node[i][j], a.node[i][k] + b.node[k][j]);return c;}//矩阵乘法,len 为方阵的大小friend square operator +(square a, square b) {for (int i = 0; i < len; ++i)for (int j = 0; j < len; ++j)a.node[i][j] += b.node[i][j];return a;}void set_sample () {for (int i = 0; i < len; ++i) node[i][i] = 0;}//设置单位矩阵ll sum () {ll S = 0;for (int i = 0; i < len; ++i)for (int j = 0; j < len; ++j)S += node[i][j];return S;}//封装的 min,+ 矩阵乘法
}f[N][20], g[N][20];//这里我用f来记录从下往上乘的矩阵倍增,g是从上往下乘的矩阵倍增int n, m, k, fa[N][20], dep[N];
ll val[N], num[N];
vector <int> e[N];void inline dfs (int now, int fat) {num[now] = val[fat];fa[now][0] = fat, dep[now] = dep[fat] + 1;for (int i = 1; i < 20; ++i)fa[now][i] = fa[fa[now][i - 1]][i - 1];for (auto nxt : e[now]) {if (val[nxt] < num[now]) num[now] = val[nxt];//处理距离为1的点权最小值if (nxt == fat) continue;dfs (nxt, now);}return void ();
}void inline SET (int i) {/*理论上来说len == 1的时候可以直接维护和,但是为了统一这里设置了一个 边长为1 的方阵转移,本质上和直接维护和是一样的*/if (len == 1) f[i][0].node[0][0] = val[i];if (len == 2) {f[i][0].node[0][0] = val[i];f[i][0].node[0][1] = val[i];f[i][0].node[1][0] = 0;}//边长为2的方阵if (len == 3) {f[i][0].node[0][0] = val[i];f[i][0].node[0][1] = val[i];f[i][0].node[0][2] = val[i];f[i][0].node[1][0] = 0;f[i][0].node[1][1] = num[i];f[i][0].node[1][2] = num[i] + val[i];f[i][0].node[2][1] = 0; }//边长为3的方阵g[i][0] = f[i][0];return void ();
}inline int LCA (int a, int b) {if (dep[a] > dep[b]) swap (a, b);int dis = dep[b] - dep[a];for (int i = 0; i < 20; ++i)if ((dis & (1 << i))) b = fa[b][i];if (a == b) return a;for (int i = 19; i >= 0; --i)if (fa[a][i] != fa[b][i])a = fa[a][i], b = fa[b][i];return fa[a][0];//正常倍增LCA
}inline void get_ans (int lca, int a, int b) {square tmp1, tmp2, st;st.node[0][0] = val[a];//初始矩阵(因为其它位置全都是inf,所以没必要把长度和宽度更改)tmp1.set_sample (), tmp2.set_sample();//初始化为单位矩阵if (lca != a) {a = fa[a][0];int dis = dep[a] - dep[lca] + 1, now = a;//这里就是我说的a的转移矩阵不参与运算for (int i = 0; i < 20; ++i)if ((dis & (1 << i))) tmp1 = g[now][i] * tmp1, now = fa[now][i];//注意矩阵乘法的先后顺序}if (lca != b) {int dis = dep[b] - dep[lca], now = b;for (int i = 0; i < 20; ++i)if ((dis & (1 << i)))tmp2 = tmp2 * f[now][i], now = fa[now][i];//注意矩阵乘法的先后顺序}tmp1 = tmp2 * tmp1;st = tmp1 * st;write (st.node[0][0]);putchar ('\n');return void ();
}void inline solve () {n = read(), m = read(), k = read(), val[0] = 1e18, len = k;for (int i = 1; i <= n; ++i) val[i] = read();for (int i = 1; i < n; ++i) {int a = read(), b = read();e[a].push_back(b), e[b].push_back(a);}for (int i = 0; i < 20; ++i)f[0][i].set_sample(), g[0][i].set_sample();dfs (1, 0);for (int i = 1; i <= n; ++i) `SET (i);for (int i = 1; i < 20; ++i)for (int j = 1; j <= n; ++j)f[j][i] = f[j][i - 1] * f[fa[j][i - 1]][i - 1],//逆序乘,下面的矩阵放在前面的位置g[j][i] = g[fa[j][i - 1]][i - 1] * g[j][i - 1];//正序乘,上面的矩阵放在前面的位置for (int i = 1; i <= m; ++i) {int a = read(), b = read(), lca = LCA (a, b);// write (lca), putchar ('\n');get_ans (lca, a, b);}return void ();
}signed Floyd () {int TIMES = 1;while (TIMES --) solve ();return 0;
}

总而言之,这是一道好题!

好评!

对矩阵乘法不熟悉的家人们可以多学习一下 \(QWQ\)

完结撒花🎉

\[\LARGE\color{darkcyan} 自能生羽翼,何必仰云梯 \]

http://www.hskmm.com/?act=detail&tid=33802

相关文章:

  • 印刷电子技术挑战传统PCB主导地位
  • 2025-10-18
  • 某兔网站加密学习
  • 5.vtk学习——点云显示进阶
  • [LangChain] 03. 缓存
  • 面试 / 答辩总卡壳?这款 AI 面试辅助新功能:上传专属资料,精准应答不翻车
  • C语言编程之旅:从入门到实战
  • 引用
  • 实用指南:大数据毕业设计 python智慧交通监控系统 Flask+Echarts可视化 百度地图 毕业设计(源码)✅
  • Selenium元素定位总失败?这8种定位策略你必须掌握
  • 2025 年钢闸门厂家最新推荐排行榜:涵盖不锈钢 / 渠道 / 河道 / 水库 / 平面 / 手动 / 电动 / 液压钢闸门,聚焦十大实力厂家核心优势
  • 2025 年最新铸铁闸门源头厂家推荐排行榜,涵盖四川 / 镶铜 / 渠道 / 圆形 / 方形等类型,助力一站式采购优质供应商
  • 内存四区
  • 2025 年钢闸门源头厂家最新推荐口碑排行榜:聚焦防腐技术与密封性能,助力水利工程采购精准选型固定卷扬/四川卷扬/螺杆/螺杆式启闭机厂家推荐
  • 2025 年耐火砖厂家最新推荐排行榜:绝热/轻质/莫来石等类型优质厂家权威榜单发布氧化铝泡沫/氧化铝空心球/抗渗碳/高温轻质莫来石耐火砖厂家推荐
  • 【转】[C#] Web API 中的常见层次
  • 【Azure Developer】使用Azure Developer CLI (azd)部署项目时候遇见无法登录中国区Azure的报错
  • 2025 年清污机源头厂家最新推荐榜单:聚焦耐腐蚀与智能清污实力,权威筛选优质品牌供采购参考回转式/回转式格栅/不锈钢/四川清污机厂家推荐
  • Intellij IDEA里的各种快捷键
  • 2025年西安买房推荐与西安学区新房推荐排名前十榜单
  • 2025年西安买房终极指南:十大高性价比楼盘权威推荐
  • AI教育应用隐忧:技术普及与培训缺失
  • JBoltAI 智能混剪:零门槛搞定 “会说话” 的专业视频,新手也能当创作高手 - 那年-冬季
  • Java技术公司如何借力AIGS,开启人工智能融合新篇章? - 那年-冬季
  • AITCA联盟生态:基于JBoltAI框架的产业格局重构前瞻 - 那年-冬季
  • jiangly 模板
  • 基于JBoltAI框架的AITCA联盟生态:渠道商转型与升级的新契机 - 那年-冬季
  • QOJ #14426. Grid Problem 题解
  • 2025 10 18
  • 【Linux】备份