算法: 状态压缩DP

什么是状态压缩DP

DP,即动态规划,传统的动态规划都是基于整数的, 比如背包问题。定义状态 d p [ i ] [ j ] dp[i][j] dp[i][j]:背包容量为 j j j时前 i i i件物品的最大收益。这里 i i i取整数。而对于状态压缩型的DP:动态规划是基于集合的,但是我们使用二进制将这个集合压缩为一个整数表示,这个过程就是状态压缩。

常用位运算

状态压缩DP过程常常用到位运算来模拟对集合的操作:

运算符运算结果
&1011 & 10001000
|1011 | 01001111
~~10110100
^1001 ^ 11110110

例题一: 旅行商问题

在这里插入图片描述
在这里插入图片描述

  • 定义 d p [ S ] [ v ] dp[S][v] dp[S][v]: 当已拜访节点集合为 S S S,且当前位置为 v v v时,回到位置0还需要经过的最短路径。

  • 目标态 d p [ 0 ] [ 0 ] dp[0][0] dp[0][0]:拜访所有城市再回到0所经过的最短路径。

  • 状态转移:

    • u ∉ S u \notin S u/S: 说明不可重复经过一点。
    • 实例: d p [ 0100 ] [ 1 ] = m i n { d p [ 0110 ] [ 2 ] + 2 , d p [ 0101 ] [ 3 ] + 2 } dp[0100][1] = min\{dp[0110][2] + 2, dp[0101][3] + 2\} dp[0100][1]=min{dp[0110][2]+2,dp[0101][3]+2}:过程如下
      在这里插入图片描述
  • 更新策略: 由状态转移方程式可知, 所有的 d p [ S ] [ v ] dp[S][v] dp[S][v]都由 d p [ S ’ ] [ u ] ( S ’ > S ) dp[S^’][u](S^’>S) dp[S][u](S>S)转移而来,故我们按照 S S S递减更新。

  • 初始化: d p [ 2 n − 1 ] [ 0 ] = 0 dp[2^n-1][0] = 0 dp[2n1][0]=0:已经全部拜访,并且当前位置为 0 0 0,因此还需经过路径长度为0.

  • 核心代码:

void solve(int n)
{
    /*
    n: 需要拜访的城市数量
    road[i][j]: 城市i到城市j的距离,若没有路,则为INF。
    dp[i][j]: 当前到达城市节点集合为i, 且当前位于城市j,剩余的路程长度。
    */
    
    // 初始化dp数组
    for(int i=0; i<1<<n; i++)
        for(int j=0; j<n; j++)
            dp[i][j] = inf;
    
    dp[(1<<n)-1][0] = 0; 
    for(int i=(1<<n)-2; i>=0; i--)
    {
        for(int v=0; v<n; v++)
        {
            for(int u=0; u<n; u++)
            {
                // 使用移位运算和按位与判断元素是否存在于集合
                if(!(i >> u & 1))
                {
                	// 使用按位或运算模拟集合求并
                	// 由于没有路初始化为inf,因此没有判断v和u间是否存在路。
                    dp[i][v] = min(dp[i][v], dp[i | (1 << u)][u] + road[v][u]); 
                }
            }
        }
    }
}

例题二:Traveling by Stagecoach(Poj 2686)

在这里插入图片描述
在这里插入图片描述

  • 定义 d p [ T ] [ v ] dp[T][v] dp[T][v]: 当前所剩票集合为 T T T,且位于城市 v v v, 要到达 b b b还需要的最小花费。
  • 目标态 d p [ 2 n − 1 ] [ a ] dp[2^n-1][a] dp[2n1][a]: 从 a a a出发,且有题目给定的票数,到达 b b b的最小花费。
  • 状态转移:
    d p [ T ] [ v ] = m i n { d p [ T ∖ t ] [ u ] + r o a d [ v ] [ u ] / t } , t ∈ T , u ∈ N v ( N v 表 示 节 点 v 的 邻 居 结 点 ) dp[T][v] = min \{dp[T\setminus t][u] + road[v][u] / t\}, t \in T, u \in N_v(N_v表示节点v的邻居结点) dp[T][v]=min{dp[Tt][u]+road[v][u]/t},tT,uNv(Nvv)
  • 更新策略:由状态转移方程式可知, T T T是用 T ’ ( T ’ &lt; T ) T^’(T^’&lt;T) T(T<T)更新的, 因此按 T T T递增更新。
  • 初始化:
    • d p [ ∗ ] [ b ] = 0 dp[*][b] = 0 dp[][b]=0, 已经到达b,还需花费为0。
    • d p [ 0 ] [ x ] = i n f , x ! = b dp[0][x] = inf, x != b dp[0][x]=inf,x!=b: 若不是终点,且没有票,则无法到达,置inf。
  • 核心代码:
void solve(int n)
{
    /*
    1代表还有该票
    0代表没有该票
    dp[i][j]: 剩下车票状态i,现在在城市j到达b还需要的花费
    */
    for(int i=0; i<=m; i++){
        dp[0][i] = INF;
    }

    for(int i=0; i< (1<<n); i++)
        dp[i][b] = 0;

    for(int s=1; s<1<<n; s++)
    {
        for(int v=1; v<=m; v++)
        {
            for(int u=1; u<=m; u++)
            {
                if(grad[v][u] != INF)
                {
                    for(int t=0; t<n; t++)
                    {
                        if(s >> t & 1)
                            dp[s][v] = min(dp[s][v], dp[s & ~(1 << t)][u] + grad[v][u] / T[t]);
                    }
                }

            }
        }
    }
}

