【集训】组合计数与构造专题

组合计数:

CF1824B2

  • k 为奇数时,注意到每次好点移动一格至少会增加 k2+1k2 的长度,所以好点个数为 1
  • k 为偶数时,注意到好点一定在一条链上,我们计算出有多少条边 (u,v) 满足 uv 为好点,答案就是边数 +1
    可以得到,必须两侧的子树大小为 k2 时,才能满足
    具体的,左侧子树大小为 x ,右侧子树大小为 nx,则方案数为 (xk2)(nxk2)
    由于算的是期望,答案需除以总方案数,即(nk)
#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define int long long
const int N=200005,mod=1e9+7;
ll fac[N],ifac[N];
int h[N],e[N*2],ne[N*2],idx;
int n,k,s[N];
ll ans;
void add(int u,int v){
	e[++idx]=v,ne[idx]=h[u],h[u]=idx;
}
ll poww(ll a,ll b){
	ll res=1;
	while(b){
		if(b&1) res=res*a%mod;
		a=a*a%mod;
		b>>=1;
	}
	return res;
}
ll C(ll n,ll m){
	if(n<0||m<0||n<m) return 0;
	return fac[n]*ifac[m]%mod*ifac[n-m]%mod;
}
void init(int n){
	fac[0]=1;
	for(int i=1;i<=n;i++) fac[i]=fac[i-1]*i%mod;
	ifac[n]=poww(fac[n],mod-2);
	for(int i=n;i;i--) ifac[i-1]=ifac[i]*i%mod;
}
 
void dfs(int u,int f){
	s[u]=1;
	for(int i=h[u];i;i=ne[i]){
		int j=e[i];
		if(j==f) continue;
		
		dfs(j,u);
		s[u]+=s[j];
		ans=(ans+C(s[j],k>>1)*C(n-s[j],k>>1)%mod)%mod;
	}
} 
signed main(){
	cin>>n>>k;
	if(k&1) return cout<<1<<endl,0;
	for(int i=1;i<=n-1;i++){
		int u,v;
		cin>>u>>v;
		add(u,v),add(v,u);
	}
	init(n),dfs(1,0);
	 
	cout<<(ans*ifac[n]%mod*fac[k]%mod*fac[n-k]%mod+1)%mod; 
	return 0;
}

ARC163D

考虑分成两个集合 AB,满足对于任意的 uA,vB 边的方向为 uv
fi,j,k 为加入了 i 个 A集合中的点, jB 集合中的点, 有 k 条边满足条件的方案数
转移则考虑第 i+1 个点

考虑将 i+j+1 号点加入到 AB,转移如下:

fi+1,j,k+x(ix)fi,j,k(0xi)

fi,j+1,k+i+x(jx)fi,j,k(0xj)

  • A 内加入最大点 uuB 内的连边都是逆向的,向 A 内的连边任意。钦定 x 条为正向的,则系数为 (ix)

  • B 内加入最大点 uuA 内的连边都是正向的,向 B 内的连边任意。钦定 x 条为正向的,则系数为 (jx)

时间复杂度 O(n3m)

#include <bits/stdc++.h>
using namespace std;
const int N=31,M=N*N>>1,mod=998244353;
int n,m,C[M][M],f[N][N][M],ans; 
int main(){
	cin>>n>>m;
	C[0][0]=1;
	for(int i=1;i<=n*(n-1)/2;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; 
	}
	f[0][0][0]=1;
	for(int i=0;i<n;i++){
		for(int j=0,s=i;s<n;j++,s++){
			for(int k=0;k<=m;k++){
				for(int x=0;x<=i;x++)
					f[i+1][j][k+x]=(f[i+1][j][k+x]+1ll*C[i][x]*f[i][j][k])%mod;
				for(int x=0;x<=j;x++)
					f[i][j+1][k+i+x]=(f[i][j+1][k+i+x]+1ll*C[j][x]*f[i][j][k])%mod;
			}
		}
	}
	int ans=mod-C[n*(n-1)/2][m];
	for(int i=0,j=n;i<=n;i++,j--) ans=(ans+f[i][j][m])%mod;
	cout<<ans<<endl;
	return 0;
} 

ARC148E

满足以下性质:

  • 不能出现两个 <k2 的数相邻;
  • 如果 x<k2,yk2,那么 |yk2||xk2|

