AcWing
  • 首页
  • 课程
  • 题库
  • 更多
    • 竞赛
    • 题解
    • 分享
    • 问答
    • 应用
    • 校园
  • 关闭
    历史记录
    清除记录
    猜你想搜
    AcWing热点
  • App
  • 登录/注册

AcWing 292. 炮兵阵地【线性状压DP+常规优化+转移优化】    原题链接    中等

作者: 作者的头像   一只野生彩色铅笔 ,  2021-07-20 23:57:22 ,  所有人可见 ,  阅读 7703


99


16

最近在补全提高课所有题目的题解,宣传一下汇总的地方提高课题解补全计划

对比y总原来的代码部分,在无效的转移状态枚举上进一步进行了优化,能更快一点

Xnip2021-07-21_00-01-24.jpg

题目描述

给定一个 $N \times M$ 矩阵,矩阵中标有 $H$ 字样的位置 不能 放置棋子,标有 $P$ 字样的位置 可以 放置棋子

棋子的 攻击方向 为 上、下、左、右 四个方向, 攻击距离 为 2个格子

现在在该棋盘上放置棋子,使得棋子之前 不能被互相攻击 到,能够放置棋子的 最大个数

分析

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

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

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

观察到数据范围 $0 \lt N \le 100, 0 \lt M \le 10$

故我们可以把 行 作为动态规划的阶段,然后对于第 i 行的摆放状态进行 状态压缩

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

但是本题不同于 小国王 和 玉米田,这两题中棋子的 攻击距离 只有1

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

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

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

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

闫氏DP分析法

状态表示—集合$f_{i,j,k}:$ 考虑前 $i$ 层,且第 $i$ 层状态是 $j$,第 $i-1$ 层状态是 $k$ 的方案
状态表示—属性$f_{i,j,k}:$ 该方案能够放置棋子的最大个数 $Sum$
状态计算—$f_{i,j,k}:$
$$ f_{i,j,k} = \max f_{i-1,k,pre} + cnt_j $$
其中$pre$是枚举的能够与 $k$ 和 $j$ 合法存在于三行中的所有状态

初始状态:$f_{0,0,0}$
目标状态:$f_{n,j,k}\quad(其中j,k枚举的是所有合法的相邻状态)$

对于本题来说,直接开所有的状态空间,空间复杂度是 $O(N \times 2^M \times 2^M)$ 是会爆内存的

因此我们必须使用 滚动数组 进行优化

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

Code

#include <iostream>
#include <vector>

using namespace std;

const int N = 110, M = 1 << 10;
int n, m;
int g[N], cnt[M];
int f[2][M][M];
vector<int> state;
vector<int> head[M];

bool check(int st)
{
    return !(st & st >> 1 || st & st >> 2);
}
int count(int st)
{
    int res = 0;
    while (st) res += st & 1, st >>= 1;
    return res;
}
int main()
{
    //input
    cin >> n >> m;
    for (int i = 1, j = 0; i <= n; ++ i, j = 0)
        for (char c; j < m && cin >> c; ++ j)   //纯属为了压行,没有其他意思w
            g[i] += (c == 'H') << j;
    //找出所有合法状态
    for (int st = 0; st < 1 << m; ++ st)
        if (check(st))
            state.push_back(st), cnt[st] = count(st);
    //找出所有合法状态的合法转移
    for (int cur_st: state)
        for (int pre_st: state)
            if (!(cur_st & pre_st))
                head[cur_st].push_back(pre_st);
    //dp
    for (int i = 1; i <= n; ++ i)
        for (int st: state)
            if (!(g[i] & st))
                for (int p1: head[st])
                    for (int p2: head[p1])
                        if (!(st & p2))
                            f[i&1][st][p1] = max(f[i&1][st][p1], f[i-1&1][p1][p2] + cnt[st]);
    //Enumerate the final state
    int res = 0;
    for (int st: state)
        for (int pre: head[st])
            res = max(res, f[n&1][st][pre]);
    //output
    cout << res << endl;
    return 0;
}

目标状态优化

