状态压缩DP

状态压缩dp通常是将每一种状态转化为二进制数,从而枚举每一个二进制数,从而判断每一种状态。

:莫德里安的猜想:

在NM的棋盘分割成若干个12的长方形,有多少种方案
直接想的话,难以下手,所以我们考虑将每一列的状态转化为用01表示的,0表示上一列的这一行没有伸出一个小方格,1则表示上一行有伸出一个小方格
f [ i ] [ j ] f[i][j] f[i][j]存储状态:表示第 i i i列伸出了多少小方格,这个状态由 j j j用二进制存储,若有5行也就是 j j j的状态有0~31,(若第一行有其余都没有,那就是10000=16)即把整列的每一行的情况转化为二进制存储。
注意我们所说的1是上一列有伸出方块(横着放),也就是说当前列的当前行放有一个长方形的右端。0代表的是没有伸出方块,也就是上一列是竖着放的。一列中是不能出现奇数个0的(一个长方形为 1 × 2 1 \times 2 1×2竖着放长度为2,是偶数)。并且连续的两列中不能同一行出现两个1,因为这样就等于有两个长方形叠放了。
具体代码

#include<isotream>

using namespace std; 

const int N = 12,M = 1 << N;
long long f[N][N];
bool st[N];

int main()
{
	int n,m;
	while(cin>>n>>m,n||m)
	{
		memset(f,0,sizeof f);
		
		for(int i = 0; i < 1 << n; i ++ );//将每种放法都遍历一遍 
		{
			st[i]=true; 
			int cnt=0;//记录连续0的个数
			for(int j = 0; j < n; j ++ )//判断当前放法每一段有多少连续的0 
			{
				if(i>>j&1)//判断是不是碰到1了 
				{
					if(cnt&1) st[i]=false; //若是奇数0,就代表这种摆法是不合理的
					cnt = 0;
				}
				else cnt++;//不是1的话就是0 那就将0的个数加1 
			}
			if(cnt&1) st[i]=false;//因为可能最后一段是以0结尾的,这样最后一段就没办法用1作为结尾,
			//所以我们要单独判断一下最后一段 
		}
		
		f[0][0]=1;//因为第一列的上一列是不存在的,所以不可能有方块伸出,且只有全部都不放这一种方案。 
		for(int i = 1; i <= m; i ++ )//遍历每一列
			for(int j = 0; j < 1 << n; j ++)//遍历这一列的每一种情况
				for(int k = 0; k < 1 << n; k ++ )//遍历上一列的每一种情况
				{
					if((j&k) == 0 && st[j|k])//如果此时两者没有冲突 (每一位没有同时为1) 
						//st[j|k] 因为两者已经保证没有重叠了,然后该列是从上一列伸出来的,所有两者会有一块方块
						//在同一列,竖着放的方块要根据这两者在同一列的位置判断,用按位或找出两者重合后还是空白的地方,判断有没有奇数空白
						//若是奇数就代表不合法,因此我们前面的预处理就起了作用 
						f[i][j]+=f[i-1][k];
				}
		cout << f[m][0] << endl;//最后的答案就是m+1列没有任何方块伸出来的情况 
	}	
	return 0; 
} 

:最短Hamilton路径

给定 n n n个点的带权无向图,无重边,点从 0 ∼ n − 1 0 \sim n-1 0n1标号,求起点0到终点n-1的最短Hamilton路径( 0 ∼ n − 1 0\sim n-1 0n1不重不漏地经过每个点恰好一次)
一样是用状态压缩来实现,用一个整数来表示一个状态
f [ i ] [ j ] f[i][j] f[i][j]:所有从 0 0 0走到 j j j,走过的点是 i i i这个整数代表的所有点的所有路径的最小值, i i i是一个二进制数。
求最小值
分类的话用走过的路径的倒数第二个点来分类
假如我们走到 j j j,上一个走过的点是 k k k,那么 k − j k-j kj的路径长度是固定的,所以就可以直接求 f [ i − { j } ] [ k ] + a [ k ] [ j ] f[i-\{j\}][k]+a[k][j] f[i{j}][k]+a[k][j]的min , i − { j } ,i-\{j\} ,i{j}表示将 j j j这个位变为0,表示还未走过。
具体代码

#include<iostream>
#include<cstring>
#include<algorithm>

using namespace std;

