相關內容:組合入門與應用。(數學公式加載好慢/kk)
一、組合基礎題
1. [HNOI2008] 越獄
題目大意:監獄有 \(n\) 個房間,每個房間關押一個犯人,有 \(m\) 種宗教,每個犯人會信仰其中一種。如果相鄰房間的犯人的宗教相同,就可能發生越獄,求有多少種狀態可能發生越獄。\(m\leq 10^8,n\leq 10^{12}\)。
Solution:
考慮取補集。
因為共有 \(n\) 個房間,每個房間有 \(m\) 個選擇。則總方案數為 \(m^n\) 。
相鄰房間的犯人的宗教都不相同,就不會發生越獄。第一個房間有 \(m\) 個選擇,后面的每一個都要和前一個不同,所以后面的每個房間都有 \(m-1\) 個選擇。則不會越獄的方案數為 \(m(m-1)^{n-1}\)。
那么,會發生越獄的方案數為 \(m^n-m(m-1)^{n-1}\) 。
#include<bits/stdc++.h> #define int long long using namespace std; const int mod=1e5+3; int n,m,ans; int mul(int x,int n,int mod){ int ans=mod!=1; for(x%=mod;n;n>>=1,x=x*x%mod) if(n&1) ans=ans*x%mod; return ans; } signed main(){ scanf("%lld%lld",&m,&n); printf("%lld\n",(mul(m,n,mod)-m*mul(m-1,n-1,mod)%mod+mod)%mod); return 0; }
補充:若改為 \(|n|\leq 10^5\)(指 \(n\) 的長度不超過 \(10^5\)),答案對一個質數取模,怎么做呢?
歐拉定理:若 \(\gcd(a,m)=1\),則 \(a^{\varphi(m)}\equiv 1 \pmod m\)。
費馬小定理:若 \(m\) 為質數,且 \(\gcd(a,m)=1\),則 \(a^{m-1}\equiv 1 \pmod m\)。
\(m^n=m^{k\cdot (p-1)+r}=(m^{p-1})^k\cdot m^r\)
根據費馬小定理得,\(m^{p-1}\equiv 1 \pmod p\),那么 \((m^{p-1})^k\equiv 1 \pmod p\)。
所以計算 \(m^r\) 就可以了。
例子:計算 \(2^{100}\) 除以 \(13\) 的余數。
\(2^{100} \bmod 13=2^{12\times 8+4} \bmod 13=(2^{12})^8\cdot 2^4 \bmod 13=2^4 \bmod 13=3\)
回到題目。我們先將大整數 \(n\) 對 \(p-1\) 取模,然后對取模后的整數進行快速冪。(還是用之前那個式子)
2. [Usaco2008 Oct] 建造柵欄
題目大意:一根長度為 \(n\) 的木板,你需要將其切成四條邊,使得邊的長度為整數且這四條邊可以構成一個面積非零的四邊形。求方案數。\(n\leq 2500\)。
Solution:
構成四邊形的條件是:任意三邊之和大於第四邊。也就是任意 \(a+b+c>d\),可以得到 \(d<\frac{n}{2}\)。(不存在 \(\geq \frac{n}{2}\) 的邊)
考慮取補集。
- 總方案數:長度為 \(n\) 的木板,有 \(n-1\) 個切點,那么 \(n-1\) 個切點切三刀的方案數就是 \(C_{n-1}^3\)。(插板法)
- 存在 \(\geq \frac{n}{2}\) 的邊的方案數(即不能構成四邊形的方案數):枚舉最長邊 \(i\),則剩下三邊之和為 \(n-i\)。類似地,長度為 \(n-i\) 的木板,有 \(n-i-1\) 個切點,那么 \(n-i-1\) 個切點切兩刀的方案數就是 \(C_{n-i-1}^2\)。則存在 \(\geq \frac{n}{2}\) 的邊的方案數為:\(4\times \sum_{i=\lceil \frac{n}{2}\rceil}^{n-3} C_{n-i-1}^2\)。其中乘 \(4\) 是因為四條邊都有可能是最長邊。
答案為 \(C_{n-1}^3-4\times \sum\limits_{i=\lceil \frac{n}{2}\rceil}^{n-3} C_{n-i-1}^2\)。時間復雜度: \(O(n)\)。
#include<bits/stdc++.h> #define int long long using namespace std; int n,ans,sum; signed main(){ scanf("%lld",&n); ans=(n-1)*(n-2)*(n-3)/6; //C(n-1,3)=(n-1)*(n-2)*(n-3)/(3*2*1)=(n-1)*(n-2)*(n-3)/6 for(int i=ceil(n/2.0);i<=n-3;i++) sum=sum+4*((n-i-1)*(n-i-2)/2); //4*C(n-i-1,2) printf("%lld\n",ans-sum); return 0; }
3. BZOJ 3907 網格
題目大意:給出一個左下角為 \((0,0)\) 右上角為 \((n,m)\) 的網格圖。現在你想要從左下角走到右上角,期間只能向上、右兩個方向走,且經過的所有格子 \((x,y)\) 需滿足 \(x\geq y\)。求方案數。
Solution:
向上或向右走,從 \((0,0)\) 走到 \((n,m)\)一共要走 \(n+m\) 步(向右走 \(n\) 步,向上走 \(m\) 步)。不妨把向上定義為 \(1\),向右定義為 \(0\)。那么一共要有 \(n\) 個 \(0\),\(m\) 個 \(1\)。經過的所有格子 \((x,y)\) 需滿足 \(x≥y\),也就是當前走的向右的步數要大於等於向上的步數,即前綴中 \(0\) 的個數要大於等於前綴 \(1\) 的個數。
問題轉化為,求有多少個有 \(n\) 個 \(0\),\(m\) 個 \(1\),並且任意前綴中 \(0\) 的個數都大於等於前綴 \(1\) 的個數的序列。用之前卡特蘭數部分證明 \({Cat}_n=\frac{1}{n+1}\times C_{2n}^n\) 的方法,可以得到答案為,\(C_{n+m}^n-C_{n+m}^{m-1}\)。
高精度。我選擇 Python 2.7 2333。
f={} def C(n,m): return f[n]/f[m]/f[n-m]; n,m=raw_input().split() n=int(n) m=int(m) f[0]=1 for i in range(1,1+n+m): # range(1,n+m) 不包括 n+m,所以要 range(1,1+n+m) f[i]=f[i-1]*i print (C(n+m,m)-C(n+m,m-1))
關於 \(\text{Catalan}\) 數的一些補充:以下問題都與 \(\text{Catalan}\) 數有關:
- \(n\) 個左括號和 \(n\) 個右括號組成的合法括號序列的數量為 \(Cat_n\)。
- \(1,2,...,n\) 經過一個棧,形成的合法出棧序列的數量為 \(Cat_n\)。(若用 \(0\) 表示入棧,\(1\) 表示出棧,那么可以得到一個 \(01\) 序列,並且前綴 \(0\) 的個數都不少於 \(1\) 的個數)
- \(n\) 個節點構成的不同二叉樹的數量為 \(Cat_n\)。
- 在平面直角坐標系上,每一步只能向上或向右走,從 \((0,0)\) 走到 \((n,n)\) 並且除兩個端點外不接觸直線 \(y=x\) 的路線數量為 \(2Cat_{n-1}\)。
4. [SDOI2016] 排列計數
題目大意:求有多少種長度為 \(n\) 的序列 \(A\),滿足以下條件:
-
\(1\) 到 \(n\) 這 \(n\) 個數在序列中各出現了一次。
-
若第 \(i\) 個數 \(A_i\) 的值為 \(i\),則稱 \(i\) 是穩定的。序列恰好有 \(m\) 個數是穩定的。
\(T\leq 5\times 10^5,n\leq 10^6,m\leq 10^6\),對 \(10^9+7\) 取模。
Solution:
\(n\) 個數中選 \(m\) 個數是穩定的,方案數為 \(C_n^m\)。
因為題目要求序列恰好 \(m\) 個數是穩定的,則剩下的 \(n-m\) 個數必須不穩定。即剩下的 \(n-m\) 個數必須滿足 \(A_i\neq i\)。其實就是錯排問題。
所以答案為 \(C_n^m\times D_{n-m}\)。
#include<bits/stdc++.h> #define int long long using namespace std; const int N=1e6+5,mod=1e9+7; int t,n,m,f[N],g[N],d[N]; int mul(int x,int n,int mod){ int ans=mod!=1; for(x%=mod;n;n>>=1,x=x*x%mod) if(n&1) ans=ans*x%mod; return ans; } void init(){ int n=1e6; f[0]=g[0]=1; for(int i=1;i<=n;i++) f[i]=f[i-1]*i%mod; g[n]=mul(f[n],mod-2,mod); for(int i=n-1;i;i--) g[i]=g[i+1]*(i+1)%mod; } int solve(int n,int m){ return f[n]*g[m]%mod*g[n-m]%mod; } signed main(){ scanf("%lld",&t),init(); d[0]=1,d[1]=0,d[2]=1; for(int i=3;i<=1e6;i++) d[i]=(i-1)*(d[i-1]+d[i-2])%mod; //錯排 while(t--){ scanf("%lld%lld",&n,&m); printf("%lld\n",solve(n,m)*d[n-m]%mod); } return 0; }
5. CF886E Maximum Element
題目大意:有人寫了一個序列求 \(\max\),它長下面這樣:
int fast_max(int n,int a[]) { int ans=0; int offset=0; for(int i=0;i<n;++i) if(ans<a[i]) ans=a[i],offset=0; else{ offset=offset+1; if(offset==k) return ans; } return ans; }//這個函數的原理是:如果碰到一個數后面連續的k個數都比它小,那么就把這個數當做序列的最大值。
求有多少個 \(1\) 到 \(n\) 的排列在這個函數的計算下答案不為 \(n\)(即返回錯誤的結果)。\(n,k\leq 10^6\)。
Solution:
考慮暴算,發現基本上枚舉個什么東西可行的情況都包含一個前提:在此之前函數並沒有退出。
那我們不妨來 dp 這個東西。
在只對元素大小關系敏感的題里頭可以將一段數等價地看作是相對大小不變的排列,合並的時候注意乘上相應的組合數即可。
令 \(f_i\) 表示 \(1\) 到 \(i\) 的排列當中有多少個是運行完整個循環之后還沒有退出的。
怎么算呢?
最大值 \(i\) 必然出現,並且只可能位於 \([i-k+1,i]\)。
考慮枚舉最大值出現的位置 \(j\)。
除去最大值 \(i\),還有 \(i-1\) 個數。選出 \(i-j\) 個數放在 \(j\) 后面,這 \(i-j\) 個數共有 \((i-j)!\) 種排列,所以放在后面有 \(C_{i-1}^{i-j}\times (i-j)!\) 種方案。前面的 \(j-1\) 個數共有 \(f_{j-1}\) 種。所以可以得到:\(f_i=\sum_{j=i-k+1}^i f_{j-1}\times C_{i-1}^{i-j}\times (i-j)!\)。
對 \(f_i\) 進行化簡:
\(f_i=\sum_{j=i-k+1}^i f_{j-1}\times C_{i-1}^{i-j}\times (i-j)!\)
\(=\sum_{j=i-k+1}^i f_{j-1}\times \frac{(i-1)!}{(i-j)!(j-1)!}\times (i-j)!\)
\(=(i-1)!\times \sum_{j=i-k+1}^i \frac{f_{j-1}}{(j-1)!}\)
\(=(i-1)!\times \sum_{j=i-k}^{i-1} \frac{f_j}{j!}\)
令 \(g_i=\frac{f_i}{i!}\),則 \(g_i=\frac{\sum_{j=i-k}^{i-1} \ \ \ g_j}{i}\)。邊計算 \(f_i\) 邊維護前綴和。
最后的答案呢?
用相似的方法枚舉 \(n\) 所在的位置計算出最終答案為 \(n\) 的排列數,再取個補集。
\(Ans=n!-\sum_{i=1}^n f_{i-1}\times C_{n-1}^{n-i}\times (n-i)!\)
\(=n!-\sum_{i=1}^n g_{i-1}\times (i-1)! \times C_{n-1}^{n-i}\times (n-i)!\)
\(=n!-\sum_{i=1}^n g_{i-1}\times (i-1)! \times \frac{(n-1)!}{(n-i)!(i-1)!}\times (n-i)!\)
\(=n!-(n-1)!\times \sum_{i=1}^n g_{i-1}\)
\(=(n-1)!\times (n-\sum_{i=1}^n g_{i-1})\)
#include<bits/stdc++.h> #define int long long using namespace std; const int N=1e6+5,mod=1e9+7; int n,k,g[N],f[N],inv[N],sum,ans; int mul(int x,int n,int mod){ int ans=mod!=1; for(x%=mod;n;n>>=1,x=x*x%mod) if(n&1) ans=ans*x%mod; return ans; } void init(){ int n=1e6; f[0]=inv[1]=1; for(int i=1;i<=n;i++) f[i]=f[i-1]*i%mod; //fac for(int i=2;i<=n;i++) inv[i]=(mod-mod/i)*inv[mod%i]%mod; //inv //inv[i] 表示 i 在 mod 1e9+7 意義下的逆元,方便計算 \frac{\sum_{j=i-k}^{i-1} g_j}{i} } signed main(){ scanf("%lld%lld",&n,&k),init(),g[0]=1,ans=n; for(int i=1;i<=n;i++){ sum=(sum+g[i-1])%mod; if(i>k) sum=(sum-g[i-k-1]+mod)%mod; //\sum_{j=i-k}^{i-1} g_j g[i]=sum*inv[i]%mod; ans=(ans-g[i-1]+mod)%mod; //n-sum{g[i-1]} } ans=ans*f[n-1]%mod,printf("%lld\n",ans); return 0; }
二、Lucas 定理
1. BZOJ 4403 序列統計
題目大意:給定三個正整數 \(N、L\) 和 \(R\),統計長度在 \(1\) 到 \(N\) 之間,元素大小都在 \(L\) 到 \(R\) 之間的單調不降序列的數量。輸出答案對 \(10^6+3\) 取模的結果。\(1\leq T\leq 100,1\leq N,L,R\leq 10^9\)。
Solution:
\(L,R\) 其實是沒用的,直接向左平移 \(L-1\) 個單位就好了。所以區間 \([L,R]\) 等價於 \([1,R-L+1]\)。
考慮枚舉長度 \(i\),並且取值為 \(1\) 到 \(m\),其中 \(m=R-L+1\)。
轉化為插板法。你可以把它看成是,枚舉一些板,這些板的意義是,每種元素所到達的最后一個位置。具體來說,比如 \(1,2,3,3,4→\{1\}|\{2\}|\{3,3\}|\{4\}\)。但是 \(1\) 到 \(m\) 這些取值中有一些是可能沒有取到的。於是可以在長度為 \(i\) 的序列中再額外加入 \(m\) 個元素,表示我們往每種取值,強制地插入一個元素。這樣,\(1\) 到 \(m\) 的每種取值的數個數都大於等於 \(1\) 了,可以直接使用插板法。\(i+m-1\) 個空隙,插 \(m-1\) 個隔板,所以方案數為 \(C_{i+m-1}^{m-1}\)。
\(\sum_{i=1}^n C_{i+m-1}^{m-1}=\sum_{i=1}^n C_{i+m-1}^{m-1}+C_m^m-1\)
\(=\sum_{i=2}^n C_{i+m-1}^{m-1}+C_m^{m-1}+C_m^m-1\)
\(=\sum_{i=2}^n C_{i+m-1}^{m-1}+C_{m+1}^m-1\)(根據楊輝三角的 \(C_n^m=C_{n-1}^m+C_{n-1}^{m-1}\) 可得,以下同理)
\(=\sum_{i=3}^n C_{i+m-1}^{m-1}+C_{m+2}^m-1\)
\(=...\)
\(=\sum_{i=n-1}^n C_{i+m-1}^{m-1}+C_{m+n-2}^m-1\)
\(=\sum_{i=n}^n C_{i+m-1}^{m-1}+C_{m+n-1}^m-1\)
\(=C_{m+n-1}^{m-1}+C_{m+n-1}^m-1\)
\(=C_{m+n}^m-1\)
#include<bits/stdc++.h> #define int long long using namespace std; const int N=1e6+5,mod=1e6+3; int t,n,m,l,r,f[N],g[N]; int mul(int x,int n,int mod){ int ans=mod!=1; for(x%=mod;n;n>>=1,x=x*x%mod) if(n&1) ans=ans*x%mod; return ans; } void init(){ //預處理階乘與逆元求組合數 int n=mod-1; f[0]=g[0]=1; for(int i=1;i<=n;i++) f[i]=f[i-1]*i%mod; g[n]=mul(f[n],mod-2,mod); for(int i=n-1;i;i--) g[i]=g[i+1]*(i+1)%mod; } int C(int n,int m){ if(n<m) return 0; //防止 RE return f[n]*g[m]%mod*g[n-m]%mod; } int lucas(int n,int m,int p){ //Lucas 定理 if(!m) return 1; return C(n%p,m%p)*lucas(n/p,m/p,p)%p; } signed main(){ scanf("%lld",&t),init(); while(t--){ scanf("%lld%lld%lld",&n,&l,&r),m=r-l+1; printf("%lld\n",(lucas(m+n,n,mod)%mod-1+mod)%mod); //C(m+n,n)-1 } return 0; }
2. BZOJ 4737 組合數問題
題目大意:給定 \(n,m,k\),求有多少個 \(C_i^j\) 是 \(k\) 的倍數。其中 \(0\leq i\leq n,0\leq j\leq \min(i,m)\)。\(1\leq n,m\leq 10^{18},1\leq t,k\leq 100\),且 \(k\) 是一個質數。
Solution:
\(\text{Lucas}\) 定理計算 \(C_n^m\) 其實是把 \(n\) 和 \(m\) 表示成 \(p\) 進制數,對 \(p\) 進制下的每一位分別計算組合數,最后再乘起來。
在本題中,我們將 \(i,j\) 看作 \(k\) 進制數,因為要求 \(C_i^j \bmod k=0\),所以 \(i,j\) 在 \(k\) 進制下的每一位的組合數之積 \(\bmod k=0\)。
因為是 \(k\) 進制數,且 \(k\) 為質數,所以這樣的組合數不存在 \(k\) 的因子。所以只要有在 \(k\) 進制下某一位的組合數為 \(0\) 就行了。
只有 \(n<m\) 時,\(C_n^m=0\)。所以必須要求存在某一位,該位上 \(i\) 的數字小於 \(j\) 的數字。於是就可以進行數位 dp 了。
令 \(f_{pos,0/1,0/1,0/1,0/1}\) 表示 dp 到第 \(pos\) 位,是否出現過某一位上 \(i\) 的數字小於 \(j\) 的數字的情況,是否出現過某一位上 \(i\) 的數字大於 \(j\) 的數字的情況,的方案數(后兩位表示 \(i,j\) 與上界之間的關系)。
#include<bits/stdc++.h> #define int long long #define MEM(x,y) memset(x,y,sizeof(x)) using namespace std; const int N=70,mod=1e9+7; int t,k,n,m,f[N][2][2][2][2],a[N],b[N]; int dfs(int i,int flag,int flag2,bool less,bool less2){ if(!i) return flag; if(f[i][flag][flag2][less][less2]!=-1) return f[i][flag][flag2][less][less2]; int end1=less?a[i]:k-1,end2=less2?b[i]:k-1,ans=0; for(int x=0;x<=end1;x++) //枚舉數位 for(int y=0;y<=end2&&(y<=x||flag2);y++) //y 不能超過 end2,也不能超過 x。如果之前出現過某一位x>y,那么這一位就不需要 y<=x 了。這也就是要記錄是否出現過x>y的原因。 ans=(ans+dfs(i-1,flag||x<y,flag2||x>y,less&&x==end1,less2&&y==end2)%mod)%mod; f[i][flag][flag2][less][less2]=ans; return ans; } int calc(int x,int y){ int n=0,m=0; while(x) a[++n]=x%k,x/=k; while(y) b[++m]=y%k,y/=k; while(m<n) b[++m]=0; return dfs(n,0,0,1,1); } signed main(){ scanf("%lld%lld",&t,&k); while(t--){ scanf("%lld%lld",&n,&m),MEM(f,-1); if(m>n) m=n; printf("%lld\n",calc(n,m)); } return 0; }
三、容斥原理
1. UVA 11806 Cheerleaders
題目大意:在一個 \(n\times m\) 的矩陣中擺放 \(k\) 個石子,要求第一行、第一列、第 \(m\) 行、第 \(n\) 列必須有石子,求方案總數。\(T\leq 50,n\leq 20,m\leq 20,k\leq 500\)。
Solution:
題目中有四個條件:第一行有石子、第一列有石子、第 \(m\) 行有石子、第 \(n\) 列有石子。
枚舉不被滿足的條件集合 \(S\) 。若沒有條件不被滿足的話,總方案數為 \(C_{nm}^k\)。
不被滿足集合的大小為 \(1\) 時:以條件一、條件二為例。條件一“第一行有石子”不被滿足,意味着“第一行沒有石子”,則只有 \(m-1\) 行、\(n\) 列可以放石子,方案數為 \(C_{(m-1)\cdot n}^{k}\)。條件二“第一列有石子”不被滿足,意味着“第一列沒有石子”,則只有 \(m\) 行、\(n-1\) 列可以放石子,方案數為 \(C_{m\cdot (n-1)}^k\)。其他的同理。
不被滿足集合的大小為 \(2\) 時:若條件一與條件三同時不被滿足,顯然方案數為 \(C_{(m-2)\cdot n}^k\)。若條件三與條件四同時不被滿足,顯然方案數為 \(C_{(m-1)\cdot (n-1)}^k\)。其余同理。
當不被滿足集合的大小為 \(3\) 或者為 \(4\) 的時候,也是可以被類似計算的。
答案為總方案數減去有任意一個條件不被滿足的方案數。設 \(A_1\) 為條件一不被滿足的方案,\(A_2\) 為條件二不被滿足的方案,\(A_3\) 為條件三不被滿足的方案,\(A_4\) 為條件四不被滿足的方案。
\(Ans=總方案數-\left| \bigcup\limits_{i=1}^{4}A_i \right|\)
\(=C_{nm}^k-(\sum\limits_{i=1}^4\left| A_i \right|-\sum\limits_{1\leq i<j\leq 4}\left|A_i\cap A_j\right|+\sum\limits_{1\leq i<j<k\leq 4}\left|A_i\cap A_j\cap A_k\right|-\sum\limits_{1\leq i<j<k<l\leq 4}\left|A_i\cap A_j\cap A_k\cap A_l\right|)\)
#include<bits/stdc++.h> #define int long long using namespace std; const int N=510,mod=1e6+7; int T,t,n,m,k,c[N][N],x,y,cnt,ans; void init(){ //計算組合數 c[0][0]=1; for(int i=1;i<=500;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; } } signed main(){ scanf("%lld",&T),init(); while(T--){ scanf("%lld%lld%lld",&m,&n,&k),ans=0; for(int S=0;S<(1<<4);S++){ //枚舉狀態 x=m,y=n,cnt=0; //x:可以放棋子的行數 y:可以放棋子的列數 cnt:在當前狀態下有幾個條件不被滿足 for(int i=0;i<4;i++) //枚舉條件 if((S>>i)&1) (i&1?x--:y--),cnt++; //如果這個條件不被滿足->若是條件一或條件三,那么有一行不能放(即 x--),若是條件二或條件四,那么有一列不能放(即y--) if(!cnt) continue; if(cnt&1) ans=(ans+c[x*y][k]%mod)%mod; //容斥原理,如果有奇數個條件不被滿足,就加,反之就減 else ans=(ans+mod-c[x*y][k]%mod)%mod; } printf("Case %lld: %lld\n",++t,(c[n*m][k]%mod-ans+mod)%mod); //總方案數減有任意一個條件不被滿足的方案數 } return 0; }
2. CF449D Jzzhu and Numbers
題目大意:給出 \(N\) 個數的序列 \(a\),你需要從這些數當中選出一個非空子集(可含重復元素),使得選出子集的 \(\text{AND}\) 和為零。求方案數。\(N,a_i\leq 10^6\)。
Solution:
看這里 233
四、第二類斯特林數
1. TopCoder 13444 CountTables
題目大意:給出 \(n,m\) 和 \(c\) ,問有多少 \(n\times m\) 的矩陣,矩陣中每個數都在 \([1,c]\) 內,且任兩行不完全相同,任兩列不完全相同。\(n,m,c\leq 4000\)。
Solution:
先考慮一個子問題。假設並不是有兩個條件(任兩行不完全相同、任兩列不完全相同),而是只有其中一個條件。
假設那一個條件是任意兩行不完全相同。那么你可以把一行看作是一個數,對於每一個數,都有 \(c^m\) 種取值(也就是說,有 \(c^m\) 種不同的行),一共有 \(n\) 行。從 \(c^m\) 種取值中選出不相同的 \(n\) 個,方案數為 \(\binom{c^m\ }{n}\cdot n!\)。(因為行之間是有順序的,所以要乘上 \(n!\))
令 \(f_i\) 表示 \(n\times i\) 的矩陣滿足條件的方案數(第 \(i\) 列的答案)。\(f_m\) 就是答案。
因為需要滿足“任兩列不完全相同”,所以如果有 \(i\) 列,那么不相同的列的數量就必須為 \(i\)。也就是說,如果不相同的列的數量在 \(1\) 到 \(i-1\) 之間,那么就是不合法的。
考慮枚舉不相同的列的數量 \(j\)。把 \(i\) 列看成有 \(i\) 個球,不相同的列的數量 \(j\) 看成 \(j\) 個盒子,把 \(i\) 個球放入 \(j\) 個盒子中,並且盒子要非空,方案數為 \(S(i,j)\cdot f_j\)。(由於第二類斯特林數的盒子之間不區分,所以還要乘上 \(f_j\)。因為如果我們把所有重復出現的列去掉的話,那么就能對應到一個 \(n\times j\) 的矩陣,並且這個矩陣擁有的列都是不相同的,所以乘上 \(f_j\) 就可以解決問題)。
於是可以得到:
\(f_i=\binom{c^i\ }{n}\cdot n!-\sum_{j=1}^{i-1}S(i,j)\cdot f_j\)
\(=\frac{c^i!}{n!(c^i-n)!}\cdot n!-\sum_{j=1}^{i-1}S(i,j)\cdot f_j\)
\(=\frac{c^i!}{(c^i-n)!}-\sum_{j=1}^{i-1}S(i,j)\cdot f_j\)
\(=c^i\times (c^i-1)\times ...\times (c^i-n+1)-\sum_{j=1}^{i-1}S(i,j)\cdot f_j\)
#include<bits/stdc++.h> #define int long long using namespace std; const int N=4e3+5,mod=1e9+7; int f[N],s[N][N],x; class CountTables{ public: int howMany(int n,int m,int c){ s[0][0]=1,x=1; for(int i=1;i<=m;i++) for(int j=1;j<=i;j++) s[i][j]=(s[i-1][j]*j%mod+s[i-1][j-1]%mod)%mod; //S(i,j)=S(i-1,j)*j+S(i-1,j-1) for(int i=1;i<=m;i++){ x=x*c%mod,f[i]=1; //x:c^i for(int j=1;j<=n;j++) f[i]=f[i]*(x-j+1)%mod; for(int j=1;j<=i-1;j++) f[i]=(f[i]-s[i][j]*f[j]%mod+mod)%mod; } return f[m]; } }; /* signed main(){ int n,m,c; CountTables ans; scanf("%lld%lld%lld",&n,&m,&c); printf("%lld\n",ans.howMany(n,m,c)); return 0; }*/ #undef int
2. Luogu P4827 Crash 的文明世界
題目大意:給出一棵 \(n\) 個點的樹,求對於每個點 \(i\) 的 \(d(i)\) 值。\(d(i)=\sum\limits_{1\leq x\leq n}^{i\neq x}dist(x,i)^k\)。\(1\leq n\leq 50000,1\leq k\leq 150\)。
Solution:
第二類 \(\text{Stirling}\) 數的性質:\(x^n=\displaystyle\sum\limits_{k=0}^n\begin{Bmatrix}n\\k\end{Bmatrix}\dbinom{x}{k}\cdot k!\)
那么,容易得到:
\(d(i)=\displaystyle\sum\limits_{1\leq j\leq n}^{j\neq i}\sum\limits_{p=1}^k \begin{Bmatrix}k\\p\end{Bmatrix}\dbinom{dist(i,j)}{p}\cdot p!=\displaystyle\sum\limits_{p=1}^k p!\begin{Bmatrix}k\\p\end{Bmatrix}\sum\limits_{1\leq j\leq n}^{j\neq i}\dbinom{dist(i,j)}{p}\)
注意到,\(k\) 較小,那么可以直接枚舉 \(p\),\(p!\) 和 \(S(k,p)\) 是可以預處理的。問題轉化為處理后面那部分。
令 \(f_{i,p}\) 表示 \(\displaystyle\sum\limits_{1\leq j\leq n}^{j\neq i}\dbinom{dist(i,j)}{p}\)。\(f_{i,p}\) 所統計的對象 \(j\),包含子樹內與子樹外。我們先考慮子樹內,於是可以令 \(f_{i,p}\) 表示 \(\displaystyle\sum\limits_{j\in subtree(i)}\dbinom{dist(i,j)}{p}\)。
組合數遞推式:\(C_{i,j}=C_{i-1,j}+C_{i-1,j-1}\)。
那么,\(\dbinom{dist(i,j)}{p}=\dbinom{dist(i,j)-1}{p}+\dbinom{dist(i,j)-1}{p-1}\)。
則,\(f_{i,p}=\sum\limits_{j\in Son(i)}f_{j,p}+f_{j,p-1}\)。
然后做一遍換根 dp。
稍微提一下換根 dp:假設當前的根是當前節點的父親,我們下一步需要根換成當前節點。這樣就可以一直做下去。具體來說,我們需要做兩件事:
- 1. 把當前節點對父親的貢獻,從父親的 dp 值里扣除(但不能直接修改,因為父親還有別的兒子,所以我們最好做個備份)。
- 2. 把父親(除去當前節點的貢獻以后,剩余的部分)作為一個新的兒子,加入到當前節點的 dp 值中。這個是要直接修改的,因為要把當前節點換成根。
具體看代碼。
#include<bits/stdc++.h> #define int long long using namespace std; const int N=5e4+5,M=160,mod=1e4+7; int n,k,x,y,s[M][M],f[M],cnt,hd[N],to[N<<1],nxt[N<<1],dp1[N][M],dp2[N][M],ans,c[N]; //dp1 是子樹內的,dp2 是整棵樹的 void add(int x,int y){ to[++cnt]=y,nxt[cnt]=hd[x],hd[x]=cnt; } void dfs1(int x,int fa){ dp1[x][0]=1; for(int i=hd[x];i;i=nxt[i]){ int y=to[i]; if(y==fa) continue; dfs1(y,x),dp1[x][0]=(dp1[x][0]+dp1[y][0])%mod; for(int j=1;j<=k;j++) dp1[x][j]=((dp1[x][j]+dp1[y][j])%mod+dp1[y][j-1])%mod; } } void dfs2(int x,int fa){ //換根 dp for(int i=0;i<=k;i++) dp2[x][i]=dp1[x][i]; if(fa){ c[0]=(dp2[fa][0]-dp1[x][0]+mod)%mod; for(int i=1;i<=k;i++) c[i]=(dp2[fa][i]-dp1[x][i]+mod-dp1[x][i-1]+mod)%mod; //把當前節點對父親的貢獻,從父親的 dp 值里扣除 dp2[x][0]=(dp2[x][0]+c[0])%mod; for(int i=1;i<=k;i++) dp2[x][i]=(dp2[x][i]+c[i]+c[i-1])%mod; //把父親作為一個新的兒子,加入到當前節點的 dp 值中 } for(int i=hd[x];i;i=nxt[i]){ int y=to[i]; if(y!=fa) dfs2(y,x); } } signed main(){ scanf("%lld%lld",&n,&k),s[0][0]=1,f[0]=1; for(int i=1;i<=k;i++) for(int j=1;j<=i;j++) s[i][j]=(s[i-1][j]*j%mod+s[i-1][j-1])%mod; for(int i=1;i<=k;i++) f[i]=f[i-1]*i%mod; for(int i=1;i<n;i++){ scanf("%lld%lld",&x,&y); add(x,y),add(y,x); } dfs1(1,0),dfs2(1,0); for(int i=1;i<=n;i++){ ans=0; for(int p=0;p<=k;p++) ans=(ans+f[p]*s[k][p]%mod*dp2[i][p]%mod)%mod; printf("%lld\n",ans); } return 0; }
3. CF1278F Cards
題目大意:有 \(n\) 個互相獨立的隨機變量 \(x_i\),每個都有 \(\frac{1}{m}\) 的概率為 \(1\),剩下概率取 \(0\)。求 \(E((\sum\limits_{i=1}^n x_i)^k)\)。\(1\leq n,m<998244353,1\leq k\leq 5000\)。
Solution:
為了方便,我們令 \(p=\frac{1}{m}\)。
考慮枚舉隨機到 \(1\) 的隨機變量的個數。
由於這 \(n\) 個隨機變量是互相區分的,如果我們枚舉了隨機到 \(1\) 的隨機變量的個數 \(i\),那么肯定要乘上 \(n\) 個隨機變量中選 \(i\) 個為 \(1\) 的方案數 \(\dbinom{n}{i}\)。選出的 \(i\) 個隨機變量,每個有 \(p\) 的概率隨機到 \(1\)。剩下的 \(n-i\) 個隨機變量,每個都有 \(1-p\) 的概率隨機到 \(0\)。所以 \(n\) 個隨機變量中,有 \(i\) 個隨機到 \(1\) 的概率為 \(p^i\cdot (1-p)^{n-i}\)。此時 \(x_i\) 的和為 \(i\)(\(i\) 個 \(1\),\(n-i\) 個 \(0\)),所以 \((\sum\limits_{i=1}^n x_i)^k\) 等於 \(i^k\)。則:
\(Ans=\displaystyle\sum\limits_{i=0}^n \dbinom{n}{i}\cdot p^i\cdot (1-p)^{n-i}\cdot i^k\)
根據第二類 \(\text{Stirling}\) 數的性質:\(x^n=\displaystyle\sum\limits_{k=0}^n\begin{Bmatrix}n\\k\end{Bmatrix} x^{\underline k}\),得
\(Ans=\displaystyle\sum\limits_{i=0}^n \dbinom{n}{i}\cdot p^i\cdot (1-p)^{n-i}\sum\limits_{j=0}^k \begin{Bmatrix}k\\j\end{Bmatrix} i^{\underline j}=\displaystyle\sum\limits_{j=0}^k \begin{Bmatrix}k\\j\end{Bmatrix}\sum\limits_{i=0}^n \dbinom{n}{i}\cdot p^i\cdot (1-p)^{n-i}\cdot i^{\underline j}\)
然后根據下降冪的一個性質:\(\displaystyle\binom{n}{i} i^{\underline j}=\binom{n-j}{i-j} n^{\underline j}\)(為什么?直接展開就行了),得
\(Ans=\displaystyle\sum\limits_{j=0}^k \begin{Bmatrix}k\\j\end{Bmatrix} n^{\underline j}\sum\limits_{i=0}^n \dbinom{n-j}{i-j}\cdot p^i\cdot (1-p)^{n-i}\)
\(=\displaystyle\sum\limits_{j=0}^k \begin{Bmatrix}k\\j\end{Bmatrix} n^{\underline j}\sum\limits_{i=0}^{n-j} \dbinom{n-j}{i}\cdot p^{i+j}\cdot (1-p)^{n-i-j}\)
\(=\displaystyle\sum\limits_{j=0}^k \begin{Bmatrix}k\\j\end{Bmatrix} n^{\underline j}\cdot p^j\sum\limits_{i=0}^{n-j} \dbinom{n-j}{i}\cdot p^i\cdot (1-p)^{n-j-i}\)
因為,\(\displaystyle\sum\limits_{i=0}^{n-j} \dbinom{n-j}{i}\cdot p^i\cdot (1-p)^{n-j-i}=(p+(1-p))^{n-j}=1^{n-j}=1\)(二項式定理),所以 \(Ans=\displaystyle\sum\limits_{j=0}^k \begin{Bmatrix}k\\j\end{Bmatrix} n^{\underline j}\cdot p^j\)。其中,\(n^{\underline j}=\frac{n!}{(n-j)!}=\frac{n\times (n-1)\times ...\times (n-j+1)\times (n-j)\times (n-j-1)\times...\times 1}{(n-j)\times (n-j-1)\times ...\times 1}=n\times (n-1)\times ...\times (n-j+1)\),可以直接計算。
#include<bits/stdc++.h> #define int long long using namespace std; const int N=5e3+5,mod=998244353; int n,m,k,p,f[N],s[N][N],v[N],ans; int mul(int x,int n,int mod){ int ans=mod!=1; for(x%=mod;n;n>>=1,x=x*x%mod) if(n&1) ans=ans*x%mod; return ans; } signed main(){ scanf("%lld%lld%lld",&n,&m,&k),p=1*mul(m,mod-2,mod); //p=1/m f[0]=1,s[0][0]=1,v[0]=1; for(int i=1;i<=k;i++) f[i]=f[i-1]*p%mod; //f[i]=p^i for(int i=1;i<=k;i++) for(int j=1;j<=i;j++) s[i][j]=(s[i-1][j]*j%mod+s[i-1][j-1]%mod)%mod; for(int i=1;i<=k;i++) v[i]=v[i-1]*(n-i+1)%mod; //v[i]=n^{\underline i} for(int i=0;i<=k;i++) ans=(ans+s[k][i]*v[i]%mod*f[i]%mod)%mod; printf("%lld\n",ans); return 0; }
五、圖的計數
1. 「ZJOI 2016」小星星
題目大意:給出一棵 \(n\) 個節點的樹 \(T\) 和一張 \(n\) 個節點的圖 \(G\)。求節點間的對應關系數量使得 \(T\) 為 \(G\) 的一個子圖。\(n\leq 17\)。
Solution:
暴力:\(n!\) 枚舉映射,再 \(O(n)\) 地判斷每條樹邊是否合法。可以拿到 \(20\%\) 的分數。
子集 DP:
考慮在樹 \(T\) 上進行樹形 dp。限制在邊上,因此有用的信息為:
- 根節點的對應節點
- 子樹對應的點集
設計 \(dp_{i,j,k}\) 為以 \(i\) 為根,\(i\) 對應 \(G\) 中節點為 \(j\),子樹對應 \(G\) 中節點集合為 \(k\) 的方案數。
枚舉子樹時判斷 \((u,v)\) 是否在圖 \(G\) 中,並做一個子集枚舉。
時間復雜度:\(O(3^n n^3)\)。
狀壓→容斥:
考慮枚舉映射到 \(G\) 中的節點集合 \(S\)(或是未出現的節點集合 \(T\))。
使用容斥原理將問題轉化為只使用 \(S\) 中節點(不使用 \(T\) 中節點)的合法映射個數。
令 \(dp_{i,j}\) 表示以 \(i\) 為根,\(i\) 映射到 \(G\) 中的節點 \(j\) 時的子樹方案數。
使用上個做法當中的樹形 dp 即可 \(O(n^3)\) 解決。
時間復雜度:\(O(2^n n^3)\)。
需要進行一定的優化。
#include<bits/stdc++.h> #define int long long using namespace std; const int N=20; int n,m,x,y,cnt,hd[N],to[N<<1],nxt[N<<1],tot,f[N][N],vis[N],ans; bool v[N][N]; void add(int x,int y){ to[++cnt]=y,nxt[cnt]=hd[x],hd[x]=cnt; } void dfs(int x,int fa){ for(int i=1;i<=n;i++) f[x][i]=1; for(int i=hd[x];i;i=nxt[i]){ int y=to[i]; if(y==fa) continue; dfs(y,x); for(int j=1;j<=tot;j++){ int sum=0; for(int k=1;k<=tot;k++) if(v[vis[j]][vis[k]]) sum+=f[y][k]; f[x][j]*=sum; } } } signed main(){ scanf("%lld%lld",&n,&m); for(int i=1;i<=m;i++){ scanf("%lld%lld",&x,&y); v[x][y]=v[y][x]=1; //鄰接矩陣 } for(int i=1;i<n;i++){ scanf("%lld%lld",&x,&y); add(x,y),add(y,x); } for(int S=0;S<(1<<n);S++){ tot=0; for(int i=0;i<n;i++) if((S>>i)&1) vis[++tot]=i+1; //優化:這樣在dfs中就不用1->n枚舉點了(改成1->tot) dfs(1,0),tot=n-tot; for(int i=1;i<=n;i++){ //容斥 if(tot&1) ans-=f[1][i]; else ans+=f[1][i]; } } printf("%lld\n",ans); return 0; }