CSP-S2025做题记录

T1

容易想歪的简单题。

考虑社团人数上限很高,是 \(\lfloor\frac{n}{2}\rfloor\),很容易发现,其实两个社团就可以塞满 \(n\) 个人。

于是对于一个人,只需考虑三个社团中的最大值和次大值,那么首先,如果按所有人都分配到对于那个人中三个社团最大值之后,人数最大的社团也是小于等于 \(\lfloor\frac{n}{2}\rfloor\) 的话,那么答案直接就是将所有人对应的最大值加起来就行了,设这个值为 \(sum\),否则的话,考虑如何微调。

那么考虑一个人的贡献,其实可以设成他的最大值和次大值的差,然后按贡献从小到大排序,容易证明这是对的。

然后其实考虑我们设目前人数超过 \(\lfloor\frac{n}{2}\rfloor\) 的社团人数和 \(\lfloor\frac{n}{2}\rfloor\) 的差为 \(s\),然后删掉前 \(s\) 个人的贡献就行了。

代码:

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
const int N = 1e5+5;
struct node
{
	int x;
	int y;
	int z;
	int maxxx;
	int cha;
}a[N];
int num[3];
int b[N];
signed main()
{
	ios::sync_with_stdio(0);
	cin.tie(0);
	cout.tie(0);
	int _;
	cin >> _;
	while(_--)
	{
		num[0] = num[1] = num[2] = 0;
		int n,sum = 0;
		cin >> n;
		for(int i = 1;i<=n;++i)
		{
			cin >> a[i].x >> a[i].y >> a[i].z;
			int maxx = a[i].x,maxxx = 0;
			if(a[i].y>maxx)
			{
				maxx = a[i].y;
				maxxx = 1;
			}
			if(a[i].z>maxx)
			{
				maxx = a[i].z;
				maxxx = 2;
			}
			int cmax;
			if(maxx == a[i].x)
			{
				if(a[i].y>a[i].z)
				{
					cmax = a[i].y;
				}
				else
				{
					cmax = a[i].z;
				}
			}
			else if(maxx == a[i].y)
			{
				if(a[i].x>a[i].z)
				{
					cmax = a[i].x;
				}
				else
				{
					cmax = a[i].z;
				}
			}
			else
			{
				if(a[i].x>a[i].y)
				{
					cmax = a[i].x;
				}
				else
				{
					cmax = a[i].y;
				}
			}
			a[i].maxxx = maxxx;
			a[i].cha = maxx-cmax;
			sum+=maxx;
			++num[maxxx];
		}
		int ss = 0;
		if(num[ss]<num[1])
		{
			ss = 1;
		}
		if(num[ss]<num[2])
		{
			ss = 2;
		}
		if((num[ss]<<1)<=n)
		{
			cout << sum << '\n';
			continue;
		}
		int cnt = 0;
		for(int i = 1;i<=n;++i)
		{
			if(a[i].maxxx == ss)
			{
				b[++cnt] = a[i].cha;
			}
		}
		sort(b+1,b+cnt+1);
		int s = num[ss]-n/2;
		for(int i = 1;i<=s;++i)
		{
			sum-=b[i];
		}
		cout << sum << '\n';
	}
	return 0;
}

T2

小小优化题。

看到 \(k \le 10\),直接考虑状压。

首先有一个朴素做法,直接建边,然后对于所有状态限制它的连边,直接做就行了。发现复杂度是 \(O(2^k (m+kn) \log (m+kn))\),直接爆炸。

考虑第一个优化,首先我们一开始没建乡镇之前跑一遍最小生成树,此时不再最小生成树之内的边之后一定是没有贡献的,所以我们的边数可以从 \(m+kn\) 变成 \(n-1+kn\),此时复杂度是 \(O(2^k (n-1+kn) \log (n-1+kn))\),还是炸。

考虑第二个优化,我们根本没有必要每次都排序,直接在枚举状态之前跑一遍就行了,复杂度是 \(O(2^k (n-1+kn))\),此时已经可以通过。

但是可以更优,因为对于每次跑最小生成树,设有 \(num\) 个乡镇可以用,然后其实当边数是 \(n+num-1\) 时,就可以退了,因为树的边数就是这个。

跑得飞快!

代码:

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
const int N = 1.15e6+5;
struct node
{
    int x;
    int y;
    int w;
}a[N];
int b[15][N];
int fa[N];
int f(int x)
{
    return x == fa[x]?x:fa[x] = f(fa[x]);
}
int cmp(node x,node y)
{
    return x.w<y.w;
}
signed main()
{
    ios::sync_with_stdio(0);
    cin.tie(0);
    cout.tie(0);
    int n,m,k;
    cin >> n >> m >> k;
    for(int i = 1;i<=n;++i)
    {
        fa[i] = i;
    }
    for(int i = 1;i<=m;++i)
    {
        cin >> a[i].x >> a[i].y >> a[i].w;
    }
	stable_sort(a+1,a+m+1,cmp);
    long long sum = 0;
    int tot = 0;
    for(int i = 1;i<=m;++i)
    {
        int x = f(a[i].x),y = f(a[i].y);
        if(x!=y)
        {
            fa[x] = y;
            sum+=a[i].w;
            ++tot;
            a[tot] = a[i];
            if(tot == n-1)
            {
            	break;
			}
        }
	}
	m = tot;
	tot = 0;
    for(int i = 1;i<=k;++i)
    {
        cin >> b[i][0];
        for(int j = 1;j<=n;++j)
        {
            cin >> b[i][j];
            ++tot;
            a[m+tot] = {j,n+i,b[i][j]};
        }
    }
    stable_sort(a+1,a+m+tot+1,cmp);
    int zong = (1<<k)-1;
    for(int i = 1;i<=zong;++i)
    {
    	long long res = 0;
    	int num = 0;
    	for(int j = 1;j<=k;++j)
    	{
    		if(i&(1<<j-1))
    		{
    			res+=b[j][0];
    			++num;
			}
		}
		for(int j = 1;j<=n+k;++j)
		{
			fa[j] = j;
		}
		int cnt = 0;
		for(int j = 1;j<=m+tot;++j)
	    {
	    	if(a[j].y>n&&!(i&(1<<a[j].y-n-1)))
	    	{
	    		continue;
			}
	        int x = f(a[j].x),y = f(a[j].y);
	        if(x!=y)
	        {
	            fa[x] = y;
	            res+=a[j].w;
	            ++cnt;
	            if(cnt == n+num-1)
	            {
	            	break;
				}
	        }
		}
		sum = min(sum,res);
	}
	cout << sum;
    return 0;
}

T3

哇!数据真水!

正解太难,考虑乱搞。

观察到题目的这个条件,\(\sum_{i = 1}^n |s_{i,0}|+|s_{i,1}| \le 5 \times 10^6\),那么显然所有匹配方案的长度种类数不会超过 \(\sqrt{5 \times 10^6}\),证明后面说,那么既然长度只有根号级别......嘿嘿,没错,考虑对于每一个询问直接枚举匹配左端点 \(l\) 然后枚举长度 \(k\),那么 \(r = l+k-1\),直接采用回滚哈希进行检测就行了(后续除了证明之外还有后话,先别走)。

补一下刚刚那个结论的证明:

因为 \(|s_{i,0}| = |s_{i,1}|\),所以:

\[\sum_{i = 1}^n |s_{i,0}|+|s_{i,1}| \le 5 \times 10^6 \]

\[= \sum_{i = 1}^n 2|s_{i,0}| \le 5 \times 10^6 \]

\[= 2\sum_{i = 1}^n |s_{i,0}| \le 5 \times 10^6 \]

\[= \sum_{i = 1}^n |s_{i,0}| \le 2.5 \times 10^6 \]

考虑什么情况下可以使得 \(|s_{i,0}|\) 的种类数最多,首先重复肯定是不优的,然后你发现最坏情况是 \(1,2,3,\dots\),这样不会重复,而且和是最小的方案,但是你很容易看到这是等差数列,直接 \(\frac{n(n-1)}{2}\),然后也就是:

\[= max\{m\},\frac{m(m-1)}{2} \le 2.5 \times 10^6,m \in \mathbb{N} \]

\[= max\{m\},m(m-1) \le 5 \times 10^6,m \in \mathbb{N} \]

考虑 \(m\) 的最大取值其实就是 \(\sqrt{5 \times 10^6}\),得证。

后话:仔细思考......不对啊,这么说的话,复杂度应该是 \(O(L_1\sqrt{L_2})\),然而 \(L_1,L_2 \le 5 \times 10^6\),这不是必炸吗!但是其实你要想到造数据的人哪有那么阴险,\(L_1,L_2\) 不会那么卡点取值,而且,你要想到我们可以在询问中加一个特判 \(|t_{i,0}| \not= |t_{i,1}|\) 就直接输出 \(0\),这样可以大大加快速度,不仅如此,你写代码时发现其实可以加特判,可以叉掉一些东西,先看这个特判:

cpp 复制代码
if(pre[i-1][0]!=pre[i-1][1])
{
	break;
}