考虑按照 |xk2| 从大到小插入,并且如果 |xk2| 相同,那么就让 k2 的数先插入,这样能保证不会出现第二种不合法情况。

维护一个可用位置数 s,然后:

  • 遇到 x<k2,出现次数为 y,那我们有 (sy) 种方案插入,并且可用位置数减去 y
  • 遇到 xk2,出现次数为 y,这个插入的方案数相当于,把这 y 个数划分成 s 段,允许有空段,把这 s 段依次插入。这个用插板法可得方案数为 (s+y1s1),并且可用位置数加上 y

时间复杂度 O(nlogn)

#include <bits/stdc++.h>
using namespace std;
#define int long long
#define ll long long
#define PII pair<ll,ll>
#define fi first
#define se second
const int N=200005,mod=998244353;
ll poww(ll a,ll b){
	ll res=1;
	while(b){
		if(b&1) res=res*a%mod;
		a=a*a%mod;
		b>>=1;
	}
	return res;
}

ll n,m,a[N],fac[N],ifac[N],idx;
PII b[N];

ll C(ll n,ll m){
	if(n<0||m<0||n<m) return 0;
	return fac[n]*ifac[m]%mod*ifac[n-m]%mod;
} 

void init(ll n){
	fac[0]=1;
	for(int i=1;i<=n;i++) fac[i]=fac[i-1]*i%mod;
	ifac[n]=poww(fac[n],mod-2);
	for(int i=n;i>=1;i--) ifac[i-1]=ifac[i]*i%mod;
}

bool cmp(PII a,PII b){
	return abs(a.fi*2-m)>abs(b.fi*2-m)||(abs(a.fi*2-m)==abs(b.fi*2-m)&&a.fi*2>m);
}

signed main(){
	cin>>n>>m;
	map<ll,ll> mp;
	for(int i=1;i<=n;i++)
		cin>>a[i],mp[a[i]]++;
	for(PII p : mp)
		b[++idx]=p;
	init(n);
	sort(b+1,b+idx+1,cmp);
	
	ll ans=1,s=1;
	for(int i=1;i<=idx;i++){
		if(b[i].fi*2<m){
			ans=ans*C(s,b[i].se)%mod;
			s-=b[i].se;
		} 
		else{
			ans=ans*C(s+b[i].se-1,s-1)%mod;
			s+=b[i].se;
		}
	} 
	cout<<ans<<endl;
	return 0;
}

P8292 [省选联考 2022] 卡牌

考虑容斥,钦定 T 集合内质因子不被选中,如果质因子集合与 T 无交的数有 x 个,方案数是 2x

自然不能 O(2ci) 枚举所有 T,不过数的范围只有 2000,因而至多只有一个 >43 的质因子,而 43 的质数只有 13 个。

另一方面,如果给出的所有质数都 >43,那么所有 si 至多包含它们中的一个,于是我们只需要把所有数按照它们包含的大质数分类,对于包含的大质数为 p 的所有数,设有 fp 个,如果 p 被要求是乘积的因子,就把方案数乘上 2fp1;否则是 2fp

对于一般的情况,我们预处理 fp,S 表示包含的大质数为 p,且包含的小质数的集合是 S 的子集的数的个数。

这个直接暴力 O(v2r) 预处理就可以,其中 v=2000,r=13

查询时,枚举给出的 ci 个数中,43 的质数的子集 T,含义是必须不能包含 T 中的质数,必须包含给出的质数中 >43 的,对于剩下的包含不包含均可;那么方案数就是枚举所有大质数 p,如果 p 被钦定包含,乘上 2fp,UT1;否则乘上 2fp,UT

每次枚举所有大质数是不行的,不过我们可以提前预处理 CS=pfp,S,查询的时候先让 ans=2CS,再单独重算给出的质数的贡献。

时间复杂度 O(2r(v+ci))

#include <bits/stdc++.h>
using namespace std;
#define int long long
const int N=2005,mod=998244353;
int n,m,p2[1000005],cnt[N];
int popcnt[1<<14],f[1<<14][305],g[1<<14];
int p[20005],pri[N],vis[N],idx,id[N],ps[N],pb[N];

