状态压缩讲解

本文深入探讨了状态压缩技术在算法问题求解中的应用,通过具体实例阐述了如何利用状态压缩来简化问题表示,减少内存消耗,并提供了解决状态压缩问题的基本方法和技巧。文中以状态压缩BFS和状态压缩DP为例,详细分析了如何通过位运算进行状态压缩,以及如何在实际编程中实现这些概念。


*注:本文对状态压缩的描述非正式化,比较随意,意在让人容易理解,下面开始谈谈我对状态压缩的理解。


1.为什么要采用状态压缩?
采用状态压缩的主要原因是原状态不容易表示或者状态数目过多,内存不够用。


2.用状态压缩有什么好处?
当然自然解决了上面的两个问题-----状态容易表达,至于内存,用一个数的二进制表示状态可以节省很多内存空间(当然也有使用的局限性)

3.状态压缩的难点?
状压一般是用于状压BFS和状压DP,状压的主要难点就是怎么压缩状态,然后就是位运算的使用,位运算一定要熟练。下面介绍位运算
& ---- 按位与,可以将某个数的某二进制位置为0,也可以用于取出某个二进制位
| ---- 按位或,可以将某个数的某二进制位置为1.
~ ---- 非,将一个数的所有二进制位取反
^ ---- 异或,相同为0,不同为1


本文将从几个例题出发,讲解状压的方法及原理


以下例题题意不在描述,还请先点开链接读题
例题一:
HDU 1429  胜利大逃亡(续)
一看此题,就是BFS搜索,怎么搜呢?考虑到钥匙最多只有10把,很容易想到状态压缩,将每把钥匙对应一个二进制数的一位,这样状态就可以轻松表示了
那么为什么要状态压缩?原因很简单,因为到达任意一个点(z,y),Ignatius身上带的钥匙的种类数量都可能不同,应该是属于不同的状态.想到这点程序就不难写了
下面给出AC代码,希望先自己写,不会了再参考下
/*
author: tpbluesky
time:	2015年8月16日14:04:48   
题解:	状态压缩 
*/
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <cmath>
#include <string>
#include <vector>
#include <set>
#include <map>
#include <queue>
#include <string>
#include <sstream>
#define inf 0x3f3f3f3f
#define eps 1e-8
#define sqr(x) ((x)*(x))
using namespace std;
typedef long long ll;
const int maxn = 22;

char mp[maxn][maxn];
int vis[maxn][maxn][1<<10], n, m , t; 
struct node
{
	int x, y, step, state; 
}st;

int dx[] = {1,-1,0,0};
int dy[] = {0,0,1,-1};

bool isok(int x,int y)
{
	if(x < 1 || y < 1 || x > n ||  y > m || mp[x][y] == '*')
		return false;
	return true;
}

int bfs()
{	
	queue<node> q;
	memset(vis,0,sizeof(vis));
	vis[st.x][st.y][0] = 1;
	q.push(st);
	while(!q.empty())
	{
		node tp = q.front();
		q.pop();
		if(mp[tp.x][tp.y] == '^')
			return tp.step;
		//cout<<"              "<<tp.x<<" "<<tp.y<<" "<<tp.step<<" "<<tp.state<<endl;
		for(int i = 0;i < 4;++i)
		{
			node temp;
			temp.x = tp.x + dx[i], temp.y = tp.y+dy[i], temp.step = tp.step+1, temp.state = tp.state;
			if(isok(temp.x,temp.y) && !vis[temp.x][temp.y][temp.state])
			{
				vis[temp.x][temp.y][temp.state] = 1;
				if(mp[temp.x][temp.y] >= 'A' && mp[temp.x][temp.y] <= 'J')
				{
					int t = mp[temp.x][temp.y] - 'A';
					if(temp.state&(1<<t))
						q.push(temp);
				}
				else if(mp[temp.x][temp.y] >= 'a' && mp[temp.x][temp.y] <= 'j')
				{
					vis[temp.x][temp.y][temp.state]= 0;
					int t = mp[temp.x][temp.y] - 'a';
					temp.state |= (1<<t);
					if(!vis[temp.x][temp.y][temp.state])
					{
						vis[temp.x][temp.y][temp.state] = 1;
						q.push(temp);
					}
				}
				else
				{
					q.push(temp);
				}
			}
		} 
	}
	return -1;
}

int main()
{
	while(scanf("%d%d%d",&n,&m,&t) == 3)
	{
		for(int i = 1;i <=n;++i)
		{
			scanf("%s",mp[i]+1);
			for(int j = 1;j <= m;++j)
			{
				if(mp[i][j] == '@')
					st.x = i, st.y = j, st.state = 0;
			}
		}
		int ans = bfs();
		if(ans == -1 || ans >= t)
			printf("-1\n");	
		else
			printf("%d\n",ans);
	}
    return 0;
}


练习:
HDU 5094 --- Maze
HDU 1044 --- Collect More Jewels


例题二:
POJ 1324 Holedox Moving
刚看完题目,可能会感觉无从下手,状态是蛇所在位子的每个坐标,这种状态实在不容易表示,坐标太多,空间不够,怎么办,这时我们也很容易想到二进制表示
状态,但仍然不是很方便表示,经过观察,我们发现每个点相对前个点的相对方向其实很容易表示的,用二位二进制位正好表示四个方向,这样我们很容易从蛇头推出
每个蛇身的坐标,转移其实比较特别,直接移位就行了,不清楚的自己在纸上画画看,想到这点已经程序就不是很难写了,但是实现的时候需要一定技巧,并且能很熟练使 用位运算

下面还是给出AC代码,代码附带注释

/*
author: tpbluesky
time:   2015年8月16日21:57:40
题解:<span style="white-space:pre">	</span>状压BFS
*/
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <cmath>
#include <string>
#include <vector>
#include <set>
#include <map>
#include <queue>
#include <string>
#include <sstream>
#define inf 0x3f3f3f3f
#define eps 1e-8
#define sqr(x) ((x)*(x))
using namespace std;
typedef long long ll;
const int maxn = 22;

int vis[maxn][maxn][1<<14];		//标记状态 
int mp[maxn][maxn];
int n, m, k;
int lor[] = {0,1,2,3};//up,down,left,right 	此处的方向和下面数组的方向是一致的,便于操作 
int dx[] = {1,-1,0,0};
int dy[] = {0,0,1,-1};
struct node
{
	int x, y, state, step;
}st;
int pos[10][2];

bool isok(int x,int y,int orx, int ory, int s)
{
	if(x < 1 || y < 1|| x > n || y > m || mp[x][y] == 1)
		return false;
	int t = 3;
	//cout<<"       "<<s<<endl;
	for(int i = 0;i < k;++i)		//此处是还原原来状态的每个点,判断拓展的点是否是蛇身 
	{
		int p = (s>>(i*2))&t; 		//将s的第i*2+1,i*2+2移到末尾,& 011,得到这两位的数,就可以推出方向 
		if(p == 0)	orx -= 1;
		if(p == 1)	orx += 1;
		if(p == 2)  ory -= 1;
		if(p == 3)  ory += 1;
	//	cout<<"        "<<p<<" "<<orx<<" "<<ory<<endl;
		if(x == orx && y == ory)			//点重复,走到了蛇身 
			return false;
	}
	return true;
}

int bfs()
{
	memset(vis,0,sizeof(vis));
	queue<node> q;
	q.push(st);
	vis[st.x][st.y][st.state] = 1;
	int res = -1;
	while(!q.empty())
	{
		node tp = q.front();
		q.pop();
		if(tp.x == 1 && tp.y == 1){
			res = tp.step;
			break;
		}
		for(int i = 0;i < 4;++i)
		{
			node temp;
			temp.x = tp.x + dx[i], temp.y = tp.y+dy[i], temp.step = tp.step+1, temp.state = (tp.state<<2)|lor[i];  
			int t = (1<<(2*k-2))-1; temp.state &= t;	//上面的右移比较巧妙,可以自己动手模拟一下看看,此处是将temp.state高位全部置为0 
			//cout<<temp.state<<endl;
			if(isok(temp.x, temp.y, tp.x, tp.y, tp.state) && !vis[temp.x][temp.y][temp.state])
			{
				vis[temp.x][temp.y][temp.state] = 1;
				q.push(temp);
			}
		}
	}
	return res;
}

int main()
{
	int cas = 1;
	while(scanf("%d%d%d",&n,&m,&k) == 3)
	{
		memset(mp,0,sizeof(mp));
		if(!n && !m && !k)
			break;
		for(int i = 0;i < k;++i)
		{
			scanf("%d%d",&pos[i][0],&pos[i][1]);
			if(i == 0){
				st.x = pos[i][0], st.y = pos[i][1],st.state = 0, st.step = 0;
			}
		}
		int p;
		scanf("%d",&p);
		for(int i = 0, a, b;i < p;++i)
		{
			scanf("%d%d",&a,&b);
			mp[a][b] = 1;
		}
		for(int i = 1;i < k;++i)			//获取初始状态,不懂得可以手动模拟下 
		{
			int s = 0;
			int tx = pos[i][0]-pos[i-1][0] , ty = pos[i][1]-pos[i-1][1];
			if(tx == -1) s = (s|lor[0])<<(i*2-2); 
			if(tx == 1) s = (s|lor[1])<<(i*2-2);
			if(ty == -1)	s = (s|lor[2])<<(i*2-2);
			if(ty == 1) s = (s|lor[3])<<(i*2-2);
			st.state |= s;
		}
	//	cout<<st.state<<endl;
		printf("Case %d: %d\n",cas++,bfs());
	}
    return 0;
}



练习:
POJ 1184 --- 聪明的打字员
HDU 1043 --- Eight


看完状压BFS,状压DP待续。。。。。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值