动态规划之状态压缩DP详细介绍和例题练习

状态压缩DP

状态压缩概述

状态压缩就是使用某种方法,简明扼要地以最小代价来表示某种状态,通常是用一串01数字(二进制数)来表示各个点的状态。这就要求使用状态压缩的对象的点的状态必须只有两种,0 或 1;

使用条件

从状态压缩的特点来看,这个算法适用的题目符合以下的条件

  • 解法需要保存一定的状态数据(表示一种状态的一个数据值),每个状态数据通常情况下是可以通过2进制来表示的。这就要求状态数据的每个单元只有两种状态,比如说棋盘上的格子,放棋子或者不放,或者是硬币的正反两面。这样用0或者1来表示状态数据的每个单元,而整个状态数据就是一个由一串0和1组成的二进制数。
  • 解法需要将状态数据实现为一个基本数据类型,比如int,long等等,即所谓的状态压缩。状态压缩的目的一方面是缩小了数据存储的空间,另一方面是在状态对比和状态整体处理时能够提高效率。这样就要求状态数据中的单元个数不能太大,比如用int来表示一个状态的时候,状态的单元个数不能超过32(32位的机器),所以题目一般都是至少有一维的数据范围很小。

状态压缩DP

  • 状态压缩DP,顾名思义,就是使用状态压缩的动态规划。
  • 动态规划问题通常有两种,一种是对递归问题的记忆化求解,另一种是把大问题看作是多阶段的决策求解。这里用的便是后一种,这带来一个需求,即存储之前的状态,再由状态及状态对应的值推演出状态转移方程最终得到最优解。

动态规划多阶段: 一个重要的特性就是无后效性。无后效性就是值对于某个给定的阶段状态,它以前各阶段的状态无法直接影响它未来的发展,而只能通过当前的这个状态。换句话说影响当前阶段状态只可能是前一阶段的状态;那么可以看出如何定义状态是至关重要的,因为状态决定了阶段的划分,阶段的划分保证了无后效性。

位运算

  • 有了状态,就需要对状态进行操作或访问

