2025CSP-S模拟赛7 比赛总结
2025CSP-S模拟赛7
挂分不多,只挂了 20 分。主要 T1 后来略有改动而忘交,失 20 分。故应模拟正式考试,至比赛最后再统一交一次代码。
T1 Lesson5!
80pts
考虑枚举删去的点。
然后现在有了一个断点,要求最长路径。考虑 dp。设 \(f_i\) 表示当前考虑到点 \(i\),以点 \(i\) 为终点的最长路径,转移即为
方便转移,需要进行拓扑排序并按照拓扑序排序即可。遇见删去的点直接 continue 即可。
答案即为 \(\max\{f_i\}\)。
100pts
依然是枚举断点考虑 dp。
类似 \(f_i\),设 \(g_i\) 表示当前考虑到点 \(i\),以点 \(i\) 为起点的最长路径,转移相同,只不过是在反图上跑。答案即为 \(\max{\{f_i+g_i\}}\)。后续需要,答案也就是 \(\max{\{f_v+1+g_u\}}\)。
然后考虑将图按照拓扑序分层。那么,将一个点连接上一层的边删除,再将连接下一层的边加入,两个过程的中间的状态前好是这个点被删除。此时的最长路即为答案。
那么,我们可以先跑出原图的 \(f\) 和 \(g\),然后利用 multiset 维护答案(\(\max\) 值),并在枚举断点的时候进行删除连接上一层边的操作和加入连接下一层边的操作,具体表现为操作 multiset 中的元素。
由于 multiset 常数过大,次数使用可删堆实现。
multiset 版代码(80 TLE):
#include <bits/stdc++.h>
using namespace std;
int read() {
int x = 0; char ch = getchar();
while (ch < '0' || ch > '9') ch = getchar();
while (ch >= '0' && ch <= '9') {
x = (x << 1) + (x << 3) + (ch ^ 48);
ch = getchar();
}
return x;
}
const int INF = 0x3f3f3f3f;
const int N = 1e5 + 10, M = 5e5 + 10;
int n, m;
vector<int> G[N], E[N];
int rudu[N];
int d[N], tot;
queue<int> q;
int f[N], g[N];
multiset<int> st;
int solve() {
n = read(), m = read();
for (int i = 1; i <= m; i++) {
int x = read(), y = read();
G[x].push_back(y);
E[y].push_back(x);
rudu[y]++;
}
for (int i = 1; i <= n; i++) {
if (rudu[i] == 0) q.push(i);
}
tot = 0;
while (!q.empty()) {
int x = q.front(); q.pop();
d[++tot] = x;
for (int y : G[x]) {
rudu[y]--;
if (rudu[y] == 0) {
q.push(y);
}
}
}
for (int i = 1; i <= n; i++) f[i] = g[i] = 0;
for (int i = 1; i <= tot; i++) {
int x = d[i];
for (int y : G[x]) {
f[y] = max(f[y], f[x] + 1);
}
}
for (int i = tot; i >= 1; i--) {
int x = d[i];
for (int y : E[x]) {
g[y] = max(g[y], g[x] + 1);
}
}
st.clear();
for (int i = 1; i <= n; i++) {
st.insert(g[i]);
}
st.insert(0);
int id = -1, ans = INF;
for (int i = 1; i <= n; i++) {
int x = d[i];
st.erase(st.find(g[x]));
for (int y : E[x]) {
st.erase(st.find(f[y] + g[x] + 1));
}
int num = *st.rbegin();
if (num < ans) {
ans = num;
id = x;
} else if (num == ans && x < id) {
id = x;
}
st.insert(f[x]);
for (int y : G[x]) {
st.insert(f[x] + g[y] + 1);
}
}
printf("%d %d\n", id, ans);
for (int i = 1; i <= n; i++) {
G[i].clear();
E[i].clear();
rudu[i] = 0;
}
return 0;
}
int main() {
int qq = read();
while (qq--) {
solve();
}
return 0;
}
可删堆版代码(100 AC):
#include <bits/stdc++.h>
using namespace std;
int read() {
int x = 0; char ch = getchar();
while (ch < '0' || ch > '9') ch = getchar();
while (ch >= '0' && ch <= '9') {
x = (x << 1) + (x << 3) + (ch ^ 48);
ch = getchar();
}
return x;
}
const int INF = 0x3f3f3f3f;
const int N = 1e5 + 10, M = 5e5 + 10;
int n, m;
vector<int> G[N], E[N];
int rudu[N];
int d[N], tot;
queue<int> q;
int f[N], g[N];
priority_queue<int> st, tmp;
inline void ist(int x) {
st.push(x);
}
inline void del(int x) {
tmp.push(x);
}
inline int query() {
while (!tmp.empty() && st.top() == tmp.top()) {
st.pop();
tmp.pop();
}
return st.top();
}
int solve() {
n = read(), m = read();
for (int i = 1; i <= m; i++) {
int x = read(), y = read();
G[x].push_back(y);
E[y].push_back(x);
rudu[y]++;
}
for (int i = 1; i <= n; i++) {
if (rudu[i] == 0) q.push(i);
}
tot = 0;
while (!q.empty()) {
int x = q.front(); q.pop();
d[++tot] = x;
for (int y : G[x]) {
rudu[y]--;
if (rudu[y] == 0) {
q.push(y);
}
}
}
for (int i = 1; i <= n; i++) f[i] = g[i] = 0;
for (int i = 1; i <= tot; i++) {
int x = d[i];
for (int y : G[x]) {
f[y] = max(f[y], f[x] + 1);
}
}
for (int i = tot; i >= 1; i--) {
int x = d[i];
for (int y : E[x]) {
g[y] = max(g[y], g[x] + 1);
}
}
while (!st.empty()) st.pop();
while (!tmp.empty()) tmp.pop();
for (int i = 1; i <= n; i++) {
ist(g[i]);
}
ist(0);
int id = -1, ans = INF;
for (int i = 1; i <= n; i++) {
int x = d[i];
del(g[x]);
for (int y : E[x]) {
del(f[y] + g[x] + 1);
}
int num = query();
if (num < ans) {
ans = num;
id = x;
} else if (num == ans && x < id) {
id = x;
}
ist(f[x]);
for (int y : G[x]) {
ist(f[x] + g[y] + 1);
}
}
printf("%d %d\n", id, ans);
for (int i = 1; i <= n; i++) {
G[i].clear();
E[i].clear();
rudu[i] = 0;
}
return 0;
}
int main() {
int qq = read();
while (qq--) {
solve();
}
return 0;
}
T2 贝尔数
50pts
按题目给的转移式子写就行了。
100pts
我们发现题目给的模数 \(95041567=31\times37\times41\times43\times47\),然后我们就可以先分别以这五个小质数为模数,利用题目给的第二个公式搞出来其对应的 \(Bell(N)\) 的值,再将这五个值利用 CRT 合并,得到正确的(以 \(95041567\) 为模数)的 \(Bell(N)\) 的值,即为答案。
问题在于如何以小质数为模数求值。设模数为 \(p\),根据题目所给的同余式子,我们可以建立一个 \(p\) 行 \(1\) 列的矩阵然后进行矩阵快速幂转移即可。
矩阵转移如下:
此处附上题目给的同余式子(第二个式子):
#include <bits/stdc++.h>
#define int long long
using namespace std;
int read() {
int x = 0; char ch = getchar();
while (ch < '0' || ch > '9') ch = getchar();
while (ch >= '0' && ch <= '9') {
x = (x << 1) + (x << 3) + (ch ^ 48);
ch = getchar();
}
return x;
}
const int Mod = 95041567;
const int N = 50;
int n;
int C[N][N], f[N];
int MOD;
struct node {
int m[N][N];
node() {
for (int i = 0; i < N; i++)
for (int j = 0; j < N; j++) m[i][j] = 0;
for (int i = 1; i < N; i++) {
m[i][i] = 1;
}
}
node operator * (const node & t) const {
node r;
for (int i = 1; i < N; i++)
for (int j = 1; j < N; j++) r.m[i][j] = 0;
for (int i = 1; i < N; i++) {
for (int k = 1; k < N; k++) {
for (int j = 1; j < N; j++) {
r.m[i][j] = (r.m[i][j] + m[i][k] * t.m[k][j] % MOD) % MOD;
}
}
}
return r;
}
};
node fpow(node a, int x) {
node ans;
while (x) {
if (x & 1) ans = ans * a;
a = a * a;
x >>= 1;
}
return ans;
}
int solve(int mod) {
MOD = mod;
node tmp;
for (int i = 1; i < mod; i++) {
tmp.m[i][i] = tmp.m[i][i + 1] = 1;
}
tmp.m[mod][1] = tmp.m[mod][2] = tmp.m[mod][mod] = 1;
node a;
for (int i = 1; i <= mod; i++) {
a.m[i][1] = f[i] % mod;
}
a = fpow(tmp, (n - 1) / mod) * a;
return a.m[(n - 1) % mod + 1][1];
}
int m[10] = {0, 31, 37, 41, 43, 47}, r[10];
int qpow(int a, int x, int MOD) {
int ans = 1;
while (x) {
if (x & 1) ans = ans * a % MOD;
a = a * a % MOD;
x >>= 1;
}
return ans;
}
inline int ny(int x, int MOD) {
return qpow(x, MOD - 2, MOD);
}
int CRT() {
int ans = 0;
for (int i = 1; i <= 5; i++) {
int c = Mod / m[i];
ans = (ans + r[i] * c % Mod * ny(c, m[i]) % Mod) % Mod;
}
return ans;
}
signed main() {
for (int i = 0; i < N; i++) {
C[i][0] = 1;
for (int j = 1; j <= i; j++) {
C[i][j] = (C[i - 1][j] + C[i - 1][j - 1]) % Mod;
}
}
f[0] = 1;
for (int i = 1; i < N; i++) {
for (int j = 0; j < i; j++) {
f[i] = (f[i] + f[j] * C[i - 1][j] % Mod) % Mod;
}
}
int qq = read();
while (qq--) {
n = read();
for (int i = 1; i <= 5; i++) {
r[i] = solve(m[i]);
}
printf("%lld\n", CRT());
}
return 0;
}
T3 穿越广场
我们对于这两个字符串建立 AC 自动机。
设 \(f_{i,j,k,s}\) 表示当前长度为 \(i\),其中有 \(j\) 个 R,走到了 AC 自动机上的 \(k\) 节点,包含字符串的状态为 \(s\)(\(0/1/2/3\),两个串是否被包含)。
边界:\(f_{0,0,1,0}=1\)
转移:
答案即为 \(\sum{f_{n+m,m,k,3}}\)。
#include <bits/stdc++.h>
using namespace std;
const int MOD = 1e9 + 7;
const int N = 205;
int n, m;
char s1[N], s2[N];
int trie[2 * N][2], tot, fail[2 * N], flag[2 * N];
void ist(char s[], int id) {
int u = 1, len = strlen(s);
for (int i = 0; i < len; i++) {
int v = (s[i] == 'R');
if (!trie[u][v]) trie[u][v] = ++tot;
u = trie[u][v];
}
flag[u] |= (1 << id - 1);
}
queue<int> q;
void getfail() {
for (int i = 0; i < 2; i++) trie[0][i] = 1;
q.push(1);
fail[1] = 0;
while (!q.empty()) {
int u = q.front();
q.pop();
for (int i = 0; i < 2; i++) {
if (!trie[u][i]) {
trie[u][i] = trie[fail[u]][i];
continue;
}
fail[trie[u][i]] = trie[fail[u]][i];
q.push(trie[u][i]);
}
}
}
int f[N][N][N][4];
int solve() {
scanf("%d%d%s%s", &n, &m, s1, s2);
tot = 1;
for (int i = 0; i < N; i++) {
trie[i][0] = trie[i][1] = fail[i] = flag[i] = 0;
}
ist(s1, 1), ist(s2, 2);
getfail();
for (int i = 1; i <= tot; i++) {
for (int j = i; j; j = fail[j]) flag[i] |= flag[j];
}
for (int i = 0; i <= n + m; i++)
for (int j = 0; j <= m; j++)
for (int k = 0; k <= tot; k++)
for (int st = 0; st < 4; st++) f[i][j][k][st] = 0;
f[0][0][1][0] = 1;
for (int i = 0; i <= n + m; i++) {
for (int j = 0; j <= m; j++) {
for (int k = 1; k <= tot; k++) {
for (int st = 0; st < 4; st++) {
(f[i + 1][j + 1][trie[k][0]][st | flag[trie[k][0]]] += f[i][j][k][st]) %= MOD;
(f[i + 1][j][trie[k][1]][st | flag[trie[k][1]]] += f[i][j][k][st]) %= MOD;
}
}
}
}
int ans = 0;
for (int i = 1; i <= tot; i++) {
(ans += f[n + m][m][i][3]) %= MOD;
}
printf("%d\n", ans);
return 0;
}
int main() {
int qq;
scanf("%d", &qq);
while (qq--) {
solve();
}
return 0;
}
T4 欢乐豆
就是求一个多源最短路。
考虑使用对每个点跑单源最短路,dijkstra。
首先明确 dijkstra 的流程,每一次找到 dis 最小的那个点,然后用这个点去松弛与其相连接的点。朴素 dijkstra 时间复杂度为 \(O(n^2)\)。通常我们考虑用小根堆来优化寻找 dis 最小点的过程,时间复杂度为 \(O(m\log n)\)。这个复杂度在稀疏图中是十分优秀的,然而对于本题的完全图并不适用。
考虑使用线段树优化 dijkstra。用一棵线段树维护 dis 数组,维护区间最小值以及最小值对应的 id。并且考虑优化松弛的过程。我们先假设没有修改操作(即从点 \(u\) 出发的边的边权均为 \(a_u\)),那么直接区间更新最小值(\(dis_u+a_u\))即可。那么修改操作如何处理呢?注意到最多只有 \(3000\) 个修改操作,那么我们就可以将 \(x=u\) 的修改操作(\(x,y,z\))一一处理。具体的,可以在区间取最小值之前记录原数据,然后在取完最小值后再单独处理即可(拿 \(dis_u+z\) 更新 \(dis_y\))。
还有一个小细节,就是由于这里并没有涉及 vis 数组,所以要在实现线段树的时候注意一些。我这边提供一种方式:处理完当先的节点 \(u\) 后,将线段树上 \(dis_u\) 赋为极小值,然后在 pushup 时特判即可。
#include <bits/stdc++.h>
using namespace std;
int read() {
int x = 0; char ch = getchar();
while (ch < '0' || ch > '9') ch = getchar();
while (ch >= '0' && ch <= '9') {
x = (x << 1) + (x << 3) + (ch ^ 48);
ch = getchar();
}
return x;
}
const int INF = 0x3f3f3f3f;
const int N = 1e5 + 10, M = 6005;
int n, m, a[N];
int mark[N];
struct edge {
int y, w;
};
vector<edge> G[N];
map<pair<int, int>, int> mp;
int id[N], tot, rk[N];
int xx[N], yy[N], zz[N];
#define min(x, y) (x < y ? x : y)
struct Tree {
int l, r, mn, id, lazy;
} tree[4 * N];
#define lc p << 1
#define rc p << 1 | 1
int dis[N], vis[N], val[N];
int ll, rr;
void pushup(int p) {
if (tree[lc].mn < 0 && tree[rc].mn < 0) {
tree[p].mn = tree[lc].mn;
tree[p].id = tree[lc].id;
return;
}
ll = tree[lc].mn >= 0 ? tree[lc].mn : INF;
rr = tree[rc].mn >= 0 ? tree[rc].mn : INF;
if (ll < rr && (tree[lc].mn >= 0 || tree[rc].mn < 0)) {
tree[p].mn = ll;
tree[p].id = tree[lc].id;
} else {
tree[p].mn = rr;
tree[p].id = tree[rc].id;
}
}
void build(int p, int l, int r) {
tree[p].l = l, tree[p].r = r;
tree[p].mn = tree[p].lazy = INF;
if (l == r) {
tree[p].id = l;
return;
}
int mid = l + r >> 1;
build(lc, l, mid), build(rc, mid + 1, r);
pushup(p);
}
void pushdown(int p) {
if (tree[p].lazy == INF) return;
tree[lc].lazy = min(tree[lc].lazy, tree[p].lazy);
tree[rc].lazy = min(tree[rc].lazy, tree[p].lazy);
tree[lc].mn = min(tree[lc].mn, tree[p].lazy);
tree[rc].mn = min(tree[rc].mn, tree[p].lazy);
tree[p].lazy = INF;
}
void update(int p, int l, int r, int v) {
if (l == tree[p].l && tree[p].r == r) {
tree[p].mn = min(tree[p].mn, v);
tree[p].lazy = min(tree[p].lazy, v);
return;
}
pushdown(p);
int mid = tree[p].l + tree[p].r >> 1;
if (r <= mid) update(lc, l, r, v);
else if (l > mid) update(rc, l, r, v);
else update(lc, l, mid, v), update(rc, mid + 1, r, v);
pushup(p);
}
void change(int p, int x, int v) {
if (tree[p].l == tree[p].r) {
tree[p].mn = v;
return;
}
pushdown(p);
int mid = tree[p].l + tree[p].r >> 1;
if (x <= mid) change(lc, x, v);
else change(rc, x, v);
pushup(p);
}
int query(int p, int x) {
if (tree[p].l == tree[p].r) return tree[p].mn;
pushdown(p);
int mid = tree[p].l + tree[p].r >> 1;
if (x <= mid) return query(lc, x);
else return query(rc, x);
}
void solve(int st) {
for (int i = 1; i <= tot; i++) dis[i] = INF, vis[i] = 0;
build(1, 1, tot);
dis[st] = 0;
change(1, st, 0);
for (int k = 1; k <= tot; k++) {
int x = tree[1].id;
for (edge i : G[x]) {
val[i.y] = query(1, i.y);
}
dis[x] = query(1, x);
update(1, 1, tot, dis[x] + a[id[x]]);
for (edge i : G[x]) {
change(1, i.y, min(val[i.y], dis[x] + i.w));
}
change(1, x, -INF);
}
}
signed main() {
n = read(), m = read();
for (int i = 1; i <= n; i++) {
a[i] = read();
}
for (int i = 1; i <= m; i++) {
int x = read(), y = read(), z = read();
mark[x] |= 1;
mark[y] |= 2;
mp[{x, y}] = 1;
xx[i] = x, yy[i] = y, zz[i] = z;
}
int mn = INF, mnid = -1;
for (int i = 1; i <= n; i++) {
if (mark[i]) id[++tot] = i;
else if (a[i] < mn) {
mn = a[i];
mnid = i;
}
}
if (mnid != -1) {
mark[mnid] |= 2;
id[++tot] = mnid;
}
for (int i = 1; i <= tot; i++) {
rk[id[i]] = i;
}
for (int i = 1; i <= m; i++) {
G[rk[xx[i]]].push_back({rk[yy[i]], zz[i]});
}
long long ans = 0;
for (int i = 1; i <= n; i++) {
if (mark[i] & 1) {
solve(rk[i]);
int mn = INF;
for (int j = 1; j <= tot; j++) {
ans += dis[j];
mn = min(mn, dis[j] + a[id[j]]);
}
ans += 1ll * mn * (n - tot);
} else {
ans += 1ll * a[i] * (n - 1);
}
}
printf("%lld\n", ans);
return 0;
}

浙公网安备 33010602011771号