csp-s2025 题解
csp-s2025 T1
相信很多人第一眼看就是dp,但是我们知道CCF经常喜欢出贪心在第一题上
首先是三维 \(DP\),\(dp[w1][w2][w3]\) 表示部门 \(1\) 有 \(w1\) 人、部门 \(2\) 有 \(w2\) 人、部门 \(3\) 有 \(w3\) 人时的最大满意度。此时答案就是容量为\(n/2\)的背包模板题,预期 \(30\) 分。
然后是特殊性质捡分,这个不用说了吧,主要就提醒一下如果你是新手你要知道部分分一定是在引导正解
接下来是正解贪心:为啥能贪心?因为每个人的最优选择是先选满意度最高的部门,这样总和肯定是最大的基础值,但可能某个部门人数超了 \(n/2\),这时候只需要把超员的人里 “换部门损失最小” 的换成次优选择就行,毕竟损失越小,总和减少得越少,最后还是最大的。
具体步骤:
遍历每个成员,先给他分配满意度最高的部门,把这个最高满意度加到总和里;
记录每个成员 “最高满意度 - 次高满意度” 的差值(这个差值就是把他换成次优部门的损失),并把差值存到对应部门的数组里;
遍历三个部门,如果某个部门的人数超过 \(n/2\),就把这个部门的差值数组排序(从小到大,因为要先换损失最小的),然后把超员的数量(比如人数是 \(m\),超了 \(m-n/2\) 个)对应的前几个最小差值从总和里减去 —— 相当于把这些人换成次优部门,人数就合规了。
时间复杂度 \(O (n log n)\),\(n=1e5\) 完全没问题,所有测试点直接拉满,预期 \(100\) 分。
AC code
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int maxn = 1e5 + 5;
int T, n;
int a[maxn], b[maxn], c[maxn];
int diff0[maxn], diff1[maxn], diff2[maxn];
int cnt0, cnt1, cnt2;
inline int r() {
int x=0, f=1;char ch=getchar();
while (ch < '0' || ch > '9') {if (ch=='-') f=-1;ch=getchar();}
while (ch >= '0' && ch <= '9') {x = x*10+ch-'0'; ch=getchar();}
return x*f;
}
int max(int x, int y) {return x > y ? x : y;}
void solve() {
cnt0 = 0;
cnt1 = 0;
cnt2 = 0;
int k = n / 2;
int sum = 0;
for (int i = 1; i <= n; i++) {
int x = a[i];
int y = b[i];
int z = c[i];
int max_val, sec_val, d;
if (x >= y && x >= z) {
max_val = x;
d = 0;
sec_val = (y > z) ? y : z;
} else if (y >= x && y >= z) {
max_val = y;
d = 1;
sec_val = (x > z) ? x : z;
} else {
max_val = z;
d = 2;
sec_val = (x > y) ? x : y;
}
sum += max_val;
int diff = max_val - sec_val;
if (d == 0) {
diff0[cnt0++] = diff;
} else if (d == 1) {
diff1[cnt1++] = diff;
} else {
diff2[cnt2++] = diff;
}
}
if (cnt0 > k) {
sort(diff0, diff0 + cnt0);
int need = cnt0 - k;
for (int i = 0; i < need; i++) {
sum -= diff0[i];
}
}
if (cnt1 > k) {
sort(diff1, diff1 + cnt1);
int need = cnt1 - k;
for (int i = 0; i < need; i++) {
sum -= diff1[i];
}
}
if (cnt2 > k) {
sort(diff2, diff2 + cnt2);
int need = cnt2 - k;
for (int i = 0; i < need; i++) {
sum -= diff2[i];
}
}
cout << sum << '\n';
}
signed main() {
T = r();
while (T--) {
n = r();
for (int i = 1; i <= n; i++) {
a[i] = r();
b[i] = r();
c[i] = r();
}
solve();
}
return 0;
}
csp-s2025 T2
考场的时候把 \(k=10\)看成 \(k=1e4\) 了,当时想了半天我说 CCF 怎么这次出的那么难呢,拿了个特殊性质 A 就跑了,你的这就算了吧,更可恶的是开二维数组 a[maxn][maxn](maxn=1e4+5)直接 MLE 了,于是乎:\(48pts\) -> \(0pts\),造孽啊......
思路非常简单,就是坑多了一点其它还好
首先对于特殊性质 A 敲一遍 kruskal 模板直接过掉,不会的去【模板】最小生成树,预期 \(48\) 分
注意到 \(k=10\) 我们直接暴力二进制枚举,对于每一次枚举都跑一遍 kruskal,时间复杂度 \(O(2^k \cdot (kn+m)\log(kn+m))\),预期 \(48\) 分
发现此时 \(2^k\) 是肯定无法再优化了,我们知道对于很多最小生成树的题目都是去想 kruskal 的算法原理,而且对于图论有一个非常经典的思路就是把时间复杂度从关于 \(m\) 的转移到关于 \(n\) 的,于是容易发现对于只考虑原本城市的图不需要一直排序,可以直接预处理出原本城市的图最小的前 \(n-1\) 条边,并在 kruskal 函数中只考虑乡镇的边和 \(n-1\) 条城市的边,时间复杂度 \(O(m\log m + 2^k (kn)\log(kn))\),预期 \(80\) 分(\(n\log n\) 时间复杂度时 \(n=2e7\))
发现时限瓶颈卡在了排序给的 \(\log\) 上,于是我们直接预处理排序,kruskal 函数里如果碰到不改造乡镇的边直接跳过即可,时间复杂度 \(O(m\log m + kn\log kn + 2^k (kn))\),预期得分 \(100\) 分
还有几个很坑的点:
- 务必要开
long long,否则 \(100 \to 16\) - 务必要开快速读入,否则 \(100 \to 96\)
- 务必要开 \(1e18\) 而不是
0x3f3f3f3f,否则 \(100 \to 16\)
代码:
AC code
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int maxn = 1e6 + 5;
int n, m, k, c[maxn], a[15][maxn], ans = 1e18;
bool change[maxn];
inline int r() {
int x = 0, f = 1;char ch = getchar();
while (ch < '0' || ch > '9') {if (ch == '-') f = -1;ch = getchar();}
while (ch >= '0' && ch <= '9') {x = x * 10 + (ch - '0'); ch = getchar();}
return f * x;
}
struct Edge { int u, v, w; };
struct DSU {
int fa[maxn];
void init(int sz) {for (int i = 1; i <= sz; i++) fa[i] = i;}
int find(int x) { return fa[x] == x ? x : fa[x] = find(fa[x]); }
bool merge(int x, int y) {
x = find(x), y = find(y);
if (x == y) return 0;
fa[x] = y;
return 1;
}
}dsu;
bool cmp(Edge x, Edge y) {
if (x.w == y.w) return x.u < y.u;
return x.w < y.w;
}
int kruskal(vector<Edge> &edges) {
int res = 0, num = 0;
for (auto e : edges) {
int u = e.u, v = e.v, w = e.w;
if (v > n && !change[v - n]) continue;
if (dsu.merge(u, v)) {
res += w;
num++;
if (num >= n + k - 1) break;
}
}
return res;
}
signed main() {
n = r(), m = r(), k = r();
vector<Edge> olde;
for (int i = 1; i <= m; i++) {
int u = r(), v = r(), w = r();
olde.push_back({u, v, w});
}
sort(olde.begin(), olde.end(), cmp);
DSU dsu0;
dsu0.init(n);
int cnt = 0;
vector<Edge> newe;
for (auto e : olde) {
if (dsu0.merge(e.u, e.v)) {
newe.push_back(e);
cnt++;
if (cnt == n-1) break;
}
}
vector<Edge> edges = newe;
for (int i = 1; i <= k; i++) {
c[i] = r();
for (int j = 1; j <= n; j++) {
a[i][j] = r();
edges.push_back({j, n + i, a[i][j]});
}
}
sort(edges.begin(), edges.end(), cmp);
for (int i = 0; i < (1 << k); i++) {
dsu.init(n + k);
int cost = 0;
for (int j = 0; j < k; j++) {
change[j + 1] = (i >> j) & 1;
cost += change[j + 1] * c[j + 1];
}
int mst_cost = kruskal(edges);
ans = min(ans, mst_cost + cost);
}
cout << ans << endl;
return 0;
}
csp-s2025 T3
必须喷一下好吧,这个题目非常容易让人误以为是要替换若干次,然后我当时本意是想骗点分,结果只看了样例1(是不是谁的一辈子) 结果连暴力都没骗。
出考场的时候旁边一堆人说这题用AC自动机,于是赛后我就琢磨了半天,调了半天一堆史山代码最高分\(80pts\),最后发现直接拿个暴力就能蹭到\(70pts\)。其实这题正解不管怎样肯定是Trie树或AC自动机,因为明显地空间给你开到了\(2GB\),而且还是字符串,还设计到前后缀。
注意到 \(t_{j,1}\) 和 \(t_{j,2}\) 只有在替换位置不同,其它位置都相同。所以先求出两个串的最长公共前缀和最长公共后缀,这样替换只能发生在中间那段。然后枚举所有可能的替换长度,在重叠的范围内枚举起始位置,检查是否符合某个替换规则。这样的时间复杂度最坏情况下不行,但很多情况数据水一点均摊还是能过的,实际 \(60\)~\(70\) 分。
update12/23: 后来又试了几次不知道为什么AC自动机过了(doge)不过有点不太稳定,大概是95pts ~ 100pts
70 points
#include <bits/stdc++.h>
#define int long long
#define map unordered_map
using namespace std;
map<int, map<string, map<string, int>>> cnt;//cnt[规则长度][原字符串][目标字符串] = 出现次数
int max(int a, int b) {return a > b ? a : b;}
int n, q, m, pre, suf;
signed main()
{
cin >> n >> q;
for (int i = 0;i < n;i++) {
string a, b;
cin >> a >> b;
cnt[a.size()][a][b]++;
}
while (q--) {
string t1, t2;
cin >> t1 >> t2;
m = t1.size();
if (m != t2.size()) {cout << "0\n"; continue;} //一个简单的特判
pre = suf = 0;
while (pre < m && t1[pre] == t2[pre]) pre++;
while (suf < m && t1[m - 1 - suf] == t2[m - 1 - suf]) suf++;
suf = m - suf - 1;
if (pre > suf) {// 检查是否存在替换空间
cout << "0\n"; continue;
}
int len = max(1, suf - pre + 1), ans = 0;//非常暴力
for (int l = len; l <= m;l++) {
if (!cnt.count(l)) continue;
for (int i = max(0, suf - l + 1); i <= min(pre, m - l); i++) {
string s1 = t1.substr(i, l);
string s2 = t2.substr(i, l);
if (cnt[l].count(s1) && cnt[l][s1].count(s2));
ans += cnt[l][s1][s2];
}
}
cout << ans << '\n';
}
return 0;
}
当然还有一个我拿到\(80pts\)的AC自动机代码:
80 points
#include <bits/stdc++.h>
using namespace std;
const int N=2e5+5,M=5e6+5,MM=25e5;
int n,q,tot,fail[MM],dep[MM],ans;
char s1[M],s2[M];
int tag[MM],fa[MM][22];
unordered_map<int,int>trie[MM];
vector<int>g[MM];
void add()
{
int now=0;
for(int i=1;s1[i];i++)
{
int p=(s1[i]-'a')*26+s2[i]-'a';
if(!trie[now][p])
{
trie[now][p]=++tot;
dep[tot]=dep[now]+1;
}
now=trie[now][p];
}
++tag[now];
}
void getfail()
{
queue<int>q;
q.push(0);
while(!q.empty())
{
int x=q.front();q.pop();
for(auto pp:trie[x])
{
int p=pp.first;
int to=pp.second;
int now=fail[x];
while(now&&trie[now].find(p)==trie[now].end())
now=fail[now];
if(x&&trie[now].find(p)!=trie[now].end())now=trie[now][p];
fail[to]=now;
g[now].push_back(to);
q.push(to);
}
}
}
void dfs(int x)
{
for(int to:g[x])
{
tag[to]+=tag[x];
fa[to][0]=x;
for(int i=1;i<=21;i++)
fa[to][i]=fa[fa[to][i-1]][i-1];
dfs(to);
}
}
int read(char *s)
{
char c;int i=0;
while((c=getchar())<'a'||c>'z')continue;
s[++i]=c;
while((c=getchar())>='a'&&c<='z')s[++i]=c;
s[i+1]=0;
return i;
}
int main()
{
scanf("%d%d",&n,&q);
for(int i=1;i<=n;i++)
{
read(s1),read(s2),add();
}
getfail();dfs(0);
for(int i=1;i<=q;i++)
{
int len=read(s1),len2=read(s2);
if(len!=len2)
{
puts("0");continue;
}
int l=len+1,r=0;
for(int i=1;i<=len;i++)
{
if(s1[i]!=s2[i])
{
l=i-1;break;
}
}
for(int i=len;i;i--)
{
if(s1[i]!=s2[i])
{
r=i+1;break;
}
}
int now=0,ans=0;
for(int i=1;i<=len;i++)
{
int p=(s1[i]-'a')*26+s2[i]-'a';
while(now&&trie[now].find(p)==trie[now].end())
now=fail[now];
if(trie[now].find(p)!=trie[now].end())now=trie[now][p];
if(i>=r-1)
{
if(dep[now]<i-l)continue;
int x=now;
for(int j=21;~j;j--)
if(dep[fa[x][j]]>=i-l)x=fa[x][j];
x=fa[x][0];
ans+=tag[now]-tag[x];
}
}
printf("%d\n",ans);
}
return 0;
}
csp-s2025 T4
啊啊啊T3代码写了好久是在没时间想T4了啊,直接全排列预期8分好吧(其实这8分我赛时也没拿到)

浙公网安备 33010602011771号