计数集锦1

计数题无处无处不在,方式五花八门。

前置知识

组合数。请注意上下位置,这是 \(n\) 个中选 \(m\) 个。

\[C^m_n=C(n,m)=\begin{pmatrix}n\\m\end{pmatrix}=\frac{n!}{m!(n-m)!} \]

组合数递推。

\[C^m_n=C^{m-1}_{n-1}+C^{m-1}_n \]

二项式定理,很多组合数的求和可以将其转化次幂的形式。

\[(a+b)^n=\sum_{i=0}^n\begin{pmatrix}n\\m\end{pmatrix}a^ib^{n-i} \]

开始

CF660E Different Subsets For All Tuples

因为不能算重,所以枚举每种子序列第一次出现的位置。一维用来枚举长度,一维枚举末尾结束的位置。组合数维护子序列各个数字出现的位置,之所以有 \((m-1)^{j-i}\) 是因为子序列每个数按时出现的要求是与子序列上个数之间不可以有相同值。

\[ans=\sum_{i=1}^n\sum_{j=i}^n\begin{pmatrix}j-1\\i-1\end{pmatrix}m^{n-j+i}(m-1)^{j-i} \]

此处略去空序列的 \(m^n\) 贡献。

\[\sum_{j=1}^n\sum_{i=1}^{j}\begin{pmatrix}j-1\\i-1\end{pmatrix}m^{n-j+i}(m-1)^{j-i} \]

\[\sum_{j=0}^{n-1}\sum_{i=0}^j\begin{pmatrix}j\\i\end{pmatrix}m^{n-j+i}(m-1)^{j-i} \]

\[\sum_{j=0}^{n-1}m^{n-j}\sum_{i=0}^jm^i(m-1)^{j-i} \]

\[\sum_{j=0}^{n-1}m^{n-j}(2m-1)^j \]

然后就可以 \(O(n\log n)\) 计算了。

P1357 花园

由于 \(m\) 很小,所以可以暴力确定转移方式,我们可以处理出一个转移矩阵 \(B\)。可是如何面对环形问题?可以枚举前 \(m\) 个位置的状态 \(s\),然后将初始矩阵 \(A_s\) 设为 \(1\),然后这种答案就是 \((A\times B^n)_s\),目的是完全与之前重合。

#include <bits/stdc++.h>
#define int long long
#define pp __builtin_popcount
using namespace std;
const int N=2.5e5+5,P=1e9+7;
int n,m,k,ans;
struct mat{
	int a[16][16];
	void init(){
		memset(a,0,sizeof(a));
	}
}f,g;
mat operator *(mat x,mat y){
	mat z;z.init();
	for(int i=0;i<(1<<m);++i)
		for(int j=0;j<(1<<m);++j)
			for(int k=0;k<(1<<m);++k)
				(z.a[i][j]+=x.a[i][k]*y.a[k][j])%=P;
	return z;
}mat qp(mat x,int y){
	mat z=x;--y;
	for(;y;y>>=1,x=x*x)
		if(y&1)z=z*x;
	return z;
}signed main(){
	ios::sync_with_stdio(0);
	cin.tie(0),cout.tie(0);
	cin>>n>>m>>k;--m;
	for(int i=0;i<(1<<m);++i)
		if(pp(i)<=k)f.a[i][i>>1]=1,f.a[i][(i>>1)|(1<<m-1)]=(pp(i)<k);
	f=qp(f,n);
	for(int i=0;i<(1<<m);++i)
		if(pp(i)<=k){
			g.init();g.a[0][i]=1;g=g*f;
			(ans+=g.a[0][i])%=P;
		}
	cout<<ans<<'\n';
	return 0;
}

CF856C Eleventh Birthday

一个数能被 \(11\) 整除的充要条件是其奇数位之和与偶数位之和的差是 \(11\) 倍数。

我们求出每个数的权值,(如 \(val(12345)=1+3+5-2-4,val(907)=9+7-0\))。对于其在最终所在位置的奇偶同,其 \(val\) 会产生正负的贡献。

考虑到数位为奇数的(如 \(131,10001\))与数位为偶数的(如 \(4005,114514\)),影响不同,我们对其分别计数再合并。