const int N=20,M=1<<N;//M表示状态数

int n;
int w[N][N];//边权
int f[M][N];

int main()
{
	cin>>n;
	
	for(int i=0;i<n;i++)
	{
		for(int j=0;j<n;j++)
		{
			cin>>w[i][j];
		}
	}
	
	memset(f,0x3f,sizeof f);
	
	f[1][0]=0;//0号点为起点,并且只有0号点走过了。所以距离为0.  
	for(int i=0;i<1<<n;i++)//遍历状态
		for(int j=0;j<n;j++)//遍历结尾点 
		{
			if(i>>j&1)//如果j是走过的,因为j为结尾,所以这个状态下,j要是走过的 
			{
				for(int k=0;k<n;k++)//遍历上一个点 
				{
					if(i-(1<<j)>>k&1)//如果k不是与j为同一个点,并且k也是走过的 
					{
						f[i][j]=min(f[i][j],f[i-(1<<j)][k]+w[k][j]);//取最小值 
					}
				}
			} 
		}
	cout<<f[(1<<n)-1][n-1]<<endl;//输出所有点都经过并且结尾为n-1号点的最小值。 
 }
i-(1<<j)>>k&1 注意这个代码的理解

:骑士

来自一本通
题目:在 n × n n \times n n×n 的棋盘上放 k k k 个国王,国王可攻击相邻的 8 8 8 个格子,求使它们无法互相攻击的方案总数。
题意:每个国王的攻击范围是与他周围的一圈格子,也就是一个’井‘。并且每一行哪些位置可以放国王,只与上一行的国王放置有关。所以我们只需要判断两行之间的关系就可以了。
首先我们看本行,一个放置方式如果是合法的,那么就不能有连续的两个国王。
接着我们看两行的关系,也就是两行之间任意连续的两列内不能有超过1个国王。
我们将这些情况转化为用01表示的状态。1表示这个位置有国王,0表示没有。
所以上述的两个限制条件可以转化为

bool check(int state)
{
	for(int i=0;i<n;i++)
	{
		if((state>>i&1)&&(state>>i+1&1))//若有连续的两个0 
			return false;
	}
	return true;
}
check(a)&&check(b)&&(a&b)==0&&check(a|b)//ab表示两行的状态

状态数组 f [ i ] [ j ] [ k ] f[i][j][k] f[i][j][k]:表示现在判断到第 i i i行,已经安置了 j j j个国王,当前行的状态为 k k k
状态转移方程为

f[i][j][a]+=f[i-1][j-c][b]//c表示a状态下的国王数

具体代码

#include<iostream>
#include<cstring>
#include<algorithm>
#include<vector> 
#define int long long
using namespace std;

const int N=12,M=1<<10,k=110;

int n,m;
vector<int>state;//记录所有合法的状态,就是没有连续的1
vector<int>head[M];//记录可以转移到当前状态的所有状态 
int cnt[M];//记录每个合法状态中骑士的数量
int f[N][k][M];

bool check(int state)
{
	for(int i=0;i<n;i++)
	{
		if((state>>i&1)&&(state>>i+1&1))//若有连续的两个0 
			return false;
	}
	return true;
}

int count(int state)
{
	int res=0;
	for(int i=0;i<n;i++) res+=state>>i&1;//计算1的数量
	return res; 
}

signed main()
{
	cin>>n>>m;
	
	for(int i=0;i<1<<n;i++)//枚举所有可能的状态,寻找合法的 
	{
		if(check(i))
		{
			state.push_back(i);//加入合法序列 
			cnt[i]=count(i);//记录这个合法状态中1的数量 
		} 
	}

	for(int i=0;i<state.size();i++)
	{
		for(int j=0;j<state.size();j++)
		{
			int a=state[i],b=state[j];
			if((a&b)==0&&check(a|b))
			{
				head[i].push_back(j);  
			}
		} 
	} 
	
	f[0][0][0]=1;
	for(int i=1;i<=n+1;i++)
	{
		for(int j=0;j<=m;j++)
		{
			for(int a=0;a<state.size();a++)
			{
				for(int b:head[a])
				{
					int c=cnt[state[a]]; 
					if(j>=c)
					{
						f[i][j][a]+=f[i-1][j-c][b];
					}
				}
			}
		}
	}
	cout<<f[n+1][m][0]<<endl;
	return 0; 
} 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值