void init(){
	for(int i=2;i<=2000;i++){
		if(!vis[i]) pri[++idx]=i;
		for(int j=1;j<=idx&&i*pri[j]<=2000;j++){
			vis[i*pri[j]]=1;
			if(i%pri[j]==0) break;
		}
	}
	for(int i=1;i<=idx;i++) id[pri[i]]=i;
}
signed main(){

	init(); // 预处理质数
	cin>>n;
	p2[0]=1; 
	for(int i=1;i<=n;i++) p2[i]=p2[i-1]*2%mod;   // 2 的幂
	for(int i=1;i<=n;i++){
		int tmp;
		cin>>tmp;
		cnt[tmp]++;
	}
	
	//ps[i] 表示 i 所有的小质数的状态,pb[i] 表示 i 的大质数 
	for(int i=1;i<=2000;i++){
		int x=i;
		for(int j=1;j<=13;j++){
			if(x%pri[j]==0) ps[i]|=1<<(j-1);
			while(x%pri[j]==0) x/=pri[j];
		}
		pb[i]=x;
	}
	pb[43*43]=43;
	//popcnt[i] i 的二进制表示的 1 的个数 
	for(int s=0;s<1<<13;s++){
		popcnt[s]=popcnt[s>>1]+(s&1);
		for(int i=1;i<=2000;i++)
			if((ps[i]&s)==0) f[s][id[pb[i]]]+=cnt[i],g[s]+=cnt[i];
	}
	
	cin>>m;
	
	vector<int> v;
	while(m--){
		int c,now=0;cin>>c;
		v.clear();
		for(int i=1;i<=c;i++){
			cin>>p[i];
			if(p[i]<=41) now|=1<<(id[p[i]]-1);
			else v.push_back(id[p[i]]);
		}
		int ans=0;
		for(int s=now,pre=1;pre;pre=s,s=(s-1)&now){
			int val=1,cnt=g[s];
			for(int x:v){
				int y=f[s][x];
				cnt-=y;
				val=val*(p2[y]-1)%mod; 
			}
			val=val*p2[cnt]%mod;
			if(popcnt[s]&1) val=mod-val;
			ans=(ans+val)%mod; 
		}
		cout<<ans<<endl;
	}
	
	
	return 0;
} 

ARC157D

如果一共有奇数个 Y,一定无解。

否则假设一共有 SY,那么一共就要分为 S2 块。然后我们枚举横竖各砍几刀,假设横着砍 p 刀,竖着砍 q 刀,接着我们要判断这样砍是否有解,以及如果有解,一共有多少砍法。

S1i 为前 i 行一共有多少个 YS2i 为前 i 列一共有多少个 Y。如果要有解,横着砍完以后每一段都要有恰好 2qY,竖着每一段都要有恰好 2pY。于是我们看一下 S1 是否包含所有 2q 的倍数,S2 是否包含所有 2p 的倍数。然后我们就可以得到一组解,然后我们用二维前缀和 check 一下是否每个块都是两个 Y。接着我们统计一下每个倍数有几个,然后乘起来就是答案。

#include<bits/stdc++.h>
using namespace std;
#define int long long
const int N=2005,mod=998244353;
int n,m,tot,ans;
int s1[N],s2[N],g[N],f1[N],f2[N],p1[N],p2[N],sum[N][N];
char s[N][N];
int S(int x1,int y1,int x2,int y2){
	return sum[x2][y2]-sum[x1][y2]-sum[x2][y1]+sum[x1][y1];
}
int cnt1[N],cnt2[N];
void solve(int k1){
	int k2=tot*2/k1,c1=0,c2=0;
	for(int i=1;i<=n;i++) if(s1[i]!=s1[i-1]&&s1[i]%k1==0) p1[++c1]=i;
	for(int i=1;i<=m;i++) if(s2[i]!=s2[i-1]&&s2[i]%k2==0) p2[++c2]=i;
	if(c1<<1!=k2||c2<<1!=k1) return;
	for(int i=1;i<=c1;i++)
		for(int j=1;j<=c2;j++)
			if(S(p1[i-1],p2[j-1],p1[i],p2[j])!=2) return;
	for(int i=1;i<=c1;i++) cnt1[i]=0;
	for(int i=1;i<=c2;i++) cnt2[i]=0;
	for(int i=1;i<=n;i++) if(s1[i]%k1==0) cnt1[s1[i]/k1]++;
	for(int i=1;i<=m;i++) if(s2[i]%k2==0) cnt2[s2[i]/k2]++;
	int res=1;
	for(int i=1;i<c1;i++) res=res*cnt1[i]%mod;
	for(int i=1;i<c2;i++) res=res*cnt2[i]%mod;
	ans=(ans+res)%mod;
}
signed main(){
    cin>>n>>m;
	for(int i=1;i<=n;i++){
	    cin>>s[i]+1;
		for(int j=1;j<=m;j++){
			sum[i][j]=sum[i-1][j]+sum[i][j-1]-sum[i-1][j-1];
			if(s[i][j]=='Y') s1[i]++,s2[j]++,sum[i][j]++;
		}
	}
	for(int i=1;i<=n;i++) s1[i]+=s1[i-1];
	for(int i=1;i<=m;i++) s2[i]+=s2[i-1];
	tot=s1[n];
	if(tot&1) return cout<<0,0;
	for(int i=2;i<=n*m;i+=2) if(tot%i==0) solve(i);
	cout<<ans<<endl;
	return 0;
}

