題目: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;
}