动态规划06-状态压缩 DP

关于状态压缩的解释,参考这一篇文章
动态规划——状态压缩DP

蒙德里安的梦想

求把 N × M N×M N×M的棋盘分割成若干个 1 × 2 1×2 1×2的长方形,有多少种方案。
例如当 N = 2 , M = 4 N=2,M=4 N=2M=4时,共有 5 5 5种方案。当 N = 2 , M = 3 N=2,M=3 N=2M=3时,共有 3 3 3种方案。
如下图所示:
image.png
输入格式
输入包含多组测试用例。
每组测试用例占一行,包含两个整数 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

解决思路

我们在向一个棋盘放长方形的时候可以根据这样的原则,先横着放,再竖着放,这样当我们将横着的长方形放完之后,就可以向剩下的位置填充竖着的长方形,同时在填充的时候需要判断剩下的格数是否合法。对于每一列来说,如果当前列的剩余格数为偶数则可以放下,认为合法。
在遵循了这个前提的条件下,我们进行动态规划。
集合状态: d p [ i ] [ j ] dp[i][j] dp[i][j]可以表示为前 i − 1 i-1 i1列已经完成,且从第 i − 1 i-1 i1列伸到第 i i i列的状态为 j j j的所有方案。
集合划分:对于第 i i i列来讲,从第 i − 1 i-1 i1列到第 i i i列的状态是已经确定的,但是从 i − 2 i-2 i2列到 i − 1 i-1 i1列的状态不是确定的,我们通过遍历最后状态的所有不同的前一个状态,可以得到总和。
d p [ i ] [ j ] = ∑ d p [ i − 1 ] [ k ] , 0 < = k < = 2 n dp[i][j] = \sum dp[i-1][k],0 <= k <= 2^n dp[i][j]=dp[i1][k]0<=k<=2n,对于每一行都有伸和不伸的选择,因此是 2 n 2^n 2n,其实也就是前面所有不和状态 j j j冲突的总和,对于状态 j j j是使用一个十进制数表示二进制,例如 5 5 5就代表 101 101 101,也就是第一行伸出,第二行不伸出,第三行伸出。
image.png

代码实现

#include <cstring>
#include <vector>
#include <algorithm>
#include <iostream>
using namespace std;

//在这个犯了错,以为12是开到下标12,忘了是数组长度,应该是开到下标11
const int N = 12, M = 1 << 11;
int n, m;
long long dp[N][M];
vector<int> state[M];
//用于存储每种方案数的0数量是否合法,即是否为偶数
bool st[M];

int main()
{
    while(cin >> n >> m, n && m)
    {
        //首先对于st数组进行预处理,判断合法的情况
        for(int i = 0; i < 1 << n; i++)
        {
            //记录当前0的数量
            int cnt = 0;
            bool flag = true;
            for(int j = 0; j < n; j++)
            {
                //判断第j+1上的数是否为1
                if(i >> j & 1)
                {
                    //如果cnt的二进制位是1,则就是奇数
                    if(cnt & 1)
                    {
                        flag = false;
                        break;
                    }
                }else
                {
                    cnt++;
                }
            }
            //判断最高位的1前面的0的个数
            if(cnt & 1)
            {
                flag = false;
            }
            st[i] = flag;
        }
        
        //对state[i]进行预处理,判断i的前一列不会冲突的状态
        for(int i = 0; i < 1 << n; i++)
        {
            //由于n,m的多次读入,需要提前清除
            //st不需要清除是由于无论读入什么数据都是相同的,不会有影响
            state[i].clear();
            for(int j = 0; j < 1 << n; j++)
            {
                if(!(i & j) && st[i | j])
                //如果 i 和 J 中存在冲突,则 i & j != 0
                //将i和j的情况合在一起,使用st进行判断
                {
                    state[i].push_back(j);
                }
            }
        }
        
        memset(dp,0,sizeof dp);
        dp[0][0] = 1;
        for(int i = 1; i <= m; i++)
        {
            for(int j = 0; j < 1 << n; j++)
            {
                //遍历第i列左侧的一列在当前状态为 j 的情况下可以选择的方案
                for(auto k : state[j])
                {
                    dp[i][j] += dp[i-1][k];
                }
            }
        }
        
        //dp[m][0],由于下标始终比列数小1个,因此这个代表前m列已经完成,
        //从m列伸到第m+1列的状态为0的方案,
        //状态为0也就代表m列没有超出的,得到的方案数就是最终的答案
        cout << dp[m][0] << endl;
    }
    return 0;
}

最短Hamilton路径

给定一张 n n n个点的带权无向图,点从 0 ∼ n − 1 0∼n−1 0n1标号,求起点 0 0 0到终点 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 , y , z x,y,z x,y,z,数据保证 a [ x , x ] = 0 , a [ x , y ] = a [ y , x ] a[x,x]=0,a[x,y]=a[y,x] a[x,x]=0a[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 ] ≤ 1 0 7 0≤a[i,j]≤10^7 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

解决思路

状态表示: d p [ i ] [ j ] dp[i][j] dp[i][j]表示从 0 0 0 j j j,经过的点的状态是 i i i的路径。
集合划分:在进行集合划分时,我们可以用最后一个状态的前一个状态来进行划分,也就是走到 j j j的前一个节点,我们称之为 k k k节点,由于 k k k j j j的路径已经确定了,那么我们仅需要知道 0 0 0 k k k,状态为去掉 j j j节点的路径即可,也就是 d p [ i − ( 1 < < j ) ] [ k ] dp[i - (1 << j)][k] dp[i(1<<j)][k],通过遍历所有的上一个节点得到其中的最小值。

代码实现

#include <iostream>
#include <cstring>
using namespace std;

const int N = 20, M = 1 << N;
int n;
int w[N][N];
// dp[i][j]表示从0到j,走过的所有点是i的全部路径
//在这里我也犯过一个错误,我一开始认为如果使用dp[i][j]表示,从0到j,经过的点是i的路径
//但实际上这在逻辑上是不正确的,因为如果这样子,我们的循环就代表了在到达某个点的情况下
//遍历我们所有的路径状态,实际上应该是在某种路径条件下,遍历可以到达的点
int dp[M][N];

int main()
{
    cin >> n;
    for(int i = 0; i < n; i++)
    {
        for(int j = 0; j < n; j++)
        {
            cin >> w[i][j];
        }
    }
    memset(dp,0x3f,sizeof dp);
    //从0到0的最短路径为0
    dp[1][0] = 0;
    for(int i = 0; i < 1 << n; i++)
    {
        for(int j = 0; j < n; j++)
        {
            //判断当前状态是否可以到达j
            //通过位运算的方式,由于j在i中的位置就是第j+1位,向右移动j位就可以将j移到最低位
            if(i >> j & 1)
            {
                for(int k = 0; k < n; k++)
                {
                    //除去j点可以到达k
                    //通过位运算的方式,j在i中的表示是 2 ^ j ,减去 2 ^ j 相当于将该位变为0
                    if((i - (1 << j) >> k) & 1)
                    {
                        dp[i][j] = min(dp[i][j], dp[i - (1 << j)][k] + w[k][j]);  
                    }
                }
            }
        }
    }
    cout << dp[(1 << n) - 1][n-1] << endl;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值