例题三:铺砖问题(Poj 2411)

此题比较开脑洞,参考了网上解法才算弄明白。简单说明一下解题思路。

  • 编码:
    在这里插入图片描述
    对铺好的砖进行编码,(1):横放,则两个格都为1。(2)竖放,则上面格为0,下面格为1可以证明编码可铺装方法是一一对应的的

  • 递推:

    • 根据编码,我们知道: (1): 铺砖的上一排和下一排一定有对应关系,必须按一定规则才算合法。(2):最后一排砖一定全为1
    • 假设我们已经知道: (1)倒数第二排所有编码对应的谱砖方法总数。 (2)最后一排对应编码的所有倒数第二排合法编码。 那我们就能够得到总数, 即为所有倒数第二排合法编码总数之和。(如下图, 2 × 2 2 \times 2 2×2的矩阵, 我们知道最后一排全一对应的合法倒数第二排为 11 11 11 00 00 00, 且它们对应的铺砖方法总数分别为 1 1 1 1 1 1, 因此总数量为 1 + 1 = 2 1+1=2 1+1=2)
      在这里插入图片描述
      而要求倒数第二排的数量,我们又需要倒数第三排的数量以及对应合法关系,因此逐层递推。
  • 求解对应关系:
    如何简便求解对应的合法关系? 考虑两排格子。对第二排的当前格子,我们有三种铺放方式:(1): 右铺。(2)不铺。(3)上铺
    在这里插入图片描述
    这样对下一排的编码方式进行深度优先搜索,我们就可以求出所有的上下两排对应合法编码。具体的,先将两排(top, down)都初始化为0。
    (1)右铺:则 t o p = ( t o p &lt; &lt; 2 ) ∣ 3 top = (top &lt;&lt; 2 ) | 3 top=(top<<2)3 d o w n = ( d o w n &lt; &lt; 2 ) ∣ 3 down = (down &lt;&lt; 2) | 3 down=(down<<2)3
    (2)上铺:则 t o p = ( t o p &lt; &lt; 1 ) top = (top &lt;&lt; 1) top=(top<<1) d o w n = ( d o w n &lt; &lt; 1 ) ∣ 1 down = (down &lt;&lt; 1) | 1 down=(down<<1)1
    (3)不铺: 则 t o p = ( t o p &lt; &lt; 1 ) ∣ 1 top = (top &lt;&lt; 1) | 1 top=(top<<1)1., d o w n = ( d o w n &lt; &lt; 1 ) down = (down &lt;&lt; 1) down=(down<<1)
    可以证明若不是刚好铺完长度为 m m m的铺法,则是不合法的, 反之则合法:
    在这里插入图片描述
    方便起见,我们人为设置第0排为全一,并将其数量置为1,因为这样设置第0排符合第一排的设定,因为第一排不可上铺。
    在这里插入图片描述

    现在回到求解动态规划的步骤:

    • 定义 d p [ i ] [ E ] dp[i][E] dp[i][E]: 第 i i i行的编码为 E E E时,前 i i i行所有铺砖方法的总数。
    • 目标态: d p [ n ] [ 2 m − 1 ] dp[n][2^m-1] dp[n][2m1], 最后一行全为1的铺法总数。
    • 状态转移: d p [ i ] [ E d o w n ] = ∑ d p [ i − 1 ] [ E t o p ] , E t o p ∈ N , 其 中 N 为 所 有 和 E d o w n 合 法 匹 配 的 上 一 行 dp[i][E_{down}] = \sum dp[i-1][E_{top}], E_{top} \in N, 其中N为所有和E_{down}合法匹配的上一行 dp[i][Edown]=dp[i1][Etop],EtopN,NEdown
    • 更新策略: 按 i i i从小到大更新。
    • 初态: d p [ 0 ] [ 2 m − 1 ] = 1 dp[0][2^m-1] = 1 dp[0][2m1]=1, 其余 d p [ 0 ] [ ∗ ] = 0 dp[0][*] = 0 dp[0][]=0
    • AC代码:
#include<iostream>
#include<cstring>
using namespace std;

typedef long long ll;
const int MAX = 1 << 13;

int ok_for_top_down[MAX][2]; 
//ok_for_top_down[i][0]表示第i个合法上下排的上
//ok_for_top_down[i][0]表示第i个合法上下排的下排

ll dp[12][MAX];
int n, m;
ll cnt;
void dfs_get_all_ok(int c, int top, int down)
{
    // 不合法
    if(c > m)
        return;

    // 合法
    if(c == m)
    {
        ok_for_top_down[cnt][0] = top;
        ok_for_top_down[cnt][1] = down;
        cnt++;
    }

    // 右铺
    dfs_get_all_ok(c+2, (top << 2) | 3, (down << 2) | 3);
    // 上铺
    dfs_get_all_ok(c+1, (top << 1), (down << 1)|1);
    // 不铺
    dfs_get_all_ok(c+1, (top << 1) | 1, (down << 1));
}
int main()
{
    while(cin >> n >> m)
    {
        if(n + m == 0)
            break;
        if(n < m)
            swap(n, m);
        cnt = 0;
        dfs_get_all_ok(0, 0, 0);

        memset(dp, 0, sizeof(dp));
        dp[0][(1<<m)-1] = 1;

        for(int i=1; i<=n; i++)
        {
            for(int k=0; k<cnt; k++)
            {
                int top = ok_for_top_down[k][0];
                int down = ok_for_top_down[k][1];
                dp[i][down] += dp[i-1][top];
            }
        }
        cout << dp[n][(1<<m)-1] << endl;
    }
    return 0;
}

  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值