炮兵阵地
Time Limit: 2000MS | | Memory Limit: 65536K |
Total Submissions: 22809 | | Accepted: 8829 |
Description
司令部的将军们打算在N*M的网格地图上部署他们的炮兵部队。一个N*M的地图由N行M列组成,地图的每一格可能是山地(用"H" 表示),也可能是平原(用"P"表示),如下图。在每一格平原地形上最多可以布置一支炮兵部队(山地上不能够部署炮兵部队);一支炮兵部队在地图上的攻击范围如图中黑色区域所示:
如果在地图中的灰色所标识的平原上部署一支炮兵部队,则图中的黑色的网格表示它能够攻击到的区域:沿横向左右各两格,沿纵向上下各两格。图上其它白色网格均攻击不到。从图上可见炮兵的攻击范围不受地形的影响。
现在,将军们规划如何部署炮兵部队,在防止误伤的前提下(保证任何两支炮兵部队之间不能互相攻击,即任何一支炮兵部队都不在其他支炮兵部队的攻击范围内),在整个地图区域内最多能够摆放多少我军的炮兵部队。
Input
第一行包含两个由空格分割开的正整数,分别表示N和M;
接下来的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 时的状态,枚举 i(层数),j(当前层状态),k(上一层状态),l(上上层状态)就可以来进行转移了。
关于解题中出现的一些小技巧:
1、关于枚举状态的预处理缩减:
因为每一个炮台左右都是会互相攻击的,也就是有些状态是不需要枚举的例如(0011),加之如果要用 0 ~ 1023 来枚举的话,1024^3 * 100 的复杂度是不能接受的,所以我们需要通过预处理并装入can数组来将其缩减,在缩减之后枚举数组中的数字(最多60个状态)即可。
对于状态 x,如何快速的判断其是否存在互相攻击的情况,我们只需要右移一位之后与原数相且(&)判断是否为零即可。
原理:如果二进制位的每一个1都是被大一等于1个零隔开的,那么错位之后绝对不会出现两个1位于同一个位置上(可以自行举例),所以 & 起来之后一定是为0,反之如果不为0,则说明至少有一个地方是出现了两个1相连的。
当然,因为这道题是可以打两格,所以右移一位判断是不够的,还需要再同理右移两位之后来判断。
2、关于判断状态之间是否冲突:
这个操作是非常简单的,在判断上下层是否冲突的时候即是在判断两个状态数有没有同一个位置都为1,也就是说我们只需要相与(&)看结果是否为0即可。如果两个数在至少某个位置都为1,那么相与起来肯定是不为0的。
3、关于状态的记录:
虽然状态一共有 0~1023 这么多,但是在经过缩减之后剩下的也只剩60种,所以我们只需要开 dp[105][65][65];就足够,后两维里面存放的并不是一个状态,而是状态的下标(dp[i][j][k]表示的是第 i 层状态为 can[j],第 i-1 层状态为 can[k] 的 dp值),这样又可以节省空间复杂度。
代码实现:
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <math.h>
using namespace std;
bool is[150][15];
int dp[110][1<<11][1<<11];
int n,m,t=0;
int can[150]; //保存二进制状态
void Init() //预处理特别妙,找到所有可能的合法状态,最多60种
{
for(int i=0;i<(1<<10);i++) //保证两或者一个不相邻或者
{
if((i&(i<<1)) || (i&(i<<2))) continue;
can[++t] = i;
}
return;
}
int check(int x,int state)
{
int cnt = 0;
if( state >= pow((int)2,m) ) return -1; //不超出范围
for(int i=1;i<=m;i++)
{
if(((1<<(i-1))&state )) cnt++; //记录炮兵数量
if(!is[x][i]) //0的地方不能放炮
{
if( ((1<<(m-i)) & state)) return -1;
}
}
return cnt;
}
int main()
{
Init();
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
{
char op; scanf(" %c",&op);
if( op =='P') is[i][j] = true;
else is[i][j] = false;
}
int ans = 0,now;
for(int i=1;i<=n;i++) //枚举行数
{
for(int j=1;j<=t;j++) //枚举状态
{
int cnt = check(i,can[j]);
if( cnt!=-1 ){
for(int k = 1; k<=t;k++)
{
if( !(can[k]&can[j]) )//保证相邻行不冲突
{
dp[i][can[j]][can[k]]=cnt;
now = 0;
for(int q = 1;q<=t;q++) //题意要求两行
{
if(i==1);
else {
if(!(can[q]&can[j])) now = max(now,dp[i-1][can[k]][can[q]]);
}
}
}
dp[i][can[j]][can[k]] += now;
if(i==n) ans = max(ans,dp[i][can[j]][can[k]]);
}
}
}
}
printf("%d\n",ans);
return 0;
}
更加规范的做法
状态压缩,把每行压缩成一个状态,用二进制表示,并用数组rec[]记录状态。
dp[i][j][k]表示第i行为状态j且第i-1行为状态k时能够放置的最大数。num[i]表示第i个状态二进制里面1的个数。
得状态转移方程:dp[i][j][k] = max(dp[i][j][k], dp[i-1][k][t] + num[j])。
方程含义:
第i行为状态j且第i-1行为状态k时能够放置的最大数
= max(自身 , 第i-1行状态为k时且i-2行时状态为t时能够放置的最大数 + 第i行为状态j时能够放置的数目)。
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <iostream>
using namespace std;
int n, m;
int state[200];
int dp[110][200][200];//dp[i][j][k]表示第i行状态为j 第i-1行状态为k时能够放置的最大数
int top;//总状态数
int rec[200], num[200];
char str[110][20];
int one(int x)//判断是否存在两个距离为2的1
{
if(x & x<<1) return 0;
if(x & x<<2) return 0;
return 1;
}
int two(int x, int y)//判断两个状态 是否有相邻的1
{
if(x & y) return 0;
return 1;
}
int count(int x)//计算二进制x里面1的个数
{
int cnt = 0;
while(x)
{
cnt++;
x &= x-1;
}
return cnt;
}
void solve()
{
int total = 1<<m;
top = 0;
for(int i = 0; i < total; i++) if(one(i)) state[++top] = i;
}
int main()
{
while(scanf("%d%d", &n, &m) != EOF)
{
solve();
int a;
for(int i = 1; i <= n; i++)
{
rec[i] = 0;
scanf("%s",str[i]);
for(int j = 0; j < m; j++)
{
if(str[i][j] == 'H')
rec[i] += 1 << j;//转换二进制
}
}
memset(dp, -1, sizeof(dp));
//处理第一行
for(int i = 1; i <= top; i++)
{
num[i] = count(state[i]);
if(two(state[i], rec[1]))
{
for(int j = 1; j <= top; j++)//第0行状态任意
dp[1][i][j] = num[i];
}
}
//更新
for(int i = 2; i <= n; i++)
{
for(int j = 1; j <= top; j++)//第i行状态
{
if(!two(state[j], rec[i])) continue;
for(int k = 1; k <= top; k++)//第i-1行状态
{
if(!two(state[j], state[k])) continue;//临近状态判断
for(int t = 1; t <= top; t++)
{
if(!two(state[t], state[j])) continue;//中间有间隔的两个状态 判断是否有相邻1
if(dp[i-1][k][t] != -1)
dp[i][j][k] = max(dp[i][j][k], dp[i-1][k][t] + num[j]);//更新
}
}
}
}
int ans = 0;
for(int i = 1; i <= top; i++)
{
for(int j = 1; j <= top; j++)
ans = max(ans, dp[n][i][j]);
}
printf("%d\n", ans);
}
return 0;
}