CSP-S 2022 题解
感觉不如校内模拟赛。
但是保持状态和不挂分是很难的。
希望 NOIP 可以做到不挂分,至少拿满暴力。
T1 假期计划
签到。
发现 \(n \leq 2.5 \times 10^3\),于是考虑一些 \(O(n^2)\) 做法。
考虑枚举中间的两个顶点 \(B, C\)。假设有一对合法的 \(B, C\),考虑把可能合法的 \(A\) 和可能合法的 \(D\) 用 bfs \(O(n^2)\) 预处理出来。
注意到 \(A, D\) 中至少有一个顶点取到可能的最优值,于是分讨 \(A, D\) 取最优值的情况,然后取较优解即可。
这样做的好处在于分讨没有讨论前三大的做法复杂,但是复杂度多一只 \(\log\)
复杂度 \(O(n^2 \log n)\)
#include <cstdio>
#include <cstring>
#include <queue>
#include <algorithm>
using namespace std;
typedef long long ll;
const int maxn = 2.5e3 + 5;
const int maxm = 2e4 + 5;
struct node
{
int to, nxt;
} edge[maxm];
int n, m, k, cnt;
int head[maxn], dis[maxn][maxn];
ll w[maxn];
bool vis[maxn];
vector<int> to[maxn];
bool cmp(int a, int b) { return (w[a] > w[b]); }
void add_edge(int u, int v)
{
cnt++;
edge[cnt].to = v;
edge[cnt].nxt = head[u];
head[u] = cnt;
}
void bfs(int st)
{
queue<int> q;
memset(dis[st], -1, sizeof(dis[st]));
memset(vis, false, sizeof(vis));
vis[st] = true;
dis[st][st] = 0;
q.push(st);
while (!q.empty())
{
int u = q.front();
q.pop();
for (int i = head[u]; i; i = edge[i].nxt)
{
int v = edge[i].to;
if (!vis[v])
{
dis[st][v] = dis[st][u] + 1;
vis[v] = true, q.push(v);
}
}
}
}
int main()
{
// freopen("holiday.in", "r", stdin);
// freopen("holiday.out", "w", stdout);
ll ans = 0;
scanf("%d%d%d", &n, &m, &k); k++;
for (int i = 2; i <= n; i++) scanf("%lld", &w[i]);
for (int i = 1; i <= m; i++)
{
int u, v;
scanf("%d%d", &u, &v);
add_edge(u, v);
add_edge(v, u);
}
for (int i = 1; i <= n; i++) bfs(i);
for (int i = 2; i <= n; i++)
for (int j = 2; j <= n; j++)
if ((dis[i][j] >= 1) && (dis[i][j] <= k) && (dis[j][1] >= 1) && (dis[j][1] <= k)) to[i].push_back(j);
for (int i = 1; i <= n; i++) sort(to[i].begin(), to[i].end(), cmp);
for (int b = 2; b <= n; b++)
for (int c = 2; c <= n; c++)
{
if ((dis[b][c] <= 0) || (dis[b][c] > k) || (to[b].empty()) || (to[c].empty())) continue;
int a = -1, d = -1;
for (int i = 0; i < to[b].size(); i++)
if (to[b][i] != c) { a = to[b][i]; break; }
if (a != -1)
{
for (int i = 0; i < to[c].size(); i++)
if ((to[c][i] != a) && (to[c][i] != b)) { d = to[c][i]; break; }
if (d != -1) ans = max(ans, w[a] + w[b] + w[c] + w[d]);
}
d = -1, a = -1;
for (int i = 0; i < to[c].size(); i++)
if (to[c][i] != b) { d = to[c][i]; break; }
if (d != -1)
{
for (int i = 0; i < to[b].size(); i++)
if ((to[b][i] != c) && (to[b][i] != d)) { a = to[b][i]; break; }
if (a != -1) ans = max(ans, w[a] + w[b] + w[c] + w[d]);
}
}
printf("%lld\n", ans);
return 0;
}
T2 策略游戏
真·签到。
考虑分讨:
-
先手取正数。
- 后手有负数
此时后手取最小负数,先手取最小正数 - 后手有 \(0\)
此时后手取 \(0\) - 后手只有正数
此时后手取最小正数,先手取最大正数
- 后手有负数
-
先手取负数
- 后手有正数
此时先手取最大负数,后手取最大正数 - 后手有 \(0\)
此时后手取 \(0\) - 后手只有负数
此时先手取最小负数,后手取最大负数
- 后手有正数
-
先手取 \(0\)
贡献为 \(0\)
由于先手足够聪明,因此会在三种情况中取最优值。用 RMQ 维护区间最值即可。
#include <cstdio>
#include <algorithm>
using namespace std;
typedef long long ll;
const int maxn = 1e5 + 5;
const int lg_sz = 20;
const ll inf = 1e18;
int q;
int lg[maxn];
int pre1[maxn], pre2[maxn];
int get_max(int a, int b)
{
if (a == 0) return b;
if (b == 0) return a;
return max(a, b);
}
int get_min(int a, int b)
{
if (a == 0) return b;
if (b == 0) return a;
return min(a, b);
}
struct seq
{
int n;
int val[maxn];
int mxv1[maxn][lg_sz], mxv2[maxn][lg_sz], mnv1[maxn][lg_sz], mnv2[maxn][lg_sz];
void init()
{
for (int i = 1; i <= n; i++)
if (val[i] > 0) mxv1[i][0] = mnv1[i][0] = val[i];
else if (val[i] < 0) mxv2[i][0] = mnv2[i][0] = val[i];
else mxv1[i][0] = mxv2[i][0] = mnv1[i][0] = mnv2[i][0] = 0;
for (int j = 1; (1 << j) <= n; j++)
for (int i = 1; i + (1 << j) - 1 <= n; i++)
{
mxv1[i][j] = get_max(mxv1[i][j - 1], mxv1[i + (1 << (j - 1))][j - 1]);
mnv1[i][j] = get_min(mnv1[i][j - 1], mnv1[i + (1 << (j - 1))][j - 1]);
mxv2[i][j] = get_max(mxv2[i][j - 1], mxv2[i + (1 << (j - 1))][j - 1]);
mnv2[i][j] = get_min(mnv2[i][j - 1], mnv2[i + (1 << (j - 1))][j - 1]);
}
}
int query_mxv1(int l, int r)
{
int k = lg[r - l + 1];
return get_max(mxv1[l][k], mxv1[r - (1 << k) + 1][k]);
}
int query_mnv1(int l, int r)
{
int k = lg[r - l + 1];
return get_min(mnv1[l][k], mnv1[r - (1 << k) + 1][k]);
}
int query_mxv2(int l, int r)
{
int k = lg[r - l + 1];
return get_max(mxv2[l][k], mxv2[r - (1 << k) + 1][k]);
}
int query_mnv2(int l, int r)
{
int k = lg[r - l + 1];
return get_min(mnv2[l][k], mnv2[r - (1 << k) + 1][k]);
}
} a, b;
ll solve1(int l, int r, int s, int t)
{
int mxv1 = a.query_mxv1(l, r);
if (mxv1 == 0) return -inf;
int mnv1 = a.query_mnv1(l, r);
int tmp = b.query_mnv2(s, t);
if (tmp != 0) return 1ll * mnv1 * tmp;
if (pre2[t] - pre2[s - 1] > 0) return 0;
tmp = b.query_mnv1(s, t);
if (tmp != 0) return 1ll * mxv1 * tmp;
return 0;
}
ll solve2(int l, int r, int s, int t)
{
int mxv2 = a.query_mxv2(l, r);
if (mxv2 == 0) return -inf;
int mnv2 = a.query_mnv2(l, r);
int tmp = b.query_mxv1(s, t);
if (tmp != 0) return 1ll * mxv2 * tmp;
if (pre2[t] - pre2[s - 1] > 0) return 0;
tmp = b.query_mxv2(s, t);
if (tmp != 0) return 1ll * mnv2 * tmp;
return 0;
}
int main()
{
// freopen("game.in", "r", stdin);
// freopen("game.out", "w", stdout);
scanf("%d%d%d", &a.n, &b.n, &q);
for (int i = 2; i <= max(a.n, b.n); i++) lg[i] = lg[i >> 1] + 1;
for (int i = 1; i <= a.n; i++)
{
scanf("%d", &a.val[i]);
pre1[i] = pre1[i - 1] + (a.val[i] == 0);
}
for (int i = 1; i <= b.n; i++)
{
scanf("%d", &b.val[i]);
pre2[i] = pre2[i - 1] + (b.val[i] == 0);
}
a.init(), b.init();
while (q--)
{
int l, r, s, t;
scanf("%d%d%d%d", &l, &r, &s, &t);
ll ans1 = solve1(l, r, s, t);
ll ans2 = solve2(l, r, s, t);
ll ans = max(ans1, ans2);
if (pre1[r] - pre1[l - 1] > 0) ans = max(ans, 0ll);
printf("%lld\n", ans);
}
return 0;
}
T3 星战
玄妙哈希乱搞。
考场暴力变量没初始化,一分没有,可惜了。
一开始想的是数据结构一类的东西维护,没想到正解是哈希。
多老师:考场看了半个小时还不会,觉得没有确定性算法,于是想到哈希。
条件 2 的含义是 \(n\) 个点 \(n\) 条有向边,且每个点的出度为 \(1\),显然是此时图是一个内向基环树森林,于是必然满足条件 1。
令 \(S\) 为当前图中所有存在的边的起点构成的可重集,考虑动态维护 \(S\) 的哈希值,和构成内向基环树的可重集比较即可 \(O(1)\) 判断合法性。
对于操作 1,3,直接修改哈希值即可。
对于操作 2,4,可以考虑先维护出每个点初始时对哈希值的贡献,然后再动态维护每个点当前对哈希值的贡献。这样 2 操作可以直接消去剩余的贡献,4 操作直接加回删去的贡献。
哈希可以用一次方和还有异或和直接草,正确性不会证。担心过不了就再加一个二次方和。
由乃与大母神原型和偶像崇拜
时间复杂度 \(O(n + m)\)
#include <cstdio>
using namespace std;
typedef long long ll;
const int maxn = 5e5 + 5;
const int maxm = 5e5 + 5;
int n, m, q;
int u[maxm], v[maxm];
ll val1[maxn], res1[maxn];
int val2[maxn], res2[maxn];
int main()
{
ll cur1 = 0;
int cur2 = 0;
scanf("%d%d", &n, &m);
ll hs1 = 1ll * n * (n + 1) / 2;
int hs2 = 0;
for (int i = 1; i <= n; i++) hs2 ^= i;
for (int i = 1; i <= m; i++)
{
int u, v;
scanf("%d%d", &u, &v);
cur1 += u, cur2 ^= u;
val1[v] += u, val2[v] ^= u;
}
for (int i = 1; i <= n; i++) res1[i] = val1[i], res2[i] = val2[i];
scanf("%d", &q);
while (q--)
{
int opt, u, v;
scanf("%d", &opt);
if (opt == 1)
{
scanf("%d%d", &u, &v);
val1[v] -= u, val2[v] ^= u;
cur1 -= u, cur2 ^= u;
}
else if (opt == 2)
{
scanf("%d", &v);
cur1 -= val1[v], cur2 ^= val2[v];
val1[v] = val2[v] = 0;
}
else if (opt == 3)
{
scanf("%d%d", &u, &v);
val1[v] += u, val2[v] ^= u;
cur1 += u, cur2 ^= u;
}
else
{
scanf("%d", &v);
cur1 += (res1[v] - val1[v]), cur2 ^= (res2[v] ^ val2[v]);
val1[v] = res1[v], val2[v] = res2[v];
}
puts(((hs1 == cur1) && (hs2 == cur2)) ? "YES" : "NO");
}
return 0;
}
T4 数据传输
倍增 + 矩乘优化 dp。
76 pts 一眼提取树链 dp,考虑优化。
你发现这是静态的,于是考虑倍增。(动态就树剖)
然而不好维护答案,考虑把提取树链做法改成直接在树上做。
此时我们需要向下枚举若干结点,可以矩乘优化。
假设提取树链序列,令 \(f_i\) 为前 \(i\) 个顶点的答案。
那么当前矩阵为:
目标矩阵为:
由于转移是取 \(\min\),所以这里放的不是系数而是要加上的常数。由此得到转移矩阵:
初始矩阵为:
然而这样做会在 \(k = 3\) 的时候寄掉,原因是可以走到树上路径以外的点。
观察一下,我们发现至多只会走到距离树上路径为 \(1\) 的点。考虑它对于状态转移的影响,实际上只是使 \(f_{i - 1}\) 多一种转移。
具体地,\(f_{i - 1}\) 的转移可以通过走上面提到的点得到。于是考虑在每个点的矩阵上面多加一种 \(f_{i - 2}\) 到 \(f_{i - 1}\) 的转移,代表走这种点的贡献。
体现在代码上就是先取这些点的点权最小值,再令 \(w[1][1]\) 和它取 \(\min\)
复杂度 \(O(3^3 n \log n)\)
#include <cstdio>
#include <algorithm>
using namespace std;
#define FOR(i, a, b) for(int i = a; i <= b; i++)
typedef long long ll;
const int maxn = 2e5 + 5;
const int maxm = 4e5 + 5;
const int lg_sz = 20;
const ll inf = 1e18;
struct node
{
int to, nxt;
} edge[maxm];
struct mat
{
int n, m;
ll w[3][3];
mat operator * (const mat& rhs)
{
mat res;
res = {n, rhs.m, {0}};
for (int i = 0; i < res.n; i++)
for (int j = 0; j < res.m; j++)
res.w[i][j] = inf;
for (int i = 0; i < res.n; i++)
for (int j = 0; j < res.m; j++)
for (int k = 0; k < m; k++)
res.w[i][j] = min(res.w[i][j], w[i][k] + rhs.w[k][j]);
return res;
}
} B, val[maxn], up[maxn][lg_sz], dn[maxn][lg_sz];
int n, q, k, cnt;
int head[maxn], w[maxn], dep[maxn], f[maxn][lg_sz];
void add_edge(int u, int v)
{
cnt++;
edge[cnt].to = v;
edge[cnt].nxt = head[u];
head[u] = cnt;
}
void dfs(int u, int fa)
{
dep[u] = dep[fa] + 1;
f[u][0] = fa;
if (k == 3)
{
for (int i = head[u]; i; i = edge[i].nxt)
{
int v = edge[i].to;
val[u].w[1][1] = min(val[u].w[1][1], 1ll * w[v]);
}
}
up[u][0] = val[fa], dn[u][0] = val[u];
for (int i = 1; i <= 19; i++)
{
f[u][i] = f[f[u][i - 1]][i - 1];
up[u][i] = up[u][i - 1] * up[f[u][i - 1]][i - 1];
dn[u][i] = dn[f[u][i - 1]][i - 1] * dn[u][i - 1];
}
for (int i = head[u]; i; i = edge[i].nxt)
{
int v = edge[i].to;
if (v != fa) dfs(v, u);
}
}
mat solve(int u, int v)
{
mat a = B, b = B;
if (dep[u] > dep[v])
for (int i = 19; i >= 0; i--)
if (dep[f[u][i]] >= dep[v])
{
a = a * up[u][i];
u = f[u][i];
}
if (dep[v] > dep[u])
for (int i = 19; i >= 0; i--)
if (dep[f[v][i]] >= dep[u])
{
b = dn[v][i] * b;
v = f[v][i];
}
if (u == v) return a * b;
for (int i = 19; i >= 0; i--)
if (f[u][i] != f[v][i])
{
a = a * up[u][i];
b = dn[v][i] * b;
u = f[u][i], v = f[v][i];
}
return a * up[u][0] * dn[v][0] * b;
}
int main()
{
scanf("%d%d%d", &n, &q, &k);
B = {k, k, {{0, inf, inf}, {inf, 0, inf}, {inf, inf, 0}}};
for (int i = 1; i <= n; i++)
{
scanf("%d", &w[i]);
val[i] = {k, k, {{inf, inf, inf}, {inf, inf, inf}, {inf, inf, inf}}};
for (int j = 0; j <= k - 1; j++) val[i].w[j][0] = w[i];
for (int j = 1; j <= k - 1; j++) val[i].w[j - 1][j] = 0;
}
for (int i = 1; i <= n - 1; i++)
{
int u, v;
scanf("%d%d", &u, &v);
add_edge(u, v);
add_edge(v, u);
}
dfs(1, 0);
while (q--)
{
int u, v;
scanf("%d%d", &u, &v);
mat tmp = {1, k, {w[u], inf, inf}};
printf("%lld\n", (tmp * solve(u, v)).w[0][0]);
}
return 0;
}