狀壓DP基礎入門

williamYcY發表於2024-06-02

狀壓DP

[SCOI2005] 互不侵犯

點選檢視題面

題目描述

\(N \times N\) 的棋盤裡面放 \(K\) 個國王,使他們互不攻擊,共有多少種擺放方案。國王能攻擊到它上下左右,以及左上左下右上右下八個方向上附近的各一個格子,共 \(8\) 個格子。

輸入格式

只有一行,包含兩個數 \(N,K\)

輸出格式

所得的方案數

樣例 #1

樣例輸入 #1

3 2

樣例輸出 #1

16

提示

資料範圍及約定

對於全部資料,\(1 \le N \le 9\)\(0 \le K \le N\times N\)


\(\text{upd 2018.4.25}\):資料有加強。

首先看到這一題就應該想到用$dfs$來解決,畢竟$n$也才$9$。$dfs$的思路就是看當前點是否要放國王,如果要放國王的話,就得判斷當前點是否可以放國王。這個思路的時間複雜度分析是:首先,每一個決策都有兩個分支也就是$O(2^?)$,那麼這個$?$就是決策的狀態數,也就是整個棋盤也就是$n^2$,也就是$k$,那麼整體複雜度就是$O(2^k)$。這個時間複雜度已經超過了$1$秒,所以說要進行最佳化。

最佳化

可以發現,這道題超時的原因是列舉的沒用狀態太多了,所以說只要我們去掉這些狀態那麼時間複雜度就是最優的了。

首先可以想到的就是\(dp\)\(dp\)的本質就是最佳化\(dfs\)。可以發現當前這一行的狀態是需要用放到\(dp\)狀態裡,又不可能把新增\(n\)維表示當前點是否放了國王。可以發現如果把這\(n\)維合併成\(1\)維,這個合共其實可以把一個二進位制當成當前的狀態比如說\(10010_2\)表示從左到右第\(1\)位有一個國王,第\(4\)位也有一個國王。這個技巧就被稱為狀態壓縮,狀態壓縮之後的操作可以使用位運算來解決。下圖是一張關於位運算的表格:

<< 二進位制左移
>> 二進位制右移
& 兩個二進位制每一位只要是\(0\)那麼結果中這一位就是\(0\)
^ 相同為\(0\),不同為\(1\)
| 兩個二進位制每一位只要是\(0\)那麼結果中這一位就是\(0\)

回到這一題那麼我們的狀態就可以設成\(f_{i,j,k}\)表示在第\(i\)行中,狀態為\(j\),放置了\(k\)個國王的方案數

好,我們現在就要寫出這一題的轉移方程,因為國王的攻擊範圍涉及到上一行所以我們需要找到上一行,因此我們就要把上一行的狀態也放進當前狀態裡。所以狀態方程就應該是:

\[dp_{i,j,l}+=dp_{i-1,x,l-num_j}; \]

\(i\)是行數,\(j\)是這一行的狀態,\(x\)是上一行的狀態,\(l\)是這一行和上一行選擇的國王數,\(num_i\)表示第i個狀態的國王數。
那麼該怎麼求道第\(i\)行的國王數和狀態的??
其實這個問題是對於一行上的,那麼就可以用\(dfs\)來列舉每一個狀態即可。
實現過程:

void dfs(int x, int sum, int cur) {
    if (cur >= n) {
		sta[++cnt] = x;
		num[cnt]=sum;
		return;
	}
    dfs(x,sum,cur+1);
	dfs(x+(1<<cur),sum+1,cur+2);
}

那麼最終的答案就應該是對於最後一行一種狀態放置所有國王的方案綜合也就是:

\[\sum_{i是狀態}{dp_{n,i,k}} \]

最終程式碼:

#include<bits/stdc++.h>
using namespace std;
#define int long long
const int N=10; 
int sta[1<<N],num[1<<N],cnt,n,k,ans;
int dp[N][1<<N][N*N];
void dfs(int x, int sum, int cur) {
    if (cur >= n) {sta[++cnt] = x,num[cnt]=sum;return;}
    dfs(x,sum,cur+1),dfs(x+(1<<cur),sum+1,cur+2);
}
bool check(int i,int j){return !(sta[i]&sta[j]||(sta[i]<<1)&sta[j]||sta[i]&(sta[j]<<1));}
signed main(){
	cin>>n>>k;
	dfs(0,0,0);	
	for(int i=1;i<=cnt;i++)dp[1][i][num[i]]=1;
	for(int i=2;i<=n;i++)
		for(int j=1;j<=cnt;j++)
			for(int x=1;x<=cnt;x++){
				if(!check(j,x))continue;
				for(int l=num[j];l<=k;l++)dp[i][j][l]+=dp[i-1][x][l-num[j]];
			}
	for(int i=1;i<=cnt;i++)ans+=dp[n][i][k];
	return cout<<ans,0;
}