人生第一次的狀壓,大部分參考题解 P1896 [SCOI2005]互不侵犯 - 暗ざ之殇 的博客 - 洛谷博客
這裡自己做一次總結。
首先,棋盤上的每一個個字都可以選擇放或者不放,我們假如我們選擇放就會是1,不放就是0,那麼棋盤上的一行可以以 二進制的形式表示,我們可以認為一行的總可能性為2^n。
既然有了可以紀錄每一行的狀態的方法,我們的dp 狀態就很好定義了。
dp[i][j][k] 為第 1 到 i行 總共放 j 個皇后的放置方法(k為放置方法)的總方案數。
我們只要提早處理一行中放不同數量的皇后的合法方法就可以在日後以O(1)的方法直接獲取。
由於左邊和右邊都不可以放皇后那要怎麼判斷呢?
我們可以利用左移和右移來進行判斷,如何實現?
00100 我們知道 01100 和 00110 這兩種都是不合法的,所以我們通過把00100左移和右移 如果&之後出現1,就是有一樣的那麼就是不合法的了。
所以現在我們的預處理的整體思路就出來了,首先枚舉一行所有合法的放置方法並且紀錄他們皇后數量。
int num = 0, legalCase[1000];
for(int s = 0; s < (1<<n); s++){
int totQueen = 0, s1 = s;
//count queen's amount
while(s1){
if(s1&1)totQueen++;
s1>>=1;
}
cnt[s] = totQueen;
// (s<<1) & s == 0 and (s>>1) & s == 0check legal?
if(((s<<1)|(s>>1))&s == 0) legalCase[++num] = s;
}
為什麼可以縮短成那個寫放是因為如果左移和右移後,原本1的位置都有該是0才是合法,因此在&一次就會變成0。
那麼現在還剩下兩個工作,狀態怎麼轉移和如何判斷左上角和右上角和左下角和右下角?
也是一樣的操作,把上一行左移和右移看看&完之後是不是0,還有 上一行和 當前行不可以是一樣的。
那麼如何轉移呢?
首先這一行是要依賴於上一行的放置方法,並且皇后的放置數量也要合理。
dp[i][j][k] += dp[i - 1][j - cntQueen[當前行放置方法]][上一行的不同狀態];
整體思路就出來了。
1. 預處理出一行中所有可能合法放置方法,還有紀錄他們的皇后數量
2. 循環n行
2.1 循環當前行的放置可能性
2.2 循環上一行的放置可能性
2.3 判斷是否有衝突,再把上一行不同的方案數加進當前合法行
#include <iostream>
using namespace std;
long long n, totQueen, dp[10][120][550], cnt[550], num = 0, legalCase[550], k;
void init(){
cin >> n >> k;
for(int s = 0; s < (1<<n); s++){
totQueen = 0; int s1 = s;
while(s1){
if(s1&1)totQueen++;
s1>>=1;
}
//check validity
cnt[s] = totQueen;
if((((s<<1)|(s>>1))&s) == 0)legalCase[++num] = s;
}
}
void work(){
init();
dp[0][0][0] = 1;
legalCase[0] = 0;
for(int row = 1; row <= n; row++){
//s is current row state, s1 is previous row
for(int i = 1; i <= num; i++){
int s = legalCase[i];
for(int j = 1; j <= num; j++){
int s1 = legalCase[j];
for(int numQueen = 0; numQueen <= k; numQueen++){
if(((s1|(s1<<1)|(s1>>1))&s)==0){
if(numQueen - cnt[s] < 0)continue;
dp[row][numQueen][s] += dp[row - 1][numQueen - cnt[s]][s1];
}
}
}
}
}
long long ans = 0;
for(int i = 1; i <= num; i++)ans += dp[n][k][legalCase[i]];
cout << ans;
}
int main(){
work();
return 0;
}
直接把放置方法作為狀態和索引是二進制的優勢