洛谷P1896互不侵犯(狀壓dp)
題目描述
在 \(N×N\) 的棋盤裡面放 \(K\) 個國王,使他們互不攻擊,共有多少種擺放方案。國王能攻擊到它上下左右,以及左上左下右上右下八個方向上附近的各一個格子,共 \(8\) 個格子。
輸入格式
只有一行,包含兩個數 \(N,K\) \(( 1 <=N <=9, 0 <= K <= N * N)\)
輸出格式
所得的方案數
輸入輸出樣例
輸入 #1
3 2
輸出 #1
16
思路
其實一開始我以為這是一道搜尋題,因為長得的確很像。
但這是一道狀壓dp。然而我也不是很清楚什麼是狀壓dp。應該就是把一個狀態壓縮成一個二進位制數,從而減少空間複雜度。那麼這個題如何將狀態轉化為二進位制數呢?其實並不難,甚至很顯而易見,我們可以用一個二進位制數來表示一行內國王的擺放,比如\(1010101\)
首先我們要判斷本身這個數能不能單獨擺放在一行裡,可以預處理解決。其次我們要判斷下一行的擺放會不會和當前這一行的擺放矛盾。其實程式碼實現很簡單,我們只需要將下一行的二進位制數分別左移和右移一位再相比較就可以了。因為一個國王佔領了周圍八個格子,所以左邊一列和右邊一列都不能有國王,在二進位制數上就體現為相鄰位上只能有一個\(1\)。左移一位,右移一位,再加上它本身。然後和當前這一行進行\(&\)這一位運算,不難想到,如果符合條件的話,那麼運算後得出的結果應該都是\(0\)。轉移條件就也解決了。
最後考慮初始化和狀態轉移方程。可以用一個三維陣列f[i][j][k]表示第i行的狀態是j(轉化為二進位制後表示狀態),而且已經放了k個國王。那麼狀態轉移方程就是f[i][j][k+tot(j)]+=f[i-1][s][k],其中tot函式用來算出二進位制下狀態中\(1\)的個數,也就是當前這一行國王的個數,而\(s\)表示上一行裡符合條件的狀態,列舉即可。初始化只需要初始第一行即可,只要放的國王小於給出的\(k\)(好像變數名重複了),那麼就將方案數變為\(1\)。然後就暴切藍題了。
不開longlong見祖宗
程式碼
#include<iostream> #include<cstdio> #include<algorithm> #include<cstring> #include<cmath> #include<queue> #include<cstdlib> #include<ctime> using namespace std; int n,k,cnt,b[2000]; long long ans,f[10][2000][100]; int tot(int x){ int sum=0; while(x){ if(x&1){ sum++; } x>>=1; } return sum; }//統計1的個數 int main() { scanf("%d%d",&n,&k); int p=pow(2,n)-1; for(int i=0;i<=p;i++){ // printf("%d %d\n",i>>1,i<<1); int r=i&(i>>1),t=i&(i<<1); // printf("%d %d\n",r,t); if((r==0)&&(t==0)){ b[++cnt]=i; // printf("%d\n",i); } }//預處理,先將每一行的符合條件的情況預處理出來,這樣迴圈是直接迴圈滿足條件的即可,不用再迴圈冗餘情況後判斷 for(int i=1;i<=cnt;i++){ if(tot(b[i])<=k){//不能超過k f[1][b[i]][tot(b[i])]=1;//第一行不管怎樣放都不會有重複情況,所以方案數都是1 } } for(int i=2;i<=n;i++){ for(int j=1;j<=cnt;j++){ for(int p=1;p<=cnt;p++){ if(b[j]&b[p]) continue; if(b[j]&(b[p]<<1)) continue; if(b[j]&(b[p]>>1)) continue;//判斷當前這一行和上一行是否合法 for(int s=1;s<=k;s++){ if(tot(b[j])+s<=k){ f[i][b[j]][tot(b[j])+s]+=f[i-1][b[p]][s];//如果放的國王沒有超過個數,那麼方案數累加 } } } } } for(int i=1;i<=n;i++){ for(int j=1;j<=cnt;j++){ ans+=f[i][b[j]][k];//因為不一定是在哪一行哪一個狀態放夠了k個,那麼就累加答案 } } printf("%lld\n",ans); return 0; }