一。方格取数问题
给定一个n×m的矩阵,行数和列数都不超过20,其中有些格子可以选,有些格子不能选,现在你需要从中选出尽可能多的格子,且保证选出的所有格子之间不相邻(没有公共边)。例如下面这个矩阵(2×3的矩阵,可选的位置标记为1,不可选的位置标记为0)
1 1 1
0 1 0
最多可选3个互不相邻的格子,方案如下(选中的位置标记为x):
x 1 x
0 x 0
那么问题来了,以下每种情况,最多能选多少格子呢?
(1) (2) (3)
1 1 1 1 1 1 1 1 0 1 1 0 1
1 1 1 1 1 0 0 1 1 1 1 1 1
1 1 1 1 1 0 0 1
1 1 1 1
答案分别是:(只是列出了其中一种情况)
(1) (2) (3)
x 1 x 1 x 1 x 1 0 x 1 0 1
1 x 1 x 1 0 0 x x 1 x 1 x
x 1 x 1 x 0 0 1
1 x 1 x
6个 6个 4个
二。方格问题解决方法(理论)
下面我们用状压DP来解决方格取数问题。
我们可以自上而下、一行行地选择格子。在一行内选择格子的时候,只和上一行的选择方案有关(从上推下),我们就可以将“当前放到第几行、当前行的选择方案”作为状态进行动态规划。
这里,我们就要用到状态压缩:一行里被选择的格子实际上是一个集合,我们要将这个集合压缩为一个整数。比如,对于一个3列的矩阵,如果当前行的状态是5(10进制)=101(二进制),那么就意味着选择了第1个和第3个格子;类似的,如果当前行的状态是6(10进制)=110(2进制),那么就意味着当前行选择了第1个和第2个格子(当然由于计算顺序的缘故,我们通常会把110(二进制)当做选择倒数第1个和倒数第2个格子,这并不影响我们理解这道题的解法)。
【判断上下行1个条件】如果上一行的状态是prev,这一行的状态是now,那么我们需要确保上下两行的选择方案里没有重复元素,也就是(now & prev) == 0就可以了。(也即两行的每一列没有相同元素)
【判断当前行2个条件】此外,我们还需要判断当前行的状态是否合法,因为读入的矩阵中,并不是每个格子都可以选择的,如果我们将矩阵中每行的值也用状态压缩来存储,不妨设为flag,那么当前行选择的格子集合,一定包含于当前行合法格子的集合。,也就是说(now | flag) == flag必须成立;同时行内也不能选择相邻的,也就是(now & (now >> 1)==0必须成立。
这样我们就可以通过枚举上一行的所有状态,来更新当前行,当前状态的最优解了。直到算到最后一行,统计一下所有状态的最大值即可。
三。实现二维状态压缩
①这一步我们先定义好需要用到的数组,定义一个int state[21]数组来记录每行能放的格子的状态(也即每行格子合法状态)。定义一个dp[21][1<<20]来记录对应的状态最大值。
int state[21];
int dp[21][1<<20];
②这一步我们先实现几个重复使用的函数
函数名称 | 函数用途 |
bool ok(int now) | 判断行内是否相交,可以直接return (now & (now >> 1) == 0. |
bool fit(int now, int i) | 判断now这个选取状态是否符合第i行的输入,可以直接return (now | state[i]) = state[i] |
bool not_intersect(int now, int prev) | 用来判断状态now和状态prev能否放在相邻的行,可以直接return (now & prev) == 0 |
int count(int now) | 用来统计状态now选取了多少个元素 |
bool ok(int now) {
return (now & (now >> 1)) == 0;
}
bool fit(int now, int i) {
return (now | state[i]) == state[i];
}
bool not_intersect(int now, int prev) {
return (now & prev) == 0;
}
bool count(int now) {
int s = 0;
while (now) {
s += (now & 1);
now >>= 1;
}
}
return s;
}
③这一步,我们先处理state数组,如果a[i][j] == 1,那么执行state[i] += (1<<j); 在main函数的return 0前面写下:
for (int i = 1; i <= n; i++) {
for (int j = 0; j <= m; j++) {
state[i] += (1<<j);
}
}
⑤这一步,我们开始实现转移部分。从第一行开始,用前一行的状态更新当前行的状态。我们先枚举一个合法的需要被更新的状态。在main函数return 0;前面写下:
for (int i = 1; i <= n; i++) {
for (int j = 0; j < (1<<m); j++) {
if (ok(j) && fit(j, i)) {
}
}
}
⑥然后我们枚举前一行的合法状态进行转移。在上一步的两层for循环的if内部继续写下。
for (int k = 0; k < (1<<m); k++) {
if (ok(k) && fit(k, i - 1) && not_intersect(j, k)) {
dp[i][j] = max(dp[i][j], dp[i - 1][k] + count(j));
}
}
⑦最后,我们枚举最后一行的状态,然后找到最值就是我们需要的答案。在main函数return 0;前面写下:
int ans = 0;
for (int i = 0; i < (1<<m); i++) {
ans = max(ans, dp[n][i]);
}
cout << ans << endl;
⑧测试
/*
Input:
2 3
1 1 1
0 1 0
Output:
3
*/
四。二维状态压缩实现(全部代码)
#include <iostream>
using namespace std;
int a[21][20];
int state[21];
int dp[21][1<<20];
bool ok(int now) {
return (now & (now >> 1)) == 0;
}
bool fit(int now ,int i) {
return (now | state[i]) == state[i];
}
bool not_intersect(int now, int prev) {
return (now & prev) == 0;
}
int count(int now) {
int s = 0;
while (now) {
s += (now & 1);
now >>= 1;
}
return s;
}
int main() {
int n, m;
cin >> n >> m;
// 行从1开始,列从0开始,这样后面处理起来方便
for (int i = 1; i <= n; i++) {
for (int j = 0; j < m; j++) {
cin >> a[i][j];
}
}
for (int i = 1; i <= n; i++) {
for (int j = 0; j < m; j++) {
if (a[i][j]) {
state[i] += (1<<j);
}
}
}
for (int i = 1; i <= n; i++) {
for (int j = 0; j < (1<<m); j++) {
if (ok(j) && fit(j, i)) {
for (int k = 0; k < (1<<m); k++) {
if (ok(k) && fit(k, i - 1) && not_intersect(i, i - 1)) {
dp[i][j] = max(dp[i][j], dp[i - 1][k] + count(j));
}
}
}
}
}
int ans = 0;
for (int i = 1; i <= n; i++) {
ans = max(ans, dp[n][i]);
}
cout << ans << endl;
return 0;
}
/*
Input:
2 3
1 1 1
0 1 0
Output:
3
/* 也即最后能取3个数。*/
*/
五。参考资料
一些内容参考了计蒜客NOIP的内容,如有侵权立即删除,并表示歉意。