AcWing 292. 炮兵阵地
. 炮兵阵地
一、题目描述
司令部的将军们打算在 的网格地图上部署他们的炮兵部队。
一个 的地图由 行 列组成,地图的每一格可能是山地(用 表示),也可能是平原(用 表示),如下图。
在每一格平原地形上最多可以布置一支炮兵部队(山地上不能够部署炮兵部队);一支炮兵部队在地图上的攻击范围如图中黑色区域所示:
如果在地图中的灰色所标识的平原上部署一支炮兵部队,则图中的黑色的网格表示它能够攻击到的区域:沿横向左右各两格,沿纵向上下各两格。
图上其它白色网格均攻击不到。
从图上可见炮兵的攻击范围不受地形的影响。
现在,将军们规划如何部署炮兵部队,在防止误伤的前提下(保证任何两支炮兵部队之间不能互相攻击,即任何一支炮兵部队都不在其他支炮兵部队的攻击范围内),在整个地图区域内 最多能够摆放多少我军的炮兵部队。
输入格式
第一行包含两个由空格分割开的正整数,分别表示 和 ;
接下来的 行,每一行含有连续的 个字符( 或者 ),中间没有空格。按顺序表示地图中每一行的数据。
输出格式
仅一行,包含一个整数 ,表示最多能摆放的炮兵部队的数量。
数据范围
输入样例:
5 4
PHPP
PPHH
PPPP
PHPP
PHHP
输出样例:
6
二、题目解析
关于如何对状态进行 二进制压缩,在 玉米田【线性状压DP】 有详细提及
关于如何不同层不同合法状态判断,在 小国王【线性状压DP】 有详细提及
以上两点在本篇博客中不会再重复提及
观察到数据范围
故我们可以把 行 作为动态规划的阶段,然后对于第 i 行的摆放状态进行 状态压缩
这就变成了一道标准的 线性状压DP
因此在这两题里,我们只需压缩存储 当前层的状态 ,然后枚举 合法的上个阶段 的状态进行 转移 即可
但是本题不同于 小国王 和 玉米田,这两题中棋子的 攻击距离 只有, 玉米田比小国王麻烦些的是因为它有些玉米地是不育的,需要处理。
但是本题的棋子攻击范围是 ,我们只压缩当前层一层状态后进行转移,是不能保证该次转移是 合法的
即不能排除第 层摆放的棋子可以攻击到第 层棋子的 不合法 情况
而解决该问题的手段就是:压缩存储两层的信息,然后枚举合法的第 层状态进行转移即可
闫氏分析法
状态表示
- 集合 : 考虑前 层,且第 层状态是 ,第 层状态是 的方案
- 属性 : 该方案能够放置棋子的最大个数
状态计算—:
=
其中是枚举的能够与 和 合法存在于三行中的所有状态
初始状态:
目标状态:(其中枚举的是所有合法的相邻状态)
对于本题来说,直接开所有的状态空间,空间复杂度是 是会爆内存的
第一维上限是行数:
第二维的状态是,就是
第三维的状态是,就是
所以三维数组的空间上限就是,整数一个数位占个,所以空间就是,而本题要求最大空间上限,所以开这么大的数组会,需要在空间上进行优化。我们联想到其实第层的情况只与第层相关,所以可以使用滚动数组来优化。
果然,直接提交原始版本,系统提示: Memory Limit Exceeded
然后也可以预处理出来 相邻行之间的合法转移,也能避免掉一些不必要的不合法状态的枚举
三、朴素版本()
#include <bits/stdc++.h>
using namespace std;
const int N = 110;
const int M = 1 << 10;
int n, m; // n行m列,n<=100,m<=10,注意:状态压缩DP对列的长度很敏感,太大不行
int g[N]; // 记录每一列的山地情况,是一个二进制标识转化后的十进制数,最大就是全都是1,也就是2^m-1
int cnt[M];
int f[N][M][M];
vector<int> st;
vector<int> head[M];
// 检查一个状态是不是合法状态[同一行必须隔两个及两个以上才是安全的]
bool check(int x) {
return !(x & x >> 1 || x & x >> 2);
}
// 统计某个状态中数字1的数量
int count(int s) {
int res = 0;
for (int i = 0; i < m; i++) // 遍历每一列
if (s >> i & 1) res++; // 如果此个位置数字是1,那么总的数量++
return res;
}
int main() {
// 输入
scanf("%d %d", &n, &m);
for (int i = 1; i <= n; i++) // n行
for (int j = 1; j <= m; j++) { // m列
char c;
scanf(" %c", &c); // 注意scanf输入char的时候,前面要加一个空格
if (c == 'H') g[i] += 1 << (j - 1); // 山地上不能够部署炮兵部队
}
// 1、预处理所有合法状态[单行合法即可]
for (int i = 0; i < 1 << m; i++)
if (check(i)) st.push_back(i), cnt[i] = count(i); // 预算出每个状态i里有多少个数位是1
// 2、找出所有合法状态的合法转移[行间状态兼容检测]
for (int a : st)
for (int b : st)
if (!(a & b)) // 上下行间不能存在同列的1
head[a].push_back(b);
// 3、dp
for (int i = 1; i <= n; i++) // 枚举每一行
for (int a : st) { // 枚举i行的每个状态
if (g[i] & a) continue; // 按i这样摆,确实是本行内不冲突,但如果准备放大炮的位置是高地,是不行的。
for (int b : head[a]) // 枚举第i-1行的每个可能状态,b肯定与a不冲突
for (int c : head[b]) // 枚举第i-2行的每个可能状态,c肯定是与b不冲突的
if (!(a & c)) // 如果c也与a不冲突的话,才是三行间完全兼容的方案
f[i][a][b] = max(f[i][a][b], f[i - 1][b][c] + cnt[a]);
}
// 4、枚举最终的状态最大值
int res = 0;
for (int a : st)
for (int b : head[a])
res = max(res, f[n][a][b]);
// 5、输出
printf("%d\n", res);
return 0;
}
四、滚动数组优化版本
#include <bits/stdc++.h>
using namespace std;
const int N = 110;
const int M = 1 << 10;
int n, m;
int g[N];
int cnt[M];
int f[2][M][M];
vector<int> st;
vector<int> head[M];
bool check(int x) {
return !(x & x >> 1 || x & x >> 2);
}
int count(int x) {
int res = 0;
while (x) {
x = x & (x - 1);
res++;
}
return res;
}
int main() {
scanf("%d %d", &n, &m);
for (int i = 1; i <= n; i++)
for (int j = 1; j <= m; j++) {
char c;
scanf(" %c", &c);
if (c == 'H') g[i] += 1 << (j - 1);
}
for (int i = 0; i < 1 << m; i++)
if (check(i)) st.push_back(i), cnt[i] = count(i);
for (int a : st)
for (int b : st)
if (!(a & b))
head[a].push_back(b);
for (int i = 1; i <= n; i++)
for (int a : st) {
if (g[i] & a) continue;
for (int b : head[a])
for (int c : head[b])
if (!(a & c))
f[i & 1][a][b] = max(f[i & 1][a][b], f[i - 1 & 1][b][c] + cnt[a]);
}
int res = 0;
for (int a : st)
for (int b : head[a])
res = max(res, f[n & 1][a][b]);
printf("%d\n", res);
return 0;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· Docker 太简单,K8s 太复杂?w7panel 让容器管理更轻松!
2020-01-04 东师理想大数据中心技术路线规划
2018-01-04 随笔