专题·状态压缩[【including Hamilton,铺瓷砖,玉米田

初见安~:)这里是理解了好久终于扯岑头了的一个因状压DP而融会贯通的一个知识点——

状态压缩

先大概介绍一下吧——所谓状态压缩,即原本我们用一个二维数组,现在拥有一个一维的来表示,其中的每一行用一个int类型的二进制数来表示。这是压缩。

用二进制数表示,不仅节省空间而且比较节省时间——二进制的话就类似于一个对电脑底部原始数据的操作,与之对应的位运算操作(详见位运算基础)亦是如此,直接操作。而二进制数又有个特点:一个m位二进制数,一定每一位都是0或1,所以我们就可以用0和1来表示状态——存的时候就反应为一个数。这就是状态。


最短Hamilton路径

这是状态压缩的一个很典型的应用——Hamilton路径,即从0~n-1不重不漏地经过每个点恰好一次的路径。最短亦是顾名思义。

从状态压缩的根本定义上出发——压缩状态,所以我们可以从状态入手——一个n位二进制数,第k位为1则走过了第k个点(算了第0位)。从每一位都是0开始,当这个二进制数的每一位都是1时,我们就说这条路径完成了。

由此我们也可以推导出公式——(有点像动态规划?)设从点i到j的距离为w[ i ][ j ]。

设i为状态,j为目前所在的点,我们要枚举的就是从状态i到点j的路径,所以需要一个中间点k。

f[i][j]=min(f[i][j],f[i xor(1<<j)])

代码及详解如下——【仅核心代码】

int f[1<<20][20],n,w[20][20];
int hamilton()
{
	memset(f,0x3f3f3f3f,sizeof f);
	f[1][0]=0;//处在第0个点 ,但在二进制数中体现为第一位
	for(int i=1;i<1<<n;i++)//枚举n位二进制数,从现在的开始枚举 
	{
		for(int j=0;j<n;j++)
		{
			if(i>>j&1)//确保点j走过 
			for(int k=0;k<n;k++)
			{
				if((i^(1<<j)>>k&1)//状态i退一步:如果点j没走过但点k走过了 ,那就从k走到j 
				f[i][j]=min(f[i][j],f[i^1<<j][k]+w[k][j]); 
			} 
		} 
	} 
	return f[(1<<n)-1][n-1]; //全部走完的状态并且在点n-1停下
}

状压DP

其实最短Hamilton路径因该也算是一种动态规划了,算是对这个内容的热身吧。

动态规划的定义这里就不阐述了,相信您是明白的:)状压DP,也和Hamilton有异曲同工之处——暴力枚举状态来寻求解。

这个暴力其实并不算暴力,毕竟内存和时间都耗费不了多少,所以是很便利的——但是也有个弊端,就是横向不能太长,否则很容易存不下。毕竟是用数来存的状态嘛~

这里有两个典例,来看看吧——

铺瓷砖

//题目摘自YCOJ

今天小信装修新家,给家里买了一种 1×2的长方形(如图1)新瓷砖。小信是个懂得审美的人,毕竟人生除了金钱,还有诗和远方。

这个时候小信就在想,这种长方形的瓷砖铺到一个 n×m 的地面上有多少种方案(如图2:是 4×4 地面的一种方案)?

输入:

输入两个整数 n,m (1≤MIN(n,m)≤10, 1≤MAX(n,m)≤100)。

输出:

输出方案总数(最后结果模 10^9 + 7)。

样例:

input 1:2 2

output 1:2

input 2: 2 3

output 2:: 3

input 3:2 4

output:5

题解:

算是很典型的枚举状态了吧——瓷砖可横可纵,涉及到跨行的问题,怎么表示呢——

我们还是按照状压的基本思路——一行一行地表示状态并枚举。由于数组dp本身的坐标就代表了这个状态,所以我们直接存到这一行达到这个状态的方案数即可。

至于十分特别的纵向瓷砖——我们可以表示一下:只要是纵向瓷砖的上半截,我们就用一个1来表示。即:

我们再来看铺瓷砖有哪些限定条件——

1.最后一行必须全是0,

2.每相邻两行的同一列不能都是1。

3..每相邻两行不能相邻奇数列全是0.即

*4.注意范围——m太大,则让n来限定二进制数的长度,否则会存不下。

那么大致如上。下面是代码及详解——

#include<bits/stdc++.h>
using namespace std;
int m,n,dp[101][1<<20];

bool ok(int x)
{
	for(int j=0;j<m; )
	{
		if(x>>j&1) j++;//为1则不用管
		else if(j+1<m&&~x>>(j+1)&1) j+=2;//为0则必须两个两个的
		else return false;
	}
	return true;
}

