《集合論與圖論》
這門課程有一道作業題,要求同學們求出的所有滿足以 下條件的子集:若 在該子集中,則 和 不能在該子集中。
同學們不喜歡這種具有枚舉性質的題目,於是把它變成了以下問題:對於任意一個正整數 ,如何求出 的滿足上述約束條件的子集的個數(只需輸出對 取模的結果),現在這個問題就 交給你了。
一道構造
+狀壓dp
的好題目。
我們可以構造這麼一個矩陣,第行第個數是,其後,每行的第個元素是該行第個數的倍,每列的第個數是該列的第個數的倍。比如這樣:
1 3 9 27 81...
2 6 18 54 162...
4 12 36 108 324...
然後,問題就變成了在這個矩陣中選數,且相鄰的數不能同時選的方案數。
考慮到其行數和列數不是很大,所以我們可以用狀壓dp
輕鬆解決。
記第行的數取或不取的狀態用表示,表示第行狀態爲時的答案。轉移具體看代碼。
注意因爲從開始構造矩陣不能把所有的數包含在內,需要把所有可能的矩陣構造出來,然後根據乘法原理,答案相乘即可。
const ll mod=1000000001;
const int N=100100,M=20;
ll line[M],f[2][1<<18],n;
bool chose[N],g[(1<<18)+5];
int a[M][M],lim[M];ll ans=1ll,t;
inline void initialization(){
for(int i=0;i<(1<<18);i++)
if ((i&(i<<1))==0)
g[i]=true;
else g[i]=false;
}//預處理g數組(g[i]:i是否爲可行集合)
inline void make_rectangle(int x){
for(int i=1;i<12;i++){
if (i==1) a[i][1]=x;
else a[i][1]=a[i-1][1]*3;
if (a[i][1]>n) break;
t=i;chose[a[i][1]]=true;line[i]=1;
for(int j=2;j<19;j++){
a[i][j]=a[i][j-1]*2;
if (a[i][j]>n) break;
line[i]=j;chose[a[i][j]]=true;
}
lim[i]=(1<<line[i])-1;
}
}//以x爲首個數字構造我們需要的矩陣
inline ll dp(){
register int i,j,k;
for(i=0;i<=lim[1];i++)
f[1][i]=g[i];
for(i=2;i<=t;i++)
for(j=0;j<=lim[i];j++)
if (g[j]){
f[i&1][j]=0ll;//不能用memset
for(k=0;k<=lim[i-1];k++)
if (g[k]&&((j&k)==0))
f[i&1][j]=(f[i&1][j]+f[(i&1)^1][k])%mod;
}
register ll res=0ll;
for(i=0;i<=lim[t];i++)
if (g[i]) res=(res+f[t&1][i])%mod;
return res;
}//狀壓dp求解子問題
int main(){
scanf("%lld",&n);
initialization();
for(int i=1;i<=n;i++)
if (!chose[i]){
make_rectangle(i);
ans=(ans*dp())%mod;
}
printf("%lld",ans);
return 0;
}
- 一般在數據範圍不大的
dp
題中,我們會優先地考慮狀壓dp
。 - 如果狀態可以轉化爲一個東西取或不取,放或不放,就可以用二進制表示這個狀態,然後把它放入狀態的定義中,使用二進制運算符把這個問題變成
狀壓dp
求解的題目。 - 二進制運算不是很好理解,在上一篇博客中我們講了二進制運算符,所以大家可以根據狀態的定義和二進制運算符的運算規矩畫圖理解。