DP进阶(1)、状压DP

关于DP的入门之前已经说过了,所以就不再赘述。DP进阶系列会选取DP的一些难度较大的部分进行探讨,有状压DP、概率DP、树型DP、DP优化,包括斜率优化、FFT加速等。

今天主讲状压DP。我们都知道,DP的关键点在于“状态”。而这个状态就是用一组参数来表达的。状态压缩的作用就是将高维的状态压缩成低维的状态,从而能够简化我们的计算。常见的状态压缩就是二进制压缩。


部分材料来自艾神赞助,感谢艾吉奥(づ ̄ 3 ̄)づ

在了解状压DP之前,不得不先说一下位运算。

左移

    在很多语言里左移的符号位<<,有些语言和伪代码常用shl
    假设有个变量x=1;
    那么x=x<<1表示的意思如下:
    x:       0000000000000001(二进制表示)
    x<<1:0000000000000010 (也就是把x的每一位向左移动1位的结果,右边不够的用0添上)
    此时x=x<<1 ==2,其实可以看做乘2
    x<右移
    在很多语言里右移的符号位>>,有些语言和伪代码常用shr
    假设有个变量x=2;
    那么x=x>>1表示的意思如下:
    x:       0000000000000010(二进制表示)
    x>>1:0000000000000001 (也就是把x的每一位向右移动1位的结果,左边不够的用0添上)
    此时x=x>>1 ==1,其实可以看做除2
    x>>i 表示的就是右移i位,可以看做除 i 次2

或运算
    在很多语言里或运算的符号位 |,这个与逻辑运算符 || 是有区别的,而一些语言和伪代码也用 or 表示
    加上x=101 y=010(都是二进制表示)
    那么x|y表示如下
    x:      101
    y:      010
    x|y:   111
    其实也就是将x和y表示成二进制,然后按位做或运算
    此时x|y==7(10进制表示)

与运算
    在很多语言里或运算的符号位 &,这个与逻辑运算符 && 是有区别的,而一些语言和伪代码也用 and 表示
    加上x=101 y=010(都是二进制表示)
    那么x&y表示如下
    x:      101
    y:      010
    x&y:  000
    其实也就是将x和y表示成二进制,然后按位做与运算
    此时x&y==0(10进制表示)

异或运算
    在很多语言里或运算的符号位 ^,而一些语言和伪代码也用 xor 表示
    加上x=1010 y=1100 (都是二进制表示)
    那么x^y表示如下
    x:      1010
    y:      1100
    x^y:   0110
    其实也就是将x和y表示成二进制,然后按位做异或运算,也就是相同为0,不同为1
    此时x^y==6(10进制表示)

1.判断一个数字x二进制下第i位是不是等于1。
方法:if ( ( ( 1 << ( i  ) ) & x ) > 0)
将1左移i位,相当于制造了一个只有第i位上是1,其他位上都是0的二进制数。然后与x做与运算,如果结果>0,说明x第i位上是1,反之则是0。
2.将一个数字x二进制下第i位更改成1。
方法:x = x | ( 1<<(i) )

证明方法与1类似,此处不再重复证明。

上述位运算都是我们学习状压DP的基础。

常见的状压DP有几种:

1、TSP(旅行商问题)

2、网格密铺类问题

3、插头dp(珍爱生命,远离插头)

4、传统状压dp(珍爱生命,远离智商题)

我们以骨牌密铺问题为例。51Nod - 1033

题意:在m*n的一个长方形方格中,用一个1*2的骨牌排满方格。

问有多少种不同的排列方法。(2 <= m <= 10^9,2 <= n <= 5)答案mod 10^9 + 7


分析:题目的范围就不难看出状压DP的可行性。这儿使用dp[i][j]表示在连续的长为k的一段中,首部状态为i,尾部状态为j时从i到j转移有多少种骨牌的铺法。这里的k指的是dp的指数为当前的行数k。


我们用DFS中对DP进行初始化,此时k为1,然后对DP^(m+1)即可。当然这儿直接矩阵乘法会T,所以要矩阵快速幂搞一下。


代码:

#include 
#include 
#include 
#include 

using namespace std;

typedef long long ll;

const int mod = 1e9 + 7;
const int MAXN = 1 << 5;

ll dp[MAXN][MAXN];
ll ret[MAXN][MAXN];
ll tmp[MAXN][MAXN];

int m, n;

void dfs(int col, int pre, int now)
{
    if (col > n)
    {
        return ;
    }
    if (col == n)
    {
        dp[pre][now]++;
        return ;
    }

    dfs(col + 1, pre << 1, (now << 1) | 1);
    dfs(col + 1, (pre << 1) | 1, now << 1);
    dfs(col + 2, pre << 2 , now << 2);
}

void mul(ll ret[][MAXN], ll a[][MAXN], ll b[][MAXN])
{
    int t = 1 << n;
    for (int i = 0; i < t; i++)
    {
        for (int j = 0; j < t; j++)
        {
            ll tmp = 0;
            for (int k = 0; k < t; k++)
            {
                tmp += a[i][k] * b[k][j];
                tmp %= mod;
            }
            ret[i][j] = tmp;
        }
    }
}

int main()
{
    scanf("%d%d", &m, &n);

    dfs(0, 0, 0);

    int t = 1 << n;
    for (int i = 0; i < t; i++)
    {
        ret[i][i] = 1;
    }

    m++;
    while (m)
    {
        for (int i = 0; i < t; i++)
        {
            for (int j = 0; j < t; j++)
            {
                tmp[i][j] = ret[i][j];
            }
        }

        if (m & 1)
        {
            mul(ret, tmp, dp);
        }
        m = m >> 1;
        mul(tmp, dp, dp);
        for (int i = 0; i < t; i++)
        {
            for (int j = 0; j < t; j++)
            {
                dp[i][j] = tmp[i][j];
            }
        }
    }

    cout << ret[0][t - 1] << endl;

    return 0;
}


你可能感兴趣的:(算法)