1. 程式人生 > >狀壓dp的學習【2018-09-18】

狀壓dp的學習【2018-09-18】

一,閱讀課本,部落格以及其中遇到的問題:

篇目1:

遇到問題:

1,NP問題是什麼?------------------------------------------------

  NP問題是指存在多項式演算法能夠解決的非決定性問題,而其中NP完全問題又是 最有可能不是 P問題 的 問題型別。所有的NP問題都可以用多項式時間劃歸到他們中的一個。所以顯然NP完全的問題具有如下性質:它可以在多項式時間內求解,當且僅當所有的其他的NP-完全問題也可以在多項式時間內求解。

  其中:

  1》多項式演算法:判定一個演算法好壞的重要條件,演算法複雜度在O(n*k)【k為常數,因題目而變】,則稱其為多項式時間內的演算法;

  2》什麼是非決定性問題:有些計算問題是確定性的,比如加減乘除之類,你只要按照公式推導,按部就班一步步來,就可以得到結果。但是,有些問題是無法按部就班直接地計算出來。比如,找大質數的問題。有沒有一個公式,你一套公式,就可以一步步推算出來,下一個質數應該是多少呢?這樣的公式是沒有的

  3》什麼是P問題?如果一個問題的 複雜度 是 該問題的 一個例項規模 n 的多項式函式,則這種可以在 多項式時間 內解決的問題屬於P類問題。【可以在多項式時間內解決的問題,polynomial problem】;

  4》什麼是NP問題?【可以在多項式的時間裡驗證一個解的問題,non-deterministic polynomial】

  5》什麼是NPC問題?【NPC問題,是NP的一個子集,且其中每一個問題均能由NP中的任何問題在多項式時間內轉化而成,np complete】

2,位運算:-----------------------------------------------------

  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二進位制下的最有一位。

  5》常見的應用:

    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)

3,Mondriaan's Dream【用1*2或2*1的磚將n*m的區域鋪滿】

     ~~~~方法1  ~~~~

  【簡直太用心了,講的很好】

  Description【題目描述】

Squares and rectangles fascinated the famous Dutch painter Piet Mondriaan. One night, after producing the drawings in his 'toilet series' (where he had to use his toilet paper to draw on, for all of his paper was filled with squares and rectangles), he dreamt of filling a large rectangle with small rectangles of width 2 and height 1 in varying ways. 

 

Input

The input contains several test cases. Each test case is made up of two integer numbers: the height h and the width w of the large rectangle. Input is terminated by h=w=0. Otherwise, 1<=h,w<=11.

Output

For each test case, output the number of different ways the given rectangle can be filled with small rectangles of size 2 times 1. Assume the given large rectangle is oriented, i.e. count symmetrical tilings multiple times.

Sample Input

1 2
1 3
1 4
2 2
2 3
2 4
2 11
4 11
0 0

Sample Output

1
0
1
2
3
5
144
51205

思路講解:

分析:用1*2的磚去恰好鋪滿n*m的空間,對於第k行第j列,有3種情況將該點鋪滿         1:由第k-1行第j列磚豎著鋪將第k行第j列鋪滿         2:由第k行第j列被橫鋪磚鋪滿         3:第k行第j列磚豎著鋪將該點鋪滿

    所以對於每一列的情況其實有兩種(1,0)表示該點鋪磚還是不鋪;而對於每一列必須到達的狀態只有一種,就是被鋪滿(1),但是由上述3種情況將鋪滿方式分成兩種:  0 和 1表示被k-1行j列豎鋪鋪滿和在k-1行被橫鋪鋪滿 。     對於每一行列舉每一種到達的狀態j,dp[j]表示到達該狀態有多少種情況。    

    分析對於第k-1行狀態j:10000111,需要到達第k行狀態i:  01111011;     如果需要到達第k行j列狀態是0,則必須第k-1行該點狀態不能是0,否則一定是連續兩列豎放衝突,所以到達第k-1行該點只能是1,也就是說 i | j 一定每一位是1;

    也可以一步步判斷是否滿足第k行j列是0第k-1行j列是1 ,如果需要到達第k行狀態j列是1,則假如第k-1行該點是0,則該點狀態可以到達,繼續判斷j+1列;假如第k-1行該點是1,則第k行j列的1一定是橫鋪到達的,所以k行第j+1列一定也被鋪滿為1;從而第k-1行j+1列一定不能豎鋪,必須被橫鋪鋪滿,所以也是1.     於是綜合的第k行j列和第k-1行j列的關係(每一行每一列都表示到達的狀態):         1:下面這種情況從第j列繼續去判斷j+1列                 1               0         2: 下面這種情況從第j列繼續去判斷j+1列                0               1         3:下面這種情況從第j列判斷第j+1列是否全是1,然後繼續判斷第j+2列               1               1 

未優化程式碼:

#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <string>
#include <queue>
#include <algorithm>
#include <map>
#include <cmath>
#include <iomanip>
#define INF 99999999
typedef long long LL;
using namespace std;
 
const int MAX=(1<<11)+10;
int n,m;
LL temp[MAX],dp[MAX],bin[15];
bool mark[MAX];
 
bool check(int i){
	while(i){
		if(i&1){
			i>>=1;
			if(!(i&1))return false;//第j列是1則第j+1列必須是1 
			i>>=1;//繼續判斷下一列 
		}else i>>=1;//繼續判斷下一列 
	}
	return true;
}
 
void Init(){
	memset(mark,false,sizeof mark);
	memset(temp,0,sizeof temp);
	for(int i=0;i<bin[m];++i){//初始化第一行和可以到達什麼狀態 
		if(check(i))temp[i]=1,mark[i]=true;
	}
}
 
void DP(){
	for(int k=2;k<=n;++k){
		for(int i=0;i<bin[m];++i)dp[i]=0;
		for(int i=0;i<bin[m];++i){
			for(int j=0;j<bin[m];++j){
				if((i|j) != bin[m]-1)continue;
                    //每一位或之後必須每一位是1(綜合前面3種情況和分析可知)
				if(!mark[i&j])continue;
                    //由初始化和前面分析三種情況分析可知i&j必須得到和初始化可以到達的狀態一樣才行
				dp[i]+=temp[j];//i可以從j到達,則增加j的方案數 
			}
		}
		for(int i=0;i<bin[m];++i)temp[i]=dp[i];
	}
}
 
int main(){
	bin[0]=1;
	for(int i=1;i<12;++i)
        bin[i]=2*bin[i-1];
	while(~scanf("%d%d",&n,&m),n+m){
		if(n<m)swap(n,m);    //始終保持m<n,提高效率 
		Init();
		DP();
		printf("%lld\n",temp[bin[m]-1]);    //輸出最後一行到達時的狀態必須全部是1 
	}
	return 0;
}

優化方案以及程式碼:

/*
優化:
不去盲目的列舉所有狀態i和j然後判斷狀態j能否到達i,
這樣效率很低,因為能到達i的狀態j很少 
因此對於每種狀態i,由i區搜尋能到達i的狀態j,
大大提高效率 

有298ms->32ms  
*/   
#include <iostream>  
#include <cstdio>  
#include <cstdlib>  
#include <cstring>  
#include <string>  
#include <queue>  
#include <algorithm>  
#include <map>  
#include <cmath>  
#include <iomanip>  
#define INF 99999999  
typedef long long LL;  
using namespace std;  
  
const int MAX=(1<<11)+10;  
int n,m;  
LL temp[MAX],dp[MAX],bin[15];  
  
bool check(int i){  
    while(i){  
        if(i&1){  
            i>>=1;  
            if(!(i&1))return false;//第j列是1則第j+1列必須是1   
            i>>=1;//繼續判斷下一列   
        }else i>>=1;//繼續判斷下一列   
    }  
    return true;  
}  
  
void Init(){  
    memset(temp,0,sizeof temp);  
    for(int i=0;i<bin[m];++i)if(check(i))temp[i]=1;//初始化第一行  
}  
  
void dfs(int k,int i,int j){  
    if(k == m){dp[i]+=temp[j];return;}  
    if(k>m)return;  
    if((i>>k)&1){  
        dfs(k+1,i,j);  
        if((i>>(k+1))&1)dfs(k+2,i,j|(1<<k)|(1<<(k+1)));  
    }  
    else dfs(k+1,i,j|(1<<k));  
}  
  
void DP(){  
    for(int k=2;k<=n;++k){  
        for(int i=0;i<bin[m];++i)dp[i]=0;  
        for(int i=0;i<bin[m];++i)dfs(0,i,0);  
        for(int i=0;i<bin[m];++i)temp[i]=dp[i];  
    }  
}  
  
int main(){  
    bin[0]=1;  
    for(int i=1;i<12;++i)bin[i]=2*bin[i-1];  
    while(~scanf("%d%d",&n,&m),n+m){  
        if(n<m)swap(n,m);//始終保持m<n,提高效率   
        Init();  
        DP();  
        printf("%lld\n",temp[bin[m]-1]);//輸出最後一行到達時的狀態必須全部是1   
    }  
    return 0;  
}

  感覺講解比程式碼好理解 == 。

  ~~~~方法2  ~~~~

深搜大法:

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

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;
	}
}

4,[vijios456]最小總代價-------------------------------------------------

題目描述:

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;
}

5,勝利大逃亡(續)(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;
}