集合計數 :容斥原理


zkt大神的同題題解:  https://www.cnblogs.com/hzoi-DeepinC/articles/11102979.html

Description

一個有N個元素的集合有2^N個不同子集(包含空集),現在要在這2^N個集合中取出若干集合(至少一個),使得它們的交集的元素個數為K,求取法的方案數,答案模1000000007。(是質數喔~)

Input

一行兩個整數N,K

Output

一行為答案。

Sample Input

3 2

Sample Output

6

Sample Explanation

假設原集合為{A,B,C}

則滿足條件的方案為:{AB,ABC},{AC,ABC},{BC,ABC},{AB},{AC},{BC}

Hint

對於100%的數據,1≤N≤1000000;0≤K≤N;

寫題解不只是為了寫題解,重在講思路,想直接看正解的兄台自行跳躍閱讀。

抱歉我腦子略笨,的確是沒能一下想到正解,第一思路是一個n2的暴力

70%算法

雖說是暴力,但其實思路並不太好想,因為乍一下沒有上正軌,所以最后的思維量幾乎比std還大

然而只有70分,沒人會體恤你有多笨QAQ

假如原問題的含義以函數f(n,k)表示,即n元素交集為k的方案數

我們可以認為這個過程分為兩步:f(n,k)=C(n,k)*f(n-k,0)

第一步是從n個元素中選出k個C(n,k),其次要任意選集合使它們的交集恰好為這k個元素

那么我們可以確定,最后被選出的集合中,每個集合都含有這k個元素,而其余元素的交集為空集:f(n-k,0)

如:{a,b,c}三個元素,k=1時,假如你選定了a,那么可能出現的集合有:{a},{a,b},{a,c},{a,b,c}

去掉你選中的元素a,剩下的是Ø,{b},{c},{b,c},你要在這幾個集合中任選,使它們交集為空

一種可能的選法是{a},{a,b},{a,b,c},去掉a后剩下Ø,{b},{b,c},它們的交集的確為空

看起來“其余元素交集為空”這個問題會更好解一些,即f(s,0)。

假如真的是這樣,那么問題就化簡成了f(n,k)=C(n,k)*f(n-k,0)

這樣所有的問題都可以依賴f(n-k,0)來求解了。我們只需要求出f(x,0)這個數列

我一直在嘗試找到這個數列的規律,於是我手模了許多點,得到了f(n,k)的表格

//Update:感謝starsing大神更正數據,f(4,0)應該是64594

這個表格里貌似規律很多,但是f(x,0)真的有規律么:2,10,218,64594...

我沒發現。。。但是我發現了最后一行:所有選法是22^n-1,因為一共2n個集合,每一個集合都可以選或不選

但不能都不選,所以要-1。

現在我們就能得到f(x,0)了,假如我們已知所有的f(1,0),f(2,0)....f(x-1,0),用總數減去∑i=1->xf(x,i)即∑i=1->xC(x,x-i)*f(x-i,0)

這樣是n2的,在求f(n,k)的過程中我們可以求出所有的f(m,s)值(m<=n)

但是大多數f值都沒有用,存下它們會爆內存,於是用完直接覆蓋就行,保留f(x,0)即可

 1 #include<cstdio>
 2 #define int long long
 3 const long long mod=1000000007;
 4 long long fac[1000005],inv[1000005],invv[1000005],x,y,n,k,f0[1000005],f[1000005];
 5 long long pow(long long b,long long t,long long modd,long long ans=1){
 6     for(;t;t>>=1,b=b*b%modd)if(t&1)ans=ans*b%modd;
 7     return ans;
 8 }
 9 signed main(){
10     scanf("%lld%lld",&n,&k);
11     fac[0]=inv[0]=invv[0]=f0[0]=invv[1]=inv[1]=fac[1]=1;f0[1]=2;
12     for(int i=2;i<=n;++i)fac[i]=fac[i-1]*i%mod,invv[i]=(-mod/i*invv[mod%i])%mod,inv[i]=inv[i-1]*invv[i]%mod;
13     for(int i=1;i<=n;++i){
14         f0[i]=(pow(2,pow(2,i,mod-1),mod)-1+mod)%mod;
15         for(int j=1;j<=i;++j)f[j]=fac[i]*inv[j]%mod*inv[i-j]%mod*f0[i-j]%mod,f0[i]=(f0[i]-f[j]+mod)%mod;
16         f[0]=f0[i];
17     }
18     printf("%lld\n",(f[k]+mod)%mod);
19 }
愚蠢的TLE70代碼

100%算法

在求f(n,k)的過程當中出現了太多的冗余運算量,肯定不是正解,死了~

不知道有沒有大神能優化那個我認為沒救了的算法,我是拋棄它了

然后我們還是好好看看題目吧:又是交集又是子集的,能想到什么?

呃。。這個懸念有點無趣,因為我在題目里就已經說了。。容斥嘛。。。