就是在查询的时候枚举左端点 \(l\) 时(我代码里把 \(l\) 写成变量 \(i\)),判一下我们存下来的前缀哈希从 \(1 \sim l-1\) 中 \(t_{i,0}\) 和 \(t_{i,1}\) 是否完全相同,不相同直接 break,原理显然,因为我们只能匹配一次,所以不在 \(l \sim r\) 范围之内的字符必须相同才行,不然根本替换不了(这不是显然叉掉了一堆情况吗)。这是你可能会想问 unordered_map 的常数问题,虽然查找和插入的确有常数,但是我们还可以加一个这个针对查找的特判:

cpp 复制代码
if(pre1[r+1][0]!=pre1[r+1][1])
{
	continue;
}

原理不多说了,这样可以减少 unordered_map 的查找次数,同时,还有一个这个特判:

cpp 复制代码
if(r>len)
{
    break;
}

这个特判没什么好说了,但是不要小看它的威力,也是可以积少成多地叉掉一些没用的东西的。

代码:

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
const int N = 2e5+5;
const int M = 5e6;
const unsigned long long base = 131;
struct node
{
    template<typename T, typename U>
    size_t operator()(const pair<T,U>&p)const
    {
        return hash<T>()(p.first)^hash<U>()(p.second);
    }
};
struct node1
{
    template<typename T, typename U>
    bool operator()(const pair<T,U>&p1,const pair<T,U>&p2)const
    {
        return p1.first == p2.first&&p1.second == p2.second;
    }
};
unordered_map<pair<unsigned long long,unsigned long long>,int,node,node1>mp[N];
string s[N][2];
int num[N];
int biao[M];
unsigned long long pre[M][2],pre1[M][2],ha[N],hb[N],p[M];
int ji[N];
signed main()
{
    ios_base::sync_with_stdio(0);
    cin.tie(0);
    cout.tie(0);
    p[0] = 1;
    for(int i = 1;i<=M;++i)
    {
        p[i] = p[i-1]*base;
    }
    int n,_,cnt = 0;
    cin >> n >> _;
    for(int i = 1;i<=n;++i)
    {
        cin >> s[i][0] >> s[i][1];
        num[++cnt] = s[i][0].size();
        int len = num[cnt];
        s[i][0] = ' '+s[i][0];
        s[i][1] = ' '+s[i][1];
        for(int j = 1;j<=len;++j)
        {
            ha[i] = ha[i]*base+s[i][0][j];
            hb[i] = hb[i]*base+s[i][1][j];
        }
    }
    stable_sort(num+1,num+cnt+1);
    int tot = unique(num+1,num+cnt+1)-num-1;
    for(int i = 1;i<=tot;++i)
    {
        biao[num[i]] = i;
        ji[i] = num[i];
    }
    for(int i = 1;i<=n;++i)
    {
        ++mp[biao[s[i][0].size()-1]][{ha[i],hb[i]}];
    }
    while(_--)
    {
        string a,b;
        cin >> a >> b;
        if(a.size()!=b.size())
        {
            cout << 0 << '\n';
            continue;
        }
        int len = a.size();
        a = ' '+a;
        b = ' '+b;
        pre[0][0] = pre[0][1] = pre1[len+1][0] = pre1[len+1][1] = 0;
        for(int i = 1;i<=len;++i)
        {
            pre[i][0] = pre[i-1][0]*base+a[i];
            pre[i][1] = pre[i-1][1]*base+b[i];
        }
        for(int i = len;i;--i)
        {
            pre1[i][0] = pre1[i+1][0]*base+a[i];
            pre1[i][1] = pre1[i+1][1]*base+b[i];
        }
        int ans = 0;
        for(int i = 1;i<=len;++i)
        {
            if(pre[i-1][0]!=pre[i-1][1])
            {
                break;
            }
            for(int j = 1;j<=tot;++j)
            {
                int l = i,r = i+ji[j]-1;
                if(r>len)
                {
                    break;
                }
                if(pre1[r+1][0]!=pre1[r+1][1])
                {
                    continue;
                }
                unsigned long long hsa = pre[r][0]-pre[l-1][0]*p[r-l+1],hsb = pre[r][1]-pre[l-1][1]*p[r-l+1];
                if(mp[j].find({hsa,hsb})!=mp[j].end())
                {
                    ans+=mp[j][{hsa,hsb}];
                }
            }
        }
        cout << ans << '\n';
    }
    return 0;
}

最后:

如果有人能卡掉我的这份代码(洛谷机子和洛谷时限),请私信交流。