状态压缩DP
棋盘类(基于连通性)
例题:1064.骑士
————————————————————————————————————————————————————
import java.util.ArrayList;
import java.util.Scanner;
public class Main {
static int N = 55;
static int M = 1 << 10;
static int K = 110;
static int n;
static int m;
static int[] cnt = new int[M];
static ArrayList<Integer>[] head = new ArrayList[M];
static ArrayList<Integer> state = new ArrayList<>();
static long[][][] f = new long[N][K][M];
public static void main(String[] args){
Scanner sc = new Scanner(System.in);
n = sc.nextInt();
m = sc.nextInt();
sc.close();
for (int i = 0; i < head.length; i++) {
head[i] = new ArrayList<>();
}
for(int i = 0;i < 1 << n;i++) {
if(check(i)) {
state.add(i);
cnt[i] = count(i);
}
}
for(int i = 0;i < state.size();i++) {
for (int j = 0; j < state.size(); j++) {
int a = state.get(i);
int b = state.get(j);
if((a & b) == 0 && check(a | b)) {
head[i].add(j);
}
}
}
f[0][0][0] = 1;
for (int i = 1; i <= n + 1; i++) {
for (int j = 0; j <= m; j++) {
for (int a = 0; a < state.size(); a++) {
for(int b : head[a]) {
int c = cnt[state.get(a)];
if(j >= c) {
f[i][j][a] += f[i - 1][j - c][b];
}
}
}
}
}
System.out.println(f[n + 1][m][0]);
}
private static int count(int state) {//返回1的个数
int res = 0;
for (int i = 0; i < n; i++) {
res += (state >> i) & 1;
}
return res;
}
private static boolean check(int state) {//状态是否合法
for(int i = 0;i < n;i++) {
if(((state >> i) & 1) == 1 && ((state >> i + 1) & 1) == 1)
return false;
}
return true;
}
}
例题:327. 玉米田
摘自"小呆呆"
注意:题目中还有着哪些坑不能种玉米,需要特殊标记一下,w[i]记录的是第 i 行能不能种玉米的状态,w[i] 的二进制表示中,某位是1表示该位置不能填坑,0表示该位置可以填坑,当枚举到当前状态state时,若state & w[i] == 0表示合法
时间复杂度:
O(n∗s∗k),n表示行数,s表示合法状态数量,k表示合法状态的转移数量
import java.util.ArrayList;
import java.util.Scanner;
public class Main {
static int N = 14,M = 1 << N ;
static int[] w = new int[N];
static int n,m;
static ArrayList<Integer> state = new ArrayList<Integer>();
static ArrayList<Integer>[] head = new ArrayList[M];
static int[][] f = new int[N][M];
static int mod = 100000000;
private static boolean check(int s) {
for(int i = 0;i < m;i ++) {
if((s >> i & 1) == 1 && (s >> i + 1 & 1) == 1)
return false;
}
return true;
}
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
n = sc.nextInt();
m = sc.nextInt();
for(int i = 1;i <= n;i ++) {
int t = 0;
for(int j = 0;j < m;j ++) {
int x = sc.nextInt();
if(x == 0) t += 1 << j;
}
w[i] = t;
}
sc.close();
//预处理出相邻的不能为1的状态
for(int i = 0;i < 1 << m;i ++) {
if(check(i))
state.add(i);
}
//判断哪些状态和哪些状态可以搭配
for(int i = 0;i < state.size();i ++) {
head[i] = new ArrayList<Integer>();
for(int j = 0;j < state.size();j ++) {
int a = state.get(i);
int b = state.get(j);
if((a & b) == 0)
head[i].add(j);
}
}
f[0][0] = 1;
for(int i = 1;i <= n;i ++) {
for(int a = 0;a < state.size();a ++) {
if((state.get(a) & w[i]) == 0) {
for(int b : head[a]) {
f[i][a] = (f[i][a] + f[i - 1][b]) % mod;
}
}
}
}
long res = 0;
for(int i = 0;i < 1 << m;i ++) res = (res + f[n][i]) % mod;
System.out.println(res);
}
}
例题:292. 炮兵阵地
摘自"小呆呆"
注意:题目中还有 ‘H’ 位置不能放大炮,需要特殊标记一下,w[i]记录的是第 i 行能不能放大炮的状态,w[i]的二进制表示中,某位是1表示该位置不能放大炮,0表示该位置可以放大炮,当枚举到当前状态state时,若state & w[i] == 0表示合法
时间复杂度
上限复杂度是O(n∗s2∗k),n 表示行数,s 表示合法状态数量,k 表示合法状态的转移数量
import java.util.ArrayList;
import java.util.Scanner;
public class Main {
static int N = 110,M = 1 << 10;
static int[] w = new int[N];
static int n , m;
static ArrayList<Integer> state = new ArrayList<Integer>();
static int[] cnt = new int[M];
static int[][][] f = new int[2][M][M];
private static boolean check(int state) {
for(int i = 0;i < m;i ++) {
if((state >> i & 1) == 1 && ((state >> i + 1 & 1) == 1 || (state >> i + 2 & 1) == 1)) return false ;
}
return true;
}
private static int count(int s) {
int res = 0;
for(int i = 0;i < m;i ++)
if((s >> i & 1) == 1)
res ++;
return res;
}
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
n = sc.nextInt();
m = sc.nextInt();
for(int i = 1;i <= n;i ++) {
char[] g = sc.next().toCharArray();
int t = 0;
//不可填表示1,可填表示0
for(int j = 0;j < m;j ++) {
if(g[j] == 'H') t += (1 << j);
}
w[i] = t;
}
sc.close();
//预处理出相邻不为1的状态
for(int i = 0; i < 1 << m;i ++) {
if(check(i)) {
state.add(i);
cnt[i] = count(i);
}
}
for(int i = 1;i <= n;i ++)
for(int j = 0;j < state.size();j ++)//当前行
for(int k = 0;k < state.size();k ++)//倒数第二行
for(int u = 0;u < state.size();u ++) {
int a = state.get(j);
int b = state.get(k);
int c = state.get(u);
if((a & b) != 0 || (a & c) != 0 || (b & c) != 0) continue;
if((w[i] & a) != 0 || (w[i - 1] & b) != 0) continue;
f[i & 1][j][k] = Math.max(f[i & 1][j][k], f[i - 1 & 1][k][u] + cnt[a]);
}
int res = 0;
for(int i = 0;i < state.size();i ++)
for(int j = 0;j < state.size();j ++)
res = Math.max(res,f[n & 1][i][j]);
System.out.println(res);
}
}
例题:524. 愤怒的小鸟
摘自y总和小呆呆题解
方法1:记忆化搜索
f[state] = dfs(state)表示从该状态覆盖所有的点需要多少条抛物线
int dfs(int state) {// state表示当前状态哪些点被覆盖
if(state 已经覆盖所有的点) return 0
if(state 状态已经搜索过) retrun f[state]的值
任选出没有被覆盖的点x
枚举所有与x号点相关的抛物线
当该抛物线不能覆盖x号点 即path[x][i] == 0 continue;
f[state] = Math.min(f[state],dfs(state | path[x][i]) + 1);
返回 f[state];
}
方法2:线性DP
- 切入点:i | path[x][j] 是大于等于 i,i 才能从小到大枚举,从 i 转移到 i | path[x][j]
- f[state] : 表示从该状态覆盖所有的点需要多少条抛物线
- 从 0 枚举到 (1 << n) - 1,找到任意一个未覆盖的点,枚举所有与 x 号点相关的抛物线,最终返回 f[(1 << n) - 1]
【记忆化搜索】
import java.util.Arrays;
import java.util.Scanner;
public class Main {
static int n,m;
static int N = 18,M = 1 << 18;
static int[][] path = new int[N][N];
static Pair[] pair = new Pair[N];
static double esp = 1e-8;
static int[] f = new int[M];//表示该状态需要抛物线的数量
private static int cmp(double a,double b) {
if(Math.abs(a - b) < esp) return 0;
if(a > b) return 1;
return -1;
}
private static int dfs(int state) {
if(state == (1 << n) - 1) return 0;
if(f[state] != -1) return f[state];
//任选出没有被覆盖的点x
int x = 0;
for(int i = 0;i < n;i ++) {
if((state >> i & 1) == 0) {
x = i;
break;
}
}
int res = 0x3f3f3f3f;
//枚举所有与x号点相关的抛物线
for(int i = 0;i < n;i ++) {
if (path[x][i] == 0) continue;//当该抛物线不能覆盖x号点
res = Math.min(res,dfs(state | path[x][i]) + 1);
}
return f[state] = res;
}
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int T = sc.nextInt();
while(T -- > 0) {
n = sc.nextInt();
m = sc.nextInt();
for(int i = 0;i < n;i ++) {
double x = sc.nextDouble();
double y = sc.nextDouble();
pair[i] = new Pair(x,y);
}
for(int i = 0;i < n;i ++) Arrays.fill(path[i], 0);
//找出所有抛物线对应点的状态
for(int i = 0;i < n;i ++) {
path[i][i] = 1 << i;
for(int j = 0;j < n;j ++) {
double x1 = pair[i].x, y1 = pair[i].y;
double x2 = pair[j].x, y2 = pair[j].y;
if(cmp(x1,x2) == 0) continue;
double a = (y1 / x1 - y2 / x2) / (x1 - x2);
double b = y1 / x1 - a * x1;
if(cmp(a,0) >= 0) continue;
int state = 0;
//判断哪些点在该抛物线上
for(int k = 0;k < n;k ++) {
double x = pair[k].x;
double y = pair[k].y;
if(cmp(a * x * x + b * x,y) == 0) state += 1 << k;
}
path[i][j] = state;
}
}
Arrays.fill(f, -1);
System.out.println(dfs(0));
}
sc.close();
}
}
class Pair {
double x,y;
Pair(double x,double y) {
this.x = x;
this.y = y;
}
}
【线性DP】
import java.util.Arrays;
import java.util.Scanner;
public class Main {
static int n,m;
static int N = 18,M = 1 << 18;
static int[][] path = new int[N][N];
static Pair[] pair = new Pair[N];
static double esp = 1e-8;
static int[] f = new int[M];//表示从该状态覆盖所有的点需要多少条抛物线
private static int cmp(double a,double b) {
if(Math.abs(a - b) < esp) return 0;
if(a > b) return 1;
return -1;
}
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int T = sc.nextInt();
while(T -- > 0) {
n = sc.nextInt();
m = sc.nextInt();
for(int i = 0;i < n;i ++) {
double x = sc.nextDouble();
double y = sc.nextDouble();
pair[i] = new Pair(x,y);
}
for(int i = 0;i < n;i ++) Arrays.fill(path[i], 0);
//找出所有抛物线对应点的状态
for(int i = 0;i < n;i ++) {
path[i][i] = 1 << i;
for(int j = 0;j < n;j ++) {
double x1 = pair[i].x, y1 = pair[i].y;
double x2 = pair[j].x, y2 = pair[j].y;
if(cmp(x1,x2) == 0) continue;
double a = (y1 / x1 - y2 / x2) / (x1 - x2);
double b = y1 / x1 - a * x1;
if(cmp(a,0) >= 0) continue;
int state = 0;
//判断哪些点在该抛物线上
for(int k = 0;k < n;k ++) {
double x = pair[k].x;
double y = pair[k].y;
if(cmp(a * x * x + b * x,y) == 0) state += 1 << k;
}
path[i][j] = state;
}
}
Arrays.fill(f, 0x3f3f3f3f);
f[0] = 0;
//注意:i | path[x][j] 是大于等于i,i才能 从小到大枚举,从i转移到i | path[x][j]
for(int i = 0;i < 1 << n;i ++) {
//找到任意一个未覆盖的点
int x = 0;
for(int j = 0;j < n;j ++) {
if((i >> j & 1) == 0) {
x = j;
break;
}
}
//枚举所有与x号点相关的抛物线
for(int j = 0;j < n;j ++)
f[i | path[x][j]] = Math.min(f[i | path[x][j]], f[i] + 1);
}
System.out.println(f[(1 << n) - 1]);
}
sc.close();
}
}
class Pair {
double x,y;
Pair(double x,double y) {
this.x = x;
this.y = y;
}
}