CSP-S 2022题解
闲话#
\(\huge{寄}\)
\(\text{T1}\) 极限脑抽,删掉暴搜打错解,\(70pts \to 0pts\);
\(\text{T2}\) 洛谷 \(100pts\) 但 \(\infin\) \(40pts\), 很慌;
\(\text{T3}\) 差点想到正解(但确实没想到)暴力 \(40pts\);
\(\text{T4}\) 甚至一分都拿不到;
期望 \(110\)。
打的模拟赛真的太少了(似乎就打过四五次……),心态真的太容易被影响(又是心态炸裂导致的水平急剧下跌)。
题解#
\(\bold{T1}\)
\(n \le 2500\),可以往 \(O(n^2)\) 考虑。
那么对于这题而言,\(O(n^2)\) 能干什么呢?
- 对每个点进行一遍 \(\text{BFS}\),预处理出它能到达的点集
- 枚举 \(4\) 个点中的 \(2\) 个
突破口也就在第二点中——对于 \(4\) 个点,我们不妨把它们拆成 \(2 + 2\) 个点,预处理出 \(w[i]\)(从 \(1\) 开始走 \(2\) 步到达 \(i\) 点的最大值),然后枚举 \(B, C\) 两点更新答案即可。
\(\large{\text{BUT}}\)
事情真的这么简单吗?
题目:请帮小熊规划一个行程,使得小熊访问的四个不同景点的分数之和最大。
注意所选的四个点是不同的,那么在更新答案时就不能只是简单地取两边的最大值。
那么为什么会出现重复的情况呢?显然是我们没有枚举到 \(A, D\) 导致的。但是现阶段的时间复杂度又不允许我们枚举 \(A, D\),怎么办呢?
我们要使答案最大,显然最大值选不了就会选次大值,次大值选不了就会选次次大值,次次大值选不了就会选……若最坏情况下 \(A\) 点会和其他点发生 \(n\) 次重复,那么就可以通过维护前 \(n + 1\) 大值来解决重复的问题。
由于图中不存在自环,所以 \(B\) 点势必不会和 \(A\) 点重复,但 \(C, D\) 两点都是可以和 \(A\) 重复的。\(D\) 点情况同理,而对于 \(B\) 点,枚举了 \(C\) 点,所以可以保证 \(C \ne B\),能与 \(B\) 点重复的点就只剩下 \(D\) 点一个点。\(C\) 点同理。因此,我们可以维护 \(w[i][0/1/2]\)(从 \(1\) 开始走 \(2\) 步到达 \(i\) 点的最大/次大/次次大值),更新答案时分类讨论一下就好啦~
$Code$
#include <bits/stdc++.h>
#define MAXN 2600
#define MAXM 100100
using namespace std;
typedef long long ll;
int n, m, p, dis[MAXN], mx[MAXN][4];
int tot, head[MAXN];
ll w[MAXN];
bool vis[MAXN], G[MAXN][MAXN];
struct Edge {
int to, nxt;
} e[MAXM << 1];
template<typename _T>
void read(_T &_x) {
_x = 0;
_T _f = 1;
char _ch = getchar();
while (_ch < '0' || '9' < _ch) {
if (_ch == '-') _f = -1;
_ch = getchar();
}
while ('0' <= _ch && _ch <= '9') {
_x = (_x << 3) + (_x << 1) + (_ch & 15);
_ch = getchar();
}
_x *= _f;
}
template<typename _T>
void write(_T _x) {
if (_x < 0) {
putchar('-');
_x = -_x;
}
if (_x > 9) write(_x / 10);
putchar('0' + _x % 10);
}
void add(int u, int v) {
e[++tot] = Edge{v, head[u]};
head[u] = tot;
}
void bfs(int s) {
queue<int> q;
memset(vis, 0, sizeof(vis));
vis[s] = 1, dis[s] = 0;
q.push(s);
while (!q.empty()) {
int u = q.front();
q.pop();
if (dis[u] > p) return;
for (int i = head[u], v; i; i = e[i].nxt) {
v = e[i].to;
if (vis[v]) continue;
vis[v] = 1;
dis[v] = dis[u] + 1;
G[s][v] = 1;
q.push(v);
}
}
}
int main() {
read(n), read(m), read(p);
for (int i = 2; i <= n; i++) read(w[i]);
while (m--) {
int u, v;
read(u), read(v);
add(u, v), add(v, u);
}
for (int i = 1; i <= n; i++) bfs(i);
w[0] = -1e18;
memset(vis, 0, sizeof(vis));
for (int i = 2; i <= n; i++) {
if (!G[1][i]) continue;
for (int j = 2; j <= n; j++) {
if (!G[i][j]) continue;
vis[j] = 1;
if (w[i] > w[mx[j][0]]) {
mx[j][2] = mx[j][1], mx[j][1] = mx[j][0], mx[j][0] = i;
} else if (w[i] > w[mx[j][1]]) {
mx[j][2] = mx[j][1], mx[j][1] = i;
} else if (w[i] > w[mx[j][2]]) mx[j][2] = i;
}
}
ll ans = 0;
for (int B = 2; B <= n; B++) {
if (!vis[B]) continue;
for (int C = B + 1; C <= n; C++) {
if (!G[B][C] || !vis[C]) continue;
ll tmp;
int i = 0, k = 0;
if (mx[B][i] == C) i++;
if (mx[C][k] == B) k++;
if (mx[B][i] != mx[C][k]) tmp = w[mx[B][i]] + w[mx[C][k]];
else {
int j = i + 1, l = k + 1;
if (mx[B][j] == C) j++;
if (mx[C][l] == B) l++;
tmp = max(w[mx[B][i]] + w[mx[C][l]], w[mx[B][j]] + w[mx[C][k]]);
}
ans = max(ans, w[B] + w[C] + tmp);
}
}
write(ans);
return 0;
}
\(\bold{T2}\)
对于特殊条件 1:小 L 一定选最大的,小 Q 一定选最小的。
对于特殊条件 2:(如果有一个人只能选 \(0\),那么答案一定是 \(0\),以下分类不包括有一人只能选 \(0\) 的情况):
- 若小 L 只能选一个数
- 若这个数是正数——小 Q 一定选最小的数(即 \(min_B\))。
- 若这个数是负数——小 Q 一定选最大的数(即 \(max_B\))。
- 若小 Q 只能选一个数
- 若这个数是正数——小 L 一定选最大的数(即 \(max_A\))。
- 若这个数是负数——小 L 一定选最小的数(即 \(min_A\))。
部分分已经在不断地带着我们往正解走了,再进行更精细的分类其实正解就出来了。
从小 L 的角度入手。
小 Q 的选择范围分为以下三种:
(分别对应着只有正数、只有负数和既有正又有负)
- 对于 \((1)\),小 L 一定会选 \(max_A\),但也得分三种情况:
对于 \((a)\),小 Q 会选 \(max_B\);对于 \((b)\),小 \(Q\) 会选 \(min_B\);对于 \((c)\),小 \(Q\) 会直接摆烂,因为无论怎样都只能得到 \(0\)。
- 对于 \((2)\),小 L 一定会选 \(min_A\),分三种情况:
对于 \((a)\),小 Q 会选 \(max_B\);对于 \((b)\),小 Q 会选 \(min_B\);对于 \((c)\),小 Q 会随便乱选,因为无论怎样都只能得到 \(0\)。
- 对于 \((3)\),小 L 能选 \(0\) 就一定会选 \(0\)(因为无论小 L 选什么非零数,小 Q 都能将其乘成一个负数),加入这类情况后:
- 如果小 L 出负数,则小 Q 会出 \(max_B\),小 L 自然清楚这一点,所以他会出最大的非正数(这里将其称为 \(nmax_A\))。
- 如果小 L 出正数,则小 Q 会出 \(min_B\),小 L 自然清楚这一点,所以他会出最小的非负数(这里将其称为 \(pmin_A\))。
- 小 L 会使结果最大,所以答案是上述两种情况的最大值,即 \(\max(nmax_A \times max_B, pmin_A \times min_B)\)。
\(6\) 个 \(\text{ST}\) 表或线段树(此题不带修,建议用常数更小的 \(\text{ST}\) 表)维护 \(max_A, min_A, max_B, min_B, nmax_A, pmin_A\),\(O(n \log n)\) 预处理,\(O(1)\) 查询。
$Code$
#include <bits/stdc++.h>
#define MAXN 100100
#define MAXK 20
using namespace std;
const int INF = 2e9;
int n, m, q, a[MAXN], b[MAXN];
int lg[MAXN], maxa[MAXN][MAXK], mina[MAXN][MAXK], maxb[MAXN][MAXK], minb[MAXN][MAXK], nmaxa[MAXN][MAXK], pmina[MAXN][MAXK];
template<typename _T>
void read(_T &_x) {
_x = 0;
_T _f = 1;
char _ch = getchar();
while (_ch < '0' || '9' < _ch) {
if (_ch == '-') _f = -1;
_ch = getchar();
}
while ('0' <= _ch && _ch <= '9') {
_x = (_x << 3) + (_x << 1) + (_ch & 15);
_ch = getchar();
}
_x *= _f;
}
template<typename _T>
void write(_T _x) {
if (_x < 0) {
putchar('-');
_x = -_x;
}
if (_x > 9) write(_x / 10);
putchar('0' + _x % 10);
}
void init() {
for (int i = 2; i <= n || i <= m; i++) lg[i] = lg[i >> 1] + 1;
for (int i = 1; i <= n; i++) maxa[i][0] = mina[i][0] = a[i];
for (int i = 1; i <= m; i++) maxb[i][0] = minb[i][0] = b[i];
for (int i = 1; i <= n; i++) {
if (a[i] == 0) nmaxa[i][0] = pmina[i][0] = 0;
else if (a[i] < 0) nmaxa[i][0] = a[i], pmina[i][0] = INF;
else nmaxa[i][0] = -INF, pmina[i][0] = a[i];
}
for (int k = 1; (1 << k) <= n; k++) {
for (int i = 1; i + (1 << k) - 1 <= n; i++) {
maxa[i][k] = max(maxa[i][k - 1], maxa[i + (1 << (k - 1))][k - 1]);
mina[i][k] = min(mina[i][k - 1], mina[i + (1 << (k - 1))][k - 1]);
nmaxa[i][k] = max(nmaxa[i][k - 1], nmaxa[i + (1 << (k - 1))][k - 1]);
pmina[i][k] = min(pmina[i][k - 1], pmina[i + (1 << (k - 1))][k - 1]);
}
}
for (int k = 1; (1 << k) <= m; k++) {
for (int i = 1; i + (1 << k) - 1 <= m; i++) {
maxb[i][k] = max(maxb[i][k - 1], maxb[i + (1 << (k - 1))][k - 1]);
minb[i][k] = min(minb[i][k - 1], minb[i + (1 << (k - 1))][k - 1]);
}
}
}
int getmaxa(int l, int r) {
int ln = lg[r - l + 1];
return max(maxa[l][ln], maxa[r - (1 << ln) + 1][ln]);
}
int getmina(int l, int r) {
int ln = lg[r - l + 1];
return min(mina[l][ln], mina[r - (1 << ln) + 1][ln]);
}
int getmaxb(int l, int r) {
int ln = lg[r - l + 1];
return max(maxb[l][ln], maxb[r - (1 << ln) + 1][ln]);
}
int getminb(int l, int r) {
int ln = lg[r - l + 1];
return min(minb[l][ln], minb[r - (1 << ln) + 1][ln]);
}
int getnmaxa(int l, int r) {
int ln = lg[r - l + 1];
return max(nmaxa[l][ln], nmaxa[r - (1 << ln) + 1][ln]);
}
int getpmina(int l, int r) {
int ln = lg[r - l + 1];
return min(pmina[l][ln], pmina[r - (1 << ln) + 1][ln]);
}
int main() {
read(n), read(m), read(q);
for (int i = 1; i <= n; i++) read(a[i]);
for (int i = 1; i <= m; i++) read(b[i]);
init();
while (q--) {
int a, b, c, d;
read(a), read(b), read(c), read(d);
int amax = getmaxa(a, b), amin = getmina(a, b), bmax = getmaxb(c, d), bmin = getminb(c, d);
if (bmin >= 0) {
if (amax < 0) write(1ll * amax * bmax);
else if (amax > 0) write(1ll * amax * bmin);
else write(0);
} else if (bmax <= 0) {
if (amin < 0) write(1ll * amin * bmax);
else if (amin > 0) write(1ll * amin * bmin);
else write(0);
} else {
int nmax = getnmaxa(a, b), pmin = getpmina(a, b);
if (nmax == -INF) write(1ll * pmin * bmin);
else if (pmin == INF) write(1ll * nmax * bmax);
else write(max(1ll * nmax * bmax, 1ll * pmin * bmin));
}
putchar('\n');
}
return 0;
}
\(\bold{T3}\)
题面很长,认认真真读下来后不难发现:如果把每个虫洞看作是一条无向边的话,“绝佳的反攻时刻”时 \(n\) 个点一定会构成基环森林。就着这个性质再读一遍条件,又不难发现每个据点能够“连续穿梭”时势必可以“实现反击”,此时每个点的出度都为 \(1\)。
于是乎,题意简化成了:
给定 \(n\) 个点,\(m\) 条边,有如下操作:
\(1\):删除一条边。
\(2\):删除一个点的所有入边。
\(3\):恢复一条边。
\(4\):恢复一个点的所有入边。
每一次操作完成后,若每个点的出度都为 \(1\),输出
YES
,否则输出NO
。
对每一个点随机一个点权,然后 \(\text{Hash}\)~
$Code$
#include <bits/stdc++.h>
#define MAXN 500100
using namespace std;
typedef long long ll;
const int MOD = 1e9 + 7;
int n, m, q;
int tot, head[MAXN];
ll cur, ok, w[MAXN], s[MAXN], pres[MAXN];
template<typename _T>
void read(_T &_x) {
_x = 0;
_T _f = 1;
char _ch = getchar();
while (_ch < '0' || '9' < _ch) {
if (_ch == '-') _f = -1;
_ch = getchar();
}
while ('0' <= _ch && _ch <= '9') {
_x = (_x << 3) + (_x << 1) + (_ch & 15);
_ch = getchar();
}
_x *= _f;
}
int main() {
srand(time(0));
read(n), read(m);
for (int i = 1; i <= n; i++) {
w[i] = rand() % MOD;
ok = (ok + w[i]) % MOD;
}
while (m--) {
int u, v;
read(u), read(v);
s[v] = (s[v] + w[u]) % MOD;
cur = (cur + w[u]) % MOD;
}
for (int i = 1; i <= n; i++) pres[i] = s[i];
read(q);
while (q--) {
int op, u, v;
read(op), read(u);
if (op == 1) {
read(v);
s[v] = (s[v] - w[u] + MOD) % MOD;
cur = (cur - w[u] + MOD) % MOD;
} else if (op == 2) {
cur = (cur - s[u] + MOD) % MOD;
s[u] = 0;
} else if (op == 3) {
read(v);
s[v] = (s[v] + w[u]) % MOD;
cur = (cur + w[u]) % MOD;
} else {
cur = (cur + pres[u] - s[u] + MOD) % MOD;
s[u] = pres[u];
}
if (cur == ok) puts("YES");
else puts("NO");
}
return 0;
}
\(\bold{T4}\)
当 \(k = 1\) 时,就是一个求两点间简单路径上点权和的问题。预处理出树上每个点 \(u\) 到根节点的权值和 \(s[u]\),若给出的起点和终点分别为 \(x, y\),则 \(ans = s[x] + s[y] - s[\text{lca}(x, y)] - s[fa[\text{lca}(x, y)]]\)。预处理 \(O(n \log n)\),查询 \(O(q \log n)\),\(k = 1\) 的情况完美解决。
接下来难点来到了 \(k = 2\) 和 \(k = 3\):
看见最小值,还在树上,乍一眼也看不出来该用啥,大概率就是个 \(\text{DP}\) 了绝对不是因为前三题都没有动态规划感觉怪怪的。
考虑如何设计状态。对于 \(s \to t\)(\(s\) 到 \(t\) 的简单路径)上的一点 \(u\),有哪些点会对从 \(s\) 出发到达 \(u\) 花费的最少时间做出贡献呢?很容易想到是所有满足 \(\text{dis}(u, v) \le k\) 的点 \(v\),再细想,\(v\) 应该不位于 \(u\) 以后(谁赶时间的时候还玩迂回啊),于是:
令 \(f_{u, i}\) 表示从起点到达与 \(u\) 距离为 \(i\) 的点花费的最少时间,\(minv_u\) 表示与 \(u\) 直接相连的点中的最小点权,\(pre\) 为从起点到终点的路径上 \(u\) 的上一个点。
当 \(k = 2\) 时:
当 \(k = 3\) 时:
\(k = 3\) 时为什么 \(f[pre][1]\) 也能更新 \(f[u][1]\)?
\(x \to pre \to u \to v (v \in \text{son}(u))\),刚好三步,\(v\) 到 \(u\) 的距离也恰好是 \(1\)。
发现这样一来时间复杂度变成了 \(O(nq)\),显然不够我们通过此题,查询的 \(O(q)\) 是不得不带的,要想提高效率就得从转移的 \(O(n)\) 出发。注意到:我们其实并不关心过程中的任何一个 \(f_{u, i}\),只关心最后的 \(f_{t, 0}\),也就是不要中间量,只要结果,可以但我一开始的确没有想到矩阵。
我们可以重定义下矩阵乘法:
对于 \(A \times B = C\),有 \(C_{i, j} = \min\{A_{i, k} + B_{k, j}\}\)。
则有:
当 \(k = 2\) 时:
当 \(k = 3\) 时:
特别地,当 \(u = s\) 时:
当 \(k = 2\) 时:
当 \(k = 3\) 时:
加法和取最小值都满足结合律,那把它们并到一起,也是满足结合律的,所以我们新定义的矩阵乘法仍满足结合律。那就可以大胆对 \(s \to \text{LCA(s, t)} \to t\) 倍增,总的时间复杂度降到 \(O((n + q) k^3 \log n)\)。
最后统计答案时,要把 \(s = \text{lca}(s, t)\) 单独拿出来讨论(防止多算 \(s\) 处的转移矩阵)。
$Code$
#include <bits/stdc++.h>
#define MAXN 200100
using namespace std;
typedef long long ll;
int n, q, p, w[MAXN], dep[MAXN], minv[MAXN], fa[MAXN][20];
int tot, head[MAXN];
ll s[MAXN];
struct Edge {
int to, nxt;
} e[MAXN << 1];
struct Matrix {
int n, m;
ll a[3][3];
Matrix() {
memset(a, 0x3f, sizeof(a));
}
Matrix operator*(const Matrix &rhs) const {
Matrix res;
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
for (int k = 0; k < 3; k++) {
res.a[i][j] = min(res.a[i][j], a[i][k] + rhs.a[k][j]);
}
}
}
return res;
}
} f[MAXN][20], g[MAXN][20];
template<typename _T>
void read(_T &_x) {
_x = 0;
_T _f = 1;
char _ch = getchar();
while (_ch < '0' || '9' < _ch) {
if (_ch == '-') _f = -1;
_ch = getchar();
}
while ('0' <= _ch && _ch <= '9') {
_x = (_x << 3) + (_x << 1) + (_ch & 15);
_ch = getchar();
}
_x *= _f;
}
template<typename _T>
void write(_T _x) {
if (_x < 0) {
putchar('-');
_x = -_x;
}
if (_x > 9) write(_x / 10);
putchar('0' + _x % 10);
}
void add(int u, int v) {
e[++tot] = Edge{v, head[u]};
head[u] = tot;
}
void dfs(int u) {
for (int i = head[u], v; i; i = e[i].nxt) {
v = e[i].to;
if (v == fa[u][0]) continue;
dep[v] = dep[u] + 1;
fa[v][0] = u;
s[v] = s[u] + w[v];
minv[v] = w[u];
minv[u] = min(minv[u], w[v]);
dfs(v);
}
}
int LCA(int u, int v) {
if (dep[u] < dep[v]) swap(u, v);
for (int k = 17; k >= 0; k--) if (dep[fa[u][k]] >= dep[v]) u = fa[u][k];
if (u == v) return u;
for (int k = 17; k >= 0; k--) if (fa[u][k] != fa[v][k]) u = fa[u][k], v = fa[v][k];
return fa[u][0];
}
void solve1() {
int u, v, lca, flca;
while (q--) {
read(u), read(v);
lca = LCA(u, v), flca = fa[lca][0];
write(s[u] + s[v] - s[lca] - s[flca]);
putchar('\n');
}
exit(0);
}
Matrix newm(int u) {
Matrix res;
if (p == 2) {
res.a[0][0] = res.a[1][0] = w[u];
res.a[0][1] = 0;
} else {
res.a[0][0] = res.a[1][0] = res.a[2][0] = w[u];
res.a[0][1] = res.a[1][2] = 0;
res.a[1][1] = minv[u];
}
return res;
}
Matrix up(int u, int v) {
Matrix res;
res.a[0][0] = res.a[1][1] = res.a[2][2] = 0;
if (dep[u] <= dep[v]) return res;
for (int k = 17; k >= 0; k--) {
if (dep[fa[u][k]] >= dep[v]) {
res = res * f[u][k], u = fa[u][k];
}
}
return res;
}
Matrix down(int u, int v) {
Matrix res;
res.a[0][0] = res.a[1][1] = res.a[2][2] = 0;
if (dep[u] <= dep[v]) return res;
for (int k = 17; k >= 0; k--) {
if (dep[fa[u][k]] >= dep[v]) {
res = g[u][k] * res, u = fa[u][k];
}
}
return res;
}
Matrix S(int u) {
Matrix res;
res.a[0][0] = w[u];
return res;
}
int main() {
read(n), read(q), read(p);
for (int i = 1; i <= n; i++) read(w[i]);
for (int i = 1, u, v; i < n; i++) {
read(u), read(v);
add(u, v), add(v, u);
}
dep[1] = 1, s[1] = w[1], minv[1] = 1e9;
dfs(1);
for (int k = 1; k <= 17; k++) {
for (int i = 1; i <= n; i++) {
fa[i][k] = fa[fa[i][k - 1]][k - 1];
}
}
if (p == 1) solve1();
for (int i = 1; i <= n; i++) f[i][0] = g[i][0] = newm(i);
for (int k = 1; k <= 17; k++) {
for (int i = 1; i <= n; i++) {
f[i][k] = f[i][k - 1] * f[fa[i][k - 1]][k - 1];
g[i][k] = g[fa[i][k - 1]][k - 1] * g[i][k - 1];
}
}
int u, v, lca;
Matrix ans;
while (q--) {
read(u), read(v);
lca = LCA(u, v);
if (lca == u) ans = S(u) * down(v, lca);
else ans = S(u) * up(fa[u][0], lca) * newm(lca) * down(v, lca);
write(ans.a[0][0]), putchar('\n');
}
return 0;
}
ENDING#
出分了。。。
\(15 + 100 + 40 + 0 = 155\)
为什么心态这么不稳啊啊啊啊啊啊啊啊啊………………
作者:chy12321
出处:https://www.cnblogs.com/chy12321/p/16864664.html
版权:本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 25岁的心里话
· 按钮权限的设计及实现