CSP-S 2022 题解
前言
由于打的肽粉了,NOIP 考前补了一下题。
T1:假期计划
题目大意
给你一个无向图,然后要你规划出一条路径,其中起点和终点都是 1 号点,然后你从中选四个不同的特殊点,形成五段路,保证每段的边数不超过 k 的情况下,四个特殊点的点权和最大。
思路
首先看到 n n 是 2000 2000 ,而且边权相同,那我们完全可以用 O ( n 2 ) O ( n 2 ) dfs 搜出如果这个点是特殊点,下一个可以是那些。
那把这些建图,就变成你要找一个其中一个点固定的五元环。
考虑怎么找,直接找肯定超时,考虑一个类似折中的感觉,你先找两条路径,然后看看能否拼起来。
那这样还是太多了,考虑走两步走到的位置,那对于每个位置有 n n 种,我们选出最优的(反正后面连着的时候也不管它)
但是这样自然有问题,就是你要五元环中每个点都不用。
那我们考虑保留前几大的,然后这些之间比一下把能用的跟最优答案比。
思考一下不难发现只要取前三大即可。
代码
#include <queue>
#include <cstdio>
#include <vector>
#include <cstring>
#define ll long long
using namespace std;
const int N = 2505 ;
int n, m, k, dis[N][N], maxn[N], maxx[N], maxp[N];
vector <int > G[N], G1[N];
queue <int > q;
bool in[N];
ll a[N], ans;
void dfs (int now, int num, ll an) {
if (num == 4 ) {
if (dis[now][1 ] <= k) ans = max (ans, an);
return ;
}
for (int i = 0 ; i < G1[now].size (); i++) {
int x = G1[now][i];
if (x > 1 && !in[x]) {
in[x] = 1 ;
dfs (x, num + 1 , an + a[x]);
in[x] = 0 ;
}
}
}
void check (int A, int b, int c, int d) {
if (A == b || A == c || A == d) return ;
if (b == c || b == d) return ;
if (c == d) return ;
if (!A || !b || !c || !d) return ;
ans = max (ans, a[A] + a[b] + a[c] + a[d]);
}
int main () {
scanf ("%d %d %d" , &n, &m, &k);
for (int i = 2 ; i <= n; i++) scanf ("%lld" , &a[i]);
memset (dis, 0x7f , sizeof (dis));
for (int i = 1 ; i <= m; i++) {
int x, y; scanf ("%d %d" , &x, &y);
G[x].push_back (y); G[y].push_back (x);
}
for (int S = 1 ; S <= n; S++) {
for (int i = 0 ; i < G[S].size (); i++) {
int x = G[S][i]; dis[S][x] = 0 ; q.push (x);
}
while (!q.empty ()) {
int now = q.front (); q.pop ();
for (int i = 0 ; i < G[now].size (); i++) {
int x = G[now][i];
if (dis[S][x] > dis[S][now] + 1 ) {
dis[S][x] = dis[S][now] + 1 ; q.push (x);
}
}
}
}
if (n <= 20 || (n <= 200 && k == 0 )) {
for (int i = 1 ; i <= n; i++)
for (int j = 1 ; j <= n; j++)
if (i != j && dis[i][j] <= k) {
G1[i].push_back (j);
}
for (int i = 0 ; i < G1[1 ].size (); i++) {
int x = G1[1 ][i];
in[x] = 1 ; dfs (x, 1 , a[x]); in[x] = 0 ;
}
}
else {
for (int i = 2 ; i <= n; i++) {
for (int j = 2 ; j <= n; j++)
if (i != j && dis[1 ][j] <= k && dis[j][i] <= k) {
if (a[j] > a[maxn[i]]) maxp[i] = maxx[i], maxx[i] = maxn[i], maxn[i] = j;
else if (a[j] > a[maxx[i]]) maxp[i] = maxx[i], maxx[i] = j;
else if (a[j] > a[maxp[i]]) maxp[i] = j;
}
}
for (int i = 2 ; i <= n; i++)
for (int j = 2 ; j <= n; j++)
if (i != j && dis[i][j] <= k) {
check (maxn[i], i, j, maxn[j]);
check (maxx[i], i, j, maxn[j]);
check (maxp[i], i, j, maxn[j]);
check (maxn[i], i, j, maxx[j]);
check (maxx[i], i, j, maxx[j]);
check (maxp[i], i, j, maxx[j]);
check (maxn[i], i, j, maxp[j]);
check (maxx[i], i, j, maxp[j]);
check (maxp[i], i, j, maxp[j]);
}
}
printf ("%lld" , ans);
return 0 ;
}
T2:策略游戏
题目大意
给你一个数组 A 和一个数组 B,每次游戏给你 A 的区间和 B 的区间。
然后先手选 A 区间里的一个数,后手再选 B 区间里的一个数,两个数相乘构成分数。
先手要最大化分数,后手要最小化分数,问你每次游戏的分数。
思路
直接思考每个人的过程,分类讨论即可。主要的讨论方式就是针对 0 0 ,正负数展开。
可以假设先手放的数,放的 0 0 就不管了,如果放的是正数,后手如果有负数就会放最小的负数,否则有 0 0 放 0 0 ,没有就放最小的正数。
那放的负数也是相同的道理。
那对于先手而言,他如果无论放正数负数都会变成负数,那就有 0 0 放 0 0 。
否则模拟一下放最小正数和最大负数,选贡献大的。
如果有一种情况能避免的话,那就正数选最大,负数选最小(如果两种都可以就都试一下选最大)。
大概就是这样的分类讨论。
代码
#include <cstdio>
#include <iostream>
#define ll long long
#define INF 0x3f3f3f3f3f3f3f3f
using namespace std;
const int N = 1e5 + 100 ;
int n, m, q, log2_[N], tmpa[3 ], tmpb[3 ];
int a[N], b[N], anum[3 ][N], bnum[3 ][N];
int l1, r1, l2, r2;
struct ST_max {
int f[21 ][N];
void Init (int n) {
for (int i = 1 ; i <= 20 ; i++)
for (int j = 1 ; j + (1 << i) - 1 <= n; j++)
f[i][j] = max (f[i - 1 ][j], f[i - 1 ][j + (1 << (i - 1 ))]);
}
int query (int l, int r) {
int k = log2_[r - l + 1 ];
return max (f[k][l], f[k][r - (1 << k) + 1 ]);
}
}Amax_z, Amax_f, Bmax_z, Bmax_f;
struct ST_min {
int f[21 ][N];
void Init (int n) {
for (int i = 1 ; i <= 20 ; i++)
for (int j = 1 ; j + (1 << i) - 1 <= n; j++)
f[i][j] = min (f[i - 1 ][j], f[i - 1 ][j + (1 << (i - 1 ))]);
}
int query (int l, int r) {
int k = log2_[r - l + 1 ];
return min (f[k][l], f[k][r - (1 << k) + 1 ]);
}
}Amin_z, Amin_f, Bmin_z, Bmin_f;
ll slove () {
if (tmpb[1 ] && tmpb[2 ]) {
if (tmpa[0 ]) return 0 ;
ll ans = -1e18 ;
if (tmpa[1 ]) ans = max (ans, 1ll * Amin_z.query (l1, r1) * Bmin_f.query (l2, r2));
if (tmpa[2 ]) ans = max (ans, 1ll * Amax_f.query (l1, r1) * Bmax_z.query (l2, r2));
return ans;
}
if (tmpb[1 ] && !tmpa[1 ]) {
if (tmpa[0 ]) return 0 ;
return 1ll * Amax_f.query (l1, r1) * Bmax_z.query (l2, r2);
}
if (tmpb[2 ] && !tmpa[2 ]) {
if (tmpa[0 ]) return 0 ;
return 1ll * Amin_z.query (l1, r1) * Bmin_f.query (l2, r2);
}
if (tmpb[0 ]) return 0 ;
ll ans = -1e18 ;
if (tmpb[1 ]) {
ans = max (ans, 1ll * Amax_z.query (l1, r1) * Bmin_z.query (l2, r2));
}
if (tmpb[2 ]) {
ans = max (ans, 1ll * Amin_f.query (l1, r1) * Bmax_f.query (l2, r2));
}
return ans;
}
int main () {
log2_[0 ] = -1 ; for (int i = 1 ; i < N; i++) log2_[i] = log2_[i >> 1 ] + 1 ;
scanf ("%d %d %d" , &n, &m, &q);
for (int i = 1 ; i <= n; i++) {
scanf ("%d" , &a[i]);
if (a[i] > 0 ) {
Amax_z.f[0 ][i] = Amin_z.f[0 ][i] = a[i];
Amax_f.f[0 ][i] = -INF; Amin_f.f[0 ][i] = INF;
}
if (a[i] < 0 ) {
Amax_f.f[0 ][i] = Amin_f.f[0 ][i] = a[i];
Amax_z.f[0 ][i] = -INF; Amin_z.f[0 ][i] = INF;
}
if (!a[i]) anum[0 ][i]++;
if (a[i] > 0 ) anum[1 ][i]++;
if (a[i] < 0 ) anum[2 ][i]++;
}
for (int i = 1 ; i <= m; i++) {
scanf ("%d" , &b[i]);
if (b[i] > 0 ) {
Bmax_z.f[0 ][i] = Bmin_z.f[0 ][i] = b[i];
Bmax_f.f[0 ][i] = -INF; Bmin_f.f[0 ][i] = INF;
}
if (b[i] < 0 ) {
Bmax_f.f[0 ][i] = Bmin_f.f[0 ][i] = b[i];
Bmax_z.f[0 ][i] = -INF; Bmin_z.f[0 ][i] = INF;
}
if (!b[i]) bnum[0 ][i]++;
if (b[i] > 0 ) bnum[1 ][i]++;
if (b[i] < 0 ) bnum[2 ][i]++;
}
Amax_z.Init (n); Amin_z.Init (n); Bmax_z.Init (m); Bmin_z.Init (m);
Amax_f.Init (n); Amin_f.Init (n); Bmax_f.Init (m); Bmin_f.Init (m);
for (int op = 0 ; op < 3 ; op++) {
for (int i = 1 ; i <= n; i++) anum[op][i] += anum[op][i - 1 ];
for (int i = 1 ; i <= m; i++) bnum[op][i] += bnum[op][i - 1 ];
}
for (int i = 1 ; i <= q; i++) {
scanf ("%d %d %d %d" , &l1, &r1, &l2, &r2);
for (int op = 0 ; op < 3 ; op++) {
tmpa[op] = anum[op][r1] - anum[op][l1 - 1 ];
tmpb[op] = bnum[op][r2] - bnum[op][l2 - 1 ];
}
printf ("%lld\n" , slove ());
}
return 0 ;
}
T3:星战
题目大意
给你一个有向图,每次操作有删除一条边,恢复某条已经删了的边,删除所有还在的连着某个点的边,恢复所有连着这个点的被删掉的边。
每次操作之后,问你是否每个点都有恰好一个出边,而且都能走到一个环里。
思路
考虑先转化题意。
发现只要每个点都满足初度恰好为 1 1 ,另一个能走到环的条件也能满足。
那你有出边嘛,那就能一直走下去,自然就是环了。
那如果只有 1 , 3 1 , 3 操作,那就很容易维护,问题是 2 , 4 2 , 4 操作,它的是入边。
所以你入度是方便维护的,但是出度就不太行。
那你考虑入度初度之间的关系,那就是入度和跟出度和一样。
而且在这个限制条件下都是 n n 。
那如果满足的这个条件,也不一定行。
那要如何判断是每个点都贡献了一次,而不是一个点贡献了多次呢?
可以用哈希,每个点随机一个点权,每条边以它做入边的点做贡献。
那合法的情况就是当前所有存在的边的权和是所有点的权和。
那你每个点记录着入边的边权和就行。
代码
T4:数据传输
题目大意
给你一棵树,多次询问每次你要从树上的一个点走到另一个点。
每次走你可以走到距离你当前点边数不超过 k 条边的点,并加上你走到的点的点权(起点和终点也要加),问你需要的最小点权。
思路
一个直观的想法是在树上按着 DP,甚至只需要在路径上的点里面看。
但这样在 k = 1 , 2 k = 1 , 2 没问题,但是在 k = 3 k = 3 的时候会有一个特别的情况:
就是走两步往外选一个点停下算贡献,再走回来然后走两步。
不过也不会有问题,你用一个 w i w i 记录它以及它一步能走到的边权最小的点权,就也可以弄。
那至于在树上,你就拆成 LCA 的两段。
那你可以用树链剖分或者倍增,这里用的是倍增,来造每一段的转移。
那思考怎么转移,那你不难想象到是从 ( x , a ) → ( y , b ) ( x , a ) → ( y , b ) ,即从 x x 点,现在上一个到的点距离 a a 条边,走到 y y 点,上一个到的点的距离 b b 条边需要的最小费用。
然后你可以根据这个 a , b a , b 建立转移矩阵,那就是 f a , b f a , b 的位置的值。
然后转移就是不放点,这个位置放点,在这个位置走出去放点。
然后考虑两段的矩阵如何算答案。
考虑枚举两边的 ( 0 , a ) , ( 0 , b ) ( 0 , a ) , ( 0 , b ) 。
那正常了来说是两点的费用加起来,不过有两个特别的:
a = b = 0 a = b = 0 ,那 lca 这个位置被统计了两次,要减一次。
a = b = 2 a = b = 2 ,那 lca 这个位置就需要往别处走存一下,所以要加上 w l c a w l c a 。
然后就可以了。
代码
#include <cstdio>
#include <iostream>
#include <algorithm>
#define ll long long
using namespace std;
const int N = 2e5 + 100 ;
struct node {
int to, nxt;
}e[N << 1 ];
int n, Q, K, le[N], KK, fa[N][21 ], deg[N];
ll v[N], w[N];
struct matrix {
int n, m;
ll a[3 ][3 ];
}f[N][21 ];
matrix operator *(matrix x, matrix y) {
matrix z; z.n = x.n; z.m = y.m;
for (int i = 0 ; i < z.n; i++)
for (int j = 0 ; j < z.m; j++)
z.a[i][j] = 1e18 ;
for (int k = 0 ; k < x.m; k++)
for (int i = 0 ; i < z.n; i++)
for (int j = 0 ; j < z.m; j++)
z.a[i][j] = min (z.a[i][j], x.a[i][k] + y.a[k][j]);
return z;
}
void add (int x, int y) {
e[++KK] = (node){y, le[x]}; le[x] = KK;
}
void dfs (int now, int father) {
deg[now] = deg[father] + 1 ;
fa[now][0 ] = father; for (int i = 1 ; i <= 20 ; i++) fa[now][i] = fa[fa[now][i - 1 ]][i - 1 ];
if (K == 1 ) {
f[now][0 ].n = f[now][0 ].m = 1 ;
f[now][0 ].a[0 ][0 ] = v[father];
}
if (K == 2 ) {
f[now][0 ].n = f[now][0 ].m = 2 ;
f[now][0 ].a[0 ][0 ] = v[father]; f[now][0 ].a[0 ][1 ] = 0 ;
f[now][0 ].a[1 ][0 ] = v[father]; f[now][0 ].a[1 ][1 ] = 1e18 ;
}
if (K == 3 ) {
f[now][0 ].n = f[now][0 ].m = 3 ;
f[now][0 ].a[0 ][0 ] = v[father]; f[now][0 ].a[0 ][1 ] = 0 ; f[now][0 ].a[0 ][2 ] = 1e18 ;
f[now][0 ].a[1 ][0 ] = v[father]; f[now][0 ].a[1 ][1 ] = 1e18 ; f[now][0 ].a[1 ][2 ] = 0 ;
f[now][0 ].a[2 ][0 ] = v[father]; f[now][0 ].a[2 ][1 ] = 1e18 ; f[now][0 ].a[2 ][2 ] = w[now];
}
for (int i = 1 ; i <= 20 ; i++) f[now][i] = f[now][i - 1 ] * f[fa[now][i - 1 ]][i - 1 ];
for (int i = le[now]; i; i = e[i].nxt)
if (e[i].to != father) dfs (e[i].to, now);
}
int LCA (int x, int y) {
if (deg[x] < deg[y]) swap (x, y);
for (int i = 20 ; i >= 0 ; i--)
if (deg[fa[x][i]] >= deg[y]) x = fa[x][i];
if (x == y) return x;
for (int i = 20 ; i >= 0 ; i--)
if (fa[x][i] != fa[y][i]) x = fa[x][i], y = fa[y][i];
return fa[x][0 ];
}
matrix work (int x, int y) {
matrix re; re.n = re.m = K;
for (int i = 0 ; i < re.n; i++) for (int j = 0 ; j < re.m; j++) re.a[i][j] = (i == j) ? v[x] : 1e18 ;
for (int i = 20 ; i >= 0 ; i--)
if (deg[fa[x][i]] >= deg[y]) re = re * f[x][i], x = fa[x][i];
return re;
}
int main () {
scanf ("%d %d %d" , &n, &Q, &K);
for (int i = 1 ; i <= n; i++) scanf ("%lld" , &v[i]), w[i] = v[i];
for (int i = 1 ; i < n; i++) {
int x, y; scanf ("%d %d" , &x, &y);
add (x, y); add (y, x);
w[x] = min (w[x], v[y]); w[y] = min (w[y], v[x]);
}
for (int i = 0 ; i <= 20 ; i++) {
f[0 ][i].n = f[0 ][i].m = K;
for (int j = 0 ; j < K; j++)
for (int k = 0 ; k < K; k++)
f[0 ][i].a[j][k] = 1e18 ;
}
dfs (1 , 0 );
while (Q--) {
int x, y; scanf ("%d %d" , &x, &y); int lca = LCA (x, y);
matrix lx = work (x, lca), ry = work (y, lca);
ll ans = 1e18 ;
for (int i = 0 ; i < K; i++)
for (int j = 0 ; j < K; j++) {
ll now = lx.a[0 ][i] + ry.a[0 ][j];
if (!i && !j) now -= v[lca];
if (i == 2 && j == 2 ) now += w[lca];
ans = min (ans, now);
}
printf ("%lld\n" , ans);
}
return 0 ;
}
__EOF__
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!