CF1574F Occurrences

前置知识

并查集,dp

思路

  1. 首先我们发现,对于一个限制最严的限制是单个字符的出现次数。这启示我们字符串中不能出现相同的字符同时每选择一个就必须将整个字符串一起放上去。但是这个整个字符可能不只是给出的,比如给了 \(AB\)\(BC\) 那么一定需要一起选。
  2. 这启示我们,对于每个字符串维护前驱和后继,将所有个字符串分成以一条条链,这些一定要一起选,同时如果有分叉就不可行,一定是一条链 。做到这一步,其实你已经会了,接下来,把这些链像物品一样放进背包(做dp)就行。
  3. 这里有一个巨大的问题。时间复杂度,到底有多少条链,粗估一下上界,你会发现好像不对。但是细想,链唯一有用的是长度,我们不关心它的其他性质,那么问题变为有多少不同长度,显然只有 \(\sqrt n\) 种。why ? 我们知道大于 \(\sqrt n\) 的链至多有 \(\sqrt n\) 条。

代码

#include <bits/stdc++.h>
using namespace std;
const int N=3e5+10,mod=998244353; 
int fa[N],vis[N],siz[N],n,m,k,a[N],pre[N],to[N],cnt[N],wz[N],tot,dp[N]; 
int find(int x)
{
	if(fa[x]==x)return x;
	return fa[x]=find(fa[x]);
}
void merge(int x,int y)
{
	int f1=find(x),f2=find(y);
	if(f1==f2)//出现环 
	{
		vis[f1]=1;//扔掉
		return; 
	}
	fa[f2]=f1;
	siz[f1]+=siz[f2];
	vis[f1]|=vis[f2];
} 
int main()
{
	ios::sync_with_stdio(0);
	cin.tie(0);cout.tie(0);
	cin>>n>>m>>k;
	for(int i=1;i<=k;i++)fa[i]=i,siz[i]=1;
	for(int i=1;i<=n;i++)
	{
		int len;cin>>len;
		for(int j=1;j<=len;j++)
		{
			cin>>a[j];
			if(j==1)continue;
			if((pre[a[j]]&&pre[a[j]]!=a[j-1])||(to[a[j-1]]&&to[a[j-1]]!=a[j])) //出现分叉 
			{
				merge(a[j],a[j-1]);//将两个和起来
				vis[find(a[j])]=1;//一起扔掉 
			}
			else if(!to[a[j-1]])
				{
					pre[a[j]]=a[j-1];
					to[a[j-1]]=a[j];//更新前后缀
					merge(a[j-1],a[j]); 
				} 
		} 
	}
	for(int i=1;i<=k;i++)
		if(fa[i]==i&&!vis[i])cnt[siz[i]]++;
	for(int i=1;i<=k;i++)
		if(cnt[i])wz[++tot]=i;
	dp[0]=1;
	for(int i=0;i<=m;i++)
		for(int j=1;j<=tot&&i+wz[j]<=m;j++)
			dp[i+wz[j]]=(dp[i+wz[j]]+1ll*dp[i]*cnt[wz[j]]%mod)%mod;
	cout<<dp[m]<<'\n';
	return 0; 
} 
posted @ 2025-05-09 08:28  exCat  阅读(9)  评论(0)    收藏  举报