P3447 [POI2006] KRY-Crystals

发现只要有某个 xi 的第 j 位没有顶到上界,那么其他的 xk 在第 j 位之后就不需要考虑那个异或的限制了,因为这个 xi 总能有(也是唯一的)方案来满足其他 xk 在这一位上的异或约束。

考虑枚举第一次出现某个 xi 不顶上界的位(那么这要求前面的位上 mi 各自的前缀异或起来恰好和 k 的前缀一样)。这一位上可能会有不止一个 xi 不顶上界,并且需要满足这一位上的异或约束。考虑 DP:f(i,0/1,0/1) 表示考虑了前 i 个数,异或和为 0/1,是否选过不顶上界的数,的方案数。如果枚举的是第 j 位,有转移:

mi+1 这一位上是 0:那么 xi+1 这一位还得顶上界,有 f(i,c,x)×(mi+1mod2j)f(i+1,c,x)

#include<bits/stdc++.h>
#define ull unsigned long long
const ull mod=1ull<<64;
void add(ull &x,ull v){ x+=v; if(x>=mod) x-=mod;}
using namespace std;
const int N=55;
ull f[N][2][2],lim[N],n,X;
signed main(){
  	cin>>n,X=0;
	ull ans=0,now=0;
	for(int i=1;i<=n;i++) cin>>lim[i],lim[i]++,now^=lim[i];
	
	for(int w=31;w>=0;w--){
		bool flag=1;
		for(int i=31;i>w;i--) 
		if((now^X)&(1<<i)){
			flag=0; break;
		}
		if(!flag) break;
		int U=(1<<w)-1;
		memset(f,0,sizeof(f));
		f[0][0][0]=1;
		for(int i=0;i<n;i++) 
		for(int c=0;c<2;c++) 
		for(int x=0;x<2;x++) 
		if(f[i][c][x]){
			if(lim[i+1]&(1<<w)){
				add(f[i+1][c^1][x],1ll*f[i][c][x]*((lim[i+1]&U)));
				add(f[i+1][c][1],1ll*f[i][c][x]*(x?(U+1):1ll));
			}
			else add(f[i+1][c][x],1ll*f[i][c][x]*((lim[i+1]&U)));
		}
		if(X&(1<<w)) add(ans,f[n][1][1]);
		else add(ans,f[n][0][1]);
	}
	cout<<ans-1<<endl;
	return 0;
}

构造:

CF1450C2

发现操作次数至多 k3 满足 抽屉原理 的格式,那么应用这个原理,问题就转化为了构造三个方案,使每个方案都为平局且操作总数为 k

将所有格子分成三类,第 i (i[0,3)) 类包含所有的格子 (x+y)mod3=i

不难发现只要一类格子全是 X,另一类格子全是 O 就合法。

那么有三种构造方案:

  • 把第 0 类格子上的 X 全改为 O,第 1 类格子上的 O 全改为 X

  • 把第 1 类格子上的 X 全改为 O,第 2 类格子上的 O 全改为 X

  • 把第 2 类格子上的 X 全改为 O,第 0 类格子上的 O 全改为 X

