簡易狀態壓縮DP

solanolika發表於2020-09-25

簡易狀壓DP

開頭甩定義
藉助二進位制完成對狀態的壓縮,從而進行動態規劃的過程,我們稱之為狀態壓縮動態規劃(簡稱“狀壓DP”)

位運算

  1. 左移右移
    左移:左移一位,等價於該數x2
    eg. 110<<1 = 1100 (十進位制從6變為12)
    右移同理
    eg. 110>>1 = 11(十進位制從6變為3)

  2. 與或非
    相信各位OIer應當在普及時就學過位運算了,但本蒟蒻還是要講
    與:按位進行與運算 滿足兩數同一位都為1時結果為1,否則為0
    或:按位進行或運算 滿足兩數同一位都為0時結果為0,否則為1
    非:按位取反

  3. 簡單運算
    當我們需要判斷或修改二進位制數S的第i位時,我們進行以下操作
    若判斷第i位是否為0 即判斷 (S&(1<<i)) 是否為0
    若要將第i位設定為1 即 (S|(1<<i)) 即1左移i位於S進行或運算
    若要將第i位設定為0 即 (S&~(1<<i)) 即S與只有第i位為0的數進行或運算

  4. 簡單應用
    經過上面的學習大家可以發現我們可以用位運算的方法來列舉子集
    假設集合A={1,2,3,4,5}
    我們可以通過將A中的數編號的方式使其轉化為一個二進位制數
    是不是非常神奇呀
    下面我們來康康具體操作
    假設我們要表示A的子集B={2,4,5}

二進位制數位43210
二進位制值11010
子集元素54020

這裡需要注意的是二進位制的數位是從0開始的,所以對應關係不要找錯
所以B集合用二進位制就可以表示為11010啦!!

那麼我們來做個練習

李白打酒 【2014藍橋杯預賽】
話說大詩人李白,一生好飲。
一天,他提著酒壺,從家裡出來,酒壺中有酒兩鬥。他邊走邊唱:
無事街上走,提壺去打酒
逢店加一倍,遇花喝一斗。
這一路上,他一共遇到店5次,遇到花10次,已知最後一次遇到的是花,他正好把酒喝光了。請你計算李白有多少種滿足要求的遇到店和花的可能情況
輸入

正確輸出
14

題解:
一道簡單的練手題
考慮使用01串表示遇到花或者店,因為最後一個一定是花所以列舉14位01串就OK
程式碼放送~

#include <bits/stdc++.h>
using namespace std;
int main()
{
	int ans=0;
	for(int i=0;i<(1<<14);i++)//列舉每種情況
	{
		int ans1=0,ans2=0;
		int num=2;
		for(int j=0;j<14;j++)
		{
			if(i&(1<<j))
			{
				ans1++;
				num*=2;
			} 
			else {ans2++;num--;}
		}
		if(ans1==5&&ans2==9&&num==1)ans++;//特判
	}
	cout<<ans;
}

好的你已經掌握了基本的位運算知識了讓我們來康康簡單狀壓DP

狀壓DP

話不多說直接上題

國王 [SGU 223]
在 n×n 的棋盤上放 k 個國王,國王可攻擊相鄰的 8 個格子,求使它們無法互相攻擊的方案總數

輸入
只有一行,包含兩個整數 n 和 k

輸出
每組資料一行為方案總數,若不能夠放置則輸出 0

樣例輸入
3 2
樣例輸出
16

1<=n<=10 , k<=n2

題解
本題為狀態壓縮DP入門題
這個題使用八皇后做法是顯然不行的(很難剪枝),就考慮使用DP,然而國王在棋盤上的狀態使用傳統的f[i][j]很難表示,由於國王的狀態只有兩種,放置與不放置,所以考慮狀壓解決
對於第i行所擺放的國王,只會有第i-1行和第i+1行的國王影響它,對其的影響是i-1行和i+1行的擺放方式和1~i-1行總共擺放的國王數很容易得出初始的動態方程
令f[i][j][s]為第i行目前擺放狀態a[j]和前i行已經擺放的國王個數s,易得
f[i][j][s]=∑f[i-1][k][t]
a[j],a[k]為兩種不同擺放方式
那麼接下來我們就要想想如何表示這兩種狀態
還記得我們之前說的李白問題嗎?不記得就回去看
看見國王在棋盤上擺放的狀態只有兩種,擺與不擺,那麼我們可以嘗試使用一個01串表示狀態,又因為一排最多10個格子,最大寬度也滿足,考慮:
(蒟蒻表格,@代替下國王,沒有就是x)

@xxx@x
100010

如上圖,每個格子對應一個二進位制位,於是乎狀態就可以用一個卡哇伊的 二進位制數表示出來啦
時間複雜度也是允許的,可以自己推推看~
放程式碼!!

#include<bits/stdc++.h> //萬能庫深得朕心
using namespace std;
typedef long long ll;
#define N 15
ll f[N][155][155],ans=0,s=0;
int n,sum[166],num[155],K;
int main()
{
    cin>>n>>K;
	memset(f,0,sizeof(f));
	for(int i=0;i<(1<<n);i++)
	{
		if(i&(i<<1)) continue; //檢查狀態
		int k=0;
		for(int j=0;j<n;j++) if(i&(1<<j))k++;
		sum[++s]=i;
		num[s]=k;
    }
    
	f[0][1][0]=1;
	for(int i=1;i<=n;i++)
	  for(int j=1;j<=s;j++)
	    for(int k=0;k<=K;k++)
	    {
	    	if(k>=num[j])
	    	for(int t=1;t<=s;t++)
	    	if(!(sum[t]&sum[j])&&!(sum[t]&(sum[j]<<1))&&!(sum[t]&(sum[j]>>1)))
	    	  f[i][j][k]+=f[i-1][t][k-num[j]];
		}
	
	for(int i=1;i<=s;i++) ans+=f[n][i][K]; //統計
	cout<<ans<<endl;
}

是不是有點感覺了
還要多練練題,基礎的思路就是判斷狀態是否可以轉化為二進位制,推導dp方程然後寫就完事
PS:位運算一定要好好記:) (慘痛教訓)

相關文章