Codeforces 903F Clear the Matrix
题目大意
考虑一个 \(4\) 行 \(n\) (\(4\le n\le 1000\))列的矩阵 \(f\),\(f\) 中的元素为 *
或 .
。
对 \(f\) 进行若干次如下变换:
将一个 \(k\times k\)(\(1\le k \le 4\))的子矩阵中的元素全部替换为 .
,代价为 \(a_k\)( \(1 \le a_k \le 1000\))。
求将 \(f\) 中的元素全置为 *
所需的最小代价。
分析
很容易想到的做法是「状压 DP」。
Codeforces 上把「状态压缩」这个方法称作「bitmasks」。我从 Codeforces 的题解上读到,这种「铺地砖」类型的状压 DP, 英文可以叫做「DP on broken profile」。
将矩阵 \(f\) 的列编号为 0 到 \(n-1\) 。
DP 状态为
\(c_{i,s}\):「第 0 列到第 \(i-1\) 列全部置为
.
,第 \(i\) 到第 \(\min(i+2,n-1)\) 列的状态为 \(s\) ,而不管其余的列的状态如何」所需的最小代价。
如此设计 DP 状态考虑的是从左到右选取子矩阵做替换的过程。
这个 DP 状态是比较容易想到的,重点讨论如何转移:
一般而言,DP 状态的转移要满足原子性。
所谓原子性是指,从状态 \(S\) 转移到状态 \(T\) 所经过的操作(或称「变换」)必须是一个不可再分的基本操作。在有的问题中(比如这道题)「不做任何操作」也是一种基本操作。
当然,从状态 \(S\) 经过多个基本操作也可能到达状态 \(T\);但是在构造状态转移方程时,我们只考虑单步转移。
这个问题中的基本操作就是选取某个子矩阵进行替换或者不做替换。
此外, DP 状态还要满足转移的完备性。
所谓转移的完备性是指,从某一状态进行一次满足最优性的基本操作所到达的新状态必须能够被表示。
这里需要注意的是,从某个状态进行一个基本操作之后所能转移到的新状态可能不唯一(但一般是常数个)。
最优性条件往往用来减小在某个状态时需要考虑的基本操作的种类。
由「最优性」条件可知:
- 每次选取的子矩阵中至少要有一个
*
。 - 从状态 \((i,s)\) 转移时,所选的子矩阵的最左一列必须是第 \(i\) 列。
经过上述分析,不难得出转移方法
枚举在状态 \((i,s)\) 时可进行的操作,计算转移到的新状态 \((i', s')\) ,更新「新状态」的 DP 值(即最小代价)。
这个问题中,「转移到的新状态」可能是两个。
容易疏忽的是:
对于状态 \((i,s)\),若第 \(i\) 行全为 .
,那么不经任何替换即可转移到状态 \((i+1, s')\);其中 \(s'\) 表示 \(i+1\) 列到 \(\min(i+3,n-1)\) 列当前的状态。
实现细节
对于状压 DP,在进行状态转移时,用 bitset
取代位运算,实现起来更方便。
通过 bitset
提供的成员函数 to_ulong()
或者 to_ullong()
可以很方便地把状态压缩到一个整数里,作为 DP 数组的下标。
bitset
还可以用 >>
算符以 01 串的格式输出,低位在右,高位在左。
注意:在索引时,第 0 位是最低位。
另外,bitset
作为 sequential container 提供了 []
运算符。
[]
有两个版本(重载),其返回值可以是 lvalue,也可以是 rvalue.
constexpr bool operator[]( std::size_t pos ) const;
和
reference operator[]( std::size_t pos );
第二个函数声明中的返回值的类型 reference
是 bitset
定义的一个 proxy class 。
reference
类型支持的操作符只有:
operator=
operator bool
operator ~
flip