显然这三种都能使局面变成平局,且操作总数为 k,所以操作次数最少的方案一定 k3

#include <bits/stdc++.h>
using namespace std;
const int N=305;
int T,n,k,cnt1,cnt2,cnt3;
int uid[N][N];
char mp[N][N],ans1[N][N],ans2[N][N],ans3[N][N];
 
void print(char s[N][N]){
	for(int i=1;i<=n;i++){
		for(int j=1;j<=n;j++) cout<<s[i][j];
		cout<<endl;
	}
}
 
void init(){
	memset(mp,'0',sizeof(mp)),memset(ans1,'0',sizeof(ans1)),memset(ans2,'0',sizeof(ans2)),memset(ans3,'0',sizeof(ans3)),
	k=cnt1=cnt2=cnt3=0;
}
int main(){
	cin>>T;
	while(T--){
		cin>>n;
		for(int i=1;i<=n;i++){
			for(int j=1;j<=n;j++){
				char ch=getchar();
				while(ch!='O'&&ch!='X'&&ch!='.') ch=getchar();
				if(ch!='.') k++;
				mp[i][j]=ch;
				uid[i][j]=(i+j)%3;
			}
		}
		memcpy(ans1,mp,sizeof(mp)),memcpy(ans2,mp,sizeof(mp)),memcpy(ans3,mp,sizeof(mp));
			for(int i=1;i<=n;i++)
				for(int j=1;j<=n;j++){
					if(uid[i][j]==0 && mp[i][j]=='O') ans1[i][j]='X',cnt1++;
					if(uid[i][j]==1 && mp[i][j]=='X') ans1[i][j]='O',cnt1++;
				}
			for(int i=1;i<=n;i++)
				for(int j=1;j<=n;j++){
					if(uid[i][j]==1 && mp[i][j]=='O') ans2[i][j]='X',cnt2++;
					if(uid[i][j]==2 && mp[i][j]=='X') ans2[i][j]='O',cnt2++;
				}
			for(int i=1;i<=n;i++)
				for(int j=1;j<=n;j++){
					if(uid[i][j]==2 && mp[i][j]=='O') ans3[i][j]='X',cnt3++;
					if(uid[i][j]==0 && mp[i][j]=='X') ans3[i][j]='O',cnt3++;
				}
			if(cnt1<=k/3) print(ans1);
			else if(cnt2<=k/3) print(ans2);
			else if(cnt3<=k/3) print(ans3);
			init();			
	}	
	return 0;
}

CF618F

dl题解

CF1270G

由于 inaii1,所以 1iain

建立一张 n 个点的有向图,对于每个点 i,连边 iiai

这张图中每个点的出度都为 1,因此这张图是一个基环内向森林。

可以发现对于任意一个环,环上的点对应的下标就是我们要找的答案。

证明:

itoi 连了边,由建图的方式得 toi=iai

一旦 S 形成了环,则 iSi=iStoi

iSi=iS(iai)

将等式右边展开得: iSi=iSiiSai

显而易见, iSai=0

证毕!

#include <bits/stdc++.h>
using namespace std;
const int N=1e6+10;
int n,a[N],vis[N];
vector<int>ans;

inline int rd()
{
	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-48;ch=getchar();}
	return x*f;
}
int main(){
    ios::sync_with_stdio(false);
    cin.tie(NULL),cout.tie(NULL);
	int T=rd();
	while(T--){
		n=rd();
		for(int i=1;i<=n;i++){
			a[i]=rd();
			a[i]=i-a[i];
			vis[i]=0;
		}
		int x=1;
		while(!vis[x]){
			vis[x]=1;
			x=a[x];
		}
		ans.push_back(x); x=a[x];
		while(x!=ans[0]){
			ans.push_back(x);
			x=a[x];
		}
		cout<<ans.size()<<endl;
		while(ans.size()){
			cout<<ans.back()<<" ";
			ans.pop_back();
		}
		cout<<endl;
	}
	return 0;
}

P6775 [NOI2020] 制作菜品

posted @   Star_F  阅读(3)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· NetPad:一个.NET开源、跨平台的C#编辑器
· PowerShell开发游戏 · 打蜜蜂
· 凌晨三点救火实录:Java内存泄漏的七个神坑,你至少踩过三个!
点击右上角即可分享
微信分享提示