You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

8.3 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

##AcWing 292. 炮兵阵地

一、题目描述

司令部的将军们打算在 N×M 的网格地图上部署他们的炮兵部队。

一个 N×M 的地图由 NM 列组成,地图的每一格可能是山地(用 H 表示),也可能是平原(用 P 表示),如下图。

在每一格平原地形上最多可以布置一支炮兵部队(山地上不能够部署炮兵部队);一支炮兵部队在地图上的攻击范围如图中黑色区域所示:

如果在地图中的灰色所标识的平原上部署一支炮兵部队,则图中的黑色的网格表示它能够攻击到的区域:沿横向左右各两格,沿纵向上下各两格。

图上其它白色网格均攻击不到。

从图上可见炮兵的攻击范围不受地形的影响

现在,将军们规划如何部署炮兵部队,在防止误伤的前提下(保证任何两支炮兵部队之间不能互相攻击,即任何一支炮兵部队都不在其他支炮兵部队的攻击范围内),在整个地图区域内 最多能够摆放多少我军的炮兵部队

输入格式 第一行包含两个由空格分割开的正整数,分别表示 NM

接下来的 N 行,每一行含有连续的 M 个字符(P 或者 H),中间没有空格。按顺序表示地图中每一行的数据。

输出格式 仅一行,包含一个整数 K,表示最多能摆放的炮兵部队的数量。

数据范围

N≤100,M≤10

输入样例

5 4
PHPP
PPHH
PPPP
PHPP
PHHP

输出样例

6

二、题目解析

关于如何对状态进行 二进制压缩,在 玉米田【线性状压DP】 有详细提及

关于如何不同层不同合法状态判断,在 小国王【线性状压DP】 有详细提及

以上两点在本篇博客中不会再重复提及

观察到数据范围 0<N≤100,0<M≤10 故我们可以把 作为动态规划的阶段,然后对于第 i 行的摆放状态进行 状态压缩

这就变成了一道标准的 线性状压DP

因此在这两题里,我们只需压缩存储 当前层的状态 ,然后枚举 合法的上个阶段 的状态进行 转移 即可

但是本题不同于 小国王玉米田,这两题中棋子的 攻击距离 只有1, 玉米田比小国王麻烦些的是因为它有些玉米地是不育的,需要处理。

但是本题的棋子攻击范围是 2,我们只压缩当前层一层状态后进行转移,是不能保证该次转移是 合法的

即不能排除第 i2 层摆放的棋子可以攻击到第 i 层棋子的 不合法 情况

而解决该问题的手段就是:压缩存储两层的信息,然后枚举合法的第 i2 层状态进行转移即可

闫氏DP分析法

状态表示

  • 集合 f_{i,j,k}: 考虑前 i 层,且第 i 层状态是 j,第 i1 层状态是 k 的方案
  • 属性 f_{i,j,k}: 该方案能够放置棋子的最大个数

状态计算f_{i,j,k}: f_{i,j,k}=max(f_{i1,k,pre}+cnt_j)

其中pre是枚举的能够与 kj 合法存在于三行中的所有状态

初始状态f_{0,0,0}

目标状态f_{n,j,k}(其中j,k枚举的是所有合法的相邻状态) 对于本题来说,直接开所有的状态空间,空间复杂度是 O(N×2^M×2^M) 是会爆内存的

第一维上限是行数:100 第二维的状态是2^M,就是2^{10}=1024 第三维的状态是2^M,就是2^{10}=1024 所以三维数组的空间上限就是100*1024*1024,整数一个数位占8byte,所以空间就是8*100*1024*1024/1024/1024=800MB,而本题要求最大空间上限64MB,所以开这么大的数组会MLE,需要在空间上进行优化。我们联想到其实第i层的情况只与第i-1层相关,所以可以使用滚动数组来优化。

果然,直接提交原始版本,系统提示: Memory Limit Exceeded

然后也可以预处理出来 相邻行之间的合法转移,也能避免掉一些不必要的不合法状态的枚举

三、朴素版本(MLE

#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;
}