1185 炮兵阵地:不能再详细了的题解!

题目大意

司令部的将军们打算在 N ∗ M N*M NM的网格地图上部署他们的炮兵部队。一个 N ∗ M N*M NM的地图由 N N N M M M列组成,地图的每一格可能是山地(用" H H H" 表示),也可能是平原(用" P P 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

思路

看了题一种状态压缩的感觉涌上心头。为什么呢?而m即一行的个数小于等于10,每个格子上只有放或不放两种情况
很自然就会想到状压DP
还有一点很重要::
要符合题目条件的 只有平原可以放炮兵。
所以还要匹配 炮兵放法 与 平原 的关系

如下是DP思考过程::

  • 定义。。我们需要考虑定义,我们可以定义dp[i][j][k]表示到第i行状态为j,且上一行状态为k时的最大方案数
  • 初始化。。然后我们要来考虑初始化,因为状态肯定由前两行推过来,所以我们需要单独处理第一二行的方案数
  • 转移方程。。取最大的话就一定要和 原来的自己、前一个状态+增长 比较,取较大的那个

首先我们一定有这样一个for循环

for (int i = 3; i < N; i++) {//枚举到第i行
	//状态转移条件
	for (int k = 0; k < cnt; k++) {//枚举第i行的所有状态
		//状态转移条件
		for (int m = 0; m < cnt; m++) {//枚举第i-1行的所有状态
			//状态转移条件
			for (int n = 0; n < cnt; n++) {//枚举第i-2行的所有状态
				//状态转移条件
				dp[i][k][m] = max(dp[i][k][m], dp[i - 1][m][n] + num[k]);
			}
		}
	}
}

其中 n u m [ s ] num[s] num[s]数组是我们预处理的,记录状态 s s s一共安排了多少炮兵(其实就是二进制位1的个数)。 c n t cnt cnt是所有状态的总数,和 M M M的值有关。可以看到里面还有状态转移条件并没有处理,我们有什么状态转移条件呢?

  • 安排的炮兵不能在山丘上——解决方案:输入时将每一行地形记录在数组mmap中,比如一行地形如果是“PHPP”,那么我们的mmap存储的是“0100”,当我们判断一个状态有没有和地形冲突时,只需要将该状态与地形按位与,如状态10111011&0100=0,说明有山丘的地方都没有炮兵!
	for (int i = 0; i < N; i++) {
		for (int j = 0; j < M; j++) {
			cin >> s;
			if (s == 'H') mmap[i] |= 1 << j;
		}
	}
  • 那么还有一个问题,就是炮兵的自相残杀问题,如何判断两个状态之间的炮兵射程不重叠,比如第一行是1011第二行是0100(假设地形合格),同样按位与,如果是0就没有位置上的覆盖,所以第i-1行要判断与第i行是否伤害,第i-2行要判断与前两行是否互相伤害。

那么我们就补全了for循环

for (int i = 3; i < N; i++) {//枚举到第i行
	for (int k = 0; k < cnt; k++) {//枚举第i行的所有状态
		//状态k与i的地形冲突
		if (S[k] & mmap[i]) continue;
		for (int m = 0; m < cnt; m++) {//枚举第i-1行的所有状态
			//状态m与i-1的地形冲突或者与i行互相伤害
			if ((S[m] & mmap[i - 1]) || (S[m] & S[k])) continue; 
			for (int n = 0; n < cnt; n++) {//枚举第i-2行的所有状态
				//状态n与i-2的地形冲突或者与i-1行互相伤害,或者与i行互相伤害
				if ((S[n] & mmap[i - 2]) || (S[n] & S[m]) || (S[n] & S[k]))  continue;
				dp[i][k][m] = max(dp[i][k][m], dp[i - 1][m][n] + num[k]);
			}
		}
	}
}

你可能已经看到了,我们并没有考虑一行之中是否互相伤害的问题,接下来我们就解决这个问题,对于状态数组S,我们在程序的开始就预处理出所有的可用状态(一行之间冲突的状态),并统计他的数目cnt,这样我们不仅可以在后面避免一行之内的冲突,而且简化了状态的数目。count1统计i的二进制数1的个数

	S[0] = 0;
	for (int i = 1; i < (1 << M); i++) {
		if (i&(i << 1) || i & (i >> 1))continue;//左右有相邻元素
		if (i&(i << 2) || i & (i >> 2))continue;//左2右2有相邻元素
		S[cnt] = i;
		num[cnt++] = count1(i);
	}

值得注意的是,主for循环我们是从第三行开始的,第一二行要单独进行处理

	for (int k = 0; k < cnt; k++) {
		if (S[k] & mmap[0])continue;//地形冲突
		dp[0][k][0] = num[k];
	}

	for (int k = 0; k < cnt; k++) {//第一行
		if (S[k] & mmap[1]) continue;
		for (int m = 0; m < cnt; m++) {//第零行
			if ((S[k] & mmap[0]) || (S[k] & S[m])) continue;
			dp[1][m][k] = max(dp[1][m][k], dp[0][k][0] + num[m]);
		}
	}

最后只需要在dp数组的最后一行进行统计,找出最大的那个元素值即可。

#include<iostream>
#include<algorithm>
#include<vector>
using namespace std;

#define ll int
#define MAX	15

//cnt:可用状态的总数 S:所有的可用状态
ll N, M, cnt = 1, S[1025];

//dp:动归属组;num:每一种状态含有的炮兵数目;mmap:每一行对应的地形
ll dp[105][70][70], num[1025], mmap[105];

int count1(int k) {
	int ans = 0;
	while (k > 0) {
		ans++;
		k -= k & (-k);//k&(-k):k的最后一位1对应的十进制数
	}
	return ans;
}

int main() {
	cin >> N >> M; char s;
	for (int i = 0; i < N; i++) {
		for (int j = 0; j < M; j++) {
			cin >> s;
			if (s == 'H') mmap[i] |= 1 << j;
		}
	}

	S[0] = 0;
	for (int i = 1; i < (1 << M); i++) {
		if (i&(i << 1) || i & (i >> 1))continue;//左右有相邻元素
		if (i&(i << 2) || i & (i >> 2))continue;//左2右2有相邻元素
		S[cnt] = i;
		num[cnt++] = count1(i);
	}

	for (int k = 0; k < cnt; k++) {
		if (S[k] & mmap[0])continue;//地形冲突
		dp[0][k][0] = num[k];
	}

	for (int k = 0; k < cnt; k++) {//第一行
		if (S[k] & mmap[1]) continue;
		for (int m = 0; m < cnt; m++) {//第零行
			if ((S[m] & mmap[0]) || (S[k] & S[m])) continue;
			dp[1][k][m] = max(dp[1][k][m], dp[0][m][0] + num[k]);
		}
	}

	for (int i = 2; i < N; i++) {//枚举到第i行
		for (int k = 0; k < cnt; k++) {//枚举第i行的所有状态
			//状态k与i的地形冲突
			if (S[k] & mmap[i]) continue;
			for (int m = 0; m < cnt; m++) {//枚举第i-1行的所有状态
				//状态m与i-1的地形冲突或者与i行互相伤害
				if ((S[m] & mmap[i - 1]) || (S[m] & S[k])) continue; 
				for (int n = 0; n < cnt; n++) {//枚举第i-2行的所有状态
					//状态n与i-2的地形冲突或者与i-1行互相伤害,或者与i行互相伤害
					if ((S[n] & mmap[i - 2]) || (S[n] & S[m]) || (S[n] & S[k]))  continue;
					dp[i][k][m] = max(dp[i][k][m], dp[i - 1][m][n] + num[k]);
				}
			}
		}
	}

	ll res = 0;
	for (ll i = 0; i < cnt; i++) {
		for (ll j = 0; j < cnt; j++) {
			res = max(res, dp[N - 1][i][j]);
		}
	}
	cout << res << endl;
}

当然存储空间还可以进一步优化,为什么这里dp可以开70*70,是因为我们首先要对状态进行筛选,选出可用的cnt个,而cnt不会超过70。当然也可以用滚动数组组对内存进行进一步的优化。

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值