\(f_{i,j,k}\) 表示前 \(i\) 个数,有 \(j\) 个数产生正贡献,贡献和 \(\equiv k\pmod{11}\),更据贡献的正负选择从 \(j/j-1\) 转移。第一维可以滚动。

		cin>>n;n1=n2=ans=0;
		for(int i=1,x,y;i<=n;++i){
			cin>>x;y=0;for(int j=x;j;j/=10)y^=1;
			if(y)a[++n1]=x%11;else b[++n2]=x%11;
		}memset(f,0,sizeof(f));memset(g,0,sizeof(g));
		f[0][0][0]=g[0][0][0]=1;
		for(int i=1;i<=n1;++i){
			memset(f[i&1],0,sizeof(f[i&1]));
			for(int j=0;j<=i;++j)
				for(int k=0;k<11;++k){
					int p=(k-a[i]+11)%11,q=(k+a[i])%11;
					f[i&1][j][k]=f[(i-1)&1][j][p];
					if(j)(f[i&1][j][k]+=f[(i-1)&1][j-1][q])%=P;
				}
		}for(int i=1;i<=n2;++i){
			memset(g[i&1],0,sizeof(g[i&1]));
			for(int j=0;j<=i;++j)
				for(int k=0;k<11;++k){
					int p=(k-b[i]+11)%11,q=(k+b[i])%11;
					g[i&1][j][k]=g[(i-1)&1][j][p];
					if(j)(g[i&1][j][k]+=g[(i-1)&1][j-1][q])%=P;
				}
		}

然后是合并,奇数位的数字正贡献数量是固定的 \(\lfloor\frac{n1}2\rfloor\),偶数位的要枚举,设其正贡献有 \(i\) 个,再枚举 \(j\in [0,10]\)对答案的贡献为 \(f_{n1,\lfloor\frac{n1}2\rfloor,j}\times g_{n2,i,11-j}\times(\lfloor\frac{n1}2\rfloor)!\times (\lceil\frac{n1}2\rceil)!\times h(i,\lceil\frac{n1}2\rceil)\times h(n2-i,\lfloor\frac{n1}2\rfloor)\)

其中 \(h(i,j)\) 为将 \(i\) 个不同数分成 \(j\) 段(可以有空)的方案数,其等于 \(i!\times C^{j-1}_{i+j-1}\)

        for(int i=0;i<=n2;++i)
			for(int j=0;j<11;++j)
				ans=(ans+g[n2&1][i][j]*f[n1&1][n1/2][(11-j)%11]%P*fac[n1/2]%P*fac[n1-n1/2]%P*calc(i,n1-n1/2)%P*calc(n2-i,n1/2+1))%P;

P13090 [FJCPC 2025] XCPC

先不考虑 \(i\) 个牌子,我们有:

\[8a_1+4a_2+2a_3+a_4=n\\ 4a_1+3a_2+2a_3+a_4\ge p \]

然后枚举 \(a_1,a_2\),下面用 \(x,y\) 代替,发现 \(a_3,a_4\) 是可以消掉的,配合 \(a_3,a_4\ge 0\) 可得:

\[8x+4y\le n\\ n-4x-y\ge p \]

所以考虑剩余全是铁,铜的极端情况,所有 \(i\in[x+y+\lfloor\frac{n-8x-4y+1}2\rfloor,x+y+(n-8x-4y)]\) 的答案都可以得到 \(1\) 的贡献。整理一下,也就是 \([\lfloor\frac{n+1}2\rfloor-3x-y,n-7x-4y]\)

枚举两个时间显然会爆。假如我们枚举 \(y\)\(x\) 不变,这个区间左端点会不断减 \(1\),右端点会不断减 \(3\),所以对应到答案的差分数组上就是一个区间加(减)和每三个数加(减)。考虑再对数组分别作公差为 \(1,3\) 的差分,枚举 \(x\),就转化成单点加减了。

#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int N=1e6+5;
int n,p;ll a[N],b[N];
signed main(){
	ios::sync_with_stdio(0);
	cin.tie(0),cout.tie(0);
	cin>>n>>p;
	for(int x=0,t,y;x<=n;++x){
		y=min(n-x*4-p,(n-x*8)/4);if(y<0)break;
		t=(n+1)/2-x*3,++a[t-y],--a[t+1];
		t=n-x*7+1,--b[t-y*3],++b[t+3];
	}for(int i=1;i<=n;++i)
		a[i]+=a[i-1],b[i]+=(i>2?b[i-3]:0);
	ll s=a[0]+b[0];
	for(int i=1;i<=n;++i)
		cout<<(s+=a[i]+b[i])<<' ';
	return 0;
}

CF1276D Tree Elimination

题目让我们求数列的数量,实际上是求有多少操作方案。可以使用树形 dp 求解。