為什么剛開始想不到啊啊啊啊(笨,不解釋)

每次我都喜歡舉這個例子:4個圈的venn圖

這是一張讓我第二次受益匪淺的圖

現在考慮n=4,k=2

每個圈分別叫{a,b,c,d},它們包含的部分都表示這些選法的交集含有這個元素

那么兩個圈的公共部分就表示他們的交集含有兩個元素

仔細想想每個圖里要填些什么,我不方便把我的草稿紙照下來。。

假如只被一個圈包含的顏色最淺的部分叫做1級部分,兩個圈的公共部分而不被其它圈包含的叫做2級部分,以此類推

最后可以發現,1級部分上都寫着218,2級上是10,3級是2,4級是1。

p級是f(n-p,0)。。。唉。。。看似沒什么進展,還是不會求啊

但是我們再找找別的規律:

重新定義級數,1級表示含有1個特定元素的部分(可以不是恰好只含有它),如圖中3個完整的圓都是1級部分,整個紅色區域也是1個1級的

對於每個4級部分,是1

對於每個3級部分,是2+1=3

對於每個2級部分,是10+2+1+2=15

對於每個1級部分,是10+2+1+2+2+10+10+218=255

對於每個s級部分,是2n-s-1

我們現在要求f(4,2),嘗試用這些部分把它表示出來吧

1級部分我們貌似用不到

2級部分用得到,一共有6個2級部分,是C(4,2),把它們加上,ans=6*15=90

然而在加2級部分的時候我們誤加了一些3級部分,把它們減去

有幾個呢?每個2級部分都包括2個3級部分,那么一共12個,ans=ans-12*3=54

然而又一次地,我們減去過多的4級部分了,該加回來多少呢?

在加2級部分時,每個2級部分包含1個4級部分,6個2級部分一共加了6個4級部分

在減3級部分時,每個3級部分包含1個4級部分,12個3級部分一共減了12個4級部分

所以一共應該加回來6個4級部分,ans=ans+6×1=60 對了!

現在只剩下了兩個問題:怎么求22^n?怎么獲得一個k級部分應該被加減幾次?

對於第一個問題,顯然不能直接把指數對1e9+7取模,答案不對,那應該怎么辦?

費馬小定理:ap-1Ξp(mod p)  p是質數

所以說指數是可以對p-1取模,即1e9+6

對於第二個問題就沒那么好想了,至少我想的超麻煩

求f(n,k):顯然k級部分需要被加C(n,k)次,為了方便研究(我懶得打),把所有數縮小這么多倍,最后再乘回來

k級:1

k+1級:-C(n-k,1)=-1(n-k)

k+2級:-C(n-k,2)+C(n-k-1,1)*C(n-k,1)=-(n-k)(n-k-1)/2+(n-k)(n-k-1)=(1/2)*(n-k)(n-k-1)

k+3級:-C(n-k,3)+C(n-k-1,2)*C(n-k,1)+C(n-k-2,1)*(-C(n-k,2)+C(n-k-1,1)*C(n-k,1))=(-1/6+1/2-1/2)(n-k)(n-k-1)(n-k-2)=(-1/6)(n-k)(n-k-1)(n-k-2)

k+4級:-C(n-k,4)+...=(-1/24+1/6-1/4+1/6)(n-k)(n-k-1)(n-k-2)(n-k-3)=(1/24)(n-k)(n-k-1)(n-k-2)(n-k-3)

紅色部分的系數,是正負交替的,而其絕對值是階乘的倒數,用逆元解決

而后面的部分顯然是一段連乘,用階乘和階乘的逆元解決。代碼實現自行處理。

 1 #include<cstdio>
 2 const long long mod=1000000007;
 3 long long fac[1000005],inv[1000005],invv[1000005],x,y,n,k,ans,res;
 4 inline long long pow(long long b,long long t,long long modd,long long ans=1){
 5     for(;t;t>>=1,b=b*b%modd)if(t&1)ans=ans*b%modd;
 6     return ans;
 7 }
 8 inline long long c(long long b,long long t){return fac[b]*inv[t]%mod*inv[b-t]%mod;}
 9 signed main(){
10     scanf("%lld%lld",&n,&k);
11     fac[0]=inv[0]=invv[0]=invv[1]=inv[1]=fac[1]=1;
12     for(int i=2;i<=n;++i)fac[i]=fac[i-1]*i%mod,invv[i]=(-mod/i*invv[mod%i])%mod,inv[i]=inv[i-1]*invv[i]%mod;res=c(n,k);
13     for(int i=k;i<=n;++i)
14         ans=(ans+(((k-i)&1)?-1:1)*res*inv[i-k]%mod*fac[n-k]%mod*inv[n-i]%mod*(pow(2,pow(2,n-i,mod-1),mod)-1))%mod;
15     printf("%lld\n",(ans+mod)%mod);
16 }
碼量很小^_^


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM