最近在补全提高课所有题目的题解,宣传一下汇总的地方提高课题解补全计划
对比y总原来的代码部分,在无效的转移状态枚举上进一步进行了优化,能更快一点
题目描述
给定一个 $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;
给各位大佬贴一个不用auto的代码
这里是f[2][i][ i - 1]
自己的一些理解:
https://www.acwing.com/solution/content/239286/
请问为什么要枚举到n+2行啊,多枚举一行不是到n+1行就可以了吗?
计算到n+2行其实是相当于对第n行和第n-1行的情况进行汇总,只计算到n+1的话,第n行的情况不能完全递推过去,可以打表查看数据
佬,请问这为啥不对啊
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是什么意思?
滚动数组 如果不开会爆空间复杂度
%%%
求救!!为什么不能把两行合并起来当作一个状态啊
应该可以吧,问了下同学得到了肯定的答复
count()函数用之前教的lowbit可以更快
有个疑问: 既然前i行的某个状态,是从前i-1行的某个状态转移而来,而且程序在出现转移方程之前已经排除了所有冲突行的情况,为何不能直接压缩掉转移方程的第3维?
巨巨,为什么p1不用于w[i - 1]判定是否合法
彩铅佬的代码总是如此的优雅呜呜呜
Orz,小优化点:其实M开个70就够了,cols=10时只有60个合法状态
Orz
巨巨,为什么这个滚动数组没有及时 清空为0呢?
而且,题目要求求出最大值, 那么为什么没把除了起点以外的点全部初始化为-INF?
第一个问题,状态值根据阶段
i
是非严格上升的,所有不初始化0和初始化0是等效的(必定更大)所以也没有初始化负无穷的必要了,因为递增,所以必定会发生转移
就不会出现遍历到了非法情况(因为没有初始化为-INF,所以为0),但是此时还又另外加了cnt[st],导致比当前值 大吗?
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
巨巨,这种额外加一个for循环,直接清空当前层的做法可取吗?(感觉这样就不用考虑那么多了)
彩铅巨巨,是我见过回复最快的巨巨。 orz。。。明白了
可以的,没问题
%%% 大爱巨巨, 从提高背包DP 开始看巨巨题解,每一篇都那么详细。
大佬,我还是没听懂为什么不用初始化,感觉要初始化的那些背包问题一样是根据阶段上升的,可他依旧在开始进行了初始化
大佬还有为啥要把i-1行放在dp循环最外面,然后再是第i和第i-2行,按理来说不应该第i行在最外面吗,这样的话可以保证枚举到第i行时第i-1行已被算过
//找出所有合法状态的合法转移
这一步找不出合法的吧,转移要看两层,你判断了一层另一层只是DP时判断的
这一步是抛开图,直接找合法转移
DP时,由于划分了阶段,所以将图和状态放在一起看
同学可以再多自己思考思考
return !(st & st >> 1 || st & st >> 2);
能解释下为什么是|吗?
这是我的理解:
如果是&&那st & st >> 2 :有3个1就包含st & st >> 1 ,111能过,但11因为缺乏st & st >> 2过不了,漏判了11,显然11是不能通过的
那个预处理合法转移,用y总的映射到索引为什么不行呢?
用
vector
可以,数组太大了tql 又学会一招
结果能输出f[(n + 2)&1][0][0]吗?
最后一个优化里,在结尾输出的就是
f[n+2&1][0][0]
呀 QAQ哦哦哦不好意思,谢谢大佬
为什么不用考虑p1, p2与g[i - 1], g[i - 2]的关系
如果 $p_1$ 和 $p_2$ 是不合法的,那么在状态转移的时候他们的值必定是 $0$
可以结合状态的定义来想一下
谢谢大佬,orz
$$f_{i,j,k} = \max\{fi−1,k,pre+cnt_j\}$$
里面,cnt是什么
$head_{i,j}$ 代表什么
$g$ 数组存的什么,
g[i] += (c == 'H') << j;
代表什么意思?(抱歉状压学得好差$cnt_j$ 是状态 $j$ 中所摆放的棋子数量
谢谢
$head_i$ 中存储的是所有与 $i$ 相邻合法转移的状态
换而言之就是,不考虑其他因素,把他们放在相邻行,可以合法放置的状态
$g_i$ 存储的是第 $i$ 行中不能放置棋子的位置
例如 $g_i = (10010)_{0b}$,表示第 $i$ 行中的第 $2$ 个位置和第 $5$ 个位置不能放置
因为这两个位置是 $1$
大佬,找出所有合法状态的时候,为什么枚举列的所有状态,而不是行的所有状态
我没太清楚你想表达的意思,可不可以说的完整一点 😭
就是为什么是st < 1 << m, 而不是 st < 1 << n嘞
n是小于等于100的,1 << n太大了
因为是行的状态而不是列的状态
因为一行有 m 列,完整的表达应该是 (枚举某一行)列的所有状态