由于操作顺序按照边的编号决定,我们称 \(u<v\) 即为边 \((fa_u,u)\)\((fa_v,v)\) 之前。取消标记说得有点怪,我们直接说初始无标记,然后打标记算了。

设计状态 \(f_{u,0/1/2/3}\) 分别为 \(u\) 不被打标记;\(u\)\((fa_u,u)\) 之前打标记;\(u\)\((fa_u,u)\) 时打标记;\(u\)\((fa_u,u)\) 之后打标记。

先来考虑简单的 \(f_{u,0}\),对于一个 \(v\in son(x)\),如果 \(v\) 没有且不能被打标记,就会使得 \(u\) 被标记,所以 \(f_{v,0}\) 不考虑。如果 \(v\) 后来才被标记,一样 \(u\) 现在也会被标记,所以 \(f_{v,3}\) 不考虑。所以最终有:

\[f_{u,0}=\prod_{v\in son(u)}(f_{v,1}+f_{v,2}) \]

然后是较为特殊的 \(f_{u,2}\),所有 \(v\in son(u),v<u\) 的都必须打上标记,不然 \(u\) 就会被打上,当然可以通过 \((u,v)\) 这条边打上。而剩余 \(v\in son(u),v>u\) 除了 \(f_{v,2}\) 都可以。所以最终有:

\[f_{u,2}=\prod_{v\in son(u),v<u}(f_{v,1}+f_{v,2})\times \prod_{v\in son(u),v>u}(f_{v,0}+f_{v,1}+f_{v,3}) \]

其实 \(f_{u,1}\)\(f_{u,3}\) 比较相似。\(u\) 不是被 \((fa_u,u)\) 打上标记,那一定是 \((u,v\in son(u))\) 打上的,最终是一种求和的形式。如果是 \((u,v)\) 打的标记,此时 \(v\) 一定没打,即 \(f_{v,0}+f_{v,3}\)。考虑 \(u\) 的其它儿子 \(w\in son(u),w\not= v\),若 \(w<v\),为了留住 \(u\) ,只能取 \(f_{w,1}+f_{w,2}\);若 \(w>v\),除了 \(f_{w,2}\),其它都可以。所以最终有:

\[f_{u,1}=\sum_{v\in son(u),v<u}(f_{v,0}+f_{v,3})\times\prod_{w<v}(f_{w,1}+f_{w,2})\times\prod_{w>v}(f_{w,0}+f_{w,1}+f_{w,3}) \]

\[f_{u,3}=\sum_{v\in son(u),v>u}(f_{v,0}+f_{v,3})\times\prod_{w<v}(f_{w,1}+f_{w,2})\times\prod_{w>v}(f_{w,0}+f_{w,1}+f_{w,3}) \]

最终答案为 \(f_{rt,0}+f_{rt,1}\),因为 \(rt\) 没有父亲,不存在父亲时与父亲后。

注意求 \(f_{u,1},f_{u,3}\) 时要进行前缀优化。存图用 vector 保证顺序。

代码:

#include<bits/stdc++.h>
#define int long long
#define pb push_back
using namespace std;
const int N=2e5+5,P=998244353;
int n,f[N][4];vector<int> g[N];
void dfs(int u,int fa){
	bool h=1;int s=f[u][0]=f[u][2]=1;
	for(auto v:g[u]){
		if(v==fa)h=0;
		else{dfs(v,u);
			(f[u][1]*=f[v][0]+f[v][1]+f[v][3])%=P;
			(f[u][3]*=f[v][0]+f[v][1]+f[v][3])%=P;
			if(h)(f[u][1]+=s*(f[v][0]+f[v][3]))%=P,
				(f[u][2]*=f[v][1]+f[v][2])%=P;
			else(f[u][3]+=s*(f[v][0]+f[v][3]))%=P,
				(f[u][2]*=f[v][0]+f[v][1]+f[v][3])%=P;
			(s*=f[v][1]+f[v][2])%=P;
			(f[u][0]*=(f[v][1]+f[v][2]))%=P;
		}
	}
}
signed main(){
	ios::sync_with_stdio(0);
	cin.tie(0),cout.tie(0);
	cin>>n;for(int i=1,u,v;i<n;++i)
		cin>>u>>v,g[u].pb(v),g[v].pb(u);
	dfs(1,0);cout<<(f[1][0]+f[1][1])%P<<'\n';
	return 0;
}

CF1085G Beautiful Matrix

前置知识:部分错排。

\(h_{i,j}\) 表示长度为 \(i\) 的排列 \(P\),对于 \(\forall k\le j\)\(P_k!=k\) 的方案数。
\(h_{i,0}=i!,h_{i,j}=h_{i,j-1}-h_{i-1,j-1}\)
前者显然,后者若不考虑 \(j\) 位置的合法性方案数是 \(h_{i,j-1}\),再考虑的话钦定 \(a_j=j\),方案数是 \(h_{i-1,j-1}\)

这种要求字典序的大多都是钦定一段前缀相当,然后这个位置小于。

\(f_i\) 表示前 \(i\) 行,字典序小于给定矩阵的合法矩阵数量。

一种是从 \(f_{i-1}\) 转移,则第 \(i\) 行直接是完全错排。否则枚举第 \(i\) 行不等的位置及其数值,然后看后面部分与上一列有某些可能重合的数字,然后就是部分错排。

直接做是 \(n^3\) 的,然后会发现我们只用求出 \(\le k\) 在下面出现/上下都出现的个数,用树桩数组即可。

#include <bits/stdc++.h>
using namespace std;
const int N=2005,P=998244353;
void add(int &x,int y){x+=y;if(x>=P)x-=P;}
int n,a[N][N],f[N];
int fac[N],d[N],h[N][N];
int st[N],vis[N];
struct BIT{
    int tr[N];
    void clear(){memset(tr,0,sizeof(tr));}
    void add(int x){while(x<=n)++tr[x],x+=x&-x;}
    int ask(int x,int k=0){while(x)k+=tr[x],x-=x&-x;return k;}
}t1,t2;
signed main(){
    ios::sync_with_stdio(0);
    cin.tie(0),cout.tie(0);
    cin>>n,fac[0]=1;
    for(int i=1;i<=n;++i)
        fac[i]=1ll*i*fac[i-1]%P;
    h[0][0]=1;
    for(int i=1;i<=n;++i){
        h[i][0]=fac[i];
        for(int j=1;j<=i;++j)
            h[i][j]=(h[i][j-1]+P-h[i-1][j-1])%P;
    }for(int i=1;i<=n;++i)
        for(int j=1;j<=n;++j)
            cin>>a[i][j];
    for(int i=1;i<=n;++i){
        add(f[i],1ll*f[i-1]*h[n][n]%P);
        memset(st,0,sizeof(st));
        memset(vis,0,sizeof(vis));
        t1.clear(),t2.clear();
        int tmp=0;
        for(int j=n;j>=1;--j){
            if(a[i][j]&&(++st[a[i][j]])==2)t2.add(a[i][j]),++tmp;
            // for(int k=1;k<a[i][j];++k){
            //     if(k==a[i-1][j])continue;
            //     if(st[k]==2)
            //         add(f[i],h[n-j][tmp-1]);
            //     else if(vis[k])
            //         add(f[i],h[n-j][tmp]);
            // }
            add(f[i],(1ll*t2.ask(a[i][j]-1)*h[n-j][tmp-1]%P+1ll*(t1.ask(a[i][j]-1)-t2.ask(a[i][j]-1))*h[n-j][tmp]%P)%P);
            if(a[i-1][j]<a[i][j]){
                if(st[a[i-1][j]]==2)
                    add(f[i],P-h[n-j][tmp-1]);
                else if(vis[a[i-1][j]])
                    add(f[i],P-h[n-j][tmp]);
            }
            if(a[i-1][j]&&(++st[a[i-1][j]])==2)t2.add(a[i-1][j]),++tmp;
            vis[a[i][j]]=1,t1.add(a[i][j]);
        }
    }
    cout<<f[n]<<'\n';
    return 0;
}

CF1221G Graph And Numbers

容斥一下 \(ans=tot-C(01)-C(02)-C(12)+C(0)+C(1)+C(2)\)。C(S) 表示整张图的边权只有 \(S\)

显然:

  • \(tot=2^n\)
  • \(C(02)=2^{cnt1}\)\(cnt1\) 为连通块个数。
  • \(C(0)=C(2)\) 为度数为 \(0\) 的点的个数。
  • \(C(1)\) 对每个连通块跑一边 dfs 判断是否有奇环,若有则 \(C(1)=0\),否则为 \(2^{cnt1}\)

