历年 CSP-S 题目总结
历年 CSP-S 题目总结
[CSP-S 2019] 括号树
Description
给定一棵大小为 \(n\) 的树,其中每个节点分配有一个括号,可能为左括号或右括号。
定义字符串 \(s_i\) 为:将根节点到 \(i\) 号节点的简单路径上的括号,按结点经过顺序依次排列依次排列组成的字符串。
试对所有的 \(i\ (i \in [1,n])\) 求出,\(s_i\) 中有多少个互不相同的子串(连续)是合法括号串。
设 \(s_i\) 对应的答案是 \(A_i\),只需输出 \(\bigoplus\limits_{i=1}^n i \times A_i\) 即可。
\(1 \le n \le 5 \times 10^5\)。
Solution
此题显然需要树上 DP。子串不太好处理,所以把贡献挂到结尾再累加答案,会简化问题。
多数括号串的题,经常把括号从前向后扔到一个栈里,然后在栈里判断合法性。
定义 \(dp_u\) 表示以 \(u\) 号节点结尾的合法括号串数。从根节点开始 DFS 一遍,分情况讨论:
- 当 \(a_u\) 为左括号时,将其入栈,\(dp_u = 0\);
- 当 \(a_u\) 为右括号时,
- 若栈为空,则 \(dp_u = 0\);
- 否则,取栈顶之元素 \(t\),有 \(dp_u = dp_{fa(t)} + 1\)。此时更新答案,\(A_u = dp_u + A_{fa(u)}\),并将栈顶弹出。
Code
#include <bits/stdc++.h>
#define int long long
using namespace std;
constexpr int N = 5e5 + 5;
int n, f[N], ans[N], fa[N], yuki;
vector<int> g[N];
stack<int> st;
char a[N];
void DFS(int u) {
if (a[u] == '(') {
ans[u] = ans[fa[u]];
st.push(u);
for (auto v : g[u]) DFS(v);
st.pop();
} else if (a[u] == ')' && !st.empty()) {
int temp = st.top();
f[u] = f[fa[temp]] + 1;
ans[u] = f[u] + ans[fa[u]];
st.pop();
for (auto v : g[u]) DFS(v);
st.push(temp);
} else {
ans[u] = ans[fa[u]];
for (auto v : g[u]) DFS(v);
}
}
signed main() {
cin.tie(0)->sync_with_stdio(0);
cin >> n >> (a + 1);
for (int u = 2; u <= n; u++) {
cin >> fa[u];
g[fa[u]].push_back(u);
}
DFS(1);
for (int i = 1; i <= n; i++) {
yuki ^= ans[i] * i;
}
cout << yuki;
return 0;
}
[CSP-S 2019] Emiya 家今天的饭
Description
给定一个 \(n \times m\) 的矩阵 \(a\),每行至多选一个数,每列选数的个数不能超过所选数字个数总数的一半,不能一个都不选,给定每个节点的选择方案数,求选数的总方案数对 \(998244353\) 取模的结果。
\(1 \le n \le 100, 1 \le m \le 2000, 1 \le a_{i,j} \le 998244353\)。
Solution
DP 计数、正难则反好题。第一步转化想到了之后,84 pts 递推是简单的,而优化需要挖掘一个守恒关系,需要细致观察。
我断言,如果某一种方案不合法,那么必定有且仅有一列的选数个数超过数字总数的一半(在 CSP-S 2025 T1 中也出现了,令人感慨)。证明显然。
在这个限制的约束下,直接维护选数集合是困难的,因此考虑正难则反。如此,合法方案总数等于无约束的总方案数(记为 \(W_1\))减去存在一列选了超过一半的方案数(记为 \(W_2\))。
接下来考虑 \(W_1\)。定义 \(f_{i,j}\) 表示无约束下,前 \(i\) 行共选了 \(j\) 个数的总方案数。
如果第 \(i\) 行不选,那么所选数字的个数不变,即
如果选,那么可以选这一行的任意一个数,则
不妨维护第 \(i\) 列的前缀和 \(s_i\),综上有
接下来考虑 \(W_2\)。定义 \(g_{i,j,k}\) 表示前 \(i\) 行,在枚举到的某列中选了 \(j\) 个数,而在其他列选了 \(k\) 个数的总方案数。
设当前枚举的是第 \(l\) 列。
对于第 \(i\) 行的数,若不选,则
如果所选的数位于第 \(l\) 列,那么
否则
综上
当且仅当 \(j>k\) 时,\(g_{n,j,k}\) 不合法。所以
至此有 84 pts。我们考虑优化 \(g\) 的转移。
注意到方案的合法性与 \(j,k\) 的具体取值无关,而与 \(j-k\) 有关。
只需维护 \(d = j-k\) 即可。可以砍掉 \(g\) 的一维。\(d\) 可能为负数,需要加上偏移量。
Code
#include <bits/stdc++.h>
#define int long long
using namespace std;
constexpr int N = 105, M = 2005, mod = 998244353;
int n, m, a[N][M], s[N], f[N][M], g[N][2 * M], ans;
signed main() {
cin.tie(0)->sync_with_stdio(0);
cin >> n >> m;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
cin >> a[i][j];
s[i] = (s[i] + a[i][j]) % mod;
}
}
f[0][0] = 1;
for (int i = 1; i <= n; i++) {
for (int j = 0; j <= i; j++) {
if (j == 0) {
f[i][j] = f[i - 1][j] % mod;
} else {
f[i][j] = (f[i - 1][j] % mod + f[i - 1][j - 1] * s[i] % mod) % mod;
}
}
}
for (int i = 1; i <= n; i++) {
ans = (ans + f[n][i]) % mod;
}
g[0][n] = 1;
for (int l = 1; l <= m; l++) {
for (int i = 1; i <= n; i++) {
for (int d = n - i; d <= n + i; d++) {
if (d == 0) {
g[i][d] = (g[i - 1][d] % mod + g[i - 1][d + 1] * (s[i] - a[i][l]) % mod) % mod;
} else {
g[i][d] = (g[i - 1][d] % mod + g[i - 1][d - 1] * a[i][l] % mod + g[i - 1][d + 1] * (s[i] - a[i][l]) % mod) % mod;
}
}
}
for (int j = 1; j <= n; j++) {
ans = (ans - g[n][n + j] + mod) % mod;
}
}
cout << ans % mod;
return 0;
}
[CSP-S 2020] 函数调用
Description
给定长度为 \(n\) 的数组 \(a\)。现有 \(m\) 个函数,第 \(i\) 个函数的类型为 \(t_i\)。
-
当 \(t_i = 1\) 时,给定参数 \(p_i, v_i\),表示执行 \(a_{p_i} \leftarrow a_{p_i} + v_i\)。
-
当 \(t_i = 2\) 时,给定参数 \(v_i\),表示对 \(\forall j \in [1,n]\),执行 \(a_j \leftarrow a_j \times v_i\)。
-
当 \(t_i = 3\) 时,给定长度为 \(c_i\) 的列表 \(g_i\),表示从前往后依次调用列表中的各个函数,保证函数调用关系中不会出现递归,其中 \(g_i\) 的第 \(j\) 项表示其所依次调用的函数编号。
同时,给定长度为 \(q\) 的操作序列 \(f\)。现从前往后依次执行操作序列中的函数,求操作结束后数组中每个元素的值对 \(998244353\) 取模的结果。
\(1 \le n,m,q \le 10^5, 1 \le \sum c_i \le 10^6\)。
Description
这种操作类问题,一般需要假设某一操作不存在,然后再考虑这个操作存在的情况怎么做。
拿到题后很容易想到建图的操作,但先需要想到第一步建虚源点的转化。然后,分别考虑某个操作不存在的情况下怎么做,又发现这些子情况是可合并的,就做出正解了。
首先发现操作序列毫无用处,因为总可以建一个虚源点 \(0\) 连向操作序列中的所有点,并令函数 \(0\) 不起任何作用,执行一次函数 \(0\) 是等价的。
考虑“依次执行”的约束如何解决。发现加边的过程有顺序性,因此直接按照题目中给定的顺序加边,遍历整个图时按照加边的顺序遍历即可实现“依次”。
由于不会出现递归,因此整个图不会出现环,即原图是一个 DAG。
然后套路性的假设。操作 1 不存在时,发现乘法的执行顺序毫无用处,其因数是可直接合并的,DAG 上跑一圈,维护某点的乘法积累量即可。操作 2 不存在时仍然 DAG 上跑一圈即可,甚至不需要记录信息。
操作 3 不存在时,需要仔细思考。假设目前的操作序列是 \([+1, \times 2, +3, \times 4]\),我们发现第一个 \(+1\) 会受到后面的两个 \(\times 3, \times 4\) 的影响而变为 \(+12\);第三个 \(+3\) 只会受到 \(\times 4\) 的影响而变为 \(\times 12\)。因此对于一个加法操作,它只会受到在它之后执行的乘法操作的影响,扩大的倍率为所有乘法操作因子的乘积。因此倒着遍历一圈记录后缀积即可。
考虑所有操作共同存在时怎么做。根据我们的关键推论,不妨对每个点维护一个乘法积累量 \(mul\),表示执行一次该操作会给原数组扩大多少倍,这显然是可并的,在原 DAG 的反图上跑一圈即可。
考虑乘法会对加法造成什么影响。对于一个加法操作 \(v\),假设当前是从 \(u\) 遍历到 \(v\) 的,那么维护 \(dp_v\) 表示 \(v\) 操作在受到乘法操作的影响下,被等价执行的次数。考虑到 \(v\) 节点的后缀兄弟积影响了函数 \(dp_u\) 等价的调用次数,因此 \(dp_v \leftarrow dp_v + dp_u \times P\),其中 \(P = \prod\limits_{v' 是 v 的后缀兄弟} dp_{v'}\)。
然后先对数组 \(a\) 全局乘上 \(mul_0\),再按照 \(dp\) 值更新每个位置的加权即可。
Code
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N = 2e5 + 5, mod = 998244353;
int n, m, q, a[N], typ[N], idx[N], val[N];
int mul[N], dp[N];
int inDeg[N], inDegRev[N];
vector<int> g[N], gRev[N];
void TopoSort1() {
queue<int> q;
for (int i = 0; i <= m; i++) {
if (inDegRev[i] == 0) q.push(i);
}
while (not q.empty()) {
int u = q.front(); q.pop();
for (int i = 0; i < (int)gRev[u].size(); i++) {
int v = gRev[u][i];
mul[v] = mul[v] * mul[u] % mod;
inDegRev[v]--;
if (inDegRev[v] == 0) q.push(v);
}
}
}
void TopoSort2() {
queue<int> q;
for (int i = 0; i <= m; i++) {
if (inDeg[i] == 0) q.push(i);
}
while (not q.empty()) {
int u = q.front(); q.pop();
int prod = 1;
for (int i = (int)g[u].size() - 1; i >= 0; i--) {
int v = g[u][i];
dp[v] = (dp[v] + dp[u] * prod % mod) % mod;
prod = prod * mul[v] % mod;
inDeg[v]--;
if (inDeg[v] == 0) q.push(v);
}
}
}
signed main() {
cin.tie(0) -> sync_with_stdio(0);
cin >> n;
for (int i = 1; i <= n; i++) {
cin >> a[i];
}
cin >> m;
mul[0] = 1;
for (int i = 1; i <= m; i++) {
mul[i] = 1;
cin >> typ[i];
if (typ[i] == 1) {
cin >> idx[i] >> val[i];
} else if (typ[i] == 2) {
cin >> mul[i];
} else {
int c; cin >> c;
for (int j = 1; j <= c; j++) {
int x; cin >> x;
inDeg[x]++;
inDegRev[i]++;
g[i].push_back(x);
gRev[x].push_back(i);
}
}
}
cin >> q;
for (int i = 1; i <= q; i++) {
int f; cin >> f;
inDeg[f]++;
inDegRev[0]++;
g[0].push_back(f);
gRev[f].push_back(0);
}
dp[0] = 1;
TopoSort1();
TopoSort2();
for (int i = 1; i <= n; i++) {
a[i] = a[i] * mul[0] % mod;
}
for (int i = 1; i <= m; i++) {
if (typ[i] == 1) {
a[idx[i]] = (a[idx[i]] + dp[i] * val[i] % mod) % mod;
}
}
for (int i = 1; i <= n; i++) {
cout << a[i] << ' ';
}
return 0;
}
[CSP-S 2021] 廊桥分配
Description
有 \(n\) 个廊桥需要分配给国内航班和国际航班使用。国内有 \(m_1\) 个航班,国际有 \(m_2\) 个航班,每个航班有到达时间和离开时间(所有时间点均互不相同)。一个廊桥在同一时间只能服务一个航班,且服务完一个航班后,只有在该航班离开后,才能服务下一个到达时间不早于其离开时间的航班。现在需要决定国内和国际各分配多少个廊桥,使得两个机场能够服务的航班总数最大。求这个最大值。
\(1 \le n, m_1+m_2 \le 10^5\)。
Solution
注意到国内航班和国际航班互不影响,因此可以对国内和国际分别预处理出“分配 \(i\) 个廊桥时最多能服务多少航班”,最后枚举分配方案合并答案。
对于一个机场,假设分配了 \(k\) 个廊桥。为了充分利用每一个廊桥,考虑总是优先服务当前剩余航班中到达时间最早的那个,然后对于同一个廊桥,继续寻找到达时间不早于当前航班离开时间的、到达时间最早的航班。这样可以使得每个廊桥尽可能多地服务航班。定义 \(ans_i\) 表示分配 \(i\) 个廊桥时总共能服务的航班数,由于 \(i\) 的答案一定由 \(i-1\) 的答案扩展而来,因此考虑一个增量过程:\(ans_i = ans_{i-1} + New\),每次用 set 二分找到合法的飞机,并将其删除,求出 \(New\) 即可。时间复杂度是线性带一个小 \(\log\)。
Code
#include <bits/stdc++.h>
#define int long long
using namespace std;
constexpr int N = 1e5 + 5;
int n, m1, m2, ans1[N], ans2[N], res;
struct Node {
int a, b;
friend bool operator<(Node x, Node y) {
return x.a < y.a;
}
};
set<Node> dom, inter;
void solve(set<Node>& s, int* ans) {
for (int i = 1; i <= n; i++) {
vector<Node> temp;
ans[i] = ans[i - 1];
for (auto it = s.begin(); it != s.end(); it = s.lower_bound({it->b, -1})) {
ans[i]++;
temp.push_back(*it);
}
for (auto x : temp) {
s.erase(x);
}
}
}
signed main() {
cin.tie(0)->sync_with_stdio(0);
cin >> n >> m1 >> m2;
for (int i = 1; i <= m1; i++) {
int a, b;
cin >> a >> b;
dom.insert({a, b});
}
for (int i = 1; i <= m2; i++) {
int a, b;
cin >> a >> b;
inter.insert({a, b});
}
solve(dom, ans1);
solve(inter, ans2);
for (int i = 0; i <= n; i++) {
res = max(res, ans1[i] + ans2[n - i]);
}
cout << res;
return 0;
}
[CSP-S 2021] 括号序列
Description
给定一个长度为 \(n\) 的字符串 \(s\),仅由 (、)、*、? 四种字符组成。其中 ? 可以替换为任意上述三类字符。定义合法括号序列的规则为:
()合法;- 若
A合法,则(A)、(AS)、(SA)均合法(其中S表示由不超过 \(k\) 个*组成的字符串,可以为空); - 若
A、B合法,则AB合法; - 若
A、B合法,S合法,则ASB合法。
求将所有 ? 替换后,能得到的不同合法括号序列的方案数对 \(10^9+7\) 取模的结果。
\(1 \le n \le 500,\ 0 \le k \le n\)。
Solution
朴素地想,令 \(dp_{l,r}\) 表示区间 \([l,r]\) 的合法串数。转移时需要枚举断点或者套上外层括号,但题目中的合法串规则比较复杂,直接写会面临两个严峻的重复计数问题。
A-S-B 的拆分方式不唯一。考虑 A-S-B,既可以把它看成 A-S 与 B 合并,也可以看成 A 与 S-B 合并。若两种转移都写进 \(dp_{l,r}\),同一个串会被统计两次。
合并的先后顺序不唯一。即便只选择一种拆分方式,例如只按照 A 与 S-B 合并,也会出问题:(A)-S-B 可以先合并 (A)-B 再与 S 合并,也可以先合并 S-B 再与 (A) 合并。同样造成重复。
因此必须对合并方式约束。我们人为地规定如下约束:
- 一个“没有经历过合并”的合法串,一定形如
(....),即最外层括号严格匹配; - 一个“经历过合并”的合法串,一定形如
(..)...(..),即左右两端括号不严格匹配。
于是设
- \(dp_{l,r,0}\):区间 \([l,r]\) 内“未经历合并”的合法串数;
- \(dp_{l,r,1}\):区间 \([l,r]\) 内“经历过合并”的合法串数。
同时为了处理 \(S\),设
- \(f_{l,r}\):区间 \([l,r]\) 内形如 \(A\text{-}S\) 的方案数;
- \(g_{l,r}\):区间 \([l,r]\) 内形如 \(S\text{-}B\) 的方案数。
初始化时直接判断两端为 ( 和 )、中间全为 * 且长度不超过 \(k\) 的情况,这些就是最内层的 ()、(*)、(**) 等,令 \(dp_{l,r,0}=1\)。
按区间长度递增转移,设当前区间为 \([l,r]\):
求 \(f_{l,r},g_{l,r}\):枚举星号段的边界。\(g_{l,r}\) 是 \(S\text{-}B\),其中 \(S\) 为 \([l,k]\) 全星号且长度不超过 \(k\),\(B\) 为 \([k+1,r]\) 任意合法串
对称地
求 \(dp_{l,r,1}\):为消除重复,规定合并时在左侧取“未合并”的 \(dp_{l,k,0}\),右侧可以是 \(S\text{-}B\) 或任意合法串,即
这样每种合并方案都只有唯一的一种规范形式,不会重复。
求 \(dp_{l,r,0}\):只有当 \(s_l\) 可为 (、\(s_r\) 可为 ) 时才有贡献。内部可以是任意合法串、\(A\text{-}S\) 或 \(S\text{-}A\):
答案为
时间复杂度 \(O(n^3)\)。
Code
#include <bits/stdc++.h>
#define int long long
using namespace std;
constexpr int N = 505, mod = 1e9 + 7;
string s;
int dp[N][N][2], f[N][N], g[N][N], n, lmt, cnt;
signed main() {
cin.tie(0)->sync_with_stdio(0);
cin >> n >> lmt >> s;
s = ' ' + s;
for (int l = 1; l <= n; l++) {
cnt = 0;
if (s[l] == '(' || s[l] == '?') {
for (int r = l + 1; r <= n; r++) {
if (s[r] == ')' || s[r] == '?') {
dp[l][r][0] = 1;
}
if (s[r] != '*' && s[r] != '?') break;
if (++cnt > lmt) break;
}
}
}
for (int len = 2; len <= n; len++) {
for (int l = 1; l + len - 1 <= n; l++) {
int r = l + len - 1;
cnt = 0;
for (int k = l; k < r; k++) {
if (s[k] != '*' && s[k] != '?') break;
if (++cnt > lmt) break;
g[l][r] = (g[l][r] % mod + (dp[k + 1][r][0] + dp[k + 1][r][1]) % mod) % mod;
}
cnt = 0;
for (int k = r; k > l; k--) {
if (s[k] != '*' && s[k] != '?') break;
if (++cnt > lmt) break;
f[l][r] = (f[l][r] % mod + (dp[l][k - 1][0] + dp[l][k - 1][1]) % mod) % mod;
}
for (int k = l; k < r; k++) {
dp[l][r][1] = (dp[l][r][1] + dp[l][k][0] * (g[k + 1][r] + dp[k + 1][r][0] + dp[k + 1][r][1])) % mod;
}
if ((s[l] == '(' || s[l] == '?') && (s[r] == ')' || s[r] == '?')) {
dp[l][r][0] = (dp[l][r][0] + (dp[l + 1][r - 1][0] + dp[l + 1][r - 1][1] + f[l + 1][r - 1] + g[l + 1][r - 1]) % mod) % mod;
}
}
}
cout << (dp[1][n][0] + dp[1][n][1]) % mod;
return 0;
}
[CSP-S 2022] 假期计划
Description
给定一张 \(n\) 个点、\(m\) 条边的无向图,每个点 \((2 \sim n)\) 有一个权值 \(w_i\)。要求找出一条经过 \(5\) 个不同景点 \(1, a, b, c, d\) 然后再回到 \(1\) 的路线,且路线上相邻两景点之间的最短路径长度(即边数)均不超过 \(k\)。求 \(w_a + w_b + w_c + w_d\) 的最大值。保证有解。
\(5 \le n \le 2500,\ 5 \le m \le 10^4,\ 1 \le k \le 100\)。
Solution
这是 T1?
由于 \(k\) 的限制是“相邻两个景点之间边数不超过 \(k\)”,因此只需要知道哪些点对之间可以在不超过 \(k\) 条边的路径内到达。对每个点跑一遍 BFS,用 \(ok_{u,v}\) 表示 \(u\)、\(v\) 之间是否存在一条长度(边数)不超过 \(k\) 的路径,复杂度 \(O(n(n+m))\)。
接下来考虑环 \(1-a-b-c-d-1\)。如果直接枚举四个点,复杂度不可接受。观察一条这样的环:
- 若钦定了 \(a-b-c\),那么 \(d\) 是唯一的:\(d\) 一定是所有“一步到达 \(c\)、又能一步到达 \(1\)、且不与 \(a,b,c,1\) 重合”的点中权值最大的那个;
- 若钦定了 \(b-c-d\),对称地,\(a\) 同样是唯一的。
于是用 \(s_x\) 表示所有满足 \(ok_{x,u}\) 且 \(ok_{1,u}\) 的点 \(u\) 构成的集合。按照上面的结论,只需枚举 \(b,c\),取 \(a \in s_b\)、\(d \in s_c\),使得 \(a,b,c,d\) 互不相同,则答案为 \(w_a + w_b + w_c + w_d\)。
但这样直接取集合中的最大值会遇到一个问题:\(s_b\) 中的最优点可能与 \(s_c\) 中的最优点,也可能恰好等于它们本身。因此不能只存一个最优点,而要存若干个备选。将 \(s_x\) 升级为一个按权值排序的集合,只保留权值前 \(\text{LIMIT}\) 大的节点(实现时用 set<pair<int,int>>,自动升序,当集合大小超过 \(\text{LIMIT}\) 时删除 begin(),即最小的那个元素)。
枚举 \(b,c\) 时,若 \(ok_{b,c}\) 成立,则在 \(s_b\) 与 \(s_c\) 中取出所有候选 \(a\)、\(d\),跳过 \(a=c\)、\(b=d\)、\(a=d\) 这些冲突情况,用 \(w_a + w_b + w_c + w_d\) 更新答案。由于每个集合只保留少量候选,枚举代价很小。
关于 \(\text{LIMIT}\) 的取值:在一个给定的 \((b,c)\) 下,\(s_b\) 中的点要避开 \(c\) 以及 \(s_c\) 中选出的 \(d\),\(s_c\) 中的点要避开 \(b\) 以及 \(s_b\) 中选出的 \(a\),冲突的点最多只有 \(3\) 个,因此理论上每个集合保留前 \(4\) 大即可保证不漏解,为了稳妥可以适当调大。
时间复杂度 \(O(n(n+m) + n^2 \cdot \text{LIMIT}^2)\)。
Code
#include <bits/stdc++.h>
#define int long long
using namespace std;
constexpr int N = 2505, LIMIT = 4;
int n, m, k, ans, w[N];
bool ok[N][N];
bool vis[N];
int dis[N];
vector<int> g[N];
set<pair<int, int>> s[N];
void BFS(int x) {
memset(dis, 0x3f, sizeof(dis));
memset(vis, 0, sizeof(vis));
queue<int> q;
q.push(x);
dis[x] = 0;
while (!q.empty()) {
int u = q.front();
q.pop();
if (u != x) {
ok[x][u] = 1;
if (x != 1 && ok[1][u]) {
s[x].emplace(w[u], u);
if (s[x].size() > LIMIT) {
s[x].erase(s[x].begin());
}
}
}
if (dis[u] > k) continue;
for (int v : g[u]) {
if (vis[v]) continue;
vis[v] = 1;
dis[v] = dis[u] + 1;
q.push(v);
}
}
}
signed main() {
cin.tie(0)->sync_with_stdio(0);
cin >> n >> m >> k;
for (int i = 2; i <= n; i++) {
cin >> w[i];
}
while (m--) {
int u, v;
cin >> u >> v;
g[u].push_back(v);
g[v].push_back(u);
}
for (int i = 1; i <= n; i++) {
BFS(i);
}
for (int b = 2; b <= n; b++) {
for (int c = 2; c <= n; c++) {
if (!ok[b][c]) continue;
for (auto [wa, a] : s[b]) {
for (auto [wd, d] : s[c]) {
if (a == c || b == d || a == d) continue;
ans = max(ans, w[a] + w[b] + w[c] + w[d]);
}
}
}
}
cout << ans;
return 0;
}
P8818 [CSP-S 2022] 策略游戏
Description
给定长度为 \(n\) 的数组 \(A\) 和长度为 \(m\) 的数组 \(B\),定义矩阵 \(C_{i,j}=A_i\times B_j\)。进行 \(q\) 轮游戏,每轮给出 \(l_1,r_1,l_2,r_2\):小 L 先从 \(A_{l_1\sim r_1}\) 中选一个下标 \(x\),小 Q 再从 \(B_{l_2\sim r_2}\) 中选一个下标 \(y\),本轮得分为 \(C_{x,y}\)。小 L 希望得分尽可能大,小 Q 希望得分尽可能小,两人都采取最优策略,求每轮游戏的最终得分。
\(1\le n,m,q\le 10^5\),\(-10^9\le A_i,B_i\le 10^9\)。
Solution
这是 T2?
由于小 L 先手、小 Q 后手,一旦小 L 选定了 \(x\),小 Q 一定会在 \(B_{l_2\sim r_2}\) 中选使 \(A_x\cdot B_y\) 最小的 \(y\)。因此对给定的 \(x\),小 Q 的返回值只取决于 \(A_x\) 的符号。于是分别讨论 \(A,B\) 中只有正数、只有负数、正负共存的情况即可。需用 ST 表维护 \(A\) 的区间最大值、最小值、非负部分的最小值、负部分的最大值,以及 \(B\) 的区间最大值、最小值、非正部分的最小值、非负部分的最大值。每次查询时,根据 \(A\) 区间的整体符号情况与 \(B\) 区间的符号情况,取出相应的几个候选值计算乘积并取最大即可。
Code
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N = 1e5 + 5, M = 20, inf = 1e18;
int n, m, q;
int a[N], b[N];
int lg[N];
void initLg() {
lg[1] = 0;
for (int i = 2; i <= max(n, m); i++) {
lg[i] = lg[i / 2] + 1;
}
}
struct SparseA {
int aMax[N][M], aMin[N][M], aPosMin[N][M], aNegMax[N][M];
bool aHas0[N][M];
void init() {
for (int i = 1; i <= n; i++) {
aMax[i][0] = a[i];
aMin[i][0] = a[i];
aPosMin[i][0] = (a[i] < 0 ? inf : a[i]);
aNegMax[i][0] = (a[i] > 0 ? -inf : a[i]);
aHas0[i][0] = (a[i] == 0);
}
for (int j = 1; (1 << j) <= n; j++) {
for (int i = 1; i <= n - (1 << j) + 1; i++) {
aMax[i][j] = max(aMax[i][j - 1], aMax[i + (1 << (j - 1))][j - 1]);
aMin[i][j] = min(aMin[i][j - 1], aMin[i + (1 << (j - 1))][j - 1]);
aPosMin[i][j] = min(aPosMin[i][j - 1], aPosMin[i + (1 << (j - 1))][j - 1]);
aNegMax[i][j] = max(aNegMax[i][j - 1], aNegMax[i + (1 << (j - 1))][j - 1]);
aHas0[i][j] = (aHas0[i][j - 1] | aHas0[i + (1 << (j - 1))][j - 1]);
}
}
}
int queryMax(int l, int r) {
int s = lg[r - l + 1];
return max(aMax[l][s], aMax[r - (1 << s) + 1][s]);
}
int queryMin(int l, int r) {
int s = lg[r - l + 1];
return min(aMin[l][s], aMin[r - (1 << s) + 1][s]);
}
int queryPosMin(int l, int r) {
int s = lg[r - l + 1];
return min(aPosMin[l][s], aPosMin[r - (1 << s) + 1][s]);
}
int queryNegMax(int l, int r) {
int s = lg[r - l + 1];
return max(aNegMax[l][s], aNegMax[r - (1 << s) + 1][s]);
}
int queryHas0(int l, int r) {
int s = lg[r - l + 1];
return aHas0[l][s] | aHas0[r - (1 << s) + 1][s];
}
} sa;
struct SparseB {
int bMax[N][M], bMin[N][M], bNegMin[N][M], bPosMax[N][M];
bool bHas0[N][M];
void init() {
for (int i = 1; i <= m; i++) {
bMax[i][0] = b[i];
bMin[i][0] = b[i];
bNegMin[i][0] = (b[i] > 0 ? inf : b[i]);
bPosMax[i][0] = (b[i] < 0 ? -inf : b[i]);
bHas0[i][0] = (b[i] == 0);
}
for (int j = 1; (1 << j) <= m; j++) {
for (int i = 1; i <= m - (1 << j) + 1; i++) {
bMax[i][j] = max(bMax[i][j - 1], bMax[i + (1 << (j - 1))][j - 1]);
bMin[i][j] = min(bMin[i][j - 1], bMin[i + (1 << (j - 1))][j - 1]);
bNegMin[i][j] = min(bNegMin[i][j - 1], bNegMin[i + (1 << (j - 1))][j - 1]);
bPosMax[i][j] = max(bPosMax[i][j - 1], bPosMax[i + (1 << (j - 1))][j - 1]);
bHas0[i][j] = (bHas0[i][j - 1] | bHas0[i + (1 << (j - 1))][j - 1]);
}
}
}
int queryMax(int l, int r) {
int s = lg[r - l + 1];
return max(bMax[l][s], bMax[r - (1 << s) + 1][s]);
}
int queryMin(int l, int r) {
int s = lg[r - l + 1];
return min(bMin[l][s], bMin[r - (1 << s) + 1][s]);
}
int queryNegMin(int l, int r) {
int s = lg[r - l + 1];
return min(bNegMin[l][s], bNegMin[r - (1 << s) + 1][s]);
}
int queryPosMax(int l, int r) {
int s = lg[r - l + 1];
return max(bPosMax[l][s], bPosMax[r - (1 << s) + 1][s]);
}
int queryHas0(int l, int r) {
int s = lg[r - l + 1];
return bHas0[l][s] | bHas0[r - (1 << s) + 1][s];
}
} sb;
signed main() {
cin.tie(0) -> sync_with_stdio(0);
// freopen(".in", "r", stdin);
// freopen(".out", "w", stdout);
cin >> n >> m >> q;
initLg();
for (int i = 1; i <= n; i++) {
cin >> a[i];
}
for (int i = 1; i <= m; i++) {
cin >> b[i];
}
sa.init();
sb.init();
for (int i = 1; i <= q; i++) {
int la, ra, lb, rb; cin >> la >> ra >> lb >> rb;
int res = -1;
if (sa.queryMin(la, ra) >= 0) {
if (sb.queryMin(lb, rb) >= 0) {
res = sa.queryMax(la, ra) * sb.queryMin(lb, rb);
} else if (sb.queryMax(lb, rb) < 0) {
res = sa.queryMin(la, ra) * sb.queryMin(lb, rb);
} else {
res = sa.queryMin(la, ra) * sb.queryMin(lb, rb);
}
} else if (sa.queryMax(la, ra) < 0) {
if (sb.queryMin(lb, rb) >= 0) {
res = sa.queryMax(la, ra) * sb.queryMax(lb, rb);
} else if (sb.queryMax(lb, rb) < 0) {
res = sa.queryMin(la, ra) * sb.queryMax(lb, rb);
} else {
res = sa.queryMax(la, ra) * sb.queryMax(lb, rb);
}
} else {
if (sb.queryMin(lb, rb) >= 0) {
res = sa.queryMax(la, ra) * sb.queryMin(lb, rb);
} else if (sb.queryMax(lb, rb) < 0) {
res = sa.queryMin(la, ra) * sb.queryMax(lb, rb);
} else {
res = max(sa.queryPosMin(la, ra) * sb.queryNegMin(lb, rb), sa.queryNegMax(la, ra) * sb.queryPosMax(lb, rb));
}
}
// if (sa.queryHas0(la, ra)) {
// res = max(0ll, res);
// }
// if (sb.queryHas0(lb, rb)) {
// res = min(0ll, res);
// }
cout << res << '\n';
}
return 0;
}
[CSP-S 2023] 消消乐
Description
给定一个长度为 \(n\) 的、仅由小写字母构成的字符串 \(s\)。定义一次操作为:删除字符串中两个相邻且相同的字符,删除后剩余部分拼接。若一个字符串可以经过若干次操作变为空串,则称其“可消除”。
求 \(s\) 的所有非空连续子串中,可消除的子串数量。
\(1\le n\le 2\times 10^6\)。
Solution
考虑对每个右端点 \(i\),统计以 \(i\) 结尾的可消除子串数量。设数组 \(pre_i\) 表示:若存在以 \(i\) 为右端点的可消除后缀,则记录其中最短的那个后缀的左端点;若不存在,则 \(pre_i=0\)。
\(pre_i\) 可以通过类似 KMP 的跳转求出。从 \(j=i-1\) 开始尝试与 \(s_i\) 匹配:
- 若 \(s_j=s_i\) 且 \(j>0\),则 \(pre_i=j\);
- 否则,以 \(j\) 结尾的可消除后缀可以先被消掉,使 \(j\) 左侧的位置与 \(i\) 相邻,因此令 \(j\leftarrow pre_j-1\) 继续尝试;
- 若跳到 \(j\le 0\) 仍无法匹配,则 \(pre_i=0\)。
由于每次跳转都会跨过一段已经确定的可消除部分,整个过程均摊为 \(O(n)\)。
接下来设 \(dp_i\) 表示以 \(i\) 结尾的可消除子串数量。
- 若 \(pre_i=0\),不存在以 \(i\) 结尾的可消除子串,\(dp_i=0\);
- 若 \(pre_i=p>0\),则子串 \([p,i]\) 本身可消除。并且对于任意一个以 \(p-1\) 结尾的可消除子串,将其与 \([p,i]\) 拼接后仍然可消除;反过来,以 \(i\) 结尾的每个可消除子串也都对应一个以 \(p-1\) 结尾的可消除子串(或空串)。因此
最终答案为
Code
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N = 2e6 + 5;
int n, pre[N], dp[N], ans;
string s;
signed main() {
cin.tie(0) -> sync_with_stdio(0);
// freopen(".in", "r", stdin);
// freopen(".out", "w", stdout);
cin >> n >> s; s = ' ' + s;
for (int i = 1; i <= n; i++) {
int j = i - 1;
while (s[j] != s[i] and j > 0) {
j = pre[j] - 1;
}
if (j < 0) {
pre[i] = 0;
} else {
pre[i] = j;
}
}
// for (int i = 1; i <= n; i++) cout << pre[i] << ' ';
for (int i = 2; i <= n; i++) {
if (pre[i]) dp[i] = dp[pre[i] - 1] + 1;
}
for (int i = 1; i <= n; i++) {
ans += dp[i];
}
cout << ans << '\n';
return 0;
}
P9755 [CSP-S 2023] 种树
Description
给定一棵以 \(1\) 为根的树,共 \(n\) 个节点。初始所有节点均未种树。每天可以选一个与已种树节点相邻的未种树节点种下一棵高度为 \(0\) 的树;第 \(1\) 天只能在 \(1\) 号节点种树。
第 \(i\) 个节点一旦被种下,在第 \(x\) 天(从任务第一天开始计)会生长 \(\max(b_i+x\cdot c_i,\,1)\) 米,且种下当天即开始生长。要求最终每个节点 \(i\) 的高度不低于 \(a_i\),求完成任务所需的最少天数。
\(1\le n\le 10^5\),\(1\le a_i\le 10^{18}\),\(1\le b_i\le 10^9\),\(|c_i|\le 10^9\),保证存在方案能在 \(10^9\) 天内完成。
Solution
答案显然有单调性,考虑二分答案 \(D\)。若节点 \(i\) 在第 \(d\) 天种下,则到第 \(D\) 天为止它的总生长量为
该和关于 \(d\) 单调不增,因此对每个节点 \(i\) 可以二分求出最晚种植日 \(t_i\):满足 \(f(i,d,D)\ge a_i\) 的最大 \(d\)。
树木在 \([l,r]\) 天内的生长量函数 \(f(i,l,r)\) 可以 \(O(1)\) 计算。
- 当 \(c_i\ge 0\) 时,\(b_i+x\cdot c_i\ge b_i\ge 1\),直接求和;
- 当 \(c_i<0\) 时,考虑把 \(\max\) 拆开,令 \(I=\lfloor(1-b_i)/c_i\rfloor\)(即为恰好使 \(b_i+x\cdot c_i\ge 1\) 的最大整数 \(x\)),则 \(x\le I\) 时取 \(b_i+x\cdot c_i\),\(x>I\) 时取 \(1\),分段求和即可。
于是问题转化为:给定每个节点 \(i\) 的截止日 \(t_i\),判断是否存在一个合法的种树方案,使得第 \(k\) 个被种的节点满足 \(k\le t_i\)。这是树上带截止时间的单位任务调度问题,可将所有节点按 \(t_i\) 升序排序,从前往后依次处理。对于当前节点 \(u\),沿父链向上找到所有尚未安排的祖先,将它们依次压入栈中(压栈顺序为 \(u\) 的父链逆序),随后从栈顶弹出,逆序地把它们安排在接下来的若干天里。每安排一个节点就检查其安排日是否不超过其截止日,一旦超过则 \(D\) 不可行。
正确性证明使用了一个交换法。截止日越早的节点越紧迫,应尽早安排。而它的所有祖先都必须排在它前面,因此沿父链向上把未安排的祖先一次性全部提前安排,不会使答案变劣。
时间复杂度为 \(O(n \log n \log V)\)。等差数列求和时,答案可超出 long long,需要用 __int128。
Code
#include <bits/stdc++.h>
#define int __int128
#define inf 1e9
// 保证存在方案能在 1e9 天内完成任务
using namespace std;
inline int read() {
int x = 0, f = 1; char c = getchar();
while (c < '0' or c > '9') { if (c == '-') f = -1; c = getchar(); }
while (c >= '0' and c <= '9') x = x * 10 + c - '0', c = getchar();
return x * f;
}
void write(int x) {
if (x < 0) putchar('-'), x = -x;
if (x > 9) write(x / 10);
putchar(x % 10 + '0'); return;
}
const int N = 2e5 + 5;
int n, a[N], b[N], c[N], idx[N], t[N], fa[N];
bitset<N> vis;
vector<int> g[N];
inline int f(int i, int l, int r) {
if (c[i] >= 0) {
return (r - l + 1) * b[i] + (l + r) * (r - l + 1) / 2 * c[i];
} else {
int I = (1 - b[i]) / c[i];
if (I < l) {
return r - l + 1;
} else if (l <= I and I <= r) {
return (I - l + 1) * b[i] + (l + I) * (I - l + 1) / 2 * c[i] + r - I;
} else {
return (r - l + 1) * b[i] + (l + r) * (r - l + 1) / 2 * c[i];
}
}
}
void DFS(int u, int from) {
for (auto v : g[u]) {
if (v == from) continue;
fa[v] = u;
DFS(v, u);
}
}
inline bool cmp(int x, int y) {
return t[x] < t[y];
}
inline bool check(int mid) {
for (int i = 1; i <= n; i++) {
// 如果最松的限制都无法满足,那么一定不合法
if (f(i, 1, mid) < a[i]) return 0;
int L = 1, R = n, Ans = -1;
while (L <= R) {
int Mid = (L + R) / 2;
if (f(i, Mid, mid) >= a[i]) {
L = Mid + 1;
Ans = Mid;
} else {
R = Mid - 1;
}
}
idx[i] = i;
t[i] = Ans;
}
vis.reset();
sort(idx + 1, idx + 1 + n, cmp);
stack<int> stk;
for (int i = 1, days = 1; i <= n; i++) {
int u = idx[i];
while (not vis[u] and u) {
stk.push(u);
vis[u] = 1;
u = fa[u];
}
while (not stk.empty()) {
int cur = stk.top(); stk.pop();
if (t[cur] < days) return 0;
days++;
}
}
return 1;
}
signed main() {
cin.tie(0) -> sync_with_stdio(0);
n = read();
for (int i = 1; i <= n; i++) {
a[i] = read(); b[i] = read(); c[i] = read();
}
for (int i = 1; i < n; i++) {
int u, v; u = read(); v = read();
g[u].push_back(v), g[v].push_back(u);
}
DFS(1, -1);
int l = n, r = inf, ans = -1;
while (l <= r) {
int mid = (l + r) / 2;
if (check(mid)) {
r = mid - 1;
ans = mid;
} else {
l = mid + 1;
}
}
write(ans);
return 0;
}
[CSP-S 2024] 超速检测
Description
一条长度为 \(L\) 的南北主干道上,有 \(n\) 辆车从位置 \(d_i\) 驶入,初速度为 \(v_i\),加速度为 \(a_i\),向北行驶。道路限速为 \(V\)。当车辆到达位置 \(L\) 或速度降为 \(0\) 时驶离主干道。道路上有 \(m\) 个测速仪,第 \(j\) 个位于位置 \(p_j\)。若某辆车经过一个开启的测速仪时瞬时速度严格超过 \(V\),则被判定为超速。
求:(1)若所有测速仪都开启,有多少辆车会被判定为超速;(2)在保证所有原本会被判定为超速的车辆仍然被判定为超速的前提下,最多可以关闭多少个测速仪。
\(1\le T\le 20\),\(1\le n,m\le 10^5\),\(1\le L\le 10^6\),\(1\le V\le 10^3\),\(0\le d_i<L\),\(1\le v_i\le 10^3\),\(|a_i|\le 10^3\),\(0\le p_1<p_2<\cdots<p_m\le L\)。
Solution
对每辆车,先求出它在主干道上瞬时速度严格大于 \(V\) 的位置区间。由于匀加速运动中速度随位置单调变化,该区间一定是一个连续的区间 \([l,r]\)。
根据加速度分情况:
- \(a_i=0\):速度恒为 \(v_i\)。若 \(v_i>V\),则整个行驶区间都超速,\([l,r]=[d_i,L]\);否则不超速。
- \(a_i>0\):速度递增。若 \(v_i>V\),则从 \(d_i\) 起就超速,\([l,r]=[d_i,L]\);否则解\[v_i^2+2a_i(s-d_i)>V^2 \]得到超速位置的下界。为避免浮点误差,用整数运算取满足严格大于的最小整数位置,并令 \(r=L\)。
- \(a_i<0\):速度递减,只有 \(v_i>V\) 时才可能超速。超速持续到速度降为 \(V\) 之前,解\[v_i^2-2|a_i|(s-d_i)>V^2 \]得到超速位置的上界,再与车辆因速度降为 \(0\) 而驶离的位置取较小值。
将每辆车的超速位置区间 \([l,r]\) 通过二分映射到测速仪下标区间 \([L_i,R_i]\):\(L_i\) 是第一个满足 \(p_j\ge l\) 的下标,\(R_i\) 是最后一个满足 \(p_j\le r\) 的下标。若 \(L_i\le R_i\),说明这辆车会被至少一个开启的测速仪判定为超速,记录该下标区间;否则,这辆车不会被任何测速仪记录。
第一问的答案就是记录下来的区间个数。
第二问等价于:在所有需要被覆盖的测速仪下标区间中,选择最少数量的测速仪位置,使得每个区间内至少有一个被选中的测速仪。然后最多可关闭的测速仪数量为 \(m\) 减去最少需要保留的数量。这是经典的区间选点贪心:将所有区间按右端点升序排序,依次扫描;维护上一次选中的测速仪下标 \(last\)。若当前区间的左端点 \(L_i>last\),说明尚未被覆盖,则选择该区间的右端点 \(R_i\),令 \(last=R_i\),并把最少保留数量加一。否则当前区间已被覆盖,直接跳过。
Code
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N = 1e5 + 5;
int T, n, m, L, V, d[N], v[N], a[N], p[N];
struct Segment {
int l, r;
friend bool operator < (Segment x, Segment y) {
if (x.r != y.r) return x.r < y.r;
return x.l > y.l;
}
};
vector<Segment> seg;
void solve() {
seg.clear();
cin >> n >> m >> L >> V;
for (int i = 1; i <= n; i++) {
cin >> d[i] >> v[i] >> a[i];
}
for (int i = 1; i <= m; i++) {
cin >> p[i];
}
for (int i = 1; i <= n; i++) {
int l = -1, r = -1, lp = -1, rp = -1;
if (a[i] == 0) {
if (v[i] > V) l = d[i], r = L;
else continue;
} else if (a[i] > 0) {
if (v[i] > V) l = d[i], r = L;
else l = d[i] + (V * V - v[i] * v[i]) / (2 * a[i]) + 1, r = L;
} else {
if (v[i] > V) l = d[i], r = d[i] + (v[i] * v[i] - V * V + 2 * -a[i] - 1) / (2 * -a[i]) - 1;
else continue;
}
lp = lower_bound(p + 1, p + 1 + m, l) - p;
rp = upper_bound(p + 1, p + 1 + m, r) - p - 1;
if (lp <= rp) seg.push_back({lp, rp});
}
sort(seg.begin(), seg.end());
int lst = 0, ans = 0;
for (auto tmp : seg) {
int l = tmp.l, r = tmp.r;
if (lst < l) ans++, lst = r;
}
cout << seg.size() << ' ' << m - ans << '\n';
}
signed main() {
cin.tie(0) -> sync_with_stdio(0);
cin >> T;
while (T--) {
solve();
}
return 0;
}
[CSP-S 2024] 染色
Description
给定长度为 \(n\) 的正整数数组 \(A\)。将每个数染成红色或蓝色。对于每个位置 \(i\),若其左侧存在与其同色的数,记最近一个的索引为 \(j\),若 \(A_i=A_j\),则产生贡献 \(A_i\),否则贡献为 \(0\)。求所有染色方案中贡献总和的最大值。
\(2\le n\le 2\times 10^5\),\(1\le A_i\le 10^6\)。
Solution
记 \(pre_i\) 为 \(A_i\) 上一次出现的位置(不存在则为 \(0\))设 \(dp_{i,0}\) 和 \(dp_{i,1}\) 分别表示考虑前 \(i\) 个数、且第 \(i\) 个数染成红色/蓝色时的最大得分。转移时根据 \(pre_i\) 与 \(i-1\) 的关系分讨。
-
若 \(pre_i=i-1\)
- 若第 \(i\) 个数与第 \(i-1\) 个数异色,则第 \(i\) 个数左侧不存在同色的 \(A_i\),贡献为 \(0\),直接由异色状态转移;
- 若同色,则产生贡献 \(A_i\),由同色状态加上 \(A_i\) 转移。
-
若 \(pre_i\ne i-1\)
- 第 \(i\) 个数可以与前一位任意颜色不同,此时不立即产生贡献,取两种颜色的最大值;
- 也可以让第 \(i\) 个数与第 \(pre_i\) 个数同色,从而获得贡献 \(A_i\)。此时 \(pre_i\) 与 \(i\) 之间的数(不包含两端点)染成同色一定不劣。为了计算区间 \([pre_i+2, i-1]\) 内的数(不能算 \(pre_i+1\) 的贡献,否则会算重)产生的贡献,必须从 \(pre_i + 1\) 转移而来,贡献可以前缀和维护。于是有\[dp_{i,c}=\max\Bigl(dp_{i,c},\ dp_{pre_i+1,\,1-c}+A_i+s_{i-1}-s_{pre_i+1}\Bigr) \]
最终答案为 \(\max(dp_{n,0},dp_{n,1})\)。
Code
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N = 1e6 + 5;
int T, n, a[N], s[N], lst[N], pre[N], dp[N][2];
void solve() {
memset(lst, 0, sizeof lst);
memset(pre, 0, sizeof pre);
memset(s, 0, sizeof s);
memset(dp, 0, sizeof dp);
cin >> n;
for (int i = 1; i <= n; i++) {
cin >> a[i];
pre[i] = lst[a[i]];
lst[a[i]] = i;
}
for (int i = 1; i <= n; i++) {
s[i] = s[i - 1] + (a[i] == a[i - 1] ? a[i] : 0);
}
for (int i = 1; i <= n; i++) {
if (pre[i] == i - 1) {
dp[i][0] = max(dp[i][0], dp[i - 1][1]);
dp[i][1] = max(dp[i][1], dp[i - 1][0]);
} else {
dp[i][0] = max({dp[i][0], dp[i - 1][0], dp[i - 1][1]});
dp[i][1] = max({dp[i][1], dp[i - 1][0], dp[i - 1][1]});
}
if (pre[i]) {
if (pre[i] == i - 1) {
dp[i][0] = max(dp[i][0], dp[i - 1][0] + a[i]);
dp[i][1] = max(dp[i][1], dp[i - 1][1] + a[i]);
} else {
dp[i][0] = max(dp[i][0], dp[pre[i] + 1][1] + a[i] + s[i - 1] - s[pre[i] + 1]);
dp[i][1] = max(dp[i][1], dp[pre[i] + 1][0] + a[i] + s[i - 1] - s[pre[i] + 1]);
}
}
}
cout << max(dp[n][0], dp[n][1]) << '\n';
}
signed main() {
cin.tie(0) -> sync_with_stdio(0);
// freopen(".in", "r", stdin);
// freopen(".out", "w", stdout);
cin >> T;
while (T--) {
solve();
}
return 0;
}
[CSP-S 2025] 道路修复
Description
给定一张包含 \(n\) 个城市与 \(m\) 条双向道路的无向连通图,第 \(i\) 条道路修复费用为 \(w_i\)。另有 \(k\) 个待改造乡镇,第 \(j\) 个乡镇的改造费用为 \(c_j\);改造后可以在该乡镇与第 \(i\) 座城市间建造道路,费用为 \(a_{j,i}\)。可以选择任意多个乡镇进行改造(可以不改造),目标是以最小费用使原有的 \(n\) 座城市两两连通。
\(1\le n\le 10^4\),\(1\le m\le 10^6\),\(0\le k\le 10\),所有费用非负,保证原图连通。
Solution
一道集合了各类预处理思想的好题。
由于 \(k\le 10\),可以枚举所有乡镇选择方案,共 \(2^k\) 种。
直接对每个子集在原图上跑 Kruskal 显然过慢。注意到原图的最小生成树只有 \(n-1\) 条边。对于任何在原图上添加新边的图,其最小生成树一定可以由原图 MST 加上新边后,再跑一次 Kruskal 得到。此时复杂度为 \(O(m \log m + 2^k kn \log kn)\)。
注意到没必要在每个状态中都排一遍序。可以先将全部 \(kn + n -1\) 条边预先排序,然后枚举状态的时候碰到没有改造的乡镇时跳过就可以了。此时复杂度为 \(O(m \log m + kn \log kn + 2^k kn)\)。
Code
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int K = 15, N = 5e4 + 5;
int n, m, k, c[N], a[K][N], ans = 1e18;
struct Edge {
int u, v, w;
friend bool operator < (Edge x, Edge y) {
return x.w < y.w;
}
};
vector<Edge> edges, mst;
bitset<16> vis;
struct DSU {
int fa[N];
void init() {
for (int i = 1; i <= n + k; i++) {
fa[i] = i;
}
}
int find(int x) {
if (x == fa[x]) return x;
return fa[x] = find(fa[x]);
}
void unite(int x, int y) {
fa[find(y)] = find(x);
}
} dsu;
void KruskalOrigin() {
for (auto tmp : edges) {
int U = dsu.find(tmp.u), V = dsu.find(tmp.v), w = tmp.w;
if (U == V) continue;
// (tmp.u, tmp.v) 是最小生成树上的一条边
mst.push_back({tmp.u, tmp.v, w});
dsu.unite(U, V);
}
}
int KruskalNew() {
int cur = 0;
for (auto tmp : mst) {
int U = dsu.find(tmp.u), V = dsu.find(tmp.v), w = tmp.w;
if (U == V) continue;
if ((tmp.u > n and vis[tmp.u - n] == 0) or (tmp.v > n and vis[tmp.v - n] == 0)) continue;
cur += w;
dsu.unite(U, V);
}
return cur;
}
signed main() {
cin.tie(0) -> sync_with_stdio(0);
cin >> n >> m >> k;
dsu.init(); // 并查集一定要初始化,且初始化上界一定要对
for (int i = 1; i <= m; i++) {
int u, v, w; cin >> u >> v >> w;
edges.push_back({u, v, w});
}
sort(edges.begin(), edges.end());
KruskalOrigin();
for (int j = 1; j <= k; j++) {
cin >> c[j];
for (int i = 1; i <= n; i++) {
cin >> a[j][i];
mst.push_back({n + j, i, a[j][i]});
}
}
sort(mst.begin(), mst.end());
for (int S = 0; S < (1 << k); S++) {
int res = 0;
dsu.init();
vis.reset();
for (int j = 1; j <= k; j++) {
if (S >> (j - 1) & 1ll) {
vis[j] = 1;
res += c[j];
}
}
res += KruskalNew();
ans = min(ans, res);
}
cout << ans << '\n';
return 0;
}
[CSP-S 2025] 谐音替换
Description
给定 \(n\) 个等长字符串二元组 \((s_{i,1},s_{i,2})\),表示一次替换:若当前串中存在子串等于 \(s_{i,1}\),可以将其替换为 \(s_{i,2}\)。两种替换不同当且仅当被替换子串的位置不同或使用的二元组不同。
再给定 \(q\) 个询问,每个询问给出两个不同的等长字符串 \(t_{j,1},t_{j,2}\),求有多少种对 \(t_{j,1}\) 的替换能够得到 \(t_{j,2}\)。
\(1\le n,q\le 2\times 10^5\),\(\sum_i(|s_{i,1}|+|s_{i,2}|),\sum_j(|t_{j,1}|+|t_{j,2}|)\le 5\times 10^6\)。
Solution
一次替换只改变原串中的一个连续子串。对于等长的两个串 \(x,y\),若它们能通过一次替换互相转化,那么它们从开头到某个位置完全相同,从某个位置到结尾也完全相同,只有中间一段可能不同。把最长公共前缀和最长公共后缀去掉后,剩下的“差异部分”就唯一刻画了这次替换。
于是对每个二元组 \((s_{i,1},s_{i,2})\) 构造特征串。记 \(p\) 为最长公共前缀的末位置,\(q\) 为最长公共后缀的起始偏移。特征串为
其中 \(\texttt{\{}\) 是字符集外的分隔符。若两个串完全相同,则特征串为空,不插入。所有规则的特征串插入 AC 自动机,并在每个模式串末尾计数。
对于询问 \((t_{j,1},t_{j,2})\),用同样的方式构造特征串 \(T\)。若 \(T\) 为空,说明两串相同,而题目保证 \(t_{j,1}\ne t_{j,2}\),答案为 \(0\)。否则在 AC 自动机上匹配 \(T\),沿途累加 fail 链上的模式串计数,即得到 \(T\) 作为模式串在规则特征串集合中出现的次数。
AC 自动机建 fail 时把计数沿 fail 链累加,查询时只需顺序遍历特征串并累加当前节点的计数即可。
Code
#include <bits/stdc++.h>
#define int long long
#define inf 1e18
#define debug cout << '!';
#define filein(x) freopen(#x".in", "r", stdin);
#define fileout(x) freopen(#x".out", "w", stdout);
#define file(x) filein(x) fileout(x)
// #define Fast_IO
using namespace std;
#ifdef Fast_IO
inline int read() {
int x = 0, f = 1; char c = getchar();
while (c < '0' or c > '9') { if (c == '-') f = -1; c = getchar(); }
while (c >= '0' and c <= '9') { x = x * 10 + c - '0'; c = getchar(); }
return x * f;
}
void write(int x) {
if (x < 0) putchar('-'), x = -x;
if (x > 9) write(x / 10);
putchar(x % 10 + '0'); return;
}
#endif
const int N = 2e5 + 5, M = 6e6 + 5, RPT = 27;
int n, q;
string s[N], t[N];
string process(string x, string y) {
if (x == y) return string();
if (x.length() != y.length()) return string();
string res;
int posLcp = -1, posLcs = 0, lenX = x.length(), lenY = y.length();
for (int i = 0; i < min(lenX, lenY); i++) {
if (x[i] != y[i]) { posLcp = i - 1; break; }
}
for (int i = 0; i < min(lenX, lenY); i++) {
if (x[lenX - i - 1] != y[lenY - i - 1]) { posLcs = i + 1; break; }
}
for (int i = 0; i <= posLcp; i++) res += x[i];
res += '{';
for (int i = posLcp + 1; i <= lenX - posLcs; i++) res += x[i];
for (int i = posLcp + 1; i <= lenY - posLcs; i++) res += y[i];
res += '{';
for (int i = lenY - posLcs + 1; i < lenY; i++) res += y[i];
return res;
}
struct ACAM {
int totNode = 0;
struct Node {
int son[RPT], fail, cnt;
} t[M];
void insert(string str) {
if (str.empty()) return;
int cur = 0;
for (auto c : str) {
int code = c - 'a';
if (t[cur].son[code] == 0) t[cur].son[code] = ++totNode;
cur = t[cur].son[code];
}
t[cur].cnt++;
}
void build() {
queue<int> q;
for (int i = 0; i < RPT; i++) {
if (t[0].son[i]) q.push(t[0].son[i]);
}
while (not q.empty()) {
int u = q.front(); q.pop();
t[u].cnt += t[t[u].fail].cnt;
for (int i = 0; i < RPT; i++) {
if (t[u].son[i]) {
t[t[u].son[i]].fail = t[t[u].fail].son[i];
q.push(t[u].son[i]);
} else {
t[u].son[i] = t[t[u].fail].son[i];
}
}
}
}
int query(string str) {
int cur = 0, res = 0;
for (auto c : str) {
int code = c - 'a';
cur = t[cur].son[code];
res += t[cur].cnt;
}
return res;
}
} acam;
signed main() {
cin.tie(0) -> sync_with_stdio(0);
cin >> n >> q;
for (int i = 1; i <= n; i++) {
string s1, s2; cin >> s1 >> s2;
acam.insert(process(s1, s2));
}
acam.build();
for (int i = 1; i <= q; i++) {
string t1, t2; cin >> t1 >> t2;
string t = process(t1, t2);
if (t.empty()) {
cout << "0\n";
} else {
cout << acam.query(t) << '\n';
}
}
return 0;
}
[CSP-S 2025] 员工招聘
Description
有 \(n\) 个人依次参加面试,小 Z 可以任意决定他们的面试顺序(即一个 \(1\sim n\) 的排列 \(p\))。第 \(i\) 天的面试题难度为 \(s_i\in\{0,1\}\):\(s_i=0\) 表示无人能做出,\(s_i=1\) 表示所有人都能做出。
编号为 \(i\) 的人有耐心上限 \(c_i\):若在他面试之前,已经被拒绝或放弃的人数不少于 \(c_i\),则他会直接放弃面试。否则,若当天题目难度为 \(0\),他会被拒绝;若难度为 \(1\),他会被录用。
求能够录用至少 \(m\) 人的排列 \(p\) 的数量,对 \(998244353\) 取模。
\(1\le m\le n\le 500\),\(0\le c_i\le n\)。
Solution
这道题“延后计算”的设计非常巧妙,值得学习。
直接对排列计数非常困难,考虑把“当前已经失败的人数”作为状态。设
定义 \(dp_{i,j,k}\) 表示已经考虑了前 \(i\) 天,当前失败(被拒绝或放弃)的人数为 \(j\),另外还有 \(k\) 个确定会通过的候选时的方案数。对于 \(c_i>j\) 的人,只要轮到他时前面的失败人数不超过 \(c_i-1\),他一定会通过;由于当前失败人数为 \(j\),这些人的具体身份并不影响后续状态,因此可以延后处理,只在 \(k\) 中记录数量。转移按第 \(i\) 天的难度 \(s_i\) 分类。
当 \(s_i=1\) 时,当天题目所有人都能做出。
-
若来的人满足 \(c>j\),他通过。具体身份延后计算,于是
\[dp_{i,j,k+1}\leftarrow dp_{i,j,k+1}+dp_{i-1,j,k} \] -
若来的人满足 \(c\le j\),他放弃,失败人数从 \(j\) 变为 \(j+1\)。当前尚未使用且满足 \(c\le j\) 的人数为
\[s_j-(i-1)+k+1=s_j-i+k+1 \]失败人数增加到 \(j+1\) 后,原先延后的 \(k\) 个人中,那些 \(c=j+1\) 的人已经不会再影响是否通过,需要现场计算。枚举从 \(k\) 中取出 \(l\) 个与 \(c=j+1\) 的人配对,\(l\le \min(k,w_{j+1})\),则
\[dp_{i,j+1,k-l}\leftarrow dp_{i,j+1,k-l} +dp_{i-1,j,k}\cdot(s_j-i+k+1)\cdot\binom{w_{j+1}}{l}\binom{k}{l}l! \]
当 \(s_i=0\) 时,当天题目无人能做出,来的人无论耐心如何都会失败。
失败人数从 \(j\) 变为 \(j+1\)。先枚举从延后的 \(k\) 个人中取出 \(l\) 个与 \(c=j+1\) 的人配对:
-
若来的人满足 \(c>j+1\),他失败,但具体身份仍可延后计算,于是
\[dp_{i,j+1,k-l+1}\leftarrow dp_{i,j+1,k-l+1} +dp_{i-1,j,k}\cdot\binom{w_{j+1}}{l}\binom{k}{l}l! \] -
若来的人满足 \(c\le j+1\),他失败,立即从候选中扣除。可选出的人数为
\[s_{j+1}-i+k+1-l \]于是
\[dp_{i,j+1,k-l}\leftarrow dp_{i,j+1,k-l} +dp_{i-1,j,k}\cdot(s_{j+1}-i+k+1-l)\cdot\binom{w_{j+1}}{l}\binom{k}{l}l! \]
初始状态为 \(dp_{0,0,0}=1\)。最终枚举失败总人数 \(j\)。要录用至少 \(m\) 人,即失败人数满足 \(j\le n-m\)。当最终失败人数为 \(j\) 时,所有满足 \(c\le j\) 的人共 \(s_j\) 个必然失败,其余 \(n-s_j\) 人全部通过。至于那些延后处理的通过者,他们可以任意排列,因此还需乘上 \((n-s_j)!\)。答案为
Code
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N = 505, mod = 998244353;
int n, m, s[N], c[N], maxC, ans;
int cntJust[N], cntMost[N];
int dp[N][N][N];
int fac[N], C[N][N];
void init() {
fac[0] = 1;
for (int i = 1; i <= 500; i++) {
fac[i] = fac[i - 1] * i % mod;
}
for (int i = 0; i <= 500; 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;
}
}
}
signed main() {
cin.tie(0) -> sync_with_stdio(0);
cin >> n >> m;
for (int i = 1; i <= n; i++) {
char x; cin >> x;
s[i] = x - '0';
}
for (int i = 1; i <= n; i++) {
cin >> c[i];
cntJust[c[i]]++;
maxC = max(maxC, c[i]);
}
cntMost[0] = cntJust[0];
for (int i = 1; i <= n; i++) {
cntMost[i] = cntMost[i - 1] + cntJust[i];
}
init();
dp[0][0][0] = 1;
for (int i = 1; i <= n; i++) {
for (int j = 0; j <= i; j++) {
for (int k = 0; k <= i; k++) {
if (not dp[i - 1][j][k]) continue;
if (s[i] == 1) {
// 来一个 c > j 的人,他通过
dp[i][j][k + 1] = (dp[i][j][k + 1] + dp[i - 1][j][k]) % mod;
// 来一个 c <= j 的人,他放弃
if (cntMost[j] - i + k + 1 > 0) {
for (int l = 0; l <= min(k, cntJust[j + 1]); l++) {
dp[i][j + 1][k - l] = (dp[i][j + 1][k - l] + dp[i - 1][j][k] * (cntMost[j] - i + k + 1) % mod * C[cntJust[j + 1]][l] % mod * C[k][l] % mod * fac[l] % mod) % mod;
}
}
} else {
for (int l = 0; l <= min(k, cntJust[j + 1]); l++) {
// 来一个 c <= j + 1 的人,选人方案数 cntJust[j + 1] - i + k - l + 1
if (cntMost[j + 1] - i + k - l + 1 > 0) {
dp[i][j + 1][k - l] = (dp[i][j + 1][k - l] + dp[i - 1][j][k] * (cntMost[j + 1] - i + k - l + 1) % mod * C[cntJust[j + 1]][l] % mod * C[k][l] % mod * fac[l] % mod) % mod;
}
// 来一个 c > j + 1 的人,k 多加 1
dp[i][j + 1][k - l + 1] = (dp[i][j + 1][k - l + 1] + dp[i - 1][j][k] * C[cntJust[j + 1]][l] % mod * C[k][l] % mod * fac[l] % mod) % mod;
}
}
}
}
}
for (int j = 0; j <= n - m; j++) {
ans = (ans + dp[n][j][n - cntMost[j]] * fac[n - cntMost[j]] % mod) % mod;
}
cout << ans << '\n';
return 0;
}

浙公网安备 33010602011771号