状态压缩DP

47 阅读3分钟

蒙德里安的梦想

  • 问题背景

Snipaste_2023-03-30_18-23-40.png

Snipaste_2023-03-30_18-32-57.png

  • 分析

    • 首先要知道,我们只要先把横着的的放好了,竖着的自然就放好了,因此总方案等于只放横着的小方块的合法方案数
      • 那么如何判断当前方案合法,即所有位置是否都能被小方块放满
      • 很显然,只要每一列内部所有连续的空着的小方块的个数为偶数个就行了
  • Dp分析

    • 状态表示

      • 二维状态表示f(i,j)
      • f(i,j)表示的是哪一个集合:所有满足如下条件的选法集合
        • 已经将前i-1列摆好,且从第i-1列,伸出到第i列的状态是j的方案
          • 从第i-1列,伸出到第i列的状态是什么意思?
            • 如上图所示:第i列的状态为11001,其中11001为二进制数
          • 对于每一列,一共有多少种状态?
            • 如果是5行,一共有从 00000(十进制:0)11111(十进制:31)32 种方案
            • 如果是n行,一共有从 000……(十进制:0)111……(十进制:2^n^-1)2^n^ 种方案
          • 每一种都是合法的吗?
            • 当然不是,所以要先对这些状态进行初步筛选,筛去连续存在奇数个0的状态,布尔数组st[]用来存每个状态是否初步合法
      • f(i,j)存的是什么属性:MaxMin数量;在这里f(i,j)存的应该是数量,即已经将前i-1列摆好,且从第i-1列,伸出到第i列的状态是j的方案的数量
    • 状态计算:f(i,j)可以怎么算出来?

      • 这里将集合划分为 2^n^ 个子集
        • 每一个子集代表一个相应的状态,对于一个从第i-1列伸出到第i列的状态j,我们要去寻找能够和这个状态适配的从第i-2列伸出到第i- 1列的状态k
          • j&k必须为零,意思就是i-2列和i-1列的某一行横向放了个小方块,那么i-1列和i列就不能在这一行也横向放一个小方块
          • j|k是不是就代表第i-1列的实际状态,需要把这个状态放到st[]里面判断一下
          • 满足上述这两个条件的状态k才是真正能够和状态j适配的状态
          • 这时候 f(i)(j) += f(i - 1)(k)
        • 因此 f(i)(j) = Σf(i - 1)(k),其中 k 要与 j 适配
        • 对于判断 k 是否与 j 适配,可以提前预处理一个state[]数组出来
    • 代码

      • //初始化
        //从第-1列伸到第0列的满足000000...这个状态的方案只有一种
        //第0列的其他状态显然是0
        f[0][0] = 1;
        //对每一列进行遍历
        for (int i = 1; i <= m; i++) {
            //对每一列的每个状态进行枚举
            for (int j = 0; j < 1 << n; j++) {
                //对于每个从第i-1列伸到第i列的状态j 寻找每个其适配的从第i-2列伸到第i-1列的状态k
                for (int k = 0; k < 1 << n; k++) {
                    if (state[j][k] == 1) {
                        f[i][j] += f[i - 1][k];
                    }
                }
            }
        }
        

最短Hamilton路径

  • 问题背景

Snipaste_2023-04-02_15-42-14.png

  • Dp分析

    • 状态表示

      • 二维状态表示f(i,j)
      • f(i,j)表示的是哪一个集合:所有满足如下条件的集合
        • 当前走过的点的状态为i,并且目前停在j号点上的所走过的路径
          • i这个状态有一个二进制数来表示,例如起点表示成……0001
          • 一共有多少种状态?
            • 如果有5个点,一共有从 00000(十进制:0)11111(十进制:31)32 种状态
            • 如果有n个点,一共有从 000……(十进制:0)111……(十进制:2^n^-1)2^n^ 种状态
      • f(i,j)存的是什么属性:MaxMin数量;在这里f(i,j)存的应该是Min,当前走过的点的状态为i,并且目前停在j号点上的所走过的路径的最小值
    • 状态计算:f(i,j)可以怎么算出来?

      • 这里将集合分成若干个子集
        • 首先f(i,j)是由条件的,即状态ij这个点是走过的
        • 对于每个 f(i,j),都从一个 f(state_k,k) 转移过来
        • k也是有条件的,即状态ik这个点是走过的
        • f(i, j) = f(i - (1 << j),k) + w(k, j),其中 i 的前提是 i >> k & 1 == 1
    • 代码

      • //初始化
        for (int i = 0; i < 1 << N; i++) {
            Arrays.fill(f[i], 0x3f3f3f3f);
        }
        //起点的状态是00000001 十进制表示就是1
        f[1][0] = 0;
        //遍历每一种状态
        for (int i = 0; i < 1 << n; i++) {
            //遍历每一种状态停在哪一个位置
            for (int j = 0; j < n; j++) {
                //如果 i 和 j 匹配 换成人话来说就是 i状态中j这个点是走过的
                if ((i >> j & 1) == 1) {
                    //枚举i状态的上一个状态停在的位置
                    for (int k = 0; k < n; k++) {
                        //如果i状态中k这个点是走过的
                        if ((i >> k & 1) == 1) {
                            //状态转移
                            f[i][j] = Math.min(f[i][j], f[i - (1 << j)][k] + w[k][j]);
                        }
                    }
                }
            }
        }
        

