[轉]狀態壓縮dp(狀壓dp)

狀態壓縮動態規劃(簡稱狀壓dp)是另一類非常典型的動態規劃,通常使用在NP問題的小規模求解中,雖然是指數級別的複雜度,但速度比搜索快,其思想非常值得借鑑。

爲了更好的理解狀壓dp,首先介紹位運算相關的知識。

1.’&’符號,x&y,會將兩個十進制數在二進制下進行與運算,然後返回其十進制下的值。例如3(11)&2(10)=2(10)。

2.’|’符號,x|y,會將兩個十進制數在二進制下進行或運算,然後返回其十進制下的值。例如3(11)|2(10)=3(11)。

3.’^’符號,x^y,會將兩個十進制數在二進制下進行異或運算,然後返回其十進制下的值。例如3(11)^2(10)=1(01)。

4.’<<’符號,左移操作,x<<2,將x在二進制下的每一位向左移動兩位,最右邊用0填充,x<<2相當於讓x乘以4。相應的,’>>’是右移操作,x>>1相當於給x/2,去掉x二進制下的最有一位。

這四種運算在狀壓dp中有着廣泛的應用,常見的應用如下:

1.判斷一個數字x二進制下第i位是不是等於1。

方法:if ( ( ( 1 << ( i - 1 ) ) & x ) > 0)

將1左移i-1位,相當於製造了一個只有第i位上是1,其他位上都是0的二進制數。然後與x做與運算,如果結果>0,說明x第i位上是1,反之則是0。

2.將一個數字x二進制下第i位更改成1。

方法:x = x | ( 1<<(i-1) )

證明方法與1類似,此處不再重複證明。

3.把一個數字二進制下最靠右的第一個1去掉。

方法:x=x&(x-1)

感興趣的讀者可以自行證明。


位運算在狀壓dp中用途十分廣泛,請看下面的例題。


【例1】有一個N*M(N<=5,M<=1000)的棋盤,現在有1*2及2*1的小木塊無數個,要蓋滿整個棋盤,有多少種方式?答案只需要mod1,000,000,007即可。

例如:對於一個2*2的棋盤,有兩種方法,一種是使用2個1*2的,一種是使用2個2*1的。

【算法分析】

在這道題目中,N和M的範圍本應該是一樣的,但實際上,N和M的範圍卻差別甚遠,對於這種題目,首先應該想到的就是,正確算法與這兩個範圍有關!N的範圍特別小,因此可以考慮使用狀態壓縮動態規劃的思想,請看下面的圖:

 

假設第一列已經填滿,則第二列的擺設方式,只與第一列對第二列的影響有關。同理,第三列的擺設方式也只與第二列對它的影響有關。那麼,使用一個長度爲N的二進制數state來表示這個影響,例如:4(00100)就表示了圖上第二列的狀態。

因此,本題的狀態可以這樣表示:

dp[i][state]表示該填充第i列,第i-1列對它的影響是state的時候的方法數。i<=M,0<=state<2N

對於每一列,情況數也有很多,但由於N很小,所以可以採取搜索的辦法去處理。對於每一列,搜索所有可能的放木塊的情況,並記錄它對下一列的影響,之後更新狀態。狀態轉移方程如下:

dp[i][state]=∑dp[i-1][pre]每一個pre可以通過填放成爲state

對於每一列的深度優先搜索,寫法如下:


//第i列,枚舉到了第j行,當前狀態是state,對下一列的影響是nex
void dfs(int i,int j,int state,int nex)
{
if (j==N)
{
dp[i+1][nex]+=dp[i][state];
dp[i+1][nex]%=mod;
return;
}
//如果這個位置已經被上一列所佔用,直接跳過
if (((1<<j)&state)>0)
dfs(i,j+1,state,nex);
//如果這個位置是空的,嘗試放一個1*2的
if (((1<<j)&state)==0)
dfs(i,j+1,state,nex|(1<<j));
//如果這個位置以及下一個位置都是空的,嘗試放一個2*1的
if (j+1<N && ((1<<j)&state)==0 && ((1<<(j+1))&state)==0)
dfs(i,j+2,state,nex);
return;
}

狀態轉移的方式如下:

for (int i=1;i<=M;i++)
{
for (int j=0;j<(1<<N);j++)
if (dp[i][j])
{
dfs(i,0,j,0);
}
}

最終,答案就是dp[M+1][0]。

【代碼實現】

/*
ID:aqx
PROG:鋪地磚
LANG:c++
*/
//第i列,枚舉到了第j行,當前狀態是state,對下一列的影響是nex
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <iostream>

using namespace std;

int N, M;
long long dp[1005][34];

