方格取数问题+实现二维状态压缩

一。方格取数问题

给定一个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的内容,如有侵权立即删除,并表示歉意。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值