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分我赛时也没拿到)

posted @ 2025-11-22 11:13  姚云潇  阅读(33)  评论(0)    收藏  举报