蓝桥杯 覆盖 状态压缩dp新手历程

引言

  • 首先本题与ACwing的另一题《蒙德里安的梦想》一模一样,只是那题是多样例点
    然后本题是本人入坑状态压缩dp的第一题,有纪念意义,故作此记录

题目

在这里插入图片描述

思路

状态压缩dp新手引入
时间复杂度
  • 状态压缩dp实际上也是一种程度上的暴力做法,通过枚举所有情况,得到答案。但不同于普通dfs,dfs的复杂度一般是N!,而状态dp一般是N*2^N 或者N*2^N *2^N。 当N=20时,前者接近10^18 , 而后者仅有10^12
个人入坑最开始难以理解的点
  • 然后是状态压缩dp本身了,这里最重要的是不要带入传统dp的观点认为他是由一个或几个状态得到,他是由上一次的所有结果得到,一般来说是2^N 个结果得到。 也就是状态压缩
    d p [ i ] [ s t a t e ] = ∑ k = 0 k = 2 N − 1 d p [ i − 1 ] [ k ] {\mathop{\rm dp}\nolimits} [i][state] = \sum\nolimits_{k = 0}^{k = {2^N} - 1} {dp[i - 1][k]} dp[i][state]=k=0k=2N1dp[i1][k]
    这里state是当前的状态,是一个二进制数(存储时是十进制存储),k是上一时刻的状态,即当前取i时全局状态为state(例如dp[2][1011]在求最短路时表示,当前点在2点同时已经走过0,1,3三个点),由前一个i-1取所有状态到当前状态得到。
题目本身

// 首先横着放好所有方块后,那么竖着的方案是唯一的,所以只用考虑横着的即可
// 设dp[i][j]为前i-1列排列好,从第i-1列伸到第i列的状态为j的总方案数,其中j为二进制数,长度与行数相同
// 则dp[i][j]=求和 k从00000到11111的所有状态dp[i-1][k],即当前状态为j由上列的所有合法情况到j的总和

合法判断

// 同时求和需要满足合法条件,即k&j==0,状态k和状态j之间不能有位相同,不然即从i-2开始一直伸到i,超出了方块的长度2
// 且要满足st[k|j]=true,st[k|j]是i-2列伸出到i-1列的k与主动从i-1列伸到i列的j的并集
// 即是i-1列被两种情况包围后的实际被横着的格子,需要判断是否合法

// 不对st[j]和st[k]进行合法判断,是因为他们都只是半吊子,还有无限可能性
// 但前后夹击后唯一确定了中间那列的排列方法,需要判断夹击后的列是否合法

初始化

// 初始化dp[1][x]=1,即从0列横着放到伸出到第一列的状态x,就是一种情况
// 总共n行m列,序号从0开始
// 最终dp[m][0]即为所求,表示的是前m-1列全部填好,且第m列为全0,即m-1列没有伸出的。
// 即最终答案m比下标m-1大,因为m才代表前m-1列都排列好了

总结

// 实际做法:1.先可以进行一列内状态的合法性判定,即一个状态中连续为空的个数是否为奇数,如果为奇数,则无法竖着填满,不合法
// 2.初始化dp[1][x]=1
// 3.计算dp数组得到答案

代码

//
// Created by starplatinum on 2024/5/16.
//
#include<iostream>

using namespace std;


// 最大的行数和列数
const int N=12;
// 状态数是2的行数次方
const int state=1<<N;


int main(){
    int dp[N][state]={0};
// 存储列内状态是否合法
    bool st[state];

    int n=8;
    int m=8;
    // 判断状态是否合法
    for(int i=0;i<(1<<n);i++){
        st[i]= true;
        int sum=0;
        // 2的n次方从右往左依次是0到第n-1位,判断连续的0是否为奇数个
        for(int j=0;j<n;j++){
            // 将i的二进制数右移j位,此时个位的数即是i原来的第j位
            // 判断第j位是否为1,为1则可以判断之前记录的空的数量
            if((i>>j)&1){
                if(sum%2){
                    st[i]= false;
                    break;
                }
            } else{
                sum++;
            }
        }
        // 都结束还没遇到1,说明还没有结算空格的个数
        if(sum%2){
            st[i]= false;
        }
    }


    // 初始化dp[1][x]=1,合法的才为1
    for(int i=0;i<(1<<n);i++){
        if(st[i]){
            dp[1][i]=1;
        }
    }



    // 计算dp数组
    for(int i=2;i<=m;i++){
        for(int j=0;j<(1<<n);j++){
            for(int k=0;k<(1<<n);k++){
                // st[k|j]是i-2列伸出到i-1列的k与主动从i-1列伸到i列的j的并集
                // 即是i-1列被两种情况包围后的实际被横着的格子
                // 不需要判断k和j,因为他们都只是半吊子,有无限可能
                // 但当他们结合起来,就能唯一确定中间列的情况,需要对这个情况进行合法性判定
                if(((k&j)==0)&&st[k|j]){
                    dp[i][j]+=dp[i-1][k];
                }
            }
        }

    }

    cout<<dp[m][0]<<endl;
    return 0;
}
后记

我在做时参考了这篇博客这篇博客,写的很好
然后还有很多类似的题,例如我就是从蓝桥杯的另一题《补给》跳到这里。也许后续会更新补给的思路。

  • 13
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
好的,我们来看一个具体的例子。假设有一个长度为 n 的数组 A,其中每个元素都是 0 或 1,现在需要求出所有长度为 k 的子串中,元素为 1 的个数的最小值。 传统的动态规划方法需要使用二维数组来记录状态,时间复杂度为 O(nk),空间复杂度为 O(nk)。而使用状态压缩dp,我们可以将状态压缩为一个长度为 n 的二进制数 i,其中第 j 位为 1 表示 A[j] 在当前子串中出现了一次或多次,为 0 则表示没有出现。因此,我们只需要使用一个一维数组 f 来记录当前状态的最小值即可。 具体实现如下: ```python def min_ones_in_k_substrings(A, k): n = len(A) f = [float('inf')] * (1 << n) f[0] = 0 for i in range(n): for j in range(1 << i): if bin(j).count('1') == k: ones = bin(j & ((1 << i) - 1)).count('1') + A[i] f[j] = min(f[j], f[j & ~(1 << i)] + ones) return f[(1 << n) - 1] ``` 其中,f[i] 表示状态为 i 时的最小值,初始化为正无穷。在状态转移时,我们枚举当前状态的所有子集 j,如果 j 中的元素个数等于 k,则计算 j 中包含的所有元素为 1 的个数 ones,然后更新 f[j] 的值为 f[j] 和 f[j - {i}] + ones 中的较小值。其中,j - {i} 表示将 j 中的第 i 位(即 A[i] 对应的位置)置为 0。 最终,我们返回状态为全集时的最小值 f[(1 << n) - 1] 即可。由于状态总数为 2^n,因此时间复杂度为 O(n^22^n),空间复杂度为 O(2^n)。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值