思路同前两题一样,这里不做额外阐述

    for (int i = 1; i <= n + 2; ++ i)
        for (int st: state)
            if (!(g[i] & st))
                for (int p1: head[st])
                    for (int p2: head[p1])
                        if (!(st & p2))
                            f[i&1][st][p1] = max(f[i&1][st][p1], f[i-1&1][p1][p2] + cnt[st]);
    //output
    cout << f[n+2&1][0][0] << endl;

60 评论


用户头像
Pokestar   2023-01-13 14:43      3    踩      回复

给各位大佬贴一个不用auto的代码

//彩笔大佬这里heap 存入对应的合法状态
//说明 head[][] :第一维是状态,第二维是读取的下标,它的值是第一维状态对应的合法状态
    for (int i = 1; i <= n; i++ ) 
        for (int j = 0; j < state.size(); j++ )
        {
            int a = state[j];
            if (!(g[i] & a))
                for (int k = 0; k < head[a].size(); k++) 
                {
                    int b = head[a][k];
                    if (!(g[i - 1] & b))
                        for (int u = 0; u < head[b].size(); u++ )
                        {
                            int c = head[b][u];
                            if ((a & c) == 0)
                            f[i & 1][a][b] = max(f[i - 1 & 1][b][c] + cnt[a], f[i & 1][a][b]);
                        }
                }
        }
用户头像
Pokestar   2023-01-13 15:00         踩      回复

这里是f[2][i][ i - 1]


用户头像
hacker2005   2025-03-27 23:04 · 山西      1    踩      回复

有点好奇为什么dp转移找p1和p2的时候为什么不用检测一下g[i-1]&p1 和 g[i-2]&p2呢?


用户头像
Touper   2025-03-18 17:11 · 重庆      1    踩      回复

贴个代码

#include<bits/stdc++.h>
using namespace std;

typedef long long ll; 
typedef pair<int, int> pii;
const int N = 1 << 11, M = 1e8;

ll n, m, g[110];
ll f[2][N][N], cnt[N];
vector<int> head[N];

bool check (int x) 
{
    for(int i = 0; i < m; i ++) {
        int a = (x >> i) & 1, b = (x >> (i + 1)) & 1;
        int c = (x >> (i + 2)) & 1;
        if(a + b + c >= 2) return false;
    }
    return true;
}
int get(int x) 
{
    int res = 0;
    for(int i = 0; i < m; i ++) 
       if(x >> i & 1)
          res ++;
    return res;
}
void Solved()
{
    cin >> n >> m;
    for(int i = 1; i <= n; i ++)
      for(int j = 0; j < m; j ++)
      {
         char c; cin >> c;
         if(c == 'H') g[i] |= (1 << j);
      }
    vector<int> arr;
    for(int i = 0; i < (1 << m); i ++)
       if(check(i)) 
       {
         arr.push_back(i);
         cnt[i] = get(i);
       }
    for(auto a : arr)
      for(auto b : arr)
         if(!(a & b))
             head[a].push_back(b);
    for(int i = 1; i <= n + 2; i ++)
      for(auto a : arr)
         for(auto b : head[a]) 
           for(auto c : head[b]) 
           {
               if((g[i] & a) || (g[i - 1] & b) || (g[i - 2] & c)) continue;
               if((a & b) || (b & a) || (c & a)) continue;
               f[i & 1][a][b] = max(f[i & 1][a][b], f[(i - 1) & 1][b][c] + cnt[a]);
           }
    cout << f[(n + 2) & 1][0][0] << "\n";
}
int main()
{
    ios::sync_with_stdio(false);
    cin.tie(0); cout.tie(0);
    int t; t = 1;
    while (t--) Solved();
    return 0;
}

用户头像
Xayanium   2024-04-05 11:18      1    踩      回复

自己的一些理解:
https://www.acwing.com/solution/content/239286/

用户头像
lrs_0   2024-04-24 16:26         踩      回复

请问为什么要枚举到n+2行啊,多枚举一行不是到n+1行就可以了吗?

用户头像
Xayanium   2024-04-28 00:18    回复了 lrs_0 的评论         踩      回复

计算到n+2行其实是相当于对第n行和第n-1行的情况进行汇总,只计算到n+1的话,第n行的情况不能完全递推过去,可以打表查看数据

用户头像
jkjk666   2024-07-12 09:21         踩      回复

佬,请问这为啥不对啊