对一个十进制下的信息访问其内部存储的二进制信息,怎么办呢?操作系统是二进制的,编译器中同样存在一种运算符:位运算

  • 为了更好的理解状压dp,首先介绍位运算相关的知识。

    • 1.’&’符号,x & y,会将两个十进制数在二进制下进行与运算,然后返回其十进制下的值。例如 3(11)&2(10)=2(10)。(注意:括号外是10进制数,括号内是2进制数。下同
    • 2.’|’符号,x | y,会将两个十进制数在二进制下进行或运算,然后返回其十进制下的值。例如 3(11)|2(10)=3(11)。
    • 3.’^’符号,x ^ y,会将两个十进制数在二进制下进行异或运算,然后返回其十进制下的值。例如 3(11)^2(10)=1(01)。
    • 4.’<<’符号,x<<2,将x在二进制下的每一位向左移动两位,最右边用0填充,x<<2相当于让x乘以4。
      >>’是右移操作,x>>1相当于给x/2,去掉x二进制下的最有一位。

这四种运算在状态压缩DP中有着广泛的应用,常见的应用如下

  • 1.判断一个数字x二进制下第i位是不是等于1。

    • 方法:if ( ((1<<(i−1)) & x) > 0)
    • 证明:将1左移i-1位,相当于制造了一个只有第i位上是1,其他位上都是0的二进制数。然后与x做与运算,如果结果>0,说明x第i位上是1,反之则是0。
		int st = 9; //  1001
        int i = 3;  
        System.out.println((1<<i));  //  1000    8
        System.out.println( (st & (1<<i) ) );  //  (9 & 8) == 8   ( 1001 & 1000) == 1000
        System.out.println(  (st & (1<<i)) > 0 ); // true
  • 2.将一个数字x二进制下第i位更改成1。
    • 方法:x = x | ( 1<<(i−1) )
    • 证明:证明方法与1类似。
  • 3.把一个数字二进制下最靠右的第一个1去掉。
    • 方法:x = x & (x−1)

优先级: 位反(~ ) > 算术 > 位左移、位右移 > 关系运算 > 位与 > 位或 > 位异或 > 逻辑运算

状态压缩DP例题练习

import java.io.*;

public class Main {

    static int n, k;
    static long ans;

    //统计x的二进制表示中有多少个1
    static int count(int x) {
        int cnt = 0;

        while (x != 0) {
            cnt++;
            x &= (x - 1);
        }

        return cnt;
    }

    //判断单行状态st是否合法
    static boolean check1(int st) {
        for (int i = 0; i + 1 < n; i++) {
            if ((st & (1 << i)) > 0 && (st & (1 << (i + 1))) > 0) {
                return false;
            }
        }

        return true;
    }

    // 判断当前行和上一行的状态是否合法
    static boolean check2(int st, int st2) {
        for (int i = 0; i < n; i++) {
            if ((st & (1 << i)) > 0) {
                if ((st2 & (1 << i)) > 0)
                    return false;
                else if ((st2 & (1 << (i + 1))) > 0)
                    return false;
                else if ((st2 & (1 << (i - 1))) > 0)
                    return false;
            }
        }

        return true;
    }

    public static void main(String[] args) throws IOException {
        BufferedReader in = new BufferedReader(new InputStreamReader(System.in));

        String[] s = in.readLine().split(" ");
        n = Integer.parseInt(s[0]);
        k = Integer.parseInt(s[1]);

        long[][][] f = new long[9][1 << 9][82];

        for (int i = 0; i < n; i++) {
            for (int st = 0; st < (1 << n); st++) {
                if (!check1(st))
                    continue;

                if (i == 0) {
                    f[i][st][count(st)] = 1;
                } else {
                    for (int j = count(st); j <= k; j++) {
                        for (int st2 = 0; st2 < (1 << n); st2++) {
                            if (!check1(st2) || !check2(st, st2)) {
                                continue;
                            }

                            f[i][st][j] += f[i - 1][st2][j - count(st)];
                        }
                    }
                }
            }
        }

        for (int st = 0; st < (1 << n); st++) {
            ans += f[n - 1][st][k];
        }

        System.out.println(ans);
    }
}
import java.util.*;

public class Main {
    static int n, k, N = 12, M = 1 << 10, K = 105;
    static long[][][] f = new long[N][K][M];
    static int[] cnt = new int[M];
    static ArrayList<Integer> st = new ArrayList<>();
    static ArrayList<ArrayList<Integer>> h = new ArrayList<>();
    static int check(int st) {
        for (int i = 0; i < n; i++) {
            if ((st >> i & 1) != 0 && (st >> i + 1 & 1) != 0)
                return 0;
        }

        return 1;
    }
    static int count(int st) {
        int ans = 0;

        for (int i = 0; i < n; i++)
            ans += st >> i & 1;

        return ans;
    }
    public static void main(String[] args) {
        Scanner reader = new Scanner(System.in);

        for (int i = 0; i < M; i++) {
            h.add(new ArrayList<>());
        }

        n = reader.nextInt();
        k = reader.nextInt();

        for (int i = 0; i < 1 << n; i++) {
            if (check(i) != 0) {
                st.add(i);
                cnt[i] = count(i);
            }
        }

        for (int i = 0; i < st.size(); i++) {
            for (int j = 0; j < st.size(); j++) {
                int a = st.get(i), b = st.get(j);

                if ((a & b) == 0 && check(a | b) != 0)
                    h.get(i).add(j) ;
            }
        }

        f[0][0][0] = 1;

        for (int i = 1; i <= n + 1; i++) {
            for (int j = 0; j <= k; j++) {
                for (int a = 0; a < st.size(); a++) {
                    for (int b : h.get(a)) {
                        int c = cnt[st.get(a)];

                        if (j >= c)
                            f[i][j][st.get(a)] += f[i - 1][j - c][st.get(b)];
                    }
                }
            }
        }

        System.out.println(f[n + 1][k][0]);
    }
}
import java.util.*;

public class Main {
    public static void main(String[] args) {
        Scanner in = new Scanner(System.in);
        // BufferedReader in = new BufferedReader(new InputStreamReader(System.in));

        String[] s = in.nextLine().split(" ");
        int M = Integer.parseInt(s[0]);
        int N = Integer.parseInt(s[1]);
        int[] G = new int[M+2];
        for (int i = 1; i <= M; i++) {
            s = in.nextLine().split(" ");
            for (int j = 0; j < N; j++) {
                //w[i] += !t * (1 << j);
                int t = Integer.parseInt(s[j]) ^ 1;
                G[i] += t * (1 << j);
            }
        }

        System.out.println(solve(M,N,G));
    }

    static List<Integer> state;
    static List<List<Integer>> head;

    private static int solve(int M,int N,int[] G) {
        int[][] dp = new int[14][1<<12];
        int mod = (int) 1e8;
        state = new ArrayList<>();
        head = new ArrayList<>();

        for (int i = 0; i < 1 << N; i ++ ) {
            if (check(i,N)) {
                state.add(i);
            }
        }

        for (int i = 0; i < state.size(); i ++ ) {
            List<Integer> ans = new ArrayList<>();
            for (int j = 0; j < state.size(); j ++ ) {
                int a = state.get(i), b = state.get(j);
                //这一行和上一行没有交集,a就可以转移到b
                if ( (a & b) == 0) {
                    ans.add(j);
                }
            }
            head.add(ans);
        }

        dp[0][0]=1;
        for(int i = 1;i <= M+1;i++) {
            for(int j=0;j<state.size();j++) {
                //G[i]的1不能放,state[j]的i表示要放,所以&一下,结果是1就不合法。
                if( (state.get(j) & G[i]) == 0)  {
                    for(int k:head.get(j))
                    {
                        dp[i][j]=(dp[i][j]+dp[i-1][k])%mod;
                    }
                }
            }
        }
        return dp[M+1][0];
    }

    //检查本状态是否能符合题意
    static boolean check(int state, int m) {
        for(int i = 0; i + 1 < m; i++) {
            //不允许存在相邻两个1
            if( ( state>>i &1) == 1 && (state>>i+1 &1) == 1) {
                return false;
            }
        }
        return true;
    }
}

你知道的越多,你不知道的越多。
有道无术,术尚可求,有术无道,止于术。
如有其它问题,欢迎大家留言,我们一起讨论,一起学习,一起进步。

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值