状压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,那么记录下的状态为:
| 1 | 0 | 1 |
|---|---|---|
| 0 | 1 | 0 |
| 1 | 0 | 1 |
这里不难理解,然后是压缩状态。我们逐行进行压缩,那么第一行包含的状态1,0,1转为十进制数记录,即5;第二行包含的状态0,1,0也转为十进制压缩储存,即2;同理,对第三行包含的状态进行压缩,得到5。再将压缩得到的数据5,2,5汇总为数组,就可以得到数组:
| 5 |
|---|
| 2 |
| 5 |
即每一行的一个数包含了该行的三个状态,也就是状态压缩,最后转为大家熟悉的横向的数组,得到:
| 5 | 2 | 5 |
|---|
这样就可以表示放牛的状态了。取用和操作状态时用位运算符直接对他们的二进制进行操作即可。
位运算的定义及运算规则可见下面两个表格:
| 类 型 | 运算符 | 含义 | 规则 |
|---|---|---|---|
| 位逻辑运算符 | & | 按位与 | 同位同为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];
}
然后考虑土地限制
为了便于数据处理,我们将读入的土地状态一律取反,如:
| 1 | 1 | 1 |
|---|---|---|
| 0 | 1 | 0 |
记录为:
| 0 | 0 | 0 |
|---|---|---|
| 1 | 0 | 1 |
这样,如果情况中与土地冲突,按位与得非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转移出错,十个点只过了三个点。希望大家不要犯我这样的错误:-)
这篇博客介绍了如何使用状压DP解决Bzoj1725 牧场的安排问题。作者详细阐述了如何通过二进制压缩存储状态,以及如何处理横向、竖向限制和土地限制,最终实现状态转移。文章提供了相应的代码示例,并提醒读者注意数组索引的统一,以避免转移错误。
931

被折叠的 条评论
为什么被折叠?



