状态压缩DP

状态压缩DP

定义:

状态压缩是一种使用二进制数来表示状态的方法,通常用于表示只有两种状态(0和1)的对象。

Acwing.291 蒙特里安的梦想

291. 蒙德里安的梦想 - AcWing题库

题目概览

求把 N × M N×M N×M的棋盘分割成若干个 1 × 2 1×2 1×2的长方形,有多少种方案。

例如当 N = 2 , M = 4 N=2,M=4 N=2M=4时,共有 55 55 55种方案。当 N = 2 , M = 3 N=2,M=3 N=2M=3时,共有 33 33 33种方案。

如下图所示:

2411_1.jpg

输入格式

输入包含多组测试用例。

每组测试用例占一行,包含两个整数 N N N M M M

当输入用例 N = 0 , M = 0 N=0,M=0 N=0M=0时,表示输入终止,且该用例无需处理。

输出格式

每个测试用例输出一个结果,每个结果占一行。

数据范围

1 ≤ N , M ≤ 11 1≤N,M≤11 1N,M11

输入样例:
1 2
1 3
1 4
2 2
2 3
2 4
2 11
4 11
0 0
输出样例:
1
0
1
2
3
5
144
51205

思路:

假设我们当前的方格是这样的:

image-20240805095031331

比如,我们先看一个简单情况,在一个 4 ∗ 3 4 * 3 43的小方格中(蓝色),我们已经放好了横向的 2 ∗ 1 2 * 1 21的小格子。

image-20240805095346505

那么,剩下的格子纵向放只有一种情况,只能一列一列放。

所以, 2 ∗ 1 2 * 1 21格子的摆放方案数等价于 2 ∗ 1 2 * 1 21格子横向摆放的方法

f [ i ] [ j ] f[i][j] f[i][j]表示现在要摆第i列,j是一个二进制数,表示上一列有那些行伸出来了小方格到我这一列

FOR EXAMPLE:

比如,我现在在第三列,但是第二行由于上一列放置小方格导致已经被占用了,于是 j = ( 01000 ) 2 j=(01000)_2 j=(01000)2

image-20240805100620574

即如图的情况

比如此时,矩阵是一个 5 ∗ 5 5 * 5 55的情况,所以j是一个5位的二进制数,即 0 < = j < = 31 0 <= j <= 31 0<=j<=31

那么,我们如何转移呢?枚举一下 i − 1 i - 1 i1列的所有状态

FOR EXAMPLE:

比如,我们当前要算的状态是这样的 f [ 3 ] [ 1 ] f[3][1] f[3][1]往前推,当前状态的表示图( j = ( 00001 ) 2 j = (00001)_2 j=(00001)2)为:

image-20240805101519461

此时,我们枚举的上一个状态的j(称他为k)是 k = ( 10010 ) 2 k = (10010)_2 k=(10010)2,判断一下这个状态能否转移过来

能否转移过来的条件很简单:

  1. j和k不能有冲突,如果冲突了,就会出现这样的情况:

    image-20240805102003861

    那如何判断呢?利用位运算就可以啦~

    (j & k) == 0时,即 j j j k k k是没有冲突的。

  2. 同一列上剩余的连续的空白格子一定是偶数个,不然竖着没法放

    image-20240805102635841

    判断方式:j | k不存在连续奇数个0

    时间复杂度: 4 ∗ 1 0 7 4 * 10^7 4107

代码实现:

#include<bits/stdc++.h>
using namespace std;

const int N = 12,M = 1 << N;

int n ,m;//行数列数
long long f[N][M];//DP数组
bool st[M];//标记

int main(){
    while(cin >> n >> m,n || m){//只要n,m不等于0就一直做
        memset(f,0,sizeof f);//清一下f
        
        //预处理是否出现连续奇数个0
        for(int i = 0;i < 1 << n;i++){
            st[i] = true;//假设它是成立的然后再来判断
            int cnt = 0;//存储当前这段连续0的个数
            for(int j = 0;j < n;j++)
                if(i >> j & 1){//如果这一位是1
                    if(cnt & 1) st[i] = 0;//如果发现是奇数个,那么不成立
                    cnt = 0;//cnt清零
                }
                else cnt++;//否则是0,继续累加
                
            if(cnt & 1) st[i] = 0;//最后cnt还需要判断一下
        }
    
        f[0][0] = 1;
        for(int i = 1;i <= m;i++)
            for(int j = 0;j < 1 << n;j++)
                for(int k = 0;k < 1 << n;k++)
                    if((j & k) == 0 && st[j | k])
                        f[i][j] += f[i - 1][k];
                    
        cout << f[m][0] << endl;
    }
    
}

