状态压缩DP
唉,上次一不小心背包等问题写的太多了,所以就停笔了,今天周末,于是赶紧把状态压缩补上
状态压缩式动态规划的一种常见的出题形式,一般在洛谷上难度是在绿题以上,所以算是一个比较提高的知识点。
接下来就来讲一下状态压缩常见的问题作为引入来解释这一问题的模型。
https://www.lanqiao.cn/problems/17136/learning/
这道题就是状态压缩的模版题了,那么对于这道题,我们要去求进攻所需的最小能量,但是怎么安排进攻的顺序是一个问题,那么最为朴素的做法就是去直接排列组合,然后再依次遍历,但是这样子就会有一个问题,那就是时间复杂度太高了,因为这里面加入有5个星球要去进攻,那么这里面讨论的情况是54321种,这样子的复杂度必然是n!*n这个对于n<=18的情况下是完全没有办法接受的,所以对于这种情况,我们要去考虑别的解决方法。
那么对于一个动态规划来说,重要的是设计状态,那么我们可以使用一个二进制的数字来表示当前已经访问过的星球,那么这样子就会是一个更优的做法,比如10011就可以表示当前已经进攻过星球1,4,5三个,那么这样子就可以表示一个状态了,但是我们不可能去每次讨论每一个节点是否访问过,即讨论每一个是0还是1,所以可以把他当成一个整体来看待,即一个二进制数字,那么就可以使用位运算来处理了。
关于位运算,这里面在每道题里面介绍需要的位运算知识,所以就接着往下看就行,相关的知识会讲到。
在这里面我们现在可以使用一个二进制数字来表示结果了,那么接下来就是看怎么去写状态转移方程,因为在输入数据里面,前三个表示的是星球的三维坐标,因此我们可以使用一个结构体去存下这个三维坐标,然后再使用一个二维数组去存下两点之间的距离,因为最多只有18个星球,所以直接枚举也就只是多了一个常数的时间,可以忽略不计,这里使用数组dist来记录,然后就是对于这个进攻来说,接下来就是要获取进攻时的防御强度w,接下来就可以列出状态转移方程。
在列状态转移方程的时候,又会发现一个问题,我们可以列举出从状态A到状态B的运动方案,但是我们没有办法去考虑到我们是怎么走的,即我们并不知道当前飞船的落脚点,就比如11111,我们可以由10111,可以由01111,11011,11101,11110这几个转移过来,因此我们还需要一个参数来去维护落脚点,怎么办,那就在DP表示里面添加在添加一个落脚点就可以了。所以在这里dp[i][j]表示当前星球的状态为i时,飞船处于j星球上,于是我们便可以得出状态转移方程
然后就可以开始写代码了
C++
#include<bits/stdc++.h>
#define ll long long
using namespace std;
struct Node {
int x, y, z, w;
};
double dist(Node a, Node b) {
return sqrt(pow(a.x - b.x, 2) + pow(a.y - b.y, 2) + pow(a.z - b.z, 2));
}
int M;
const int N = 20;
vector<Node> planets;
Node a1;
double length[N][N];
double dp[1 << N][N];
int main() {
cin >> M;
for (int i = 1; i <= M; i++) {
cin >> a1.x >> a1.y >> a1.z >> a1.w;
planets.push_back(a1);
}
for (int i = 0; i < M; i++) {
for (int j = 0; j < M; j++) {
length[i][j] = dist(planets[i], planets[j]);
}
}
//首先先初始化dp数组;
for (double* v : dp)
fill(v, v + N, 0x3f3f3f3f);
//dp表示当前处于状态i,现在位于第j个星球
//讨论第一个出发的星球的状态 星球的编号从0开始
for (int i = 0; i < N; i++) {
dp[1 << i][i] = 0;
}
for (int i = 0; i < (1<<M); i++) {//枚举当前的状态
for (int j = 0; j < M; j++) {//枚举下一个转移的地方
if ((i >> j) & 1) {
for (int k = 0; k < M; k++) {//枚举上一个转移的地方
if ((i >> k) & 1) {
dp[i][j] = min(dp[i][j], dp[i - (1 << j)][k] + length[k][j] * planets[j].w);
}
}
}
}
}
double res = 0x3f3f3f3f;
int pos = (1 << M) - 1;
for (int i = 0; i < M; i++) {
res = min(res, dp[pos][i]);
}
cout << fixed << setprecision(2) << res << endl;
}
Python
from math import sqrt, inf
class Node:
def __init__(self, x, y, z):
self.x = x
self.y = y
self.z = z
n = int(input())
val = [0] * n
nodes = []
for i in range(n):
x, y, z, w = map(int, input().split())
val[i] = w
nodes.append(Node(x, y, z))
dist = [[0] * n for _ in range(n)]
for i in range(n):
for j in range(n):
dist[i][j] = sqrt((nodes[i].x - nodes[j].x)**2 + (nodes[i].y - nodes[j].y)**2 + (nodes[i].z - nodes[j].z)**2)
#f[s][i] = max(f[s ^ (1 << j)][j] + dist(i, j), f[s][j])
#print(dist)
N = 1 << n
f = [[inf] * n for _ in range(N)]
for i in range(n):
f[1 << i][i] = 0
for s in range(N):
for i in range(n):
if s >> i & 1:
for j in range(n):
if s >> j & 1 and i != j:
f[s][i] = min(f[s][i], f[s ^ (1 << i)][j] + dist[i][j] * val[i])
# print(min(f[-1]), f)
print('{:.2f}'.format(min(f[-1])))
JAVA
import java.util.Arrays;
import java.util.Scanner;
public class Main {
static int N=20,M=1<<N;
static double f[][]=new double[M][N];
static double dis[][]=new double[N][N];
static int w[]=new int[N];
static class Node{
public Node(int x, int y, int z) {
this.x = x;
this.y = y;
this.z = z;
}
public Node() {
}
int x,y,z;
}
static Node[] nodes=new Node[N];
static int square(int x){
return x*x;
}
static double dist(Node a,Node b){
return Math.sqrt(square(a.x-b.x)+square(a.y-b.y)+square(a.z-b.z));
}
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
int n=in.nextInt();
for(int i=0;i<n;i++){
nodes[i]=new Node(in.nextInt(),in.nextInt(),in.nextInt());
w[i]=in.nextInt();
}
for(double v[]:f) Arrays.fill(v,0x3f3f3f3f);
for(int i=0;i<n;i++){
for(int j=0;j<n;j++){
dis[i][j]=dist(nodes[i],nodes[j]);
}
}
for(int i=0;i<n;i++){
f[(1<<i)][i]=0;
}
for(int i=0;i<(1<<n);i++){
for(int j=0;j<n;j++){
if((i>>j&1)==1){
for(int k=0;k<n;k++){
if((i>>k&1)==1)
f[i][j]=Math.min(f[i][j],f[i-(1<<j)][k]+dis[k][j]*w[j]);
}
}
}
}
double res = 0x3f3f3f3f;
int pos = (1 << n) - 1;
for(int i = 0;i < n;i ++) {
res = Math.min(res, f[pos][i]);
}
System.out.printf("%.2f",res);
}
}
对于状态转移方程,这里面1<<j表示左移,即为2**j,同时,这里面还有就是需要做一个判断,判断当前着落的位置在状态中是否为1,不然就出现了当前着落的位置是未攻打的情况,这个是不合法的,故在这里需要使用if ((i >> k) & 1)来判断第k个位置是否为1,这里是和运算,所以是只要当前位置是1,这里面就是&运算就会判定为1。
在最后,只要一次判断最后一行,取最小值,就可以了。
同类型的题目可以参考这个题,作为练习,AC源代码在下面
https://www.luogu.com.cn/problem/P1433
#include<bits/stdc++.h>
#define double long double
#define ll long long
using namespace std;
int n;
struct Node {
double x, y;
};
vector<Node> Nodes;
double path[20][20];
double square(double num) {
return num * num;
}
double dist(Node a, Node b) {
return sqrt(square(a.x - b.x) + square(a.y - b.y));
}
double dp[1<<16][16];
int main() {
cin >> n;
Node a;
for (double* v : dp) {
fill(v, v + n+1, 0x3f3f3f);
}
//初始化DP数组
a.x = 0; a.y = 0;
Nodes.push_back(a);
for (int i = 1; i <= n; i++) {
cin >> a.x >> a.y;
Nodes.push_back(a);
}
for (int i = 0; i <= n; i++) {
for (int j = 0; j <= n; j++) {
path[i][j] = dist(Nodes[i], Nodes[j]);
}
}
//初始化DP数组
for (int i = 0; i <= n; i++) {
if (i == 0) {
dp[0][0] = 0;
}
else {
ll k = (1 << i) + 1;
dp[k][i] = path[0][i];
}
}
//枚举状态
for (int i = 1; i <= ((1 << (n + 1)) - 1); i++) {
//枚举终点
for (int j = 0; j < (n + 1); j++) {
if ((i >> j) & 1) {
//枚举每一个起点
for (int k = 0; k < (n + 1); k++) {
if ((i >> k) & 1) {
dp[i][j] = min(dp[i][j], dp[i - (1 << (j))][k] + path[k][j]);
}
}
}
}
}
double res = 0x3f3f3f3f;
for (int i = 1; i <= n; i++) {
res = min(res, dp[(1 << (n + 1)) - 1][i]);
}
cout << fixed<<setprecision(2)<<res << endl;
}
(下面这里已经是好久之后写的,忙着实习,就忘记编辑了好久,罪过,罪过)
下面是这个是一个状态压缩的变例,是使用了状态压缩的思想,但是并没有使用真正意义上的状态压缩。
https://www.lanqiao.cn/problems/2110/learning/
对于一道动态规划的题目,首先要处理的是怎么描述出状态,然后再设计状态转移方程,最后再写出程序。
在这里,题目要求一定要填满,所以在这里,我们只需要去讨论边缘的变化情况,就可以了。那么在这道题里面,边缘的变化有三种情况,第一种是全部都是平的,称之为平滑型,其次为下凸型,还有就是上凸型。可以见下面图片的演示
但是,如何过只考虑当前的列,就会出现问题,也就是像下面这样子
因此我们在讨论的时候就必须满足他是一个被填充满的情况再去做下一步处理,
以及这种情况,在处理这种情况时,我们就需要考虑前面两列来做状态转移,在这里,我们可以将状态设置为dp[i][j],在这里i表示第i列,j表示当前的状态为j
方便起见,我们将0,1,2分别表示平滑型, 下凸型和上凸型
那么,接下来看一下代码
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int mod = 1000000007;
const int N = (int)1e7 + 100;
int dp[N][3];
//dp表示处理完第i列后
int main() {
dp[0][0] = 1;
int n;
cin >> n;
for (int i = 1; i <= n; i++) {
//首先的三种情况
dp[i][0] = dp[i - 1][0];
dp[i][0] = (dp[i][0] + dp[i - 1][1]) % mod;
dp[i][0] = (dp[i][0] + dp[i - 1][2]) % mod;
if (i >= 2) {
//涉及了多行之后的情况
dp[i][0] = (dp[i][0] + dp[i - 2][0]) % mod;
dp[i][1] = dp[i - 2][0];
dp[i][2] = dp[i - 2][0];
}
//加入了I型方块的情况
dp[i][1] = (dp[i][1] + dp[i - 1][2]) % mod;
dp[i][2] = (dp[i][2] + dp[i - 1][1]) % mod;
}
cout << dp[n][0] << endl;
}
在这里,就是一个分类讨论的事情了
首先就是对于第i行,其为平滑型,这个是怎么形成的,可以看成下面三种情况,第一种是在i-1是平滑型的基础加上一个I型的积木,然后就是在上凸型或下凸型的基础上加上一个L型积木(在这里先只讨论),然后就是后续的深入讨论,即如果涉及了多行应该怎么处理(i>=2),在这里,就处理了三种情况,1.最开始为平整型,然后加入了两个I型方块的情况,2.然后就是就是最开始是平整型,后来加入了一个L型方块的情况,这时形成上凸出,3.与第二种情况相反,形成了下凸出,
最后就是原本就是L型的情况,然后加入了I型方块使凸出的情况改变,这就是怎么去处理他的每一种情况。
然后就是关于动态规划的初始化的问题,在这里我们假设存在1个第0列,这一列是默认填满的,所以不用管别的,直接就是dp[0][0]=1,在这之后,就是看第一列,第一列无论如何都是满填充的状态,所以只有在第二列才出现凸出型,所以在这里假设存在第零列不会对讨论产生干扰。
下面就是java和python的代码,供参考
Python
MOD = int(1e9) + 7
N = int(1e7) + 100
# 使用Python的列表代替C++的数组
dp = [[0 for _ in range(3)] for _ in range(N)]
def fill_dp(n):
dp[0][0] = 1
for i in range(1, n + 1):
dp[i][0] = dp[i - 1][0] # 竖着放一个 I 形
dp[i][0] = (dp[i][0] + dp[i - 1][1]) % MOD # 放一个L形
dp[i][0] = (dp[i][0] + dp[i - 1][2]) % MOD # 放一个L形
if i >= 2:
dp[i][0] = (dp[i][0] + dp[i - 2][0]) % MOD # 横着放2个 I 形
if i >= 2:
dp[i][1] = dp[i - 2][0] # 放一个L形
dp[i][2] = dp[i - 2][0] # 放一个L形
dp[i][1] = (dp[i][1] + dp[i - 1][2]) % MOD # 横着放1个 I 形
dp[i][2] = (dp[i][2] + dp[i - 1][1]) % MOD # 横着放1个 I 形
return dp[n][0]
# 主程序
n = int(input())
result = fill_dp(n)
print(result)
# -------------分界线-------------------
MOD = int(1e9) + 7
N = int(1e7) + 100
# 使用Python的列表代替C++的数组
dp = [[0 for _ in range(3)] for _ in range(3)]
def fill_dp(n):
dp[2][0] = 1
for i in range(1, n + 1):
dp[0] = dp[1]
dp[1] = dp[2]
dp[2] = [0 for _ in range(3)]
dp[2][0] = dp[2 - 1][0] # 竖着放一个 I 形
dp[2][0] = (dp[2][0] + dp[2 - 1][1]) % MOD # 放一个L形
dp[2][0] = (dp[2][0] + dp[2 - 1][2]) % MOD # 放一个L形
dp[2][0] = (dp[2][0] + dp[2 - 2][0]) % MOD # 横着放2个 I 形
dp[2][1] = dp[2 - 2][0] # 放一个L形
dp[2][2] = dp[2 - 2][0] # 放一个L形
dp[2][1] = (dp[2][1] + dp[2 - 1][2]) % MOD # 横着放1个 I 形
dp[2][2] = (dp[2][2] + dp[2 - 1][1]) % MOD # 横着放1个 I 形
return dp[2][0]
# 主程序
n = int(input())
result = fill_dp(n)
print(result)
JAVA
import java.util.*;
public class Main {
private static final int MOD = 1000000007;
private static final int N = 10000010;
private static final int[][] dp = new int[N][3];
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
int n = scanner.nextInt();
scanner.close();
fillDp(n);
System.out.println(dp[n][0]);
}
private static void fillDp(int n) {
dp[0][0] = 1;
for (int i = 1; i <= n; ++i) {
dp[i][0] = dp[i - 1][0]; // 竖着放一个 I 形
dp[i][0] = (dp[i][0] + dp[i - 1][1]) % MOD; // 放一个L形
dp[i][0] = (dp[i][0] + dp[i - 1][2]) % MOD; // 放一个L形
if (i >= 2) {
dp[i][0] = (dp[i][0] + dp[i - 2][0]) % MOD; // 横着放2个 I 形
}
if (i >= 2) {
dp[i][1] = dp[i - 2][0]; // 放一个L形
dp[i][2] = dp[i - 2][0]; // 放一个L形
}
dp[i][1] = (dp[i][1] + dp[i - 1][2]) % MOD; // 横着放1个 I 形
dp[i][2] = (dp[i][2] + dp[i - 1][1]) % MOD; // 横着放1个 I 形
}
}
}
最后这个题是一个蓝题,在洛谷上,还是不错
https://www.luogu.com.cn/problem/P1896
#include<bits/stdc++.h>
#define ll long long
using namespace std;
int N, K;
int a[2000];
ll f[13][2000][13];
vector<int> correct;
int getnum(ll num) {
int n = 0;
while (num) {
if (num & 1) {
n += 1;
}
num >>= 1;
}
return n;
}
int main() {
cin >> N >> K;
//初始化最开始的数组
for (int i = 0; i < 2000; i++) {
a[i] = getnum(i);
}
//确保同一行的数字并不出现不合法
for (int s = 0; s < (1 << N); s++) {
if ((((s << 1) | (s >> 1)) & s) == 0) {
correct.push_back(s);
}
}
f[0][0][0] = 1;
int num = correct.size();
//枚举当前的行
for (int i = 1; i <= N; i++) {
//枚举这一行的状态
for (int l = 0; l < num; l++) {
int s1 = correct[l];
//枚举上一行的状态
for (int r = 0; r < num; r++) {
int s2 = correct[r];
if ((((s2 >> 1) | (s2 << 1) | s2)&s1)==0){
//枚举这一行的
for (int j = 0; j <= K; j++) {
if (a[s1] <= j) {
f[i][s1][j] += f[i - 1][s2][j - a[s1]];
}
}
}
}
}
}
ll ans = 0;
for (int i = 0; i < num; i++) {
ans += f[N][correct[i]][K];
}
cout << ans << endl;
}
这道题虽然不像之前的那种是求最小的路线的,但是这道题的特殊之处在于它是去求安排位置,保证他的位置是相互隔开的,在这里,dp[i][j][k]表示对于第i行,状态为j时以及当前的所使用的国王数量为k时的种类数,同时,在预处理的时候,这里这里通过做位运算来处理每一个可能状态的二进制数字,以保证水平方向上是合理的,使用a数组来记录每一个二进制数字里面存在多少个1,最后做状态压缩来讨论。
那么,就到这里了。
本文作者:fufufuf
本文链接:https://www.cnblogs.com/fufufuf/p/18669352
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步