状压DP基础练习

状压DP练习

  • HDU 1565 方格取数(1)

    题意:给定 n × n n\times n n×n的棋盘,每个格子的数非负,从中选若干个方格取出数字且保证选定方格间没有公共边,问取出的数的和最大为多少。输入规模 n ≤ 20 n\le 20 n20

    思路:可以考虑到用0表示不取,1表示取,则每行有1<<n个状态,如果直接枚举1~n比较浪费时间,可以先预处理出哪些状态是符合要求的。首先我们关注相邻行,假设两个单行状态 s 1 , s 2 s1,s2 s1,s2已经满足要求,显然状态对应的二进制不能有上下相邻的1,即1不能和1对上。我们考虑两个二进制数进行&的操作,1 & 1才能得到1,其他情况都是0,那么 s 1 & s 2 = = 0 s1 \& s2==0 s1&s2==0。再考虑如何让单行状态满足要求,对于一个数 x x x,考虑将它们错开一位
    在这里插入图片描述
    将二进制为从左往右分别记为x[0]~x[k],那么x[0]和x[1]不同时为1,转化为上下不同时为1,x[1]和x[2]不同时为1,同样转化为上下不同时为1,所以只要 x & ( x > > 1 ) = = 0 x \& (x >> 1) == 0 x&(x>>1)==0 x x x就是一个合法状态

    细节见代码

    #include <cstdio>
    #include <iostream>
    #include <cstring>
    const int N = 21;
    
    int n, a[N][N], dp[2][1 << N], state[1 << N];
    
    inline int calc(int row, int state) {
        int ans = 0, p = n - 1;
        while(state) {
            if(state & 1)
                ans += a[row][p];
            state >>= 1;
            p--;
        }
        return ans;
    }
    
    int main() {
        while(~scanf("%d", &n)) {
            for(int i = 0; i < n; i++)
                for(int j = 0; j < n; j++)
                    scanf("%d", &a[i][j]);
            memset(dp, 0, sizeof(dp));
            int cnt = 0;
            for(int i = 0; i < (1 << n); i++) if((i & (i >> 1)) == 0) state[++cnt] = i;
            for(int i = 1; i <= cnt; i++)
                dp[0][i] = calc(0, state[i]);
            for(int i = 1; i < n; i++) {
                int t = i & 1;
                for(int j = 1; j <= cnt; j++) {
                    for(int k = 1; k <= cnt; k++)
                        if((state[j] & state[k]) == 0)
                            dp[t][k] = std::max(dp[t ^ 1][j] + calc(i, state[k]), dp[t][k]);
                }
            }
            int ans = 0;
            for(int i = 0; i < 2; i++)
                for(int j = 1; j <= cnt; j++)
                    ans = std::max(ans, dp[i][j]);
            printf("%d\n", ans);
        }
    
        return 0;
    }
    
  • POJ 3254 Corn Fields

    题意:给定一个 n × m n\times m n×m的01矩阵,0代表不可以种玉米,1代表可以种玉米,John想要知道使得每两株玉米都不相邻(上下左右均不相邻)的种植方法总共有多少种。值得注意的是,他可以选择一株也不种,这也算一种种法

    思路:去掉01矩阵的条件和上一题很像,我们可以将玉米地的每行进行状态压缩,如将’111’压缩成整数7,将可种植状态记为map[]。和上题类似,我们用state[]数组来代表单行没有相邻1的状态,和上题不同的是,我们不仅需要考虑state[j] & state[k]是否为0(即上下是否有相邻1),还要考虑状态是否和玉米地的可种植状态匹配,因为玉米只能种在1的位置,也就是说state的1必须对应map的1,而state的0可以对应map的0或1,也就是说state[x] & map[i] == state[x],因为state的所有1都对应map的1

    细节见代码

    #include <cstdio>
    const int N = 13;
    const int MOD = 1e8;
    
    int n, m, dp[N][1 << N], map[N], state[1 << N], cnt;
    
    int main() {
        scanf("%d%d", &n, &m);
        for(int i = 0; i < n; i++)
            for(int j = 0; j < m; j++) {
                int tmp;
                scanf("%d", &tmp);
                if(tmp) map[i] += (1 << (m - j - 1));
            }
        for(int i = 0; i < (1 << m); i++) if((i & (i >> 1)) == 0) state[++cnt] = i;
        for(int i = 1; i <= cnt; i++)
            dp[0][i] = (map[0] & state[i]) == state[i];
        for(int i = 1; i < n; i++)
            for(int j = 1; j <= cnt; j++) {
                if((map[i - 1] & state[j]) != state[j]) continue;
                for(int k = 1; k <= cnt; k++)
                    if(((map[i] & state[k]) == state[k]) && (state[j] & state[k]) == 0)
                        dp[i][k] = (dp[i][k] + dp[i - 1][j]) % MOD; // 可以滚动数组优化一下,但是懒
            }
        int ans = 0;
        for(int i = 1; i <= cnt; i++)
            ans = (ans + dp[n - 1][i]) % MOD;
        printf("%d\n", ans % MOD);
        return 0;
    }
    
  • LG 1896 互不侵犯(SCOI 2005)

    题意: N × N N\times N N×N的棋盘中放置K个国王,国王可以攻击其周围8个格子,求有多少种放置方法使得K个国王互不攻击

    思路:首先预处理处单行不冲突状态 s t a t e [ ] state[] state[],用 d p [ i ] [ k ] [ l ] dp[i][k][l] dp[i][k][l]表示第 i i i行状态为 s t a t e [ k ] state[k] state[k]时,前 i i i行中已经放置了 l l l个国王的方法数,得
    d p [ i ] [ k ] [ l ] = ∑ s t a t e [ j ] ∈ S d p [ i − 1 ] [ j ] [ l − n u m ] dp[i][k][l]=\sum_{state[j]\in S}dp[i-1][j][l-num] dp[i][k][l]=state[j]Sdp[i1][j][lnum]
    其中 S S S是与 s t a t e [ k ] state[k] state[k]相容的状态的集合, n u m num num s t a t e [ k ] state[k] state[k]的二进制表示中 1 1 1的个数

    #include <cstdio>
    const int N = 10;
    
    int n, k1, cnt;
    int state[1 << N];
    long long dp[N][1 << N][N * N]; // 防止爆int
    
    inline int calc(int state) {
        int ans = 0;
        while(state) {
            if(state & 1) ans++;
            state >>= 1;
        }
        return ans;
    }
    
    inline bool check(int j, int k) {
        return ((state[j] & state[k]) == 0) && (((state[j] << 1) & state[k]) == 0) && (((state[j] >> 1) & state[k]) == 0);
    }
    
    int main() {
        scanf("%d%d", &n, &k1);
        for(int i = 0; i < (1 << n); i++) 
            if(((i & (i >> 1)) == 0) && ((i & (i << 1)) == 0)) state[++cnt] = i;
        dp[1][0][0] = 1;
        for(int i = 1; i <= cnt; i++)
            dp[1][i][calc(state[i])] = 1;
        for(int i = 2; i <= n; i++)
            for(int k = 1; k <= cnt; k++) {
                int num = calc(state[k]);
                for(int j = 1; j <= cnt; j++)
                    if(check(j, k))
                        for(int l = num; l <= k1; l++)
                            dp[i][k][l] += dp[i - 1][j][l - num]; // 还是可以滚动数组优化,懒得写
            }
        long long ans = 0;
        for(int i = 1; i <= cnt; i++)
            ans += dp[n][i][k1];
        printf("%lld\n", ans);
        return 0;
    }
    
  • POJ 1185 炮兵阵地

    题意:题目是中文的直接看题吧懒得解释题意

    思路:这道题和POJ 3254玉米地有点像,都利用地图的01限制能否放置到该位置。因为炮兵可以打到下面两行的位置,所以要分别枚举

    细节见代码写得巨丑

    #include <cstdio>
    #include <cstring>
    #include <iostream>
    const int N = 101, M = 7; // 可行状态其实很少,所以1 << 7 = 128不会爆,貌似开70就够
    
    int map[N], state[1 << M], dp[2][1 << M][1 << M];
    int n, m, cnt;
    
    inline int calc(int state) {
        int ans = 0;
        for(; state; state >>= 1)
            if(state & 1) ans++;
        return ans;
    }
    
    inline bool check(int row, int state) {
        return (state & map[row]) == state;
    }
    
    int main() {
        while(~scanf("%d%d", &n, &m)) {
            for(int i = 1; i <= n; i++) {
                char ch[10];
                map[i] = 0;
                scanf("%s", ch);
                for(int j = 0; j < m; j++) {
                    map[i] <<= 1;
                    if(ch[j] == 'P') map[i]++;
                }
            }
            cnt = 0;
            memset(dp, 0, sizeof(dp));
            for(int i = 0; i < (1 << m); i++) if((i & (i >> 1)) == 0 && (i & (i >> 2)) == 0) state[++cnt] = i;
            for(int i = 1; i <= cnt; i++)
                if(check(1, state[i]))
                dp[1][i][1] = calc(state[i]);
            for(int i = 2; i <= n; i++) {
                int t = i & 1;
                for(int j = 1; j <= (i == 2 ? 1 : cnt); j++) {
                    if(!check(i - 2, state[j])) continue;
                    for(int k = 1; k <= cnt; k++) {
                        if((state[j] & state[k]) || !check(i - 1, state[k])) continue;
                        for(int l = 1; l <= cnt; l++)
                            if((state[k] & state[l]) == 0 && (state[j] & state[l]) == 0 && check(i, state[l]))
                                dp[t][l][k] = std::max(dp[t][l][k], dp[t ^ 1][k][j] + calc(state[l])); // 滚动数组优化,主要是因为如果数组开太大上面的memset会有点慢
                    }
                }
            }
            int ans = 0;
            for(int i = 1; i <= cnt; i++)
                for(int j = 1; j <= cnt; j++)
                    ans = std::max(ans, dp[n & 1][i][j]);
            printf("%d\n", ans);
        }
        return 0;
    }
    
  • POJ 2411 Mondriaan’s Dream
    题意:给一个 n × m n\times m n×m的棋盘 1 ≤ n , m ≤ 11 1\le n,m\le 11 1n,m11,问用 1 × 2 1\times 2 1×2的矩形铺满棋盘有多少中方法
    思路:对于横着的矩形,我们将其标记为[1 1],对于竖着的矩形,我们将上面记为0,下面记为1,可以发现最后合法的情况必定是最后一行全为1。现在考虑如何得到合法的转移,我们只需要考虑相邻的两行,假设我们已经将前 c o l col col列放好了合法的矩形,那么目前合法的放置方法只有三种:

    • 在上面的行放一个竖着的
    • 上面的行放的是一个竖着的块的1,下面的行放的是另一个竖着的块的0
    • 上下两行都放横着的
      我们可以用dfs先处理出合法的上下行转移表,然后dp
    #include <cstdio>
    #include <cstring>
    #include <iostream>
    const int N = 12;
    typedef long long ll;
    
    int n, m, cnt, state[1 << 16][2];
    ll dp[N][1 << N];
    
    void dfs(int col, int pre, int suc) {
        if(col > m) return ;
        if(col == m) {
            state[cnt][0] = pre;
            state[cnt++][1] = suc;
            return ; 
        }
        dfs(col + 1, pre << 1, suc << 1 | 1);
        dfs(col + 1, pre << 1 | 1, suc << 1);
        dfs(col + 2, pre << 2 | 3, suc << 2 | 3);
    }
    
    int main() {
        while(~scanf("%d%d", &n, &m) && n) {
            cnt = 0;
            if(n < m) std::swap(n, m);
            memset(dp, 0, sizeof(dp));
            dfs(0, 0, 0);
            dp[0][(1 << m) - 1] = 1;
            for(int i = 1; i <= n; i++) {
                for(int j = 0; j < cnt; j++)
                    dp[i][state[j][1]] += dp[i - 1][state[j][0]];
            }
            printf("%lld\n", dp[n][(1 << m) - 1]);
        }
        return 0;
    }
    
  • Gym 101933E

    题意:给定己方 n n n个小兵,敌方 m m m个小兵,每个小兵都有 1 ∼ 6 1 \sim 6 16的的整数生命值,此时使用魔法对场上随机目标造成1点伤害,共造成 d d d次,每次伤害会使一个小兵的生命值减1,小兵生命值为0时会离场,问该魔法使得敌方所有小兵都离场的概率,己方小兵是否离场都没关系,数据范围: 1 ≤ n , m ≤ 5 , 1 ≤ d ≤ 100 1\le n,m\le5, 1\le d\le100 1n,m5,1d100

    思路:因为小兵的生命值只有 [ 1 , 6 ] [1,6] [1,6],我们可以用一个12位的十进制整数来表示场上的情况,每个位置表示血量为 i i i的小兵的人数(敌我双方分开表示),因为我们只需要杀掉敌方小兵,我方的情况不用管,所以我们可以将敌方的情况放在12位整数的前6位,这样如果表示目前状态的整数 s t a < 1 e 6 sta<1e6 sta<1e6,就说明前6为全为0,即敌方小兵全部离场了,这样可以剪枝,同时我们采用记忆化搜索来记录某个状态的值,具体细节见代码

    #include <cstdio>
    #include <unordered_map>
    typedef long long ll;
    
    int n, m, d;
    int life[2][9];
    std::unordered_map<ll, double> dp;
    
    inline ll getSta() {
        ll sta = 0;
        for(int i = 1; i <= 6; i++) sta = sta * 10 + life[1][i];
        for(int i = 1; i <= 6; i++) sta = sta * 10 + life[0][i];
        return sta;
    }
    
    inline int count() {
        int sum = 0;
        for(int i = 1; i <= 6; i++) sum += life[1][i];
        for(int i = 1; i <= 6; i++) sum += life[0][i];
        return sum;
    }
    
    double dfs(ll sta, int d) {
        if(dp.count(sta)) return dp[sta];
        if(sta < 1000000) return 1.0;
        if(!d) return 0.0;
        int cnt = count();
        double ans = 0.0;
        for(int i = 0; i < 2; i++) {
            for(int j = 1; j <= 6; j++) {
                if(!life[i][j]) continue;
                life[i][j]--, life[i][j - 1]++;
                ll newsta = getSta();
                double tmp = dfs(newsta, d - 1);
                dp[newsta] = tmp;
                life[i][j]++, life[i][j - 1]--;
                ans += tmp * life[i][j] / cnt;
            }
        }
        return ans;
    }
    
    int main() {
        scanf("%d%d%d", &n, &m, &d);
        int num;
        for(int i = 1; i <= n; i++) scanf("%d", &num), life[0][num]++;
        for(int i = 1; i <= m; i++) scanf("%d", &num), life[1][num]++;
        printf("%.8f\n", dfs(getSta(), d));
        return 0;
    }
    
  • 之后再写的时候大概会更新
    在这里插入图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值