for(int i=1;i<=n+2;i++){
        for(int j=0;j<state.size();j++){//第i
            for(int k=0;k<state.size();k++){//第i-1
                for(int u=0;u<state.size();u++){//第i-2
                    int a=state[j],b=state[k],c=state[u];
                    if((a&b)|(b&c)|(a&c))continue;
                    if(g[i]&a|g[i-1]&b|g[i-2]&c)continue;
                    f[i&1][j][k]=max(f[i&1][j][k],f[i-1&1][k][u]+cnt[a]);
                }
            }
        }
    }
    cout<<f[n+2&1][0][0];
用户头像
坐在高数上看拉格朗日   2024-11-28 16:58    回复了 lrs_0 的评论         踩      回复

还得看 n 行的状态
int res = 0;
for (int i = 0; i < 1 << m; ++i) res = max(res, dp[(n + 1) & 1][0][i]);
cout << res;


用户头像
baikun   2024-03-06 21:08      1    踩      回复

f[i&1][st][p1] = max(f[i&1][st][p1], f[i-1&1][p1][p2] + cnt[st]);
这句话 为什么i&1 i-1&1 ? &1是什么意思?

用户头像
baikun   2024-03-06 21:28         踩      回复

滚动数组 如果不开会爆空间复杂度


用户头像
donaldqian   2023-06-15 18:13      1    踩      回复

%%%


用户头像
Jesse_3   2022-04-20 19:13      1    踩      回复

求救!!为什么不能把两行合并起来当作一个状态啊

用户头像
kistor   2022-04-21 19:26      1    踩      回复

应该可以吧,问了下同学得到了肯定的答复


用户头像
白日梦_9   2025-03-05 17:29 · 内蒙古         踩      回复

orz


用户头像
Rsadj88   2024-08-28 10:51         踩      回复

count()函数用之前教的lowbit可以更快

int lowbit(int x){
    return x & (-x);
}
int count(int x){
    int ans = 0;
    while(x){
        x -= lowbit(x);
        ans ++;
    }
    return ans;
}
用户头像
团子团子   2025-03-01 11:13 · 河北         踩      回复

也可__builtin_popcount


用户头像
chenhaoze0423   2024-04-09 08:41         踩      回复

有个疑问: 既然前i行的某个状态,是从前i-1行的某个状态转移而来,而且程序在出现转移方程之前已经排除了所有冲突行的情况,为何不能直接压缩掉转移方程的第3维?


用户头像
HuRaymond   2024-02-04 19:31         踩      回复

巨巨,为什么p1不用于w[i - 1]判定是否合法

用户头像
hacker2005   2025-03-28 07:51 · 山西         踩      回复

同问


用户头像
世蒂   2023-04-17 19:04         踩      回复

彩铅佬的代码总是如此的优雅呜呜呜


用户头像
Fzldq   2022-10-21 09:34         踩      回复

Orz,小优化点:其实M开个70就够了,cols=10时只有60个合法状态


用户头像
Littlecat   2022-05-27 22:10         踩      回复

Orz


用户头像
种花家的小兔子   2022-03-31 15:36         踩      回复

巨巨,为什么这个滚动数组没有及时 清空为0呢?

用户头像
种花家的小兔子   2022-03-31 15:40         踩      回复

而且,题目要求求出最大值, 那么为什么没把除了起点以外的点全部初始化为-INF?

用户头像
一只野生彩色铅笔   2022-03-31 15:52    回复了 种花家的小兔子 的评论      1    踩      回复

第一个问题,状态值根据阶段 i 是非严格上升的,所有不初始化0和初始化0是等效的(必定更大)

所以也没有初始化负无穷的必要了,因为递增,所以必定会发生转移

用户头像
种花家的小兔子   2022-03-31 15:58    回复了 一只野生彩色铅笔 的评论         踩      回复

就不会出现遍历到了非法情况(因为没有初始化为-INF,所以为0),但是此时还又另外加了cnt[st],导致比当前值 大吗?

f[i&1][st][p1] = max(f[i&1][st][p1], f[i-1&1][p1][p2] + cnt[st]);
用户头像
一只野生彩色铅笔   2022-03-31 16:05    回复了 种花家的小兔子 的评论         踩      回复

if (!(g[i] & st)) 这一步判断了当前状态是否合法

