状压DP入门——Bzoj1725 牧场的安排题解

这篇博客介绍了如何使用状压DP解决Bzoj1725 牧场的安排问题。作者详细阐述了如何通过二进制压缩存储状态,以及如何处理横向、竖向限制和土地限制,最终实现状态转移。文章提供了相应的代码示例,并提醒读者注意数组索引的统一,以避免转移错误。

状压DP入门——Bzoj1725 牧场的安排

状压DP是DP中不太好理解的一种算法,其关键点就在于将每个状态转化为二进制再储存为十进制储存(如:True,False,True,False四个状态转为二进制数1010再转为十进制数10储存),其优点是可以在较小的内存占用中表示较多的状态(long long int大约可压缩64种状态),同时运用位运算可以得到数组访问处理所达不到的速度,所以状压DP非常重要。

话不多说,先上例题

【题目描述】
原题来自:USACO 2006 Nov. Gold
Farmer John 新买了一块长方形的牧场,这块牧场被划分成
M 行 N列 (1≤M≤12;1≤N≤12),每一格都是一块正方形的土地。FJ 打算在牧场上的某几格土地里种上美味的草,供他的奶牛们享用。遗憾的是,有些土地相当的贫瘠,不能用来放牧。并且,奶牛们喜欢独占一块草地,于是 FJ 不会选择两块相邻的土地,即:没有哪两块草地有公共边。当然,FJ 还没有决定在哪些土地上种草。
作为一个好奇的农场主,FJ 想知道,如果不考虑草地的总块数,那么,一共有多少种种植方案可供他选择。当然,把新的牧场荒废,不在任何土地上种草,也算一种方案。请你帮 FJ 算一下这个总方案数。
【输入】
第 1 行:两个正整数M 和 N,用空格隔开;
第 2到 M+1行:每行包含N 个用空格隔开的整数,描述了每块土地的状态。输入的第i+1行描述了第 i行的土地。所有整数均为 0或 1,1表示这块土地足够肥沃,0则表示这块地上不适合种草。
【输出】
第1行:输出一个整数,即牧场分配总方案数除以 108 的余数。
【输入样例】
2 3
1 1 1
0 1 0
【输出样例】
9

先考虑记录方案方法

对于每块地只有两种情况,放牛和不放牛,所以可以用二进制储存状态进行压缩。
先看个例子:

放牛不放牛放牛
不放牛放牛不放牛
放牛不放牛放牛

将放牛状态记录为1,不放牛记录为0,那么记录下的状态为:

101
010
101

这里不难理解,然后是压缩状态。我们逐行进行压缩,那么第一行包含的状态1,0,1转为十进制数记录,即5;第二行包含的状态0,1,0也转为十进制压缩储存,即2;同理,对第三行包含的状态进行压缩,得到5。再将压缩得到的数据5,2,5汇总为数组,就可以得到数组:

5
2
5

即每一行的一个数包含了该行的三个状态,也就是状态压缩,最后转为大家熟悉的横向的数组,得到:

525

这样就可以表示放牛的状态了。取用和操作状态时用位运算符直接对他们的二进制进行操作即可。
位运算的定义及运算规则可见下面两个表格:

类 型运算符含义规则
位逻辑运算符&按位与同位同为1该位为1,否则为0
位逻辑运算符|按位或同位中有一个为1该位为1,否则为0
位逻辑运算符^按位异或同位相同为1,否则为0
位逻辑运算符~取反0变1,1变0
移位运算符<<左移整体左移,右边末尾添0,左溢出截断
移位运算符>>右移整体右移,左边末尾添0,右溢出截断

再考虑每行的所有情况(暂不考虑土地的影响及竖向限制)

因为我们是横向压缩状态,所以我们只考虑横向的所有情况。因为每行的列数相同,所以所有可能的情况是相同的! 我们仅需枚举一次每行中所有不冲突的情况即可套用到所有行。情况的穷举首先会想到dfs,那就先用dfs穷举。
Code:

int list[4099];//用于记录所有已枚举的情况,大小为1<<12
int item = 0;//记录情况总数
int range = 0;//设置枚举情况的长度
void dfs(int deep, int last)
{
	if (deep >= range)//判断枚举位数是否达到长度
	{
		list[item++] = last;//记录当前情况
		return;
	}
	dfs(deep + 1, last << 1);//无条件左移
	if (!(last & 1))//判断最后一位是否为1,因为1的二进制为0001,如果最后一位为1,last&1就为true
	{//如果最后一位为1,左移到第二位后加1(即设置最后一位为1)会出现两个1状态相邻,不符合题意
		dfs(deep + 1, (last << 1) + 1);
	}
	return;
}

