状态压缩动态规划进阶——方格取数问题

本文介绍了一种使用状态压缩动态规划解决方格取数问题的方法,通过位运算判断相邻格子是否选中,确保选择的格子不相邻。详细解释了解题思路,包括代码实现过程和合法情况的判断,最终通过动态规划找到最大选择数量。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

题目

给定一个 n × m n \times m n×m 的矩阵,行数和列数都不超过 20,其中有些格子可以选,有些格子不能选。现在你需要从中选出尽可能多的格子,且保证选出的所有格子之间不相邻(没有公共边)。

例如下面这个矩阵( 2 × 3 2 \times 3 2×3的矩阵)

1 1 1
0 1 0

最多可选 33 个互不相邻的格子,方案如下(选中的位置标记为x):

x 1 x
0 x 0

解法详解

我们可以自上而下一行一行选择格子。在选择格子的过程中,只和上一行选择的方案有关,所以我们可以将“当前放到第几行,当前行的选择方案”作为状态进行状态压缩动态规划。
一行里被选择的格子可以看作一个集合,我们要将这个集合压缩为一个二进制数,如果选了就即将当前位设为1,否则没选为0.比如,对于一个3列的矩阵,如果当前行的状态是 ( 5 ) 10 = ( 101 ) 2 (5)_{10}=(101)_{2} (5)10=(101)2,代表当前行选择了第一个和第三个各自。类似地,如果当前行的状态时 ( 6 ) 10 = ( 110 ) 2 (6)_{10}=(110)_{2} (6)10=(1102,代表,当前行选择了第一个和第二个格子那么意味着(当然由于计算顺序的缘故,我们通常会将 ( 110 ) 2 (110)_{2} (110)2看作选择了倒数第一个和倒数第二个格子,但这并不影响我们理解此解法)
如果上一行的状态为now,下一行的状态为nxt,那么我们只需要确保上下两行的选择方案中没有重复的元素,也就是KaTeX parse error: Expected 'EOF', got '&' at position 6: (now &̲ nxt)==0就可以了
此外,我们还需要判断当前行的状态是否合法,因为读入的矩阵并不是每一个都可以选职责的,如果我们将矩阵中的每行的值也用状态压缩来存储,记其为flag,那么当前行选择的各自的集合一定包含于当前行合法格子的集合,也就是说(now|flag)==flag必须成立;同时行内选择的各自也不能相邻,也就是 n o w 与 ( n o w > > 1 ) = = 0 now与(now>>1)==0 now(now>>1)==0必须成立。
综上所述,设上一行为now,下一行时nxt,当前行的合法情况为flag,条件就应该是:

  1. (now & nxt)==0
  2. (now|flag)==flag
  3. now&(now>>1)==0

这样,我们就可以枚举上一行的所有状态,用于更新当前行,当前状态的最优解了。直到算完最后一行,统计一下所有状态的最大值就可以了。

代码的实现过程

要求1的实现:

bool not_intersect(int now, int nxt) {  // 判断状态 now 和状态 nxt 是否能放在相邻的行
    return (now & nxt) == 0;
}

要求2的实现:

bool fit(int now, int i) {  // 用来判断 now 这个选取状态是否和符合第 i 行的输入
    return (now | state[i]) == state[i];
}

要求3的实现:

bool ok(int now) {  // 判断行内是否相交
    return (now & (now >> 1)) == 0;
}

统计当前状态有选择了多少个

那么就要应该是将当前状态的子集不断地向右移,并&1,也就是判断当前这位是否为1,再让计数器+=当前右移后的子集&1。

int count(int now) {  // 统计 now 状态选了多少个元素
    int s = 0;
    while (now) {
        s += (now & 1);
        now >>= 1;
    }
    return s;
}

合法情况读入

行从 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);
            }
        }
 }

dp过程

先用i枚举当前在枚举第几行

for(int i=1;i<=n;i++){

j枚举当前的这一行的状态

for(int j=0;j<(1<<m);j++){

用ok(j)查看当前这行是否相邻两个没有同时选择
用fit(i,j)查看第i行j状态是否合法

if(ok(j) && fit(j,i)){

k枚举上一行的状态

for(int k=0;k<(1<<m);k++){

用ok(k)查看当前上一行是否相邻两个没有同时选择
用fit(i-1,k)查看第(i-1)行k状态是否合法
用not_intersect(j,k)判断这行的状态j和上一行的状态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));

输出答案

分别看dp数组中第n行的所有情况是否成立,如成立就与上一个成立的情况比较,取较大的为方案最多的个数,最后输出ans

int ans=0;
    for(int i=0;i<(1<<m);i++){
        ans=max(ans,dp[n][i]);
    }
    cout<<ans<<endl;

示例代码

#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) {  // 用来判断 now 这个选取状态是否和符合第 i 行的输入
    return (now | state[i]) == state[i];
}
bool not_intersect(int now, int nxt) {  // 判断状态 now 和状态 nxt 是否能放在相邻的行
    return (now & nxt) == 0;
}
int count(int now) {  // 统计 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(j,k)){
                        dp[i][j]=max(dp[i][j],dp[i-1][k]+count(j));
                    }
                }
            }
        }
    }
    int ans=0;
    for(int i=0;i<(1<<m);i++){
        ans=max(ans,dp[n][i]);
    }
    cout<<ans<<endl;
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值