Acwing.91 最短Hamilton路径

题目概览

给定一张 n n n个点的带权无向图,点从 0 ∼ n − 1 0∼n−1 0n1 标号,求起点 00 到终点 n − 1 n−1 n1的最短 H a m i l t o n Hamilton Hamilton路径。

H a m i l t o n Hamilton Hamilton路径的定义是从 0 0 0 n − 1 n−1 n1不重不漏地经过每个点恰好一次。

输入格式

第一行输入整数 n n n

接下来 n n n行每行 n n n个整数,其中第 i i i行第 j j j个整数表示点 i i i j j j的距离(记为 a [ i , j ] a[i,j] a[i,j])。

对于任意的 x x x, y y y, z z z数据保证 a [ x , x ] = 0 a[x,x]=0 a[x,x]=0 a [ x , y ] = a [ y , x ] a[x,y]=a[y,x] a[x,y]=a[y,x],并且 a [ x , y ] + a [ y , z ] ≥ a [ x , z ] a[x,y]+a[y,z]≥a[x,z] a[x,y]+a[y,z]a[x,z]

输出格式

输出一个整数,表示最短 H a m i l t o n Hamilton Hamilton路径的长度。

数据范围

1 ≤ n ≤ 20 1≤n≤20 1n20
0 ≤ a [ i , j ] ≤ 107 0≤a[i,j]≤107 0a[i,j]107

输入样例:
5
0 2 4 5 1
2 0 6 5 3
4 6 0 8 3
5 5 8 0 5
1 3 3 5 0
输出样例:
18

思路:

H a m i l t o n Hamilton Hamilton路径:每个点经过且只经过1次

如果暴力来做的话,那么就是要枚举出1 ~ n - 1点的所有走法进行判断,时间复杂度为 n ! ⋅ n n!·n n!n废了

状态压缩DP:

例如,当 i = ( 110011 ) 2 i = (110011)_2 i=(110011)2时,表示,第0145个点已经走过,但第2、3个点还没有走过。

image-20240806092819969

FOR EXAMPLE:
比如,我现在已经到达了 k k k点,然后抵达 j j j号点,即倒数第二个点为 k k k号点
现在,从 k k k号点到 j j j号点这条边是固定不变的,我们只需要找到 0 → k 0→k 0k号中最短的路径即可

目前,从 0 → j 0→j 0j号点的表示为 f [ i ] [ j ] f[i][j] f[i][j],那么, 0 → k 0→k 0k 号点的路径则为 f [ i − j 点 ] [ k ] f[i - j_点][k] f[ij][k]

那么, f [ i ] [ j ] = f [ i − j 点 ] [ k ] + a [ k ] [ j ] f[i][j] = f[i - j_点][k] + a[k][j] f[i][j]=f[ij][k]+a[k][j]

LaTeX \LaTeX LATEX~

代码实现:

#include<bits/stdc++.h>
using namespace std;

const int N = 20, M = 1 << N;

int n;
int w[N][N];//两点间距离
int f[M][N];//dp状态

int main(){
    cin >> n;
    for(int i = 0; i < n;i++)
        for(int j = 0;j < n;j++)
            cin >> w[i][j];//输入两点间的距离
            
    memset(f,0x3f,sizeof f);
    f[1][0] = 0;
    for(int i = 0;i < 1 << n;i++)
        for(int j = 0;j < n;j++)
            if(i >> j & 1)//i一定包含j,i的第j为是否为1 
                for(int k = 0;k < n;k++)
                    if((i - (1 << j)) >> k & 1)
                        f[i][j] = min(f[i][j],f[i - (1 << j)][k] + w[k][j]);
                        
    cout << f[(1 << n) - 1][n - 1] << endl;
    
    
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值