状压dp

Posted stungyep

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了状压dp相关的知识,希望对你有一定的参考价值。

状态压缩DP

1.状态压缩的定义

状态压缩的定义:我们知道任何一个二进制都可以对应唯一的十进制数,反过来也成立。所以我们可以用一个数来代替一组数从而降低维数。这种解题手段我们叫做状态压缩。

举个例子:如果数组中的某一行全是0或全是1,例如000,001,我们可以将000用0表示,001用1来表示,这样一来就降低了维数,起到了状态压缩的作用。状态压缩dp就是基于此的dp。

2.位运算

状压dp与位运算关系密切,所以先了解一下位运算。

基本的一些位运算(如&,|,^,~,<<,>>)的基本用法就不多做介绍。来说一下它们的一些运用:

1:判断一个数x的二进制下第 i 位是不是1:if((1<<(i-1))&x>0)

2:将一个数x的二进制下第 i 位改为1:x=x|(1<<(i-1))

3:把一个数二进制下最靠右的第一个1去掉:x=x&(x-1)

......待补充

3.入门例题

1:poj-3254

题意:一个n*m的草地,1代表有草,0有没。两个草不能相邻,问有多少种选法。

状态:dp[i][state[j]]表示前 i 行,第 i 行采用第 j 个状态时得到的总方案数。所以可得状态转移方程:[dp[i][state(j)]=dp[i-1][state(k1)]+dp[i-1][state(k2)]+......+dp[i-1][state(kn)]。]

show code:

#include<bits/stdc++.h>

using namespace std;
typedef long long ll;
const int mod=1e8;
const int maxn=1e5+10;
int state[maxn],dp[15][maxn];
int arr[15][15],n,m,tot;
void init()                     //求出合理状态打个表
{
    int maxlen=1<<12;
    for(int i=0;i<maxlen;++i){
        if((i&(i<<1))==0)    state[++tot]=i;    //与运算要打括号!!!
    }
}
bool check(int row,int x)
{
    for(int i=1;i<=m;++i)
    {
        if(x&(1<<(i-1))&&!arr[row][i])      //检查一下x的所有位是否与已求出的合法状态冲突
            return false;
    }
    return true;
}

int main()
{
    init();
    while(scanf("%d%d",&n,&m)!=EOF)
    {
        for(int i=1;i<=n;++i)
            for(int j=1;j<=m;++j)
                scanf("%d",&arr[i][j]);
        memset(dp,0,sizeof(dp));
        for(int i=1;i<=n;++i)
        {
            for(int j=1;state[j]<(1<<m);++j)
            {
                if(!check(i,state[j]))  continue;      //保证下面的情况都与输入不冲突
                if(i==1){
                    dp[i][j]=1;
                    continue;
                }
                for(int k=1;state[k]<(1<<m);++k){     //根据状态转移方程而来
                    if((state[k]&state[j])==0)        //同一列没有两个1,两个数做与运算一定要打括号!!!!
                        dp[i][j]+=dp[i-1][k];
                }
            }
        }
        /*for(int i=1;i<=n;++i){
            for(int j=1;state[j]<(1<<m);++j)
                printf("%d ",dp[i][j]);
            printf("
");
        }*/
        ll res=0;
        for(int i=1;state[i]<(1<<m);++i)
            res=(res+dp[n][i])%mod;
        printf("%d
",res);
    }
    return 0;
}

2:poj-3311

题意:一个有向图,一个点可以走多次,问从0号点开始走把所有点都走一遍且最后回到原点的最短路径是多少。

#include<bits/stdc++.h>

using namespace std;
const int inf=0x3f3f3f3f;
const int maxn=15;
int arr[maxn][maxn];
int n,dp[1<<11][maxn];

int main()
{
    while(scanf("%d",&n)&&n)
    {
        for(int i=0;i<=n;++i)
            for(int j=0;j<=n;++j)
                scanf("%d",&arr[i][j]);
        for(int k=0;k<=n;++k)                 //预处理求出任意两点之间的最小距离
            for(int i=0;i<=n;++i)
                for(int j=0;j<=n;++j)
                    if(arr[i][k]+arr[k][j]<arr[i][j])
                        arr[i][j]=arr[i][k]+arr[k][j];
        for(int s=0;s<(1<<n);++s)       //s表示状态
        {
            for(int i=1;i<=n;++i)                       //状态要从1开始表示,但其实是第i+1号节点(从1开始编号的话)
            {
                if(s&(1<<(i-1)))
                {
                    if(s==(1<<(i-1)))           dp[s][i]=arr[0][i]; //如果刚好只到城市i,也是边界情况
                    else{
                        dp[s][i]=inf;
                        for(int j=1;j<=n;++j)                       //找一个合适的点使转过去之后距离最小
                            if ((s>>(j-1))&1 && j!=i )
                                dp[s][i]=min(dp[s][i],dp[s^(1<<(i-1))][j]+arr[j][i]);
                                //在s状态没有经过城市i中找一点,使得距离更短
                    }
                }
            }
        }
        int res=dp[(1<<n)-1][1]+arr[1][0];
        for(int i=2;i<=n;++i)
            res=min(res,dp[(1<<n)-1][i]+arr[i][0]);
        printf("%d
",res);
    } s
}

3:HDU-1074

题意:有多门作业,每门作业有截止日期和花费天数,没门作业超过截止期限一天就要减去1分,问最少减几分,并输出做作业顺序方案。

因为作业最多只有15个,所以可以用状压dp来记录,方案在dp中加个pre和now即可,用栈记录输出一下,代码:

#include<bits/stdc++.h>

using namespace std;
const int inf=1<<30;
const int maxn=1e6;
struct node
{
    string cur;
    int dead;
    int cost;
}arr[20];
int n,T;
struct Node
{
    int time;
    int score;
    int pre;
    int now;
}dp[maxn];

int main()
{
    ios::sync_with_stdio(false);
    cin>>T;
    while(T--)
    {
        memset(dp,0,sizeof(dp));
        cin>>n;
        for(int i=1;i<=n;++i)
            cin>>arr[i].cur>>arr[i].dead>>arr[i].cost;
        for(int s=1;s<(1<<n);++s)
        {
            dp[s].score=inf;
            for(int i=n;i>=1;--i)   //按字典序顺序输入,所以逆向枚举即可
            {
                int temp=1<<(i-1);                                  //看i是否完成
                if(s&temp)
                {
                    int past=s-temp;                                //past为除了i的剩余课程
                    int st=dp[past].time+arr[i].cost-arr[i].dead;   //st为扣得分数
                    if(st<0)    st=0;
                    if(dp[past].score+st<dp[s].score)
                    {
                        dp[s].score=st+dp[past].score;
                        dp[s].now=i;                                 //由i转移过来
                        dp[s].pre=past;
                        dp[s].time=dp[past].time+arr[i].cost;
                    }
                }
            }
            //cout<<"state is "<<s<<dp[s].score<<endl;
        }
        stack<int>  s;
        int t=(1<<n)-1;
        cout<<dp[t].score<<endl;
        while(t)
        {
            s.push(dp[t].now);
            t=dp[t].pre;
        }
        while(!s.empty())
        {
            cout<<arr[s.top()].cur<<endl;
            s.pop();
        }
    }
}

以上是关于状压dp的主要内容,如果未能解决你的问题,请参考以下文章

POJ1699 Best Sequence(AC自动机+状压DP)

状压DP之初尝插头DP

集合划分(状压DP)

dp-状压dp

动态规划---状压dp

种植方案(状压dp)