這裏我給大家寫下我這一週裏邊對狀態壓縮的理解,這裏邊我是借用了很多前學博士的勞動成果,加上了自己的了一片二的理解
希望能夠幫助到大家
狀態壓縮
Abstract
信息學發展勢頭迅猛,信息學奧賽的題目來源遍及各行各業,經常有一些在實際應用中很有價值的問題被引入信息學並得到有效解決。然而有一些問題卻被認爲很可能不存在有效的(多項式級的)算法,本文以對幾個例題的剖析,簡述狀態壓縮思想及其應用。
Keywords
狀態壓縮、集合、Hash、NPC
Content
Introduction
作爲OIers,我們不同程度地知道各式各樣的算法。這些算法有的以O(logn)的複雜度運行,如二分查找、歐幾里德GCD算法(連續兩次迭代後的餘數至多爲原數的一半)、平衡樹,有的以O()運行,例如二級索引、塊狀鏈表,再往上有O(n)、O(nplogqn)……大部分問題的算法都有一個多項式級別的時間複雜度上界,我們一般稱這類問題爲P類(deterministic Polynomial-time)問題,例如在有向圖中求最短路徑。然而存在幾類問題,至今仍未被很好地解決,人們懷疑他們根本沒有多項式時間複雜度的算法,它們是NPC(NP-Complete)和NPH(NP-Hard)類,例如問一個圖是否存在哈密頓圈(NPC)、問一個圖是否不存在哈密頓圈(NPH)、求一個完全圖中最短的哈密頓圈(即經典的Traveling Salesman Problem貨郎擔問題,NPH)、在有向圖中求最長(簡單)路徑(NPH),對這些問題尚不知有多項式時間的算法存在。P和NPC都是NP(Non-deterministic Polynomial-time)的子集,NPC則代表了NP類中最難的一類問題,所有的NP類問題都可以在多項式時間內歸約到NPC問題中去。NPH包含了NPC和其他一些不屬於NP(也更難)的問題(即NPC是NP與NPH的交集), NPC問題的最優化版本一般是NPH的,例如問一個圖是否存在哈密頓圈是NPC的,但求最短的哈密頓圈則是NPH的,原因在於我們可以在多項式時間內驗證一個迴路是否真的是哈密頓迴路,卻無法在多項式時間內驗證其是否是最短的,NP類要求能在多項式時間內驗證問題的一個解是否真的是一個解,所以最優化TSP問題不是NP的,而是NPH的。存在判定性TSP問題,它要求判定給定的完全圖是否存在權和小於某常數v的哈密頓圈,這個問題的解顯然可以在多項式時間內驗證,因此它是NP的,更精確地說是NPC的。
如上所述,對於NPC和NPH問題,至今尚未找到多項式時間複雜度的算法。然而它們的應用又是如此的廣泛,我們不得不努力尋找好的解決方案。毫無疑問,對於這些問題,使用暴力的搜索是可以得到正確的答案的,但在信息學競賽那有限的時間內,很難寫出速度可以忍受的暴力搜索。例如對於TSP問題,暴力搜索的複雜度是O(n!),如此高的複雜度使得它對於高於10的數據規模就無能爲力了。那麼,有沒有一種算法,它可以在很短的時間內實現,而其最壞情況下的表現比搜索好呢?答案是肯定的——狀態壓縮(States Compression,SC)。
作爲對下文的準備,這裏先爲使用Pascal的OIers簡要介紹一下C/C++樣式的位運算(bitwise operation)。
一、 基本運算符
名稱 |
C/C++樣式 |
Pascal樣式 |
簡記法則 |
按位與 (bitwise AND) |
& |
and |
全一則一 否則爲零 |
按位或 (bitwise OR) |
| |
or |
有一則一 否則爲零 |
按位取反 (bitwise NOT) |
~ |
not |
是零則一 是一則零 |
按位異或 (bitwise XOR) |
^ |
xor |
不同則一 相同則零 |
以上各運算符的優先級從高到低依次爲:~,&,^,|
二、 特殊應用
a) and:
i. 用以取出一個數的某些二進制位
ii. 取出一個數的最後一個1(lowbit) :x&-x
b) or :用以將一個數的某些位設爲1
c) not:用以間接構造一些數:~0=4294967295=232-1
d) xor:
i. 不使用中間變量交換兩個數:a=a^b;b=a^b;a=a^b;
ii. 將一個數的某些位取反
有了這些基礎,就可以開始了。
Getting Started
我們暫時避開狀態壓縮的定義,先來看一個小小的例題。
【引例】
在n*n(n≤20)的方格棋盤上放置n個車(可以攻擊所在行、列),求使它們不能互相攻擊的方案總數。
【分析】
這個題目之所以是作爲引例而不是例題,是因爲它實在是個非常簡單的組合學問題:我們一行一行放置,則第一行有n種選擇,第二行n-1,……,最後一行只有1種選擇,根據乘法原理,答案就是n!。這裏既然以它作爲狀態壓縮的引例,當然不會是爲了介紹組合數學。我們下面來看另外一種解法:狀態壓縮遞推(States Compressing Recursion,SCR)。
我們仍然一行一行放置。取棋子的放置情況作爲狀態,某一列如果已經放置棋子則爲1,否則爲0。這樣,一個狀態就可以用一個最多20位的二進制數表示。例如n=5,第1、3、4列已經放置,則這個狀態可以表示爲01101(從右到左)。設f[s]爲達到狀態s的方案數,則可以嘗試建立f的遞推關係。
考慮n=5,s=01101。這個狀態是怎麼得到的呢?因爲我們是一行一行放置的,所以當達到s時已經放到了第三行。又因爲一行能且僅能放置一個車,所以我們知道狀態s一定來自:
①前兩行在第3、4列放置了棋子(不考慮順序,下同),第三行在第1列放置;②前兩行在第1、4列放置了棋子,第三行在第3列放置;
③前兩行在第1、3列放置了棋子,第三行在第4列放置。
這三種情況互不相交,且只可能有這三種情況。根據加法原理,f[s]應該等於這三種情況的和。寫成遞推式就是:
f[01101]=f[01100]+f[01001]+f[00101]
根據上面的討論思路推廣之,得到引例的解決辦法:
f[0]=1
f[s]=∑f[s^2i]
其中s∈[0…01,1…11],s的右起第i+1位爲1。
我的第一次理解的代碼(未修正,有錯誤,有待考慮):
#include<iostream>
#include<cstdio>
using namespace std;
#define LL long long
LL a[22][1<<9],dp[22][1<<9],mark[1<<9];
bool ok(LL i)
{
return true;
}
bool ok_cal(LL x)
{
for(LL i=)
}
int main()
{
while(~scanf("%d",&n))
{
LL zt=0;
for(LL i=0;i<=(1<<n);i++)
if(ok(i))
{
dp[1][zt]=1;
mark[zt]=i;
a[1][zt++]=(i-1)&i^i;
}
for(LL i=2;i<=n;i++)
for(x=0;x<zt;x++)
for(y=0;y<zt;y++)
{
if(ok_cal(x))
dp[i][x]+=dp[i-1][y];
}
LL ans=0;
for(LL i=0;i<zt;i++)
ans+=dp[n][i];
printf("%lld\n",ans);
}
}
正確代碼(網上找的,另外也加上了自己的點點思想):
#include <iostream>
#include<cstdio>
using namespace std; __int64 f[1<<24] = {0};
int lowbit(int x){return x&(-x);}
int main()
{
__int64 i,n,t;
while(~scanf("%I64d", &n))
{
for(i=1;i<=(1<<n);i++)
f[i]=0;
f[0] = 1;
for (i = 1; i <(1<<n); i++)//這裏基本上是模板,可以試着套一下
for (t = i; t > 0; t -=lowbit(t))
f[i] += f[i & ~lowbit(t)];
printf("%I64d\n", f[(1<<n) - 1]);
}
return 0;
}
再然後就是我自己按照那個題目的指引自己寫的,應該是對的,只可惜找了很久,沒有這道題,否則我會提交上去看看對不對的 :
在座的各位讀者可以自己找幾組樣例來幫我試一下這個程序到底錯沒錯?不過我也找了很多組,基本上能夠確定應該是沒做的!!!
#include <iostream>
#include<cstdio>
#include<cstring>
#define LL long long
using namespace std; __int64 ff[1<<24] = {0};
int main()
{
__int64 i,n,t;
while(~scanf("%I64d", &n))
{
memset(ff,0,sizeof ff);
ff[0] = 1;
for(LL s=0;s<=(1<<n)-1;s++)
for(int i=0;i<=n;i++)
ff[s]+=ff[s^(1<<i)];
printf("%I64d\n", ff[(1<<n) - 1]);
}
return 0;
}
反思這個算法,其正確性毋庸置疑(可以和n!對比驗證)。但是算法的時間複雜度爲O(n2n),空間複雜度O(2n),是個指數級的算法,比循環計算n!差了好多,它有什麼優勢?較大的推廣空間。
Sample Problems
【例1】
在n*n(n≤20)的方格棋盤上放置n個車,某些格子不能放,求使它們不能互相攻擊的方案總數。
【分析】
對於這個題目,如果組合數學學得不夠紮實,你是否還能一眼看出解法?應該很難。對於這個題目,確實存在數學方法(容斥原理),但因爲和引例同樣的理由,這裏不再贅述。
聯繫引例的思路,發現我們並不需要對算法進行太大的改變。引例的算法是在枚舉當前行(即s中1的個數,設爲r)的放置位置(即枚舉每個1),而對於例1,第r行可能存在無法放置的格子,怎麼解決這個問題呢?枚舉1的時候判斷一下嘛!事實的確是這樣,枚舉1的時候判斷一下是否是不允許放置的格子即可。
但是對於n=20,O(n2n)的複雜度已經不允許我們再進行多餘的判斷。所以實現這個算法時應該應用一些技巧。對於第r行,我們用a[r]表示不允許放置的情況,如果某一位不允許放置則爲1,否則爲0,這可以在讀入數據階段完成。運算時,對於狀態s,用tmps=s^a[r]來代替s進行枚舉,即不枚舉s中的1轉而枚舉tmps中的1。因爲tmps保證了無法放置的位爲0,這樣就可以不用多餘的判斷來實現算法,代碼中只增加了計算a數組和r的部分,而時間複雜度沒有太大變化。
這樣,我們直接套用引例的算法就使得看上去更難的例1得到了解決。你可能會說,這題用容斥原理更快。沒錯,的確是這樣。但是,容斥原理在這題上只有當棋盤爲正方形、放入的棋子個數爲n、且棋盤上禁止放置的格子較少時纔有簡單的形式和較快的速度。如果再對例1進行推廣,要在m*n的棋盤上放置k個車,那麼容斥原理是無能爲力的,而SCR算法只要進行很少的改變就可以解決問題。這也體現出了引例中給出的算法具有很大的擴展潛力。
棋盤模型是狀態壓縮最好的展示舞臺之一。下面再看幾個和棋盤有關的題目。
下邊就是我自己寫的代碼:
#include <iostream>
#include<cstring>
#include<cstdio>
using namespace std;
__int64 f[1<<24],n,m;
#define LL long long
inline int lowbit(int x){return x&(-x);}
int main()
{
__int64 i,mark[24],a,b;
while(~scanf("%I64d%I64d", &n,&m))//這裏總共有n行m列
{
memset(mark,0,sizeof mark);
for(int i=0;i<m;i++)
{
scanf("%d%d",&a,&b);
mark[a]+=(1<<(b-1));//標記第a行裏邊的第b列出現了1,就是不能填充旗子
}
for(i=1;i<=(1<<n);i++)
f[i]=0;
f[0] = 1;
for (i = 1; i <(1<<n); i++)
{
LL num=0;
for(LL j=i;j>0;j-=lowbit(j))num++;//爲了計算出要填充的第i行裏邊有多少個1?
//也就是填充這麼多的1需要用到的行號
for(LL j=i;j>0;j-=lowbit(j))//這個已經是模板了!!!
if(!(mark[num] & lowbit(j)))//另外判斷這個,也即是有些地方不能放!!!
f[i] += f[i & ~lowbit(j)];
}
printf("%I64d\n", f[(1<<n) - 1]);
}
}
【例2】
給出一個n*m的棋盤(n、m≤80,n*m≤80),要在棋盤上放k(k≤20)個棋子,使得任意兩個棋子不相鄰。每次試驗隨機分配一種方案,求第一次出現合法方案時試驗的期望次數,答案用既約分數表示。
這道題我一開始使用DFS模板,能夠解決行列和對角線的重複部分,但是最後TLE了,如果提交這個的話,最好是在時間要求不高的情況下提交,主要是這個DFS不是最優的解,詳細見代碼如下:
#include <iostream>
#include <cstring>
#define ULL unsigned long long
#define UINT unsigned int
#define LL long long
#include<cstdio>
using namespace std;
LL N,M,K,ans;
void dfs(LL row,LL deep,UINT col){//所在行,已放皇后數量,(列)的狀態
if(deep == K){
ans++;
return;
}
if(row==N) return;
UINT t=1;
for(LL i=0;i<M;i++){
if((t&col) ){
t<<=1;
continue;
}
dfs(row+1,deep+1,(t|col));
t<<=1;
}
if((N-row-1)+deep>=K) //這行不擺放。優化:如果剩下行數都擺放了還不能滿足條件,就不用搜下去了
dfs(row+1,deep,col);
}
int main(){
while(scanf("%I64d%I64d%I64d",&N,&M,&K)){
if(N<M) N=N^M,M=M^N,N=M^N;//保證列數少,方便狀態壓縮
ans=0;
dfs(0,0,0);
printf("%I64d\n",ans);
}
return 0;
}
當然這個代碼是來自這個模板的:
#include <iostream>
#include <cstring>
#define ULL unsigned long long
#define UINT unsigned int
#define LL long long
#include<cstdio>
using namespace std;
LL N,M,K,ans;
void dfs(LL row,LL deep,UINT col,UINT dig,UINT arg){//所在行,已放皇后數量,(列、左對角線、右對角線)的狀態
if(deep == K){
ans++;
return;
}
if(row==N) return;
UINT t=1;
for(LL i=0;i<M;i++){
if((t&col)||(t&dig)||(t&arg) ){
t<<=1;
continue;
}
dfs(row+1,deep+1,(t|col),((t|dig)>>1),(t|arg)<<1);
t<<=1;
}
if((N-row-1)+deep>=K) //這行不擺放。優化:如果剩下行數都擺放了還不能滿足條件,就不用搜下去了
dfs(row+1,deep,col,dig>>1,arg<<1);
}
int main(){
while(scanf("%I64d%I64d%I64d",&N,&M,&K)){
if(N<M) N=N^M,M=M^N,N=M^N;//保證列數少,方便狀態壓縮
ans=0;
dfs(0,0,0,0,0);
printf("%I64d\n",ans);
}
return 0;
}
當然也還有一個DFS初始化的一個代碼,但不幸的是也是TLE,詳見如下:
1. #include<iostream>
2. #include<cstdio>
3. #include<cmath>
4. #include<algorithm>
5. using namespace std;
6. int s[1<<10],c[1<<10],f[82][1<<9][21];
7. int n,m,num,flag,val;
8.
9. void DFS(int ans,int pos,int flag){ ///////////////////////
10. if(pos>n) {
11. s[++num] = ans;
12. c[num] = flag;
13. return;
14. }
15. DFS(ans,pos+1,flag);
16. DFS(ans+(1<<pos-1),pos+2,flag+1);
17. }
18.
19. int main(){
20. while(cin>>n>>m>>val){
21. if(n>m) swap(n,m); // 行列交換
22. num = 0; // 狀態數初始化
23. DFS(0,1,0); // 參數:當前狀態,位置,1的個數 ,找出一行中符合條件的總數
24. memset(f,0,sizeof(f));
25. ///////////////////////////////////////////
26. for(int i=1;i<=num;i++) //第一行進行初始化,狀態中有多少個1就初始多少
27. f[1][s[i]][c[i]] = 1;
28. for(int i=2;i<=m;i++){ //第幾行
29. for(int j=1;j<=num;j++){ //某一行的某個狀態
30. for(int r=1;r<=num;r++){ //上一行的某個狀態
31. if(!(s[j]&s[r])){ //當前行和上一行狀態不衝突
32. for(int k=0;k<=val;k++){ //枚舉當前一行棋子的個數
33. if(k>=c[j]) f[i][s[j]][k] += f[i-1][s[r]][k-c[j]]; //藉助上一行的狀態枚舉當前狀態
34. }
35. }
36. }
37. }
38. }
39. long long sum=0;
40. for(int i=1;i<=num;i++) // 累加最後一行符合條件的總數
41. sum += f[m][s[i]][val];
42. cout<<sum<<endl;
43. }
44. }
再然後自己又寫了個這道題的能夠AC的壯壓:
#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
#define LL long long
LL dp[82][22][1<<6],mark[1<<6],zt,ans;
int n,m,k;
int num(LL x)
{
int sum=0;
while(x)
{
if(x&1)sum++;
x=x>>1;
}
return sum;
}
bool judge_row(int x)
{
return (!(x&(x<<1)));
}
int main()
{
while(~scanf("%d%d%d",&n,&m,&k))
{
zt=0;
memset(dp,0,sizeof dp);
memset(mark,0,sizeof mark);
if(n<m)
{
n=n^m;
m=n^m;
n=n^m;
}
// cout<<"******************"<<endl<<n<<m<<endl;
for(LL i=0;i<(1<<m);i++)
{
if(judge_row(i))
{
dp[1][num(i)][zt]=1;
mark[zt++]=i;
}
}
for(int i=2;i<=n;i++)
for(int j=0;j<=k;j++)
for(LL x=0;x<zt;x++)
for(LL y=0;y<zt;y++)
{
// LL temp=num(mark[x]);
if(((mark[x]&mark[y])==0)&&j>=num(mark[x]))
{
dp[i][j][x]+=dp[i-1][j-num(mark[x])][y];
}
}
ans=0;
for(LL i=0;i<zt;i++)
ans+=dp[n][k][i];
printf("%lld\n",ans);
}
}
【分析】
顯然,本題中的期望次數應該爲出現合法方案的概率的倒數,則問題轉化爲求出現合法方案的概率。而概率=,方案總數顯然爲C(n*m,k),則問題轉化爲求合法方案數。整理一下,現在的問題是:在n*m的棋盤上放k個棋子,求使得任意兩個棋子不相鄰的放置方案數。
這個題目的狀態壓縮模型是比較隱蔽的。觀察題目給出的規模,n、m≤80,這個規模要想用SC是困難的,若同樣用上例的狀態表示方法(放則爲1,不放爲0),280無論在時間還是在空間上都無法承受。然而我們還看到n*m≤80,這種給出數據規模的方法是不多見的,有什麼玄機呢?能把狀態數控制在可以承受的範圍嗎?稍微一思考,我們可以發現:9*9=81>80,即如果n,m都大於等於9,將不再滿足n*m≤80這一條件。所以,我們有n或m小於等於8,而28是可以承受的。我們假設m≤n(否則交換,由對稱性知結果不變)n是行數m是列數,則每行的狀態可以用m位的二進制數表示。但是本題和例1又有不同:例1每行每列都只能放置一個棋子,而本題卻只限制每行每列的棋子不相鄰。但是,上例中枚舉當前行的放置方案的做法依然可行。我們用數組s[1..num]保存一行中所有的num個放置方案,則s數組可以在預處理過程中用DFS求出,同時用c[i]保存第i個狀態中1的個數以避免重複計算。開始設計狀態。如註釋一所說,維數需要增加,原因在於並不是每一行只放一個棋子,也不是每一行都要求有棋子,原先的表示方法已經無法完整表達一個狀態。我們用f[i][j][k]表示第i行的狀態爲s[j]且前i行已經放置了k個棋子的方案數。沿用枚舉當前行方案的做法,只要當前行的方案和上一行的方案不衝突即可,“微觀”地講,即s[snum[i]]和s[snum[i-1]]沒有同爲1的位,其中snum[x]表示第x行的狀態的編號。然而,雖然我們枚舉了第i行的放置方案,但卻不知道其上一行(i-1)的方案。爲了解決這個問題,我們不得不連第i-1的狀態一起枚舉,則可以寫出遞推式:
f[0][1][0]=1;
f[i][j][k]=∑f[i-1][p][k-c[j]]
其中s[1]=0,即在當前行不放置棋子;j和p是需要枚舉的兩個狀態編號,且要求s[j]與s[p]不衝突,即s[j]&s[p]=0。
當然,實現上仍有少許優化空間,例如第i行只和第i-1行有關,可以用滾動數組節省空間。
有了合法方案數,剩下的問題就不是很困難了,需要注意的就只有C(n*m,k)可能超出64位整數範圍的問題,這可以通過邊計算邊用GCD約分來解決,具體可以參考附件中的代碼。這個算法時間複雜度O(n*pn*num2),空間複雜度(滾動數組)O(pn*num),對於題目給定的規模是可以很快出解的。
通過上文的例題,讀者應該已經對狀態壓縮有了一些感性的認識。下面這個題目可以作爲練習。
【例3】
在n*n(n≤10)的棋盤上放k個國王(可攻擊相鄰的8個格子),求使它們無法互相攻擊的方案數。
【分析】
其實有了前面幾個例子的分析,這個題目應該是可以獨立解決的。不過既然確實有疑問,那我們就來分析一下。
首先,你應該能想到將一行的狀態DFS出來(如果不能,請返回重新閱讀,謝謝),仍然設爲s[1..num],同時仍然設有數組c[1..num]記錄狀態對應的1的個數。和例2相同,仍然以f[i][j][k]表示第i行狀態爲s[j],且前i行已經放置了k個棋子的方案數。遞推式仍然可以寫作:
f[0][1][0]=1;
f[i][j][k]=∑f[i-1][p][k-c[j]]
其中仍要求s[j]和s[p]不衝突。
可是問題出來了:這題不但要求不能行、列相鄰,甚至不能對角線相鄰!s[j]、s[p]不衝突怎麼“微觀地”表示呢?其實,稍微思考便可以得出方法:用s[p]分別和s[j]、s[j]*2、s[j]/2進行衝突判斷即可,原理很顯然。解決掉這唯一的問題,接下來的工作就沒有什麼難度了。算法複雜度同例2。
這是那個第二個TLE代碼的昇華:
來幾組測試樣例:
3 3 2-------->16
2 2 1-------->4
3 3 3-------->8
9 9 2-------->2968
1. #include<iostream>
2. #include<cmath>
3. #include<algorithm>
4. #include<cstring>
5. #include<cstdio>
6. using namespace std;
7. long long f[11][1<<10][30];
8. int s[1<<10],c[1<<10],num;
9. int n,m,val;
10.
11. void DFS(int ans,int pos,int flag){
12. if(pos>n){
13. s[++num] = ans;
14. c[num] = flag;
15. return ;
16. }
17. DFS(ans,pos+1,flag);
18. DFS(ans+(1<<pos-1),pos+2,flag+1);
19. }
20.
21. int main(){
22. while(cin>>n>>m>>val){
23. num = 0;
24. DFS(0,1,0);
25. memset(f,0,sizeof(f));
26. for(int i=1;i<=num;i++)
27. f[1][s[i]][c[i]]=1;
28. for(int i=2;i<=n;i++){
29. for(int j=1;j<=num;j++){ //當前行
30. for(int r=1;r<=num;r++){ //上一行
31. if((s[j]&s[r]) || ((s[j]>>1)&s[r]) || ((s[j]<<1)&s[r])) continue; //八個方向判斷
32. for(int k=0;k<=val;k++){
33. if(k>=c[j]) f[i][s[j]][k] += f[i-1][s[r]][k-c[j]];
34. }
35. }
36. }
37. }
38. long long sum=0;
39. for(int i=1;i<=num;i++)
40. sum += f[n][s[i]][val];
41. cout<<sum<<endl;
42. }
43. }
下邊就是能夠A的代碼:
#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
#define LL long long
LL dp[82][30][1<<10],mark[1<<10],zt,ans;
int n,m,k;
int num(LL x)
{
int sum=0;
while(x)
{
if(x&1)sum++;
x=x>>1;
}
return sum;
}
bool judge_row(int x)
{
return (!(x&(x<<1)));
}
int main()
{
while(~scanf("%d%d%d",&n,&m,&k))
{
zt=0;
memset(dp,0,sizeof dp);
memset(mark,0,sizeof mark);
if(n<m)
{
n=n^m;
m=n^m;
n=n^m;
}
// cout<<"******************"<<endl<<n<<m<<endl;
for(LL i=0;i<(1<<m);i++)
{
if(judge_row(i))
{
dp[1][num(i)][zt]=1;
mark[zt++]=i;
}
}
for(int i=2;i<=n;i++)
for(int j=0;j<=k;j++)
for(LL x=0;x<zt;x++)
for(LL y=0;y<zt;y++)
{
// LL temp=num(mark[x]);
if(((mark[x]&mark[y])==0)
&&j>=num(mark[x])
&&(!((mark[x]<<1)&mark[y]))//只是變了這個地方,另外還加上那個數組的大小開大了點,其他的都沒變
&&(!((mark[x]>>1)&mark[y]))
)
{
dp[i][j][x]+=dp[i-1][j-num(mark[x])][y];
}
}
ans=0;
for(LL i=0;i<zt;i++)
ans+=dp[n][k][i];
printf("%lld\n",ans);
}
}
下一個例題是狀態壓縮棋盤模型的經典題目,希望解決這個經典的題目能夠增長你的自信。
【例4】
給出一個n*m(n≤100,m≤10)的棋盤,一些格子不能放置棋子。求最多能在棋盤上放置多少個棋子,使得每一行每一列的任兩個棋子間至少有兩個空格。
【分析】
顯然,你應該已經有DFS搜出一行可能狀態的意識了(否則請重新閱讀之前的內容3遍,謝謝),依然設爲s[1..num],依舊有c[1..num]保存s中1的個數,依照例1的預處理搞定不能放置棋子的格子。
問題是,這個題目的狀態怎麼選?繼續像例2、3那樣似乎不行,原因在於棋子的攻擊範圍加大了。但是我們照葫蘆畫瓢:例2、3的攻擊範圍只有一格,所以我們的狀態中只需要有當前行的狀態即可進行遞推,而本題攻擊範圍是兩格,因此增加一維來表示上一行的狀態。用f[i][j][k]表示第i行狀態爲s[j]、第i-1行狀態爲s[k]時前i行至多能放置的棋子數,則狀態轉移方程很容易寫出:
f[i][j][k]=max{f[i-1][k][l]}+c[j]
其中要求s[j],s[k],s[l]互不衝突。
因爲棋子攻擊範圍爲兩格,可以直觀地想象到num不會很大。的確,由例2中得到的num的計算式並代入d=2、m=10,得到num=60。顯然算法時間複雜度爲O(n*num3),空間複雜度(滾動數組)O(num2)。此算法還有優化空間。我們分別枚舉了三行的狀態,還需要對這三個狀態進行是否衝突的判斷,這勢必會重複枚舉到一些衝突的狀態組合。我們可以在計算出s[1..num]後算出哪些狀態可以分別作爲兩行的狀態,這樣在DP時就不需要進行盲目的枚舉。這樣修改後的算法理論上比上述算法更優,但因爲num本身很小,所以這樣修改沒有顯著地減少運行時間。值得一提的是,本題筆者的算法雖然在理論上並不是最優,但由於位運算的使用,截至2月9日,筆者的程序在PKU OJ上長度最短,速度第二快。
這個題目是國內比賽中較早出現的狀態壓縮題。它告訴我們狀態壓縮不僅可以像前幾個例題那樣求方案數,而且可以求最優方案,即狀態壓縮思想既可以應用到遞推上(SCR),又可以應用到DP上(SCDP),更說明其有廣泛的應用空間。
這裏有個example:
輸入
3 3 3
2 1
2 2
2 3
輸出:
2
樣例展示:
1 0 0
X x x
0 0 1
或者:
0 0 1
X x x
1 0 0
X代表不能放東西,1代表放了一個東西,0代表沒有放東西
求最後最多放幾個?
DFS代碼:有幾個不是很理解。。。
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<cmath>
using namespace std;
long long f[11][1<<10];
int n,m,s[1<<10],c[1<<10];
int num,ant,a[1<<10];
void DFS(int ans,int pos,int flag){
if(pos>m){
s[++num] = ans;
c[num] = flag;
return ;
}
DFS(ans,pos+1,flag);
DFS(ans+(1<<pos-1),pos+3,flag+1);
}
int main(){
while(cin>>n>>m>>ant){
int p,q;
for(int i=0;i<ant;i++){
cin>>p>>q;
a[p] += (1<<q-1);
}
memset(f,0,sizeof(f));
num = 0;
DFS(0,1,0);
// for(int i=1;i<=num;i++) cout<<s[i]<<" "<<c[i]<<endl;
for(int i=1;i<3 && i<=n;i++){
for(int j=1;j<=num;j++){ // i==2時,表示當前行 , i==1時表示當前行
if(i==1){
if(a[i]&s[j]) continue; // 不能放置
f[i][s[j]]=c[j];
}
else {
if(a[i]&s[j]) continue; // 不能放置
for(int r=1;r<=num;r++){ // 不能放置並且和狀態 1 不衝突 , 表示前一行的狀態
if(a[i-1]&s[r]) continue; // 不能放置
if((s[j]&s[r])) continue;
f[i][s[j]] = max(f[i][s[j]],f[i-1][s[r]]+c[j]);
}
}
}
}
for(int i=3;i<=n;i++){
for(int j=1;j<=num;j++){//當前行
if(s[j]&a[i]) continue;
for(int r=1;r<=num;r++){//上一行 i-1
if((s[r]&a[i-1]) || (s[j]&s[r])) continue;
for(int h=1;h<=num;h++){//再上一行 i-2
if((s[h]&a[i-3])||(s[j]&s[h])||(s[r]&s[h])) continue;
f[i][s[j]] = max(f[i][s[j]],f[i-2][s[h]]+c[j]+c[r]);
}
}
}
}
long long sum = 0;
for(int i=1;i<=num;i++)
sum = max(f[n][s[i]],sum);
cout<<sum<<endl;
}
}
然後就是我自己寫的:
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<cmath>
using namespace std;
#define LL long long
long long f[11][1<<10];
int n,m,s[1<<10],c[1<<10];
int num,ant,a[1<<10];
int NUM(LL x)
{
int num=0;
for(LL j=x;j;j-=(j&(-j)))num++;
return num;
}
bool judge_row(int x)
{
return ((!(x&(x<<1)))&&(!(x&(x<<2))));
}
int main(){
while(cin>>n>>m>>ant){
int p,q;
for(int i=0;i<ant;i++){
cin>>p>>q;
a[p] += (1<<q-1);
}
if(n<m){n=n^m;m=n^m;n=n^m;}
memset(f,0,sizeof(f));
num = 1;
// DFS(0,1,0);
for(LL i=0;i<(1<<m);i++)
{
if(judge_row(i))
{
f[1][num]=1;
c[num]=NUM(i);
s[num++]=i;
}
}
// for(int i=1;i<=num;i++) cout<<s[i]<<" "<<c[i]<<endl;
for(int i=2;i<=3 && i<=n;i++){
for(int j=1;j<=num;j++){
if(a[i]&s[j]) continue;
// i==2時,表示當前行
if(i==2){
for(int r=1;r<=num;r++)
{
if(s[r]&s[j])continue;
// 不能放置
f[i][s[j]]=c[j];
}
}
else {//也就是i==3,代表放置第三行
// 不能放置
for(int r=1;r<=num;r++){ // 和狀態 1 不衝突 , 表示前一行的狀態,就是第二行的狀態
if(a[i-1]&s[r]) continue; // 第二行不能放置
if((s[j]&s[r])) continue;
for(int h=1;h<=num;h++)//這裏檢索第一行看看能不能放置
{
if((s[h]&s[j])||(s[h]&s[r])||(s[h]&a[i-2]))continue;
// f[i][s[j]] = max(f[i][s[j]],f[i-1][s[r]]+c[j]);
f[i-1][s[r]]=max(f[i-1][s[r]],f[i-2][s[h]]);
f[i][s[j]] = max(f[i][s[j]],f[i-1][s[r]]+c[j]);
}
}
}
}
}
for(int i=4;i<=n;i++){
for(int j=1;j<=num;j++){//當前行
if(s[j]&a[i]) continue;
for(int r=1;r<=num;r++){//上一行 i-1
if((s[r]&a[i-1]) || (s[j]&s[r])) continue;
for(int h=1;h<=num;h++){//再上一行 i-2
if((s[h]&a[i-2])||(s[j]&s[h])||(s[r]&s[h])) continue;
// f[i][s[j]] = max(f[i][s[j]],f[i-1][s[r]]+c[j]);一路下來找到最大的棋子數
f[i][s[j]] = max(f[i][s[j]],f[i-2][s[h]]+c[j]+c[r]);
}
}
}
}
long long sum = 0;
for(int i=1;i<=num;i++)
sum = max(f[n][s[i]],sum);
cout<<sum<<endl;
}
}
//3 3 3
//2 1
//2 2
//2 3
看了這麼多棋盤模型應用狀態壓縮的實例,你可能會有疑問,難道狀態壓縮只在棋盤上放棋子的題目中有用?不是的。我們暫時轉移視線,來看看狀態壓縮在其他地方的應用——覆蓋模型。
【例5】
給出n*m (1≤n、m≤11)的方格棋盤,用1*2的長方形骨牌不重疊地覆蓋這個棋盤,求覆蓋滿的方案數。
【分析】
這也是個經典的組合數學問題:多米諾骨牌完美覆蓋問題(或所謂二聚物問題)。有很多關於這個問題的結論,甚至還有個專門的公式:如果m、n中至少有一個是偶數,則結果=。這個公式形式比較簡單,且計算的複雜度是O()的,很高效。但是這個公式內還有三角函數,且中學生幾乎不可能理解,所以對我們能力的提高沒有任何幫助。用SCR算法能較好地解決這個問題。
顯然,如果n、m都是奇數則無解(由棋盤面積的奇偶性知),否則必然有至少一個解(很容易構造出),所以假設n、m至少有一個偶數,且m≤n(否則交換)。我們依然像前面的例題一樣把每行的放置方案DFS出來,逐行計算。用f[i][s]表示把前i-1行覆蓋滿、第i行覆蓋狀態爲s的覆蓋方案數。因爲在第i行上放置的骨牌最多也只能影響到第i-1行,則容易得遞推式:
f[0][1…11]=1
f[i][s1]=∑f[i-1][s2]
其中(s1,s2)整體作爲一個放置方案,可以把所有方案DFS預處理出來。下面討論一下本題的一些細節。
首先討論DFS的一些細節。對於當前行每一個位置,我們有3种放置方法:①豎直覆蓋,佔據當前格和上一行同一列的格;②水平覆蓋,佔據當前格和該行下一格;③不放置骨牌,直接空格。如何根據這些枚舉出每個(s1,s2)呢?下面介紹兩種方法:
第一種:
DFS共5個參數,分別爲:p(當前列號),s1、s2(當前行和上一行的覆蓋情況),b1、b2(上一列的放置對當前列上下兩行的影響,影響爲1否則爲0)。初始時s1=s2=b1=b2=0。①p=p+1,s1=s1*2+1,s2=s2*2,b1=b2=0;②p=p+1,s1=s1*2+1,s2=s2*2+1,b1=1,b2=0;③p=p+1,s1=s1*2,s2=s2*2+1,b1=b2=0。當p移出邊界且b1=b2=0時記錄此方案。
第二種:
觀察第一種方法,發現b2始終爲0,知這種方法有一定的冗餘。換個更自然的方法,去掉參數b1、b2。①p=p+1,s1=s1*2+1,s2=s2*2;②p=p+2,s1=s1*4+3,s2=s2*4+3;③p=p+1,s1=s1*2,s2=s2*2+1。當p移出邊界時記錄此方案。這樣,我們通過改變p的移動距離成功簡化了DFS過程,而且這種方法更加自然。
DFS過程有了,實現方法卻還有值得討論的地方。前面的例題中,我們爲什麼總是把放置方案DFS預處理保存起來?是因爲不合法的狀態太多,每次都重新DFS太浪費時間。然而回到這個題目,特別是當採用第二種時,我們的DFS過程中甚至只有一個判斷(遞歸邊界),說明根本沒有多少不合法的方案,也就沒有必要把所有方案保存下來,對於每行都重新DFS即可,這不會增加運行時間卻可以節省一些內存。
這個算法時間複雜度爲多少呢?因爲DFS時以兩行爲對象,每行2m,共進行n次DFS,所以是O(n*4m)?根據“O”的上界意義來看並沒有錯,但這個界並不十分精確,也可能會使人誤以爲本算法無法通過1≤n、m≤11的測試數據,而實際上本算法可以瞬間給出m=10,n=11時的解。爲了計算精確的複雜度,必須先算出DFS得到的方案數。
考慮當前行的放置情況。如果每格只有①③兩個選擇,則應該有2m种放置方案;如果每格有①②③這3個選擇,且②中p只移動一格,則應該有3m种放置方案。然而現在的事實是:每格有①②③這3個選擇,但②中p移動2格,所以可以知道方案數應該在2m和3m之間。考慮第i列,則其必然是:第i-1列採用①③達到;第i-2列採用②達到。設h[i]表示前i列的方案數,則得到h[i]的遞推式:
h[0]=1,h[1]=2
h[i]=2*h[i-1]+h[i-2]
應用組合數學方法求得其通項公式h[m]=。注意到式子的第二項是多個絕對值小於1的數的乘積,其對整個h[m]的影響甚小,故略去,得到方案數h[m]≈0.85*2.414m,符合2m<h[m]<3m的預想。
因爲總共進行了n次DFS,每次複雜度爲O(h[m]),所以算法總時間複雜度爲O(n*h[m])=O(n*0.85*2.414m),對m=10,n=11不超時也就不足爲奇了。應用滾動數組,空間複雜度爲O(2m)。
對於本題,我們已經有了公式和SCR兩種算法。公式對於m*n不是很大的情況有效,SCR算法在競賽中記不住公式時對小的m、n有效。如果棋盤規模爲n*m(m≤10,n≤231-1),則公式和SCR都會嚴重超時。有沒有一個算法能在1分鐘內解決問題呢?答案是肯定的,它仍然用到SC思想。
此算法中應用到一個結論:給出一個圖的鄰接矩陣G(允許有自環,兩點間允許有多條路徑,此時G[i][j]表示i到j的邊的條數),則從某點a走k步到某點b的路徑數爲Gk[a][b]。本結論實際上是通過遞推得到的,簡單證明如下:從i走k步到j,必然是從i走k-1步到t,然後從t走1步到j,根據加法原理,即G[k][i][j]=∑G[k-1][i][t]*G[t][j]。是否感到這個式子很眼熟?沒錯,它和矩陣乘法一模一樣,即:G[k]=G[k-1]*G。因爲矩陣乘法滿足結合律,又由G[1]=G,所以我們得到結果:G[k]=Gk。
下面介紹這個算法。考慮一個有2m個頂點的圖,每個頂點表示一行的覆蓋狀態,即SCR算法中的s1或s2。如果(s1,s2)爲一個放置方案,則在s2和s1之間連一條(有向)邊,則我們通過DFS一次可以得到一個鄰接矩陣G。仍然按照逐行放置的思想來考慮,則要求我們每行選擇一個覆蓋狀態,且相鄰兩行的覆蓋狀態(s1,s2)應爲一個放置方案,一共有n行,則要求選擇n個狀態,在圖中考慮,則要求我們從初始(第0行)頂點(1...111)n步走到(1…111),因爲圖的鄰接矩陣是DFS出來的,每條邊都對應一個放置方案,所以可以保證走的每條邊都合法。因此,我們要求的就是頂點(1…111)走n步到達(1…111)的路徑條數。由上面的結論知,本題的答案就是Gn[1…111][1…111]。
現在的問題是,如何計算G的n次冪?連續O(n)次矩陣乘法嗎?不可取。矩陣的規模是2m*2m,一次普通矩陣乘法要O((2m)3)=O(8m),O(n)次就是O(n*8m),比SCR算法還要差得多。其實我們可以借用二分的思想。如果要計算38的值,你會怎麼算呢?直接累乘將需要進行7次乘法。一種較簡單的方法是:3*3=32,32*32=34,34*34=38,只進行了3次乘法,效率高了許多。因爲矩陣乘法滿足結合律,所以可以用同樣的思路進行優化。這種思路用遞歸來實現是非常自然的,然而,本題的矩陣中可能有210*210=220=1048576個元素,如果用(未經優化的)遞歸來實現,將可能出現堆棧溢出。不過慶幸的是我們可以非遞歸實現。用bin[]保存n的二進制的每一位,從最高位、矩陣G開始,如果bin[當前位]爲0,則把上一位得到的矩陣平方;如果爲1,則平方後再乘以G。這種方法的時間複雜度容易算出:O(logn) 次矩陣乘法,每次O(8m),共O(8m*logn)。
這樣對於m≤7就可以很快出解了。但對於m=n=8,上述算法都需要1s才能出解,無法令人滿意。此算法還有優化空間。
我們的矩陣規模高達2m*2m=4m,但是其中有用的(非0的)有多少個呢?根據介紹SCR算法時得到的h[m]計算式,G中有4m-h[m]=4m-0.85*2.414m個0,對於m=8,可以算出G中98.5%的元素都是0,這是一個非常非常稀疏的矩陣,使用三次方的矩陣乘法有點大材小用。我們改變矩陣的存儲結構,即第p行第q列的值爲value的元素可以用一個三元組(p,q,value)來表示,採用一個線性表依行列順序來存儲這些非0元素。怎樣對這樣的矩陣進行乘法呢?觀察矩陣乘法的計算式,當a[i][k]或者b[k][j]爲0時,結果爲0,對結果沒有影響,完全可以略去這種沒有意義的運算。則得到計算稀疏矩陣乘法的算法:枚舉a中的非0元素,設爲(p,q,v1),在b中尋找所有行號爲q的非0元素(q,r,v2),並把v1*v2的值累加到c[p][r]中。這個算法多次用到一個操作:找出所有行號爲q的元素,則可以給矩陣附加一個數組hp[q],表示線性表中第一個行號爲q的元素的位置,若不存在則hp[q]=0。算出二維數組c之後再對其進行壓縮存儲即可。此矩陣乘法的時間複雜度爲O(),在最壞情況下,a.not0=b.not0=4m,算法的複雜度爲O(8m),和經典算法相同。因爲矩陣非常稀疏,算法複雜度近似爲O(4m) 。考慮整個算法的時間複雜度:O(logn)次矩陣乘法,每次O(4m),則總時間複雜度O(logn*4m),對於m≤9也可以很快出解了,對於m=10,n=2147483647,此算法在筆者機器上(Pm 1.6G,512M)運行時間少於20s。雖然仍然不夠理想,但已經不再超時數小時。此算法空間複雜度爲O(max_not0+4m),對於m=10,max_not0小於190000。
以上給出了公式、SCR、矩陣乘方這3個算法,分別適用於不同的情況,本題基本解決。
讀者應該已經注意到,覆蓋模型和棋盤模型有很多共同點,譬如都是在矩形某些位置放入棋子(或某種形狀的骨牌)來求方案數(如上例)或最優解(下面將會給出幾個例題)。但不難看出,覆蓋模型和棋盤模型又有着很大的不同:棋盤模型中,只要棋子所在的位置不被別的棋子攻擊到即可,而覆蓋模型中,棋子的攻擊範圍也不可以重疊。所以簡單來說,覆蓋模型就是攻擊範圍也不能重疊的棋盤模型。下面再給出一個與上例類似的覆蓋模型的例題以加深印象。
來幾組樣例:
輸入;
2 3
輸出:
3
輸入:
3 4
輸出
11
輸入:
4 5
輸出:
95
使用矩形快速冪的方法也可以求解,這樣可以解決一個很大的數和一個很小的數的棋盤,比如說一個N*4的棋盤,當然這裏N<=10^9,這裏N特別大,不適合其他的方法,如果選用輪廓DP的話,那麼對應的DP[i][s],s不可能取到了2^(10^9)這麼大,其他的方法也是不行的,所以這裏我假設m=4,然後就求N*4的棋盤上,放置1*2的矩形,最後得到的方案數:
思考:
我們建立一個矩陣,分別是由上一行的狀態和該行的狀態組成的長和寬,然後上由上一個矩陣到達下一個矩陣就代表從上兩行到達下兩行,當然具體就是該行的上一行加上該行 到達 該行加上該行的下一行,我們爲了方便起見,只是把每一個矩陣的兩個狀態(兩行)歸結爲一個狀態(一行),那就是相鄰的兩行中的第一行,那麼也就是代表從第0行(本來是不允許放置旗子的,在棋盤的上方一行),到達第n行所有的方案數,當然每一行裏邊(其實是每相鄰兩行的第一行的另一種表示)都要滿足一個前提要求才算是加一個,這個要求就是一定要滿足每行的每一列都要是1,都要填滿。。。矩陣G[i][j]代表從狀態i到達狀態j,所有的方案數,當然我已近知道了從i到達j總共有n步需要走,那麼最後的方案數就是T[i][j]=G[i][j]^n,矩陣的快速冪...
最後答案就是T[2^4-1][2^4-1]。。。
輸入(分別是N和mod):
1 10000
3 10000
5 10000
0 0
輸出:
1
11
95
詳細代碼見下邊:
說明下,以後以後到了矩陣,最好用一個結構體去解決。。。而矩陣的初始化最好就是在結構體裏邊去初始化,在構造函數裏邊。。。
#include<iostream>
#include<cstring>
#include<cstdio>
using namespace std;
#define LL long long
int n,d;
int mod;
#define MAX 16
#define mem(s,k) memset(s,k,sizeof s)
#define ref(k,i,n) for(int k=i;k<n;k++)
#define Ref(k,i,n) for(int k=i;k<=n;k++)
#define ENDL cout<<***********<<endl;
typedef struct Matrix
{
int m[MAX][MAX];
Matrix(){memset(m,0,sizeof m);}//基本上以後使用 矩陣 的話都會用到 結構體,而這個基本上必不可少,否則錯誤就是意想不到的了。。。
Matrix operator*(Matrix x){
Matrix ans;
ref(i,0,d)
ref(j,0,d)
{
ans.m[i][j]=0;
for(int k=0;k<d;k++)
ans.m[i][j]=(ans.m[i][j]+m[i][k]*x.m[k][j])%mod;
}
return ans;
}
};
Matrix x;
Matrix quick_pow(Matrix a,int k)//矩陣快速冪
{
Matrix p;
mem(p.m,0);
ref(i,0,d)
p.m[i][i]=1;
while(k)
{
if(k&1)p=p*a;
k/=2;
a=a*a;
}
return p;
}
void dfs(int cal,int last,int now)//dfs出來第0行和第一行的所有符合條件的情況,並且初始化爲1,last是第0行,1是第一行的狀態
{
if(cal>=4)
{
if(cal==4) x.m[last][now]=1;
return;
}
dfs(cal+1,last<<1,now<<1|1);
dfs(cal+1,last<<1|1,now<<1);
dfs(cal+2,last<<2|3,now<<2|3);
}
int main()
{
while(~scanf("%d%d",&n,&mod),n)
{
d=16;
mem(x.m,0);
dfs(0,0,0);
x=quick_pow(x,n);
printf("%d\n",x.m[15][15]);
}
}
適合於求一個不太大的而且要求時間快的話,輪廓或者那個DFS加一個循環的。。
註明該輪廓線並不是以一行爲基準的,而是以上一行裏邊該列開始到這一行裏邊的該列的前一列結束,自然輪廓首位自然就是第m-1位,輪廓的尾部就是該行該列的需要考慮的格子的前一列就是第0位,當如果轉移到下一個狀態,那就是輪廓的上一個狀態的輪廓首位就會去掉,換成了那一列的下一列,而輪廓尾部就是上一個狀態的輪廓尾部往後移動一列,當然也是我上一個回合需要考慮的那個格子。。。
補充一點:這裏都是以右下角的那個格子開始來考慮的,分別有向上,向左,和不放的選擇;
如果向上,那麼就要求上一個狀態中要放的那個位置的上邊一定是0,而且還不是第0行,因爲上邊沒有格子放;
如果向左,那麼就要求上一個狀態中要放的那個位置的左邊一定是0,而且還不是第0列,因爲上邊沒有格子放;
如果不放置的話,當然也沒特殊限制了;
大概表示就是:
假設m=5;
# # 4 3 2
1 0 X **
註明一下:
#代表已經放過了的格子;
數字代表在狀態中的位數編號,這裏的4,3,2,1,0,分別就是一個二進制位的編號從0~m-1的編號;
*代表還沒有放東西的格子;
X代表正在需要考慮放的格子;
#include<cstdio>
#include<algorithm>
#include<cstring>
using namespace std;
#define swap(a,b) {a=a^b;b=a^b;a=a^b;}
const int maxn = 15; //這裏的maxn是代表n和m裏邊的 較小 的那個
long long d[2][1<<maxn]; //開的範圍較小,只有兩種狀態的位置交替更新
int n,m,cur;
void update(int a,int b)//a是包含m位2進制數的老狀態,b是包含m+1位2進制數的新狀態
{
if(b&(1<<m)) d[cur][b^(1<<m)] += d[1-cur][a];//只有新輪廓線首位爲1時才更新,因爲需要保證在更新完成後要考慮放旗子的放置的上邊一個格子一定要是已經放了的,只有這樣才能繼續往後邊更新,否則就是非法的更新,當不可取!!!
}
int main()
{
while( scanf("%d%d",&n,&m)==2&&n&&m )
{
if(m>n)swap(n,m);
memset(d,0,sizeof(d));
cur=0;
d[cur][(1<<m)-1]=1;
for(int i=0;i<n;i++)
for(int j=0;j<m;j++)
{
cur ^=1;
memset(d[cur],0,sizeof(d[cur]));
for(int k=0;k<(1<<m);k++)//k的二進制形式表示前一個格子的輪廓線狀態
{
update(k,k<<1);//當前格不放,直接k左移一位就表示帶m+1位的新輪廓線的狀態 //這裏三個順序可以顛倒,無礙
if(i && !(k&(1<<(m-1)) )) update(k,(k<<1)^(1<<m)^1);//上放,要求輪廓線首爲0 ,也就是需要考慮放的地方的上邊一定是0
if(j && (!(k&1)) ) update(k,(k<<1)^3);//左放,要求輪廓線尾0,首1
}
}
printf("%I64d\n",d[cur][(1<<m)-1]);
}
return 0;
}
//最快的代碼 ,DFS加一個循環:
#include <iostream>
#include <cstring>
#include <cstdio>
#define LL long long
using namespace std;
int n, m, x;
LL f[12][1 << 12];
void dfs (int k, int last, int now)
{
if (k ==m )f[x][now] += f[x - 1][last];
if (k > m) return;
dfs (k + 2, last << 2 | 3, now<<2|3);
dfs (k + 1, last << 1 | 1, now <<1);
dfs (k + 1, last << 1, now<<1|1);
}
int main() {
while (~scanf ("%d %d", &n, &m) ) {
if (n == 0) break;
if (n > m) swap (n, m);
memset (f, 0, sizeof f);
f[0][ (1 << m) - 1] = 1;
for (x = 1; x <= n; x++)
dfs (0, 0, 0);
printf ("%lld\n", f[n][ (1 << m) - 1]);
}
}
代碼2實驗失敗的代碼:
#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
#define LL long long
int m,n,dp[12][1<<12];
int ans;
void dfs(int row,int pos,LL s)
{
if(pos==m){
dp[row][s]+=ans;
return ;
}
if(pos>m)return;
dfs(row,pos+1,s);
if(pos<=m-2&&(!(s&(1<<pos+1)))&&(!(s&(1<<pos+2))))
{
dfs(row,pos+2,s|(1<<pos+1)|(1<<pos+2));
}
// if(pos<m-1&&(s&(1<<pos+1)==0)&&(s&(1<<pos+2)==0))
// dfs(row,pos+2,s|(1<<pos+1)|(1<<pos+2));
}
int main()
{
while(~scanf("%d%d",&n,&m),n+m)
{
if(n<m){n=n^m;m=n^m;n=n^m;}
ans=1;
memset(dp,0,sizeof dp);
dfs(1,0,0);
for(int i=2;i<=n;i++)
for(LL x=0;x<(1<<m);x++)
{
if(dp[i-1][x])
{
// cout<<"j:"<<x<<endl;
ans=dp[i-1][x];
// cout<<ans<<endl;
dfs(i,0,(~x) & ((1<<m)-1));
}
}
LL sum=0;
for(LL x=0;x< (1<<m);x++)
sum+=dp[n][x];
printf("%lld\n",sum);
}
}
DFS代碼:
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<cmath>
#include<string>
using namespace std;
long long f[12][1<<14];
int s1[1<<14],s2[1<<14],s[1<<14],ss[1<<14];
int n,m,num,flag;
bool vist[1<<14];
void DFS(int ans1,int ans2,int pos){
if(pos>n) {
if(pos==n+1) {
s1[++num] = ans1;
s2[num] = ans2;
}
return ;
}
DFS(ans1,ans2,pos+1); // 不放
DFS(ans1+(1<<pos-1)+(1<<pos),ans2,pos+2); // 橫放
DFS(ans1+(1<<pos-1),ans2+(1<<pos-1),pos+1); // 豎放
}
void dfs(int ans,int pos){
if(pos>n){
if(pos==n+1)
s[++flag] = ans;
return;
}
dfs(ans,pos+1);
dfs(ans+(1<<pos-1)+(1<<pos),pos+2);
}
int main(){
while(cin>>n>>m){
if(n==0 && m==0) break;
if(n%2 && m%2){ cout<<'0'<<endl; continue; }
if(n > m) swap(n,m);
if(n%2) swap(n,m);
if(m==1) { cout<<'1'<<endl; continue; }
num = 0; flag = 0;
DFS(0,0,1);
dfs(0,1); // 第一行所有狀態搜索
memset(f,0,sizeof(f));
for(int i=1;i<=flag;i++)
f[1][s[i]] = 1;
int ant = (1<<n) - 1;
for(int i=1;i<=num;i++){ //第二行
ss[i] = s1[i];
for(int j=1;j<=flag;j++){ //第一行
if((s2[i]&s[j])) continue;
if((ant-s2[i])&(ant-s[j])) continue; //處理相同的 0
f[2][s1[i]] += f[1][s[j]];
}
}
sort(ss+1,ss+num);
int ans = 0;
ss[++ans] = ss[1];
for(int i=2;i<=num;i++){ //除去相同的 ,避免重複計算
if(ss[i]!=ss[i-1]) ss[++ans] = ss[i];
}
for(int i=3;i<=m;i++){ //第三行
for(int j=1;j<=num;j++){//第 i 行狀態遍歷
for(int r=1;r<=ans;r++){ // 第 i-1 行 *****相同的 s1[r]只能計算一次 WA了N久
if(s2[j]&ss[r]) continue;
if((ant-s2[j])&(ant-ss[r])) continue;
f[i][s1[j]] += f[i-1][ss[r]];
}
}
}
cout<<f[m][(1<<n)-1]<<endl;
}
}
最慢的代碼:
#include<algorithm>
#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
int n , m;
long long dp[12][1<<12];
/*對第一行的預處理懲罰*/
void init(int state , int pos){
if(pos >= m){
dp[0][state] = 1;
return;
}
if(pos < m)
init(state<<1 , pos+1);
if(pos + 1 < m)
init(state<<2|3 , pos+2);
}
/*斷定當前兩種狀況是否滿足*/
bool judge(int stateX , int stateY){
int x , y , pos;
pos = 0;
// while(pos < m){
// x = stateX & (1<<(m-pos-1));/*當前行的這個地位的值*/
// y = stateY & (1<<(m-pos-1));/*上一行的這個地位的值*/
// if(x){ /*若是當前這一行動1,上一行0就滿足,若是是1斷定下一個地位*/
// if(y){/*若是上一行也爲1,斷定下一個地位*/
// pos++;
// x = stateX & (1<<(m-pos-1));
// y = stateY & (1<<(m-pos-1));
// if(!x || !y)/*有一個爲0,就不滿足*/
//也就是說如果前邊一個上下都是1,也即是這裏需要橫放,那麼下一個就一定也都是1,否則非法!!!
// return false;
// }
//這裏當前行是1,上一行是0,也就是代表當前行與上一行豎直放置,該列合法,繼續判斷下一列
// pos++;
// }
// else{/*當前行動0,上一行必然要爲1才滿足,也就是當前行與下一行一起豎直放置,只有這樣才合法*/
// if(!y)//非法情況,直接退出false
// return false;
// pos++;//合法情況,繼續判斷下一列
// }
// }
/*
這個註釋部分是我自己按照自己的思路堆上邊的while循環情況分析添加的
while(pos<m)
{
x=stateX & (1<<(m-pos-1));
y=stateY & (1<<(m-pos-1));
if(!x)
{
if(!y)return false;
pos++;
}
else
{
pos++;
if(y)
{
x=stateX & (1<<(m-pos-1));
y=stateY & (1<<(m-pos-1));
if(!x || !y)return false;
pos++;
}
}
// pos++;
}
return true;
}
*/
void solve(){
int i , j , k;
if(n<m)//這裏很關鍵,如果沒有一定TLE
{
n=n^m;
m=n^m;
n=n^m;
}
memset(dp , 0 , sizeof(dp));
init(0 , 0);
for(i = 1; i < n ; i++){/*列舉每一行*/
for(j = 0 ; j < 1<<m ; j++){/*列舉當前行的狀況*/
for(k = 0 ; k < 1<<m ; k++){/*列舉上一行的狀況*/
if(judge(j , k))/*若是滿足前提*/
dp[i][j] += dp[i-1][k];/*加上*/
}
}
}
cout<<dp[n-1][(1<<m)-1]<<endl;
}
int main(){
//freopen("input.txt","r",stdin);
while(scanf("%d%d" , &n , &m)){
if(!n && !m)
break;
if((n*m)%2)
printf("0\n");
else
solve();
}
return 0;
}
總體來說比較快的代碼:
#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
int n,m;
long long dp[12][(1<<12)];
void init(int state,int pos){ //預處理第一行的所有狀態
if(pos==m){
dp[0][state]=1;
return ;
}
if(pos>m)return;
if(pos+1<=m) /*當前豎放那麼就是0(state<<1,下一位就變成0),那麼直接向下一個地位*/
init(state<<1,pos+1);
if(pos+2<=m) /*當前地位橫放那麼就是兩個1(state<<2|3把後兩位變成1),所以向後兩個地位*/
init(state<<2|3,pos+2);
}
void DFS(int i,int curstate,int prestate,int pos){ /*dfs出每一行的可滿足的狀況*/
if(pos==m){
dp[i][curstate]+=dp[i-1][prestate]; /*當前行加上上一行的規劃數*/
return ;
}
if(pos+1<=m){ /*若是可以豎放,推敲兩種景象*/
DFS(i,curstate<<1|1,prestate<<1,pos+1); /*這一行和上一行豎放,那麼這一行動1,上一行動0*/
DFS(i,curstate<<1,prestate<<1|1,pos+1); /*上一行0(相當於這一行)和這一行1(相當於下一行)豎放,那麼上一行動1,這一行動0*/
}
if(pos+2<=m) /*若是可以橫放*/
DFS(i,curstate<<2|3,prestate<<2|3,pos+2);
}
int main(){
//freopen("input.txt","r",stdin);
while(~scanf("%d%d",&n,&m)){
if(n==0 && m==0)
break;
if(n*m%2==1){
printf("0\n");
continue;
}
memset(dp,0,sizeof(dp));
init(0,0);
for(int i=1;i<n;i++) /*從1開端列舉*/
DFS(i,0,0,0);
printf("%I64d\n",dp[n-1][(1<<m)-1]);
}
return 0;
}
#include<algorithm>
#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
int n , m;
#define LL long long
int dp[12][1<<12];
//對第一駚ũ„處理懲罰*/
void init(int state , int pos){
if(pos == m)
{
dp[0][state] = 1;
// mark[num++]=state;
return;
}
if(pos>m)return;
if(pos < m)
init(state<<1 , pos+1);
if(pos + 1 < m)
init(state<<2|3 , pos+2);
}
/*斷定當前兩種狀況是否滿足*/
bool judge(int stateX , int stateY){
int x , y , pos;
pos = 0;
while(pos < m){
x = stateX & (1<<(m-pos-1));/*當前行的這個地位的值*/
y = stateY & (1<<(m-pos-1));/*上一行的這個地位的值*/
if(x){ /*若是當前這一行動1,上一行0就滿足,若是是1斷定下一個地位*/
if(y){/*若是上一行也爲1,斷定下一個地位*/
pos++;
x = stateX & (1<<(m-pos-1));
y = stateY & (1<<(m-pos-1));
if(!x || !y)/*有一個爲0,就不滿足*/
return false;
}
pos++;
}
else{/*當前行動0,上一行必然要爲1才滿足*/
if(!y)
return false;
pos++;
}
}
return 1;
}
void solve()
{
LL i , j , k;
memset(dp , 0 , sizeof(dp));
if(n<m)
{
n=n^m;
m=n^m;
n=n^m;
}
// for(LL i=0;i<(1<<m);i++)
// {
// dp[0][num]=1;
// mark[num++]=i;
// }
init(0 , 0);
for(i = 1; i < n ; i++){/*列舉每一行*/
for(j = 0 ; j< 1<<m ; j++){/*列舉當前行的狀況*/
for(k = 0 ; k < 1<<m; k++){/*列舉上一行的狀況*/
if(judge(j, k))/*若是滿足前提*/
dp[i][j] += dp[i-1][k];/*加上*/
}
}
}
cout<<dp[n-1][(1<<m)-1]<<endl;
}
int main(){
//freopen("input.txt","r",stdin);
while(scanf("%d%d" , &n , &m),n+m){
if((n*m)%2)
printf("0\n");
else
solve();
}
return 0;
}
【例6】
給出n*m (1≤n、m≤9)的方格棋盤,用1*2的矩形的骨牌和L形的(2*2的去掉一個角)骨牌不重疊地覆蓋,求覆蓋滿的方案數。
【分析】
觀察題目條件,只不過是比例5多了一種L形的骨牌,因此很自然地順着例5的思路走。本題中兩種骨牌的最大長度和例5一樣,所以仍然用f[i][s]表示把前i-1行覆蓋滿、第i行覆蓋狀態爲s的覆蓋方案數,得到的遞推式和例5完全一樣:
f[0][1…11]=1
f[i][s1]=∑f[i-1][s2]
其中(s1,s2)整體作爲一個放置方案。例5中有兩種DFS方案,其中第二種實現起來較第一種簡單。但在本題中,新增的L形骨牌讓第二種DFS難以實現,在例5中看起來有些笨拙的第一種DFS方案在本題卻可以派上用場。回顧第一種DFS,我們有5個參數,分別爲:p(當前列號),s1、s2(當前行和對應的上一行的覆蓋情況),b1、b2(上一列的放置對當前列兩行的影響,影響爲1否則爲0)。本題中,可選擇的方案增多,故列表給出:
|
覆蓋情況 |
條件 |
參數s變化 |
參數b變化 |
1 |
0 0 0 0 |
無 |
s1=s1*2+b1 s2=s2*2+1-b2 |
b1=0 b2=0 |
2 |
0 0 1 1 |
b1=0 |
s1=s1*2+1 s2=s2*2+1-b2 |
b1=1 b2=0 |
3 |
1 0 1 0 |
b1=0 b2=0 |
s1=s1*2+1 s2=s2*2 |
b1=0 b2=0 |
4 |
1 0 1 1 |
b1=0 b2=0 |
s1=s1*2+1 s2=s2*2 |
b1=1 b2=0 |
5 |
0 1 1 1 |
b1=0 |
s1=s1*2+1 s2=s2*2+1-b2 |
b1=1 b2=1 |
6 |
1 1 0 1 |
b2=0 |
s1=s1*2+b1 s2=s2*2 |
b1=1 b2=1 |
7 |
1 1 1 0 |
b1=0 b2=0 |
s1=s1*2+1 s2=s2*2 |
b1=0 b2=1 |
容易看出,在本題中此種DFS方式實現很簡單。考慮其複雜度,因爲L形骨牌不太規則,筆者沒能找到一維的方案數的遞推公式,因此無法給出複雜度的解析式。但當m=9時,算法共生成放置方案79248個,則對於n=m=9,算法的複雜度爲O(9*79248),可以瞬間出解。和上例一樣,本題也沒有必要保存所有放置方案,也避免MLE。
那麼,對於本題是否可以應用上題的矩陣算法呢?答案是肯定的,方法也類似,複雜度爲O(8m*logn)。然而,對於本題卻不能通過上題的稀疏矩陣算法加速,原因在於剛開始時矩陣中只有1-79248/49=70%的0,而運算結束後整個矩陣中只有2個0,根本無法達到加速效果。
由於有上題的鋪墊,基本相同的本題也很快得到了解決。
【例7】
給出n*m(n,m≤10)的方格棋盤,用1*r的長方形骨牌不重疊地覆蓋這個棋盤,求覆蓋滿的方案數。
【分析】
本題是例5的直接擴展。如果說例5中公式比SCR好,本題可以指出當公式未知時SCR依然是可行的算法。直接思考不容易發現方法,我們先考慮r=3時的情況。首先,此問題有解當且僅當m或n能被3整除。更一般的結論是:用1*r的骨牌覆蓋滿m*n的棋盤,則問題有解當且僅當m或n能被r整除。當r=2時,則對應於例5中m、n至少有一個是偶數的條件。此結論的組合學證明從略。
不同於例5,1*3骨牌的“攻擊範圍”已經達到了3行,可以想象例5中的表示方法已經無法正確表示所有狀態,但其思路依然可以沿用。例5中用f[i][s]表示把前i-1行覆蓋滿、第i行覆蓋狀態爲s的覆蓋方案數,是因爲當前行的放置方案至多能影響到上一行,狀態中只要包含一行的覆蓋狀態即可消除後效性。本題中當前行的放置方案可以影響到上兩行,故可以想到應保存兩行的覆蓋狀態以消除後效性,即增加一維,用f[i][s1][s2]表示把前i-2行覆蓋滿、第i-1行覆蓋狀態爲s1、第i行覆蓋狀態爲s2的覆蓋方案數。先不論上述表示方法是否可行(答案是肯定的),r=2時狀態有2維,r=3時有3維,推廣後狀態變量居然有r維,這樣的方法不具有推廣價值,而且空間複雜度也太高。
仔細分析上述方案,可以發現其失敗之處。s1的第p位s1p爲1(覆蓋)時,s2p是不可能爲0的(要求覆蓋滿),則這兩位(s1p, s2p)的(0,0),(0,1),(1,0),(1,1)四種組合中有一種不合法,而上述狀態表示方法卻冗餘地保存了這個組合,造成空間複雜度過高,也進行了多餘的計算。通過上面的討論可以知道,每一位只有3種狀態,引導我們使用三進制。我們用f[i][s]表示把前i-2行覆蓋滿、第i-1和第i行覆蓋狀態爲s的覆蓋方案數,但這裏狀態s不再是二進制,而是三進制:sp=0表示s1p=s2p=0;sp=1表示s1p=0,s2p=1;sp=2表示s1p=s2p=1。這樣,我們就只保留了必要的狀態,空間和時間上都有了改進。當r=4時,可以類推,用四進製表示三行的狀態,r=5時用五進制……分別寫出r=2,3,4,5的程序,進行歸納,統一DFS的形式,可以把DFS(p,s1,s2)分爲兩部分:①for i=0 to r-1 do DFS(p+1,s1*r+i,s2*r+(i+1)mod r);②DFS(p+r,s1*rr+rr-1,s2*rr+rr-1) 問題解決。但DFS的這種分部方法是我們歸納猜想得到的,並沒有什麼道理,其正確性無法保證,我們能否通過某種途徑證明它的正確性呢?仍以r=3爲例。根據上面的討論,sp取值0到2,表示兩行第p位的狀態,但sp並沒有明確的定義。我們定義sp爲這兩行的第p位從上面一行開始向下連續的1的個數,這樣的定義可以很容易地遞推,遞推式同上兩例沒有任何改變,卻使得上述DFS方法變得很自然。
分析算法的時間複雜度,同例5一樣需要用到DFS出的方案個數h[m],並且仿照例5中h[m]的遞推式,我們可以得到:
h[i]=ri (i=0~r-1)
h[j]=r*h[j-1]+h[j-r] (j=r~m)
理論上我們可以根據遞推式得到例5中那樣的精確的通項公式,但需要解高於三次的方程,且根多數爲複數,無法得到例5那樣簡單優美的表達式,這裏僅給出r=2..5,m=10時h[m]的值依次爲:5741,77772,1077334,2609585,9784376。對於推廣後的問題,例5的矩陣算法依然可行,但此時空間將是一個瓶頸。
至於這裏邊的代碼,你們可以自己想一下,其實只是變了下那個DFS函數,其他的都是照葫蘆畫瓢,試着自己動動手吧,這兩道題可以作爲你們的課後作業,如果遇到了難點或是什麼補鞥解決的問題,歡迎加我QQ 2553627958,不會的我們一起討論,一起進步