状态压缩讲解

本文深入探讨了状态压缩技术在算法问题求解中的应用,通过具体实例阐述了如何利用状态压缩来简化问题表示,减少内存消耗,并提供了解决状态压缩问题的基本方法和技巧。文中以状态压缩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待续。。。。。

<think>好的,我现在要讲解状态压缩动态规划(状态压缩DP)和蒙德里安问题。首先,我需要确保自己对这两个概念有足够的理解,然后才能清晰地传达给用户。 状态压缩DP,我记得它是一种动态规划的优化方法,用于处理态空间较大的问题,尤其是当态可以用位运算来表示的时候。比如在旅行商问题(TSP)或者棋盘覆盖问题中,态可能涉及多个位置是否被访问过,这时候用二进制位来态,可以节省空间和时间。不过,具体的实现细节可能需要再复习一下,比如态转移方程的设计,以及如何用位运算来高效处理态。 然后是蒙德里安问题,可能指的是用特定的方块(比如1x2或2x1的砖块)来铺满一个矩形区域的方案数计算。这个问题通常可以用动态规划来解决,而状态压缩DP在这里可能被用来表示每一行的铺设态,从而进行态转移。比如经典的铺砖问题中,每一行的态可以用二进制表示,0表示未覆盖,1表示已覆盖,然后通过态之间的兼容性来转移。 接下来,我需要整理这两个概念的关系,以及如何用状态压缩DP解决蒙德里安问题。可能需要举一个具体的例子,比如计算一个N×M的棋盘用1×2的砖块铺满的方案数。这时候每一行的态可以用二进制表示,态之间的转移需要满足两行之间不会产生冲突,比如上一行的竖放砖块会影响下一行的态。 需要注意的是,状态压缩DP的关键点在于如何高效地表示态,并设计合适的态转移方式。对于蒙德里安问题,可能还需要预处理所有合法的相邻行态组合,以减少计算量。另外,初始化条件和边界情况也需要处理得当,比如第一行的合法态可能只有某些特定的模式。 在讲解过程中,可能会遇到的疑问点包括:如何用二进制位表示行的态,如何处理态之间的兼容性,态转移的具体实现步骤,以及如何优化时间复杂度。需要确保这些部分解释清楚,避免让用户感到困惑。 可能需要参考一些经典例题的解法,例如LeetCode上的相关题目或者《算法竞赛入门经典》中的例子,以确保所讲的内容正确无误。同时,要注意用中文表达,避免使用过于专业的术语,保持讲解的通俗易懂。 最后,总结状态压缩DP在蒙德里安问题中的应用,强调其如何通过态来高效解决问题,以及这种方法适用的场景和限制。</think>状态压缩动态规划(状态压缩DP)和蒙德里安问题都是算法中经典的组合优化问题,下面分步讲解: --- ### **一、状态压缩DP** **核心思想**:将复杂态用二进制数缩表示,通过位运算高效处理态转移。 **适用场景**: - 态维度较高(如网格、棋盘) - 每个位置仅有两种态(如选中/未选中) - 典型问题:旅行商问题、棋盘覆盖、铺砖问题 **关键步骤**: 1. **态表示**:用二进制数每一位代表一个位置的态(例如:1表示已覆盖,0表示未覆盖) 2. **态转移**:通过位运算(如与、或、异或)判断相邻态是否合法 3. **预处理**:提前计算合法态集合,减少重复判断 **示例**: 计算3×2网格用1×2砖块铺满的方案数: ```plaintext 合法态转移(二进制表示): 上一行态 110 → 当前行需为 001 (竖放砖块的下半部分) ``` --- ### **二、蒙德里安问题** **问题描述**:用1×2或2×1的砖块铺满N×M的矩形区域,求方案总数。 **关键发现**: 当确定所有横放砖块的位置后,竖放砖块的位置唯一确定。因此只需计算横放砖块的合法方案。 **状态压缩DP解法**: 1. **态定义**: `dp[i][s]` 表示处理到第`i`行,且第`i`行态为`s`的方案数。 (`s`的二进制每一位表示该列是否被横放砖块的末尾覆盖) 2. **合法性判断**: - 当前行态`s`不能有连续奇数个0(否则竖放砖块无法填充) - 相邻行态`prev`与`s`需满足 `(prev & s) == 0`(横放砖块不重叠) 3. **转移方程**: ```python if prev和s兼容: dp[i][s] += dp[i-1][prev] ``` **经典优化**: 预处理所有合法相邻态组合,时间复杂度从`O(N*2^M*2^M)`降为`O(N*K)`(`K`为合法组合数) --- ### **三、蒙德里安问题示例(2×3网格)** 1. **合法行态**: - 二进制表示:`000`(全竖放)、`110`(横放前两列) 2. **态转移**: ```plaintext 第1行态110 → 第2行态001 → 总方案数1 ``` 3. **最终结果**:3种方案 --- ### **四、总结** - **状态压缩DP**通过二进制高效处理高维态,是解决棋盘类问题的利器。 - **蒙德里安问题**的解法体现了“先横后竖”的分解思想,结合状态压缩可将指数复杂度优化为可接受范围。 - 实际代码需注意位运算技巧(如判断连续偶数个0可用`(s | (s<<1)) & 3`循环检测)。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值