上一层状态 f[i-1&1][p1][p2] 如果不合法,那值为0,这里就是 f[i&1][st][p1] = max(f[i&1][st][p1], cnt[st]);

相当于前面什么都没放,只在这一层摆放了 st

用户头像
种花家的小兔子   2022-03-31 16:06    回复了 一只野生彩色铅笔 的评论         踩      回复

巨巨,这种额外加一个for循环,直接清空当前层的做法可取吗?(感觉这样就不用考虑那么多了)

   for (int i = 1; i <= n; ++ i)
    {
        for (int st: state)
        {
             for(int p1: head[st])
             f[i & 1][st][p1] = 0;
             if (!(g[i] & st))
             {
                 for (int p1: head[st])
                 {
                    for (int p2: head[p1])
                    {
                        if (!(st & p2))
                            f[i&1][st][p1] = max(f[i&1][st][p1], f[i-1&1][p1][p2] + cnt[st]);
                    }         
                }       
             }      
        }
    }
用户头像
种花家的小兔子   2022-03-31 16:08    回复了 一只野生彩色铅笔 的评论         踩      回复

彩铅巨巨,是我见过回复最快的巨巨。 orz。。。明白了

用户头像
一只野生彩色铅笔   2022-03-31 16:08    回复了 种花家的小兔子 的评论         踩      回复

可以的,没问题

用户头像
种花家的小兔子   2022-03-31 16:10    回复了 一只野生彩色铅笔 的评论         踩      回复

%%% 大爱巨巨, 从提高背包DP 开始看巨巨题解,每一篇都那么详细。

用户头像
月无阴晴   2022-05-08 20:15    回复了 一只野生彩色铅笔 的评论      1    踩      回复

大佬,我还是没听懂为什么不用初始化,感觉要初始化的那些背包问题一样是根据阶段上升的,可他依旧在开始进行了初始化

用户头像
月无阴晴   2022-05-08 20:34    回复了 一只野生彩色铅笔 的评论         踩      回复

大佬还有为啥要把i-1行放在dp循环最外面,然后再是第i和第i-2行,按理来说不应该第i行在最外面吗,这样的话可以保证枚举到第i行时第i-1行已被算过


用户头像
acmdyh   2022-03-08 08:15         踩      回复

//找出所有合法状态的合法转移
这一步找不出合法的吧,转移要看两层,你判断了一层另一层只是DP时判断的

用户头像
一只野生彩色铅笔   2022-03-08 09:21         踩      回复

这一步是抛开图,直接找合法转移
DP时,由于划分了阶段,所以将图和状态放在一起看
同学可以再多自己思考思考


用户头像
包子云   2022-02-25 20:37         踩      回复

return !(st & st >> 1 || st & st >> 2);
能解释下为什么是|吗?

用户头像
包子云   2022-02-25 20:39         踩      回复

这是我的理解:
如果是&&那st & st >> 2 :有3个1就包含st & st >> 1 ,111能过,但11因为缺乏st & st >> 2过不了,漏判了11,显然11是不能通过的


用户头像
唐完了   2022-02-09 18:44         踩      回复

那个预处理合法转移,用y总的映射到索引为什么不行呢?

用户头像
一只野生彩色铅笔   2022-02-09 19:38      1    踩      回复

用 vector 可以,数组太大了


用户头像
Chiullian   2021-10-22 20:57         踩      回复

tql 又学会一招


用户头像
whlbest   2021-09-03 20:47         踩      回复

结果能输出f[(n + 2)&1][0][0]吗?

用户头像
一只野生彩色铅笔   2021-09-03 20:57         踩      回复

最后一个优化里,在结尾输出的就是 f[n+2&1][0][0] 呀 QAQ

用户头像
whlbest   2021-09-03 20:59    回复了 一只野生彩色铅笔 的评论         踩      回复

哦哦哦不好意思,谢谢大佬


App 内打开
你确定删除吗?
1024
x

© 2018-2025 AcWing 版权所有  |  京ICP备2021015969号-2
用户协议  |  隐私政策  |  常见问题  |  联系我们
AcWing
请输入登录信息
更多登录方式: 微信图标 qq图标 qq图标
请输入绑定的邮箱地址
请输入注册信息