只剩 \(C(01)\)\(C(12)\),两者数值相等,只用考虑 \(C(01)\),也就是不能有一条边两旁的点权都为 \(1\)。这个 \(n=40\) 十分容易让人想到 \(2^{\frac n2}\) 的做法。将图分为两半,个枚举点权为 \(1\) 的集合 \(s\) 并找出在那一半中合法的集合。

然后合并时将对于前一半的所有可能集合找出点权为 \(1\) 的点可直接连接后一半集合中的点集,然后不能与后一半点权为 \(1\) 的点集相交,取个补集做高维前缀和即可。

#include <bits/stdc++.h>
#define ll long long
using namespace std;
const int N=42,M=782;
int n,m,d[N],id[N];
int a[1<<22],b[1<<22],tot;
int u[M],v[M],c1[N],c2[N],c12[N];
vector<int> e[N];ll base=1;
int find(int x){return id[x]==x?x:id[x]=find(id[x]);}
ll s01(){
    int t1=n/2,t2=n-t1;
    for(int i=1;i<=t1;++i){
        for(int j:e[i])
            if(j<=t1)c1[i-1]|=(1<<j-1);
            else c12[i-1]|=(1<<j-t1-1);
    }for(int i=1;i<=t2;++i){
        for(int j:e[t1+i])
            if(j>t1)c2[i-1]|=(1<<j-t1-1);
    }for(int s=0;s<(1<<t1);++s){
        bool isok=0;
        for(int i=0;i<t1;++i){
            if((s>>i&1)&&(s&c1[i]))
                isok=1;
        }if(isok)continue;
        int t=0;
        for(int i=0;i<t1;++i){
            if((s>>i)&1)
                t|=c12[i];
        }a[++tot]=t;
    }for(int s=0;s<(1<<t2);++s){
        bool isok=0;
        for(int i=0;i<t2;++i){
            if((s>>i&1)&&(s&c2[i]))
                isok=1;
        }if(isok)continue;
        b[s]=1;
    }for(int i=0;i<t2;++i){
        for(int s=0;s<(1<<t2);++s)
            if(s>>i&1)
                b[s]+=b[s^(1<<i)];
    }ll ans=0;
    for(int i=1;i<=tot;++i)
        ans+=b[((1<<t2)-1)^a[i]];
    return ans;
}ll sol(){
    ll ans=1;
    for(int i=1;i<=n;++i)
        ans=ans*2;
    return ans;  
}ll s02(){
    ll ans=1;
    for(int i=1;i<=n;++i)
        id[i]=i;
    for(int i=1;i<=m;++i)
        if(find(u[i])!=find(v[i]))
            id[find(u[i])]=find(v[i]);
    for(int i=1;i<=n;++i)
        if(id[i]==i)
            ans=ans*2;
    return ans;
}void dfs(int u,ll&ans){
    for(int v:e[u]){
        if(id[v]!=-1){
            if(id[u]==id[v])
                ans=0;
        }else id[v]=(id[u]^1),dfs(v,ans);
    }
}ll s1(){
    for(int i=1;i<=n;++i)
        id[i]=-1;
    ll ans=1;
    for(int u=1;u<=n;++u)
        if(id[u]==-1)
            id[u]=0,dfs(u,ans);
    for(int i=1;i<=n;++i)
        id[i]=i;
    for(int i=1;i<=m;++i)
        if(find(u[i])!=find(v[i]))
            id[find(u[i])]=find(v[i]);
    for(int i=1;i<=n;++i)
        if(id[i]==i)
            ans=ans*2;
    return ans;
}signed main(){
    ios::sync_with_stdio(0);
    cin.tie(0),cout.tie(0);
    cin>>n>>m;
    if(m==0)return cout<<0<<'\n',0;
    for(int i=1;i<=m;++i)
        cin>>u[i]>>v[i],++d[u[i]],++d[v[i]],
        e[u[i]].emplace_back(v[i]),
        e[v[i]].emplace_back(u[i]);
    for(int i=1;i<=n;++i)
        if(!d[i])base=base*2;
    cout<<sol()+base*2+s1()-s01()*2-s02()<<'\n';
    return 0;
}

P5999 [CEOI 2016] kangaroo

挺神的一道题。

相当于问有多少长为 \(n\) 的排列 \(p\)\(p_1=s,p_n=t\),对于 \(\forall 1<i<n\)\(p_{i-1}>p_i,p_{i+1}>p_i\)\(p_{i-1}<p_i,p_{i+1}<p_i\)

依然考虑从小到大插入

posted @ 2025-08-19 21:08  zzy0618  阅读(17)  评论(0)    收藏  举报