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

组合计数:

CF1824B2

  • \(k\) 为奇数时,注意到每次好点移动一格至少会增加 $ \lfloor \frac{k}{2} \rfloor + 1 - \lfloor \frac{k}{2} \rfloor$ 的长度,所以好点个数为 \(1\)
  • \(k\) 为偶数时,注意到好点一定在一条链上,我们计算出有多少条边 \((u,v)\) 满足 \(u\)\(v\) 为好点,答案就是边数 \(+1\)
    可以得到,必须两侧的子树大小为 \(\frac{k}{2}\) 时,才能满足
    具体的,左侧子树大小为 \(x\) ,右侧子树大小为 \(n-x\),则方案数为 \(\binom{x}{\frac{k}{2}} \binom{n-x}{\frac{k}{2}}\)
    由于算的是期望,答案需除以总方案数,即\(\binom{n}{k}\)
#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

考虑分成两个集合 \(A\)\(B\),满足对于任意的 \(u \in A,v \in B\) 边的方向为 \(u \to v\)
\(f_{i,j,k}\) 为加入了 \(i\) 个 A集合中的点, \(j\)\(B\) 集合中的点, 有 \(k\) 条边满足条件的方案数
转移则考虑第 \(i+1\) 个点

考虑将 \(i + j + 1\) 号点加入到 \(A\)\(B\),转移如下:

\[f_{i + 1, j, k + x} \gets \binom ixf_{i, j, k} (0 \le x \le i) \]

\[\\ f_{i, j + 1, k + i + x} \gets \binom jxf_{i, j, k} (0 \le x \le j)\\ \]

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

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

时间复杂度 \(O(n^3m)\)

#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

满足以下性质:

  • 不能出现两个 \(< \frac{k}{2}\) 的数相邻;
  • 如果 \(x < \frac{k}{2}, y \ge \frac{k}{2}\),那么 \(|y - \frac{k}{2}| \ge |x - \frac{k}{2}|\)

考虑按照 \(|x - \frac{k}{2}|\) 从大到小插入,并且如果 \(|x - \frac{k}{2}|\) 相同,那么就让 \(\ge \frac{k}{2}\) 的数先插入,这样能保证不会出现第二种不合法情况。

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

  • 遇到 \(x < \frac{k}{2}\),出现次数为 y,那我们有 \(\binom{s}{y}\) 种方案插入,并且可用位置数减去 \(y\)
  • 遇到 \(x \ge \frac{k}{2}\),出现次数为 y,这个插入的方案数相当于,把这 \(y\) 个数划分成 \(s\) 段,允许有空段,把这 \(s\) 段依次插入。这个用插板法可得方案数为 \(\binom{s + y - 1}{s - 1}\),并且可用位置数加上 \(y\)

时间复杂度 \(O(n \log n)\)

#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\) 个,方案数是 \(2^x\)

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

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

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

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

查询时,枚举给出的 \(c_i\) 个数中,\(\leq 43\) 的质数的子集 \(T\),含义是必须不能包含 \(T\) 中的质数,必须包含给出的质数中 \(> 43\) 的,对于剩下的包含不包含均可;那么方案数就是枚举所有大质数 \(p\),如果 \(p\) 被钦定包含,乘上 \(2^{f_{p,U-T}} - 1\);否则乘上 \(2^{f_{p,U-T}}\)

每次枚举所有大质数是不行的,不过我们可以提前预处理 \(C_S = \sum_p f_{p,S}\),查询的时候先让 \(ans = 2^{C_S}\),再单独重算给出的质数的贡献。

时间复杂度 \(O(2^r(v + \sum c_i))\)

#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\),一定无解。

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

\(S1_i\) 为前 \(i\) 行一共有多少个 \(Y\)\(S2_i\) 为前 \(i\) 列一共有多少个 \(Y\)。如果要有解,横着砍完以后每一段都要有恰好 \(2q\)\(Y\),竖着每一段都要有恰好 \(2p\)\(Y\)。于是我们看一下 \(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

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

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

\(m_{i+1}\) 这一位上是 \(0\):那么 \(x_{i+1}\) 这一位还得顶上界,有 \(f(i,c,x)\times (m_{i+1}\bmod 2^j)\to 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

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

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

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

那么有三种构造方案:

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

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

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

显然这三种都能使局面变成平局,且操作总数为 \(k\),所以操作次数最少的方案一定 \(\leq \lfloor\frac{k}{3}\rfloor\)

#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

由于 \(i - n \le a_i \le i - 1\),所以 \(1 \le i - a_i \le n\)

建立一张 \(n\) 个点的有向图,对于每个点 \(i\),连边 \(i \to i - a_i\)

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

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

证明:

\(i\)\(to_i\) 连了边,由建图的方式得 \(to_i=i-a_i\)

一旦 \(S\) 形成了环,则 \(\sum_{i\in S}i=\sum_{i\in S}to_i\)

\(\sum_{i\in S}i=\sum_{i\in S}(i-a_i)\)

将等式右边展开得: \(\sum_{i\in S}i=\sum_{i\in S}i-\sum_{i\in S}a_i\)

显而易见, \(\sum_{i\in S}a_i=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 @ 2025-01-22 20:07  Star_F  阅读(31)  评论(0)    收藏  举报