调用dfs(0, 0)即可。
同样可以根据取值范围一个一个枚举。枚举n位的二进制数等于(1<<n)-1的二进制数(1<<n的二进制数最高位为1,随后n位为0。而n位的二进制数最大为n个1,加1就等于1加n个0即1<<n。所以范围为0~(1<<n)-1)。那么,我们可以用循环枚举所有情况。
Code:

int list[4099];//用于记录所有已枚举的情况,大小为1<<12
int item = 0;//记录情况总数
void init()
{
	int num = (1 << n);						
	for (int i = 0; i < num; i++)
	{
		if (!(i&(i << 1)))//错一位再按位与,若连续两位为1,错位并按位与后结果不为0
		{
			list[++item] = i;//记录当前情况
		}
	}
}	

这样就可以枚举所有横向的情况。

再考虑竖向冲突情况

竖向冲突只与上下两行的情况有关,那么我们一律让当前行使用的方案row1与上一行使用的方案row2判断是否冲突即可。如果上下两行放牛的位置相错开,那么两行的按位与得0,否则非0。
Code:

bool row(int row1,int row2)
{
	return list[row1] & list[row2];
}

然后考虑土地限制

为了便于数据处理,我们将读入的土地状态一律取反,如:

111
010

记录为:

000
101

这样,如果情况中与土地冲突,按位与得非0,否则为0。
Code:

bool check(int line, int item)
{
	return !(ary[line] & list[item]);//ary为土地压缩后的状态
}

最后进行状态转移

定义二维数组dp[12][4096],dp[i][j]=k表示第i行第j种情况有k种可行方案。
对于第i行的第j种情况与上一行即i-1行的第k种情况不冲突,则有转移方程:
dp[i][j]=(dp[i][j]+dp[i-1][k])%MOD
第一行枚举每一种情况后判断是否与土地相冲突,冲突dp[1][i]=0,否则dp[1][i]=1。
状态转移从第二行开始转至第m行,最后将第m行的所有值相加取模即可得到答案。

AC代码

#include<iostream>
#include<cstdio>

int ary[13];
int dp[13][4099];
int n, m;
int list[4099];
int item = 0;
int range = 0;
void dfs(int deep, int last)
{
	if (deep >= range)
	{
		list[item++] = last;
		return;
	}
	dfs(deep + 1, last << 1);
	if (!(last & 1))
	{
		dfs(deep + 1, (last << 1) + 1);
	}
	return;
}
bool check(int line, int item)
{
	return !(ary[line] & list[item]);
}
void init()
{
	int num = (1 << n);
	for (int i = 0; i < num; i++)
	{
		if (!(i&(i << 1)))
		{ 
			list[++item] = i;
		}
	}
}						
int main()					
{
	scanf("%d%d", &m, &n);
	for (int i = 1; i <= m; i++)
	{
		int t = 0;
		for (int j = 1; j <= n; j++)
		{
			int x;
			scanf("%d", &x);
			t = (t << 1) + 1 - x;
		}
		ary[i] = t;
	}
	range = n;
	//dfs(0, 0);
	init();
	for (int i = 1; i <= item; i++)
	{
		if (check(1, i))
		{
			dp[1][i] = 1;
		}
	}
	for (int i = 2; i <= m; i++)
	{
		for (int j = 1; j <= item; j++)
		{
			if (!check(i, j))
			{
				continue;
			}
			for (int k = 1; k <= item; k++)
			{
				if (!check(i - 1, k))
				{
					continue;
				}
				if (row(j, k))
				{
					continue;
				}
				dp[i][j] = (dp[i][j] + dp[i - 1][k]) % 100000000;
			}
		}
	}
	int ans = 0;
	for (int i = 1; i <= item; i++)
	{
		ans = (ans + dp[m][i]) % 100000000;
	}
	printf("%d\n", ans);
	return 0;
}

//我最开始时ary索引起止和list索引起止不统一导致dp转移出错,十个点只过了三个点。希望大家不要犯我这样的错误:-)

这是我的第一篇题解,如有勘误还请大佬指出!

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值