【集训】组合计数与构造专题
组合计数:
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\),转移如下:
-
在 \(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
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;
}