炮兵陣地(經典狀壓dp)(poj 1185) + 狀壓dp小技巧詳解
Time Limit: 2000MS | Memory Limit: 65536K |
Total Submissions: 22809 | Accepted: 8829 |
Description
司令部的將軍們打算在N*M的網格地圖上部署他們的炮兵部隊。一個N*M的地圖由N行M列組成,地圖的每一格可能是山地(用"H" 表示),也可能是平原(用"P"表示),如下圖。在每一格平原地形上最多可以佈置一支炮兵部隊(山地上不能夠部署炮兵部隊);一支炮兵部隊在地圖上的攻擊範圍如圖中黑色區域所示:如果在地圖中的灰色所標識的平原上部署一支炮兵部隊,則圖中的黑色的網格表示它能夠攻擊到的區域:沿橫向左右各兩格,沿縱向上下各兩格。圖上其它白色網格均攻擊不到。從圖上可見炮兵的攻擊範圍不受地形的影響。
現在,將軍們規劃如何部署炮兵部隊,在防止誤傷的前提下(保證任何兩支炮兵部隊之間不能互相攻擊,即任何一支炮兵部隊都不在其他支炮兵部隊的攻擊範圍內),在整個地圖區域內最多能夠擺放多少我軍的炮兵部隊。
Input
接下來的N行,每一行含有連續的M個字元('P'或者'H'),中間沒有空格。按順序表示地圖中每一行的資料。N <= 100;M <= 10。
Output
僅一行,包含一個整數K,表示最多能擺放的炮兵部隊的數量。Sample Input
5 4 PHPP PPHH PPPP PHPP PHHP
Sample Output
6
題意:
中文題不過多描述。
思路:
按層數來dp,如果用 dp[i][j][k] 來表示在第 i 行,狀態為 j ,i-1行狀態為 k 時的狀態,那麼有轉移方程
dp[i][j][k] = max(dp[i][j][k],dp[i-1][k][l] + num[i]);
列舉 i(層數),j(當前層狀態),k(上一層狀態),l(上上層狀態)就可以來進行轉移了。
關於解題中出現的一些小技巧:
1、關於列舉狀態的預處理縮減:
因為每一個炮臺左右都是會互相攻擊的,也就是有些狀態是不需要列舉的例如(0011),加之如果要用 0 ~ 1023 來列舉的話,1024^3 * 100 的複雜度是不能接受的,所以我們需要通過預處理並裝入sta陣列來將其縮減,在縮減之後列舉陣列中的數字(最多60個狀態)即可。
對於狀態 x,如何快速的判斷其是否存在互相攻擊的情況,我們只需要右移一位之後與原數相且(&)判斷是否為零即可。
原理:如果二進位制位的每一個1都是被大一等於1個零隔開的,那麼錯位之後絕對不會出現兩個1位於同一個位置上(可以自行舉例),所以 & 起來之後一定是為0,反之如果不為0,則說明至少有一個地方是出現了兩個1相連的。
當然,因為這道題是可以打兩格,所以右移一位判斷是不夠的,還需要再同理右移兩位之後來判斷。
2、關於存圖:
對於這道題來說我們是沒必要把圖用char型陣列存下來的,其實對於圖我們只關心圖上的 ‘H’ 位置是否會和我們列舉的狀態衝突,換句話說我們其實可以把每一排也用二進位制壓縮的方式變成一個數字,整張圖就會變成一個一位陣列,然後可以非常方便的進行判斷是否衝突。 這裡用到了按位移動之後 或(|)操作,可以自行理解。
3、關於判斷狀態之間是否衝突:
這個操作是非常簡單的,在判斷上下層是否衝突的時候即是在判斷兩個狀態數有沒有同一個位置都為1,也就是說我們只需要相與(&)看結果是否為0即可。如果兩個數在至少某個位置都為1,那麼相與起來肯定是不為0的。
4、關於滾動陣列:
雖然這道題並沒有使用滾動陣列的必要,但是在某些情況下為了節省空間複雜度是需要用到的。觀察轉移方程會發現,每一次轉移其實僅僅與上一層有關,也就是說我們關心的僅僅是”這一層“ 和”上一層“ ,也就是說我們沒必要把所有的dp值都轉移出來,那麼我們用一個now來記錄當前位於的層數,用 now^1 來取上一層的值,並且在轉移完成以後將“當前層” 變成“上一層” ( now ^= 1),即可。
5、關於狀態的記錄:
雖然狀態一共有 0~1023 這麼多,但是在經過縮減之後剩下的也只剩60種,所以我們只需要開 dp[105][65][65];就足夠,後兩維裡面存放的並不是一個狀態,而是狀態的下標(dp[i][j][k]表示的是第 i 層狀態為 sta[j],第 i-1 層狀態為 sta[k] 的 dp值),這樣又可以節省空間複雜度。
#include"iostream"
#include"cstring"
#include"cstdio"
#include"algorithm"
using namespace std;
int n,m;
int maze[105];
int dp[2][65][65];
int num[65];
int sta[65];
int ns;
int getnum(int x) //用來獲取x狀態中有多少個1
{
int ans = 0;
for(int i = 0;i < 10;i++)
{
if(x & (1<<i))
ans ++;
}
return ans;
}
void init() //預處理出最極限情況下可能用到的狀態
{
ns = 0; //ns記錄了一共有多少個可用狀態
memset(num,0,sizeof(num));
for(int i = 0;i < (1<<10);i++)
{
int i1 = (i>>1);
int i2 = (i>>2);
if(!(i1 & i) && !(i2 & i))
{
sta[ns] = i; //如果出現了合法狀態,就放入sta中儲存起來
num[ns++] = getnum(i); //並且獲取該狀態一共有多少個1,之後方便使用
}
}
}
int main(void)
{
init();
while(~scanf("%d%d",&n,&m))
{
if(n == 0 && m == 0)
{
cout << 0 << endl;
continue;
}
memset(maze,0,sizeof(maze));
memset(dp,0,sizeof(dp));
for(int i = 0;i < n;i++)
{
for(int j = 0;j < m;j++)
{
char ch;
cin >> ch;
if(ch == 'H')
maze[i] = maze[i] | (1<<j); //存圖,將‘H’認為是 1 ,即會衝突的點。將一個字串狀態壓縮成一個數,注意
} //本題中認為從左到右是從低位到高位。
}
int now = 0; //滾動陣列標記初始為0,(ps:其實初始為1也沒有影響)
int res = 0;
for(int i = 0;i < ns;i++)
{
if(sta[i] >= (1<<m)) break; //如果sta[i]這個狀態已經超過了當前這組樣例的最大狀態,則退出,以後的均是這個意思
if(!(maze[0]&sta[i])) //如果第0行和sta[i]狀態相 & 為0,則可以處理 dp[now][i][0]
{
dp[now][i][0] = num[i];
res = max(res,dp[now][i][0]);
}
}
if(n == 1)
{
cout << res << endl;
continue;
}
now ^= 1; //更改標記
for(int i = 0;i < ns;i++) //列舉狀態預處理第2層
{
if(sta[i] >= (1<<m)) break;
if(maze[1]&sta[i]) continue;
for(int j = 0;j < ns;j++)
{
if(sta[j] >= (1<<m)) break;
if(sta[i]&sta[j]) continue;
dp[now][i][j] = max(dp[now][i][j],dp[now^1][j][0] + num[i]);
res = max(res,dp[now][i][j]);
}
}
now ^= 1;
if(n == 2)
{
cout << res << endl;
continue;
}
for(int l = 2;l < n;l++) //列舉 層數(l),當前狀態(i),上一層狀態(j),上上層狀態(k)
{
for(int i = 0;i < ns;i++)
{
if(sta[i] >= (1<<m)) break; //同之前,超過範圍退出
if(maze[l]&sta[i]) continue; //如果當前狀態和圖相沖突,continue;
for(int j = 0;j < ns;j++)
{
if(sta[j] >= (1<<m)) break;
if(maze[l-1]&sta[j]) continue;
if(sta[i]&sta[j]) continue; //如果當前層和上一層相沖突,continue;
for(int k = 0;k < ns;k++)
{
if(sta[k] >= (1<<m)) break;
if(maze[l-2]&sta[k]) continue;
if((sta[i]&sta[k]) || (sta[j]&sta[k])) continue;
dp[now][i][j] = max(dp[now][i][j],dp[now^1][j][k] + num[i]);
res = max(res,dp[now][i][j]);
}
}
}
now ^= 1; //更改滾動陣列標記
}
printf("%d\n",res);
}
return 0;
}