题目:http://acm.hdu.edu.cn/showproblem.php?pid=1693
题意:
这是一道插头dp的入门题
给出一个n*m的地图,标记为1的格子上有树,有个英雄要吃掉所有的树,吃树的时候要遵循规则:
1)一圈一圈地吃(只能走上下左右,每个圈至少4个格子,头尾相连);
2)只能走有树的格子,吃过的树就消失了,即圈不能相交,每个格子只能走1次;
3)可以分成几个圈来吃。
思路:
1)概念
【插头】每个格子有四条边,每条边上可以插一个插头(与边垂直)。因为每个格子只能经过一次,即在每个格子中选一条边进一条边出,每个格子必插两个插头。
【轮廓线】
轮廓线形状:
dp[ i ][ j ][ k ] 表示的轮廓线经过第 i 行第 j 列格子的下边和右边
如dp[1][2][k]的轮廓线:
给轮廓线经过的每条边编号,每条边为一个插头位置
用过状态压缩枚举状态k,如
0 1 2 3 4
1 0 1 1 1 表示第0,2,3,4条边上的插头插上了, 如下
2)步骤
从左上角开始,一格一格枚举状态(轮廓线在经该格的右边和下边)
状态k下,x为该格的下边,y为右边
若xy边都有插头,则该格的左边和上边没有插头
若xy边都没插头,则该格的左边和上边都有插头
若x,y边中只有1边有插头,则该格的另一个插头在左边或上边上
即确定了xy边的状态,可以得到对应的格子左边和上边的状态
(红色为dp[ i ][ j ]轮廓线,蓝色为dp[ i ][ j - 1 ]轮廓线)
观察序号之间的关系可以发现
dp[ i ][ j ][ k1 ] 要找对应的左边和上边状态
即找dp[ i ][ j - 1 ][ k2 ],使得k1和k2,除了x和y位,其他位都一样,所以可以由k1状态通过位运算轻松表示出对应的k2状态
还需注意,初始化 dp[i][0]时,dp[i][0][k << 1] = dp[i - 1][m][k];
可由下图看出
代码:
#include <stdio.h>
#include <string.h>
#include <math.h>
#include <algorithm>
#define MOD 1000000007
#define INF 0x7fffffff
using namespace std;
typedef long long ll;
int cell[15][15];
ll dp[15][15][5005];
int main()
{
#ifdef LOCAL
freopen("dpdata.txt", "r", stdin);
#endif
int t, n, m;
scanf("%d", &t);
for(int cas = 1; cas <= t; cas++)
{
scanf("%d%d", &n, &m);
for(int i = 1; i <= n; i++)
for(int j = 1; j <= m; j++)
scanf("%d", &cell[i][j]);
memset(dp, 0, sizeof(dp));
dp[0][m][0] = 1;
for(int i = 1; i <= n; i++)
{
for(int k = 0; k < (1 << m); k++)
{ //初始化决策到第i行第0格时的状态
dp[i][0][k << 1] = dp[i - 1][m][k];
}
for(int j = 1; j <= m; j++)
{
for(int k = 0; k < (1 << (m + 1)); k++)
{
int y = 1 << j, x = 1 << (j - 1);
if(cell[i][j])
{ //这格可到达
if((k & x) && (k & y))
dp[i][j][k] = dp[i][j - 1][k - x - y];
else if(!(k & x) && !(k & y))
dp[i][j][k] = dp[i][j - 1][k + x + y];
else //k^x^y异或运算使得x和y插了变成不插,没插变成插
dp[i][j][k] = dp[i][j - 1][k] + dp[i][j - 1][k ^ x ^ y];
}
else
{
if(!(k & x) && !(k & y))
dp[i][j][k] = dp[i][j - 1][k];
else
dp[i][j][k] = 0;
}
}
}
}
printf("Case %d: There are %I64d ways to eat the trees.\n", cas, dp[n][m][0]);
}
return 0;
}