蒙德里安的梦想
- 问题背景
-
分析
- 首先要知道,我们只要先把横着的的放好了,竖着的自然就放好了,因此总方案等于只放横着的小方块的合法方案数
- 那么如何判断当前方案合法,即所有位置是否都能被小方块放满
- 很显然,只要每一列内部所有连续的空着的小方块的个数为偶数个就行了
- 首先要知道,我们只要先把横着的的放好了,竖着的自然就放好了,因此总方案等于只放横着的小方块的合法方案数
-
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)
存的是什么属性:Max
,Min
,数量
;在这里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[]
数组出来
- 每一个子集代表一个相应的状态,对于一个从第
- 这里将集合划分为 2^n^ 个子集
-
代码
-
//初始化 //从第-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路径
- 问题背景
-
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)
存的是什么属性:Max
,Min
,数量
;在这里f(i,j)
存的应该是Min
,当前走过的点的状态为i
,并且目前停在j
号点上的所走过的路径的最小值
- 二维状态表示
-
状态计算:
f(i,j)
可以怎么算出来?- 这里将集合分成若干个子集
- 首先
f(i,j)
是由条件的,即状态i
中j
这个点是走过的 - 对于每个 f(i,j),都从一个 f(state_k,k) 转移过来
k
也是有条件的,即状态i
中k
这个点是走过的- 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 蒙德里安的梦想
- 题目
- 题解
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路径
- 题目
- 题解
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();
}
}