int main()
{
	cin>>n>>m;
	if(m>n) swap(m,n);//满足条件4

	for(register int i=0;i<1<<m;i++)
	{
		if(ok(i)) dp[0][i]=1;//先处理第一行的状态,因为它没有上一行状态作为限定,只有基础限定
	}
	
	for(register int i=1;i<n;i++)
	{
		for(register int j=0;j<1<<m;j++)//枚举上一行状态
		{
			for(register int k=0;k<1<<m;k++)//枚举这一行的状态,这个先后应该是不影响的
			{
				if(!(j&k)&&ok(j|k))//满足条件2、3
					dp[i][k]=(dp[i-1][j]+dp[i][k])%((int)1e+9+7);
			}
		}
	}
	
	cout<<dp[n-1][0];//满足条件1
	return 0;
}

代码量还是比较少的,思路也比较简单:)

我们再来看一个——本题出自洛谷P1879

P1879 [USACO06NOV]玉米田Corn Fields

描述

Farmer John has purchased a lush new rectangular pasture composed of M by N (1 ≤ M ≤ 12; 1 ≤ N ≤ 12) square parcels. He wants to grow some yummy corn for the cows on a number of squares. Regrettably, some of the squares are infertile and can't be planted. Canny FJ knows that the cows dislike eating close to each other, so when choosing which squares to plant, he avoids choosing squares that are adjacent; no two chosen squares share an edge. He has not yet made the final choice as to which squares to plant.

Being a very open-minded man, Farmer John wants to consider all possible options for how to choose the squares for planting. He is so open-minded that he considers choosing no squares as a valid option! Please help Farmer John determine the number of ways he can choose the squares to plant.

农场主John新买了一块长方形的新牧场,这块牧场被划分成M行N列(1 ≤ M ≤ 12; 1 ≤ N ≤ 12),每一格都是一块正方形的土地。John打算在牧场上的某几格里种上美味的草,供他的奶牛们享用。

遗憾的是,有些土地相当贫瘠,不能用来种草。并且,奶牛们喜欢独占一块草地的感觉,于是John不会选择两块相邻的土地,也就是说,没有哪两块草地有公共边。

John想知道,如果不考虑草地的总块数,那么,一共有多少种种植方案可供他选择?(当然,把新牧场完全荒废也是一种方案)

输入格式

第一行:两个整数M和N,用空格隔开。

第2到第M+1行:每行包含N个用空格隔开的整数,描述了每块土地的状态。第i+1行描述了第i行的土地,所有整数均为0或1,是1的话,表示这块土地足够肥沃,0则表示这块土地不适合种草。

输出格式:

一个整数,即牧场分配总方案数除以100,000,000的余数。

输入样例#1: 

2 3
1 1 1
0 1 0

输出样例#1: 

9

题解

这个题我在学会状压dp前用排列组合来做,结果只过了20分……关键是我还不知道为什么……

好吧,看到01和铺设方法,状态什么的就可以想到状压DP了。1种0不种,状态判定也很简单——相邻两行同一列没有都种,即两行的状态相与为0。当然多出来的操作就是——先判断这块土地到底能不能种。这个也很简单,看代码及详解前相信你可以想出来的——

好了下面是代码及详解。【过了前两题的话这道题就真的很基础了】

#include<bits/stdc++.h>
#define MAXN 1000000000
using namespace std;
int n,m;
int f[30][1<<14],a[20];
long long ans=0;
void init()
{
    cin>>m>>n;
    int x;
    for(int i=1;i<=m;i++)
    {
        for(int j=1;j<=n;j++)
        {
            cin>>x;
            if(x) a[i]+=(1<<(j-1));//用二进制数a存图
        }
    }
}

void dp()
{
    f[0][0]=1;
    for(int i=1;i<=m;i++)
    {
        for(int j=0;j<=a[i];j++)//这就是在枚举了
        {
            if((j|a[i])>a[i]) continue;//有贫瘠土地种上了
            if(j&(j<<1)) continue;//有相邻草地,不合法
            for(int k=0;k<=a[i-1];k++)
            {
                if(!(k&j))//与上一行不冲突
                {
                    f[i][j]+=f[i-1][k];//方法累加
                    f[i][j]%=MAXN;//避免出界
                }
            }
        }
    }
}

void print()
{
    for(int i=0;i<=a[m];i++)
    {
        ans+=f[m][i];//m行的各种合法状态累加
        ans%=MAXN;
    }
    cout<<ans<<endl;
}

int main()
{
    init();	
    dp();
    print();
    return 0;
}

以上就是关于状态压缩的一篇文章啦~【您内敲了一个半小时:)

迎评:)
——End——

猜你喜欢

转载自blog.csdn.net/qq_43326267/article/details/86682525