练习

01 蒙德里安的梦想

  • 题目

Snipaste_2023-03-30_20-49-07.png

  • 题解
import java.io.*;
import java.util.*;

public class Main {
    public static final int N = 12;
    public static final int M = 1 << N;
    public static long[][] f = new long[N][M];
    //每一列都存在2^n^种状态 st存这些状态是否初步合法
    public static boolean[] st = new boolean[M];
    //对于从第i-1列伸到第i列的每个j状态 判断每个从第i-2列伸到第i-1列的每个k状态是否合法
    //1表示合法 0表示不合法
    public static int[][] state = new int[M][M];

    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        PrintWriter pw = new PrintWriter(new OutputStreamWriter(System.out));
        while (true) {
            String[] str1 = br.readLine().split(" ");
            int n = Integer.parseInt(str1[0]);
            int m = Integer.parseInt(str1[1]);
            if (n == 0 && m == 0) {
                break;
            }
            //预处理st数组
            for (int i = 0; i < 1 << n; i++) {
                //记录连续的0的个数
                int cnt = 0;
                boolean flag = true;
                for (int j = 0; j < n; j++) {
                    if ((i >> j & 1) == 1) {
                        //如果当前位为1 判断cnt是否为奇数
                        if ((cnt & 1) == 1) {
                            flag = false;
                            break;
                        }
                        //如果不为奇数 cnt置0 也可以省略这一步
                        //cnt = 0;
                    } else {
                        cnt++;
                    }
                }
                //最后再判断一次cnt是否为奇数
                if ((cnt & 1) == 1) {
                    flag = false;
                }
                //将状态是否合法存入数组
                st[i] = flag;
            }

            //预处理state数组
            //对于每个从第i-1列伸到第i列的状态j 寻找每个其适配的从第i-2列伸到第i-1列的状态k
            for (int j = 0; j < 1 << n; j++) {
                //由于不止有一组数据 所有要清零
                Arrays.fill(state[j], 0);
                for (int k = 0; k < 1 << n; k++) {
                    if ((j & k) == 0 && st[j | k]) {
                        state[j][k] = 1;
                    }
                }
            }

            //由于不止有一组数据 所有要清零
            for (int i = 0; i < N; i++) {
                Arrays.fill(f[i], 0);
            }
            //初始化
            //从第-1列伸到第0列的满足000000...这个状态的方案只有一种
            //第0列的其他状态显然是0
            f[0][0] = 1;
            //对每一列进行遍历
            for (int i = 1; i <= m; i++) {
                //对每一列的每个状态进行枚举
                for (int j = 0; j < 1 << n; j++) {
                    //对于每个从第i-1列伸到第i列的状态j 寻找每个其适配的从第i-2列伸到第i-1列的状态k
                    for (int k = 0; k < 1 << n; k++) {
                        if (state[j][k] == 1) {
                            f[i][j] += f[i - 1][k];
                        }
                    }
                }
            }
            //从第m-1列伸到第m列的状态是0000....的方案数 就是将所有位置填满的方案总数
            pw.println(f[m][0]);
        }
        pw.close();
        br.close();
    }
}

02 最短Hamilton路径

  • 题目

Snipaste_2023-04-02_16-32-55.png

  • 题解
import java.io.*;
import java.util.*;

public class Main {
    public static final int N = 20;
    public static final int M = 1 << N;
    public static int n;
    //存路径
    public static int[][] w = new int[N][N];
    //当前走过的点的状态为i 并且目前停在j号点上的所走过的路径的最小值
    public static int[][] f = new int[M][N];

    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        PrintWriter pw = new PrintWriter(new OutputStreamWriter(System.out));
        n = Integer.parseInt(br.readLine());
        for (int i = 0; i < n; i++) {
            String[] str1 = br.readLine().split(" ");
            for (int j = 0; j < n; j++) {
                w[i][j] = Integer.parseInt(str1[j]);
            }
        }

        //初始化
        for (int i = 0; i < 1 << N; i++) {
            Arrays.fill(f[i], 0x3f3f3f3f);
        }
        //起点的状态是00000001 十进制表示就是1
        f[1][0] = 0;

        //遍历每一种状态
        for (int i = 0; i < 1 << n; i++) {
            //遍历每一种状态停在哪一个位置
            for (int j = 0; j < n; j++) {
                //如果 i 和 j 匹配 换成人话来说就是 i状态中j这个点是走过的
                if ((i >> j & 1) == 1) {
                    //枚举i状态的上一个状态停在的位置
                    for (int k = 0; k < n; k++) {
                        //如果i状态中k这个点是走过的
                        if ((i >> k & 1) == 1) {
                            //状态转移
                            f[i][j] = Math.min(f[i][j], f[i - (1 << j)][k] + w[k][j]);
                        }
                    }
                }
            }
        }
        //终点的状态是(1 << n) - 1 停在 n-1 这个点上
        pw.println(f[(1 << n) - 1][n - 1]);
        pw.close();
        br.close();
    }
}