void dfs(int i,int j,int state,int nex)
{
if (j==N)
{
dp[i+1][nex]+=dp[i][state];
return;
}
//如果這個位置已經被上一列所佔用,直接跳過
if (((1<<j)&state)>0)
dfs(i,j+1,state,nex);
//如果這個位置是空的,嘗試放一個1*2的
if (((1<<j)&state)==0)
dfs(i,j+1,state,nex|(1<<j));
//如果這個位置以及下一個位置都是空的,嘗試放一個2*1的
if (j+1<N && ((1<<j)&state)==0 && ((1<<(j+1))&state)==0)
dfs(i,j+2,state,nex);
return;
}

int main()
{
while (cin>>N>>M)
{
memset(dp,0,sizeof(dp));
if (N==0 && M==0) break;
dp[1][0]=1;
for (int i=1;i<=M;i++)
{
for (int j=0;j<(1<<N);j++)
if (dp[i][j])
{
dfs(i,0,j,0);
}
}
cout<<dp[M+1][0]<<endl;
}
}

【例2】最小總代價(Vijos-1456)

題目描述:

n個人在做傳遞物品的遊戲,編號爲1-n。

遊戲規則是這樣的:開始時物品可以在任意一人手上,他可把物品傳遞給其他人中的任意一位;下一個人可以傳遞給未接過物品的任意一人。

即物品只能經過同一個人一次,而且每次傳遞過程都有一個代價;不同的人傳給不同的人的代價值之間沒有聯繫;

求當物品經過所有n個人後,整個過程的總代價是多少。

輸入格式:

第一行爲n,表示共有n個人(16>=n>=2);

以下爲n*n的矩陣,第i+1行、第j列表示物品從編號爲i的人傳遞到編號爲j的人所花費的代價,特別的有第i+1行、第i列爲-1(因爲物品不能自己傳給自己),其他數據均爲正整數(<=10000)。

(對於50%的數據,n<=11)。

輸出格式:

一個數,爲最小的代價總和。

輸入樣例:

2

-1 9794

2724 –1

輸出樣例:

2724

【算法分析】

看到2<=n<=16,應想到此題和狀態壓縮dp有關。每個人只能夠被傳遞一次,因此使用一個n位二進制數state來表示每個人是否已經被訪問過了。但這還不夠,因爲從這樣的狀態中,並不能清楚地知道現在物品在誰 的手中,因此,需要在此基礎上再增加一個狀態now,表示物品在誰的手上。

dp[state][now]表示每個人是否被傳遞的狀態是state,物品在now的手上的時候,最小的總代價。

初始狀態爲:dp[1<<i][i]=0;表示一開始物品在i手中。

所求狀態爲:min(dp[(1<<n)-1][j]); 0<=j<n

狀態轉移方程是:

dp[state][now]=min(dp[pre][t]+dist[now][t]);

pre表示的是能夠到達state這個狀態的一個狀態,t能夠傳遞物品給now且只有二進制下第t位與state不同。

狀態的大小是O((2n)*n),轉移複雜度是O(n)。總的時間複雜度是O((2n)*n*n)。

【代碼實現】

/*
ID:shijieyywd
PROG:Vijos-1456
LANG:c++
*/
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <vector>

#define MAXN 20
#define INF 0x3f3f3f3f

using namespace std;

int n;
int edges[MAXN][MAXN];
int dp[65546][MAXN];

int min(int a, int b)
{
if (a == -1) return b;
if (b == -1) return a;
return a < b ? a : b;
}

int main() {
freopen("p1456.in", "r", stdin);
scanf("%d", &n);
int t;
for (int i = 0; i < n; i++)
{
for (int j = 0; j < n; j++)
{
scanf("%d", &edges[i][j]);
}
}
memset(dp, -1, sizeof(dp));
for (int i = 0; i < n; i++)
{
dp[1 << i][i] = 0;
}
int ans = -1;
for (int i = 0; i < 1 << n; i++)
{
for (int j = 0; j < n; j++)
{
if (dp[i][j] != -1)
{
for (int k = 0; k < n; k++)
{
if (!(i & (1 << k)))
{
dp[i | (1 << k)][k] = min(dp[i | (1 << k)][k], dp[i][j] + edges[j][k]);
if ((i | (1 << k)) == (1 << n) - 1) ans = min(ans, dp[i | (1 << k)][k]);
}
}
}
}
}
if (ans != -1)
printf("%d\n", ans);
else printf("0\n");

return 0;
}

【例3】勝利大逃亡(續)(Hdoj-1429)

題目描述:

Ignatius再次被魔王抓走了(搞不懂他咋這麼討魔王喜歡)……

 

這次魔王汲取了上次的教訓,把Ignatius關在一個n*m的地牢裏,並在地牢的某些地方安裝了帶鎖的門,鑰匙藏在地牢另外的某些地方。剛開始Ignatius被關在(sx,sy)的位置,離開地牢的門在(ex,ey)的位置。Ignatius每分鐘只能從一個座標走到相鄰四個座標中的其中一個。魔王每t分鐘回地牢視察一次,若發現Ignatius不在原位置便把他拎回去。經過若干次的嘗試,Ignatius已畫出整個地牢的地圖。現在請你幫他計算能否再次成功逃亡。只要在魔王下次視察之前走到出口就算離開地牢,如果魔王回來的時候剛好走到出口或還未到出口都算逃亡失敗。

輸入格式:

每組測試數據的第一行有三個整數n,m,t(2<=n,m<=20,t>0)。接下來的n行m列爲地牢的地圖,其中包括:

 

. 代表路

* 代表牆

@ 代表Ignatius的起始位置

^ 代表地牢的出口

A-J 代表帶鎖的門,對應的鑰匙分別爲a-j

a-j 代表鑰匙,對應的門分別爲A-J

 

每組測試數據之間有一個空行。

輸出格式:

針對每組測試數據,如果可以成功逃亡,請輸出需要多少分鐘才能離開,如果不能則輸出-1。

 

輸入樣例:

4 5 17

@A.B.

a*.*.

*..*^

c..b*

輸出樣例:

16

【算法分析】

初看此題感覺十分像是寬度優先搜索(BFS),但搜索的過程中如何表示鑰匙的擁有情況,卻是個問題。借鑑狀態壓縮的思想,使用一個10位的二進制數state來表示此刻對10把鑰匙的擁有情況,那麼,dp[x][y][state]表示到達(x,y),鑰匙擁有狀況爲state的最短路徑。另外,需要注意到一旦擁有了某一把鑰匙,那個有門的位置就如履平地了。

代碼的實現方式可以採用Spfa求最短路的方式。值得一提的是,Spfa算法本來就是一種求解最短路徑問題的動態規劃算法,本文假設讀者已經非常熟悉Spfa等基礎算法,在此處不再贅述。

狀態壓縮dp可以出現在各種算法中,本題就是典型的搜索算法和狀態壓縮dp算法結合的題目。另外,很多狀態壓縮dp本身就是通過搜索算法實現的狀態轉移。

【代碼實現】

/*
ID:shijieyywd
PROG:Hdu-1429
LANG:c++
*/
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <iostream>
#include <queue>

using namespace std;

struct Node{
int x;
int y;
int step;
int key;
Node() {}
Node(int a, int b, int s, int k) : x(a), y(b), step(s), key(k) {}
};

int n, m, t;
int arr[25][25];
int door[25][25];
int key[25][25];
int Go[4][2] = {{0, 1}, {0, -1}, {-1, 0}, {1, 0}};
int sx, sy;
int ex, ey;
int vis[25][25][1049];

bool canGo(int x, int y, int k)
{
if (x >= 0 && x < n && y >= 0 && y < m && !arr[x][y])
{
if (vis[x][y][k]) return false;
if ((k & door[x][y]) == door[x][y]) return true;
}
return false;
}

int bfs() {
memset(vis, 0, sizeof(vis));
queue<Node> q;
Node s = Node(sx, sy, 0, 0);
q.push(s);
vis[sx][sy][0] = 1;
while (!q.empty())
{
Node e = q.front();
q.pop();
if (e.x == ex && e.y == ey) return e.step;
for (int i = 0; i < 4; i++)
{
int nx = e.x + Go[i][0];
int ny = e.y + Go[i][1];
if (canGo(nx, ny, e.key))
{
Node nex = Node(nx, ny, e.step + 1, e.key | key[nx][ny]);
vis[nx][ny][nex.key] = 1;
q.push(nex);
}
}
}
return 0;
}

int main() {
while (~scanf("%d %d %d\n", &n, &m, &t))
{
memset(arr, 0, sizeof(arr));
memset(door, 0, sizeof(door));
memset(key, 0, sizeof(key));
char c;
for (int i = 0; i < n; i++)
{
for (int j = 0; j < m; j++)
{
scanf("%c", &c);
if (c == '*') arr[i][j] = 1;
else if (c == '@') sx = i, sy = j;
else if (c == '^') ex = i, ey = j;
else if (c >= 'a' && c <= 'z') key[i][j] = 1 << (c - 'a');
else if (c >= 'A' && c <= 'Z') door[i][j] = 1 << (c - 'A');
}
getchar();
}
int ans = bfs();
if (ans < t && ans) printf("%d\n", ans);
else printf("-1\n");
}
return 0;
}

---------------------
作者:qxAi
來源:CSDN
原文:https://blog.csdn.net/u011077606/article/details/43487421
版權聲明:本文爲博主原創文章,轉載請附上博文鏈接!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章