状压dp的学习【2018-09-18】

一,阅读课本,博客以及其中遇到的问题:


篇目1:

资料来源:https://blog.csdn.net/u011077606/article/details/43487421

遇到问题:

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  ~~~~

    参看题解:https://blog.csdn.net/xingyeyongheng/article/details/21692655

  【简直太用心了,讲的很好】

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

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值