狀壓DP入門詳解+題目推薦


在動態規划的題型中,一般叫什么DP就是怎么DP,狀壓DP也不例外

所謂狀態壓縮,一般是通過用01串表示狀態,充分利用二進制數的特性,簡化計算難度。舉個例子,在棋盤上擺放棋子的題目中,我們可以用1表示當前位置擺放棋子,用0表示當前位置不擺放棋子。

這樣的話,就能夠直接運用許多二進制運算的特性來實現對時間和空間的優化

例如:如果給你一個\(n*m\)的棋盤,讓你放棋子,但是棋子兩兩不能相鄰,求方案數

我們僅考慮暴力枚舉每一行的情況,如果是普通用數組來存儲,判斷的時候對於相鄰兩行需要一個數一個數的看,不論是時間還是空間,都很難讓人接受

但是如果用一個二進制數來存儲每一行的情況,對於相鄰兩行,我們只需要進行一下與運算,如果有值,就說明不合法,這樣不論是時間還是空間上,都進行了極大的優化

當然,這並不是說狀壓DP就是暴力枚舉,這里只是舉個例子來解釋一下狀壓DP中的狀態壓縮這個過程

看完上面這些,還是不知道這是個什么東西,所以我們結合一道題目來演示一下,什么是狀壓DP

互不侵犯

這是一道非常經典的狀壓例題,題意過於簡單,我們不再贅述

我們首先考慮一下國王的特性:能夠攻擊到八個方向的棋子。也就是說,如果某個位置放置了棋子,它的九宮格內就不能再放置了。

我們可以將問題拆解,把九宮格看為三行,就可以分開考慮了

首先我們先考慮在當前行的棋子,只要一個棋子的左右沒有相鄰的棋子,那么就不沖突

再考慮上面那一行,只要一個棋子正上即左右側上沒有棋子則合法

最后再考慮一下下面那一行是否滿足條件

那么我們只要枚舉所有情況,再判斷就可以了

顯然,這樣做復雜度實在太高了,9*9的棋盤計算到人類滅亡連枚舉都枚舉不完

看來我們需要把算法優化一下

我們考慮可不可以逐行枚舉,先對每行的狀態進行判斷,在對上面一行和下面一行進行判斷。我們會發現這樣的做法是可行的,而且判斷下一行是沒有意義的,因為我們是逐行枚舉,下一行還沒枚舉到有什么好枚舉的。那么我們又會發現,這不是遞推嘛。可是這樣還是有點慢,怎么優化呢?

下面就是狀壓時刻了

我們可以在一開始將所有行內可行狀態存儲下來,由於可行狀態遠遠小於總的狀態數,這樣可以大大優化我們的復雜度。如何判斷這種狀態是否沒有沖突呢?用與運算就可以輕松判斷。我們先預處理好第一行的情況,然后遞推即可。

下面來看一下代碼吧(內附詳解)

#include<algorithm>
#include<iostream>
#include<cstring>
#include<cstdio>
#include<cctype>
#define ll long long
#define gc getchar
#define maxn 10
#define maxm 105     //由於可行狀態占比極小,所以行內的可行狀態不到100
using namespace std;

inline ll read(){
	ll a=0;int f=0;char p=gc();
	while(!isdigit(p)){f|=p=='-';p=gc();}
	while(isdigit(p)){a=(a<<3)+(a<<1)+(p^48);p=gc();}
	return f?-a:a;
}

int get(int x){     //求當前狀態中1的數量
	int sum=0;
	while(x){
		++sum;
		x&=x-1;    //初賽考了,你做對了嗎
	}
	return sum;
}

int n,m,N,tot,s[maxm],w[maxm];    //s[i]表示第i種可行狀態的具體擺放,w[i]表示第i種狀態棋子的個數
ll ans,f[maxn][maxm][maxm];    //ans最終用來統計答案,f[i][j][l]表示第i行狀態為第j種擺放l個棋子的方案數
int main(){
	n=read();m=read();N=1<<n;
	for(int i=0;i<N;++i){
		if(i&i<<1)continue;  //如果非零說明有相鄰棋子,則不合法
		s[++tot]=i;
		w[tot]=get(i);
		f[1][tot][w[tot]]=1;    //第一行需要單獨初始化
	}
	for(int i=2;i<=n;++i)
		for(int j=1;j<=tot;++j)
			for(int k=1;k<=tot;++k){
				if(s[j]&s[k])continue;    //三種與上一行有沖突的情況
				if(s[j]&s[k]<<1)continue;
				if(s[k]&s[j]<<1)continue;
				for(int l=w[j];l<=m;++l)     //這里非常重要!!l的取值影響到f數組的意義,l如果是從w[j]開始,那么f數組表示的是這一行及以前所有放l個棋子的方案數;如果l從w[j]+1開始,那么f數組表示的是到這一行正好有l個棋子的方案數,以前夠了l個棋子的方案則沒有被記錄在這里面。這會影響到最后統計答案的,並且樣例太水並不能看出來。這里一定要注意!!!
					f[i][j][l]+=f[i-1][k][l-w[j]];
			}
	for(int i=1;i<=tot;++i)    //統計答案就很簡單了。不過如果是l是另一種寫法還需要枚舉1~n
		ans+=f[n][i][m];
	printf("%lld",ans);
	return 0;
}

這就是狀壓DP,你是否理解了呢?不妨自己打一遍代碼,能夠加深理解哦。

例題

中國象棋

玉米田

染色(一道我認為可用四進制狀壓解決的題目,如果有神仙成功了,希望能夠告訴我一聲,以證明我沒口胡錯)

山賊集團

茫茫人海相遇也算緣分,點個推薦好不好\(QwQ\)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM