学习笔记:状态压缩dp

概念

状态压缩,和上一讲的状态机模型息息相关。因为状态的分类可能很多,比如一条路径上的每一条边选或者不选这种状态,如果直接手写数组,每条边设选和不选的状态会累死你,这时就需要状态压缩了。

方法

就拿上面的来举例,可以发现,这个问题只有 0 , 1 0,1 0,1两种状态,很像二进制的每一位,于是我们可以把每一条边的状态变成二进制的某一位,用这个二进制表示的十进制数来表示这些状态。找到某一条边是否选择直接查找某一位即可。

例题

AcWing 1064

这个题就是,每一个格子可以选择放还是不放,如果用数组直接做会很长一串,所以我们考虑状态压缩。可以先预处理出一行所有可能的放法,然后枚举每一行怎么选。设 f i , j , k f_{i,j,k} fi,j,k表示处理完了前 i i i行,第 i i i行选择的是第 j j j个放法,已经放了 k k k个国王。首先,枚举每一行和第 i − 1 i-1 i1行的放法,然后枚举新加的一行的放法。如果两行的方案放在一起会攻击,那么就不能这样放;如果不会攻击就枚举 s s s,表示前面几行放了多少国王。用 n u m j num_j numj表示第 j j j个方案放的个数,则新加了这一排的方案是 f i , j 2 , s + n u m j 2 f_{i,j2,s+num_{j2}} fi,j2,s+numj2,有状态转移方程 f i , j 2 , s + n u m j 2 + = f i − 1 , j 1 , s f_{i,j2,s+num_{j2}}+=f_{i-1,j1,s} fi,j2,s+numj2+=fi1,j1,s。我们要判断两行放在一起是否可行,则可以用一个 g g g数组来存压缩后的状态。最后答案就是处理完 n n n行且放完了 k k k个国王的所有方案,设 t o t tot tot为每一行的放法的总数,即 ∑ j = 0 t o t f n , j , k \displaystyle\sum_{j=0}^{tot}f_{n,j,k} j=0totfn,j,k。最后,我们可以考虑优化上述算法,发现如果某两个状态在某两行不适用,则在任意两行都不适用,则可以先预处理出使用的,避免重复计算适用的。

#include<bits/stdc++.h>
using namespace std;
const int NN=14,MM=1<<NN;
long long f[NN][MM][NN*NN];
int num[MM];
vector<int>state,can[MM];
int main()
{
    int n,k;
    scanf("%d%d",&n,&k);
    for(int i=0;i<1<<n;i++)
        if(!((i<<1)&i))
        {
            state.push_back(i);
            int t=i;
            while(t)
            {
                num[state.size()-1]++;
                t-=t&-t;
            }
        }
    for(int i=0;i<state.size();i++)
        for(int j=0;j<state.size();j++)
            if(!((state[i]&state[j])||(state[i]<<1&state[j])||(state[i]>>1&state[j])))
                can[i].push_back(j);
    f[0][0][0]=1;
    for(int i=1;i<=n;i++)
        for(int j1=0;j1<state.size();j1++)
            for(int j2=0;j2<can[j1].size();j2++)
                for(int use=0;use+num[j1]<=k;use++)
                    f[i][j1][use+num[j1]]+=f[i-1][can[j1][j2]][use];
    long long ans=0;
    for(int i=0;i<state.size();i++)
        ans+=f[n][i][k];
    printf("%lld",ans);
    return 0;
}

AcWing 327

这个题和上一题几乎没有区别,在判断两行放在一起时改成只判断有没有上下相邻的情况以及该放法放在了不可种植的土地上。因为本题没有要求玉米的数量,所以不用关心数量问题,直接转移即可,数组也可以省去表示数量的一维,循环也不用循环得到的个数。

#include<bits/stdc++.h>
using namespace std;
const int NN=16,MM=1<<NN;
int g[NN],f[NN][MM],n,m;
vector<int>state,can[MM];
int main()
{
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++)
        for(int j=0;j<m;j++)
        {
            int t;
            scanf("%d",&t);
            g[i]+=!t<<j;
        }
    for(int i=0;i<1<<m;i++)
        if(!((i<<1)&i))
            state.push_back(i);
    for(int i=0;i<state.size();i++)
        for(int j=0;j<state.size();j++)
            if(!(state[i]&state[j]))
                can[i].push_back(j);
    f[0][0]=1;
    for(int i=1;i<=n+1;i++)
        for(int j=0;j<state.size();j++)
            if(!(g[i]&state[j]))
                for(int k=0;k<can[j].size();k++)
                    (f[i][j]+=f[i-1][can[j][k]])%=100000000;
    printf("%d",f[n+1][0]);
    return 0;
}

AcWing 292

这个题目是互相攻击类的题目,很容易想到每一排的状态然后判断两排是否能放在一起。但是不难发现,本题行与行之间的限制有两排,则在 f f f数组中要存下最两排选择的是什么状态才可以确定是否可以新加某一行。设 f i , j , k f_{i,j,k} fi,j,k表示确定完了前 i i i行,倒数两行的状态分别是 j , k j,k j,k的最大放置个数。设 c n t u cnt_u cntu为选择状态 u u u能够多放的炮车数量,则有状态转移方程 f i , k , u = max ⁡ ( f i , k , u , f i − 1 , j , k + c n t u ) f_{i,k,u}=\max(f_{i,k,u},f_{i-1,j,k}+cnt_u) fi,k,u=max(fi,k,u,fi1,j,k+cntu)。然而我们发现,本题的数据范围较大,这样肯定会超过空间限制。我们又发现,第 i i i行的状态只依赖于第 i − 1 i-1 i1行,则可以考虑滚动数组,只用存下第 i i i行和第 i − 1 i-1 i1行的状态即可。

#include<bits/stdc++.h>
using namespace std;
const int NN=(1<<10)+4;
int g[104],cnt[NN],f[2][NN][NN];
vector<int>state;
int main()
{
    int n,m;
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++)
    {
        char s[14];
        scanf("%s",s);
        for(int j=0;j<m;j++)
            g[i]+=(s[j]=='H')<<j;
    }
    for(int i=0;i<1<<m;i++)
        if(!((i>>1&i)||(i>>2&i)))
        {
            state.push_back(i);
            int t=i;
            while(t)
            {
                cnt[state.size()-1]++;
                t-=t&-t;
            }
        }
    for(int i=1;i<=n;i++)
        for(int j=0;j<state.size();j++)
            for(int k=0;k<state.size();k++)
                for(int u=0;u<state.size();u++)
                {
                    int a=state[j],b=state[k],c=state[u];
                    if(!((a&b)||(a&c)||(b&c)||(g[i-1]&b)||(g[i]&c)))
                        f[i&1][k][u]=max(f[i&1][k][u],f[i-1&1][j][k]+cnt[u]);
                }
    int ans=0;
    for(int i=0;i<state.size();i++)
        for(int j=0;j<state.size();j++)
            ans=max(ans,f[n&1][i][j]);
    printf("%d",ans);
    return 0;
}

AcWing 524

不难发现,每个抛物线都可以覆盖一些点,我们可以统计每种抛物线覆盖了哪些点。但是抛物线的个数是无限的,但是只有选择过了某些点的抛物线才是有价值的,所以可以枚举过哪些点。抛物线的表达式 y = a x 2 + b x + c y=ax^2+bx+c y=ax2+bx+c,本题中 c = 0 c=0 c=0,要求覆盖某集合点的抛物线,则给定 x x x,求 a , b a,b a,b,两个未知数用两个式子即可,则两个点唯一确定一个抛物线,判断是否满足题目要求即可。每个点一定有任意一个抛物线经过它,则枚举的两个点相同时可以有一个只覆盖它一个点的抛物线。解决了预处理,就是计算最优的覆盖了。设 f i , j f_{i,j} fi,j为处理了前 i i i个抛物线,已经覆盖了 j j j的最小代价。因为一定要覆盖到每一个点,则可以枚举最前面且没覆盖的点,然后枚举用哪一个覆盖它的抛物线。设 g g g为抛物线能覆盖的点集,最前面没有覆盖的点为 x x x,则有 f i + 1 , j ∣ g x , j = min ⁡ ( f i , j + 1 , f i , j ∣ g x , j , f i + 1 , j ∣ g x , j ) f_{i+1,j|g_{x,j}}=\min(f_{i,j}+1,f_{i,j|g_{x,j}},f_{i+1,j|g_{x,j}}) fi+1,jgx,j=min(fi,j+1,fi,jgx,j,fi+1,jgx,j),发现 i + 1 i+1 i+1的状态只依赖于 i i i的状态,则可以省去 i i i的一维。最后,输出覆盖全部的最小代价即可。边界条件:只有一个点都不覆盖没有代价,其余都为正无穷。

#include<bits/stdc++.h>
using namespace std;
const int NN=1000004,MM=20;
int f[NN],g[MM][MM];
double x[MM],y[MM];
int cmp(double x,double y)
{
    if(abs(y-x)<1e-6)
        return 0;
    if(x<y)
        return -1;
    return 1;
}
int main()
{
    int t;
    scanf("%d",&t);
    while(t--)
    {
        int n;
        scanf("%d%*d",&n);
        for(int i=0;i<n;i++)
            scanf("%lf%lf",&x[i],&y[i]);
        memset(g,0,sizeof(g));
        for(int i=0;i<n;i++)
            for(int j=0;j<n;j++)
                if(i!=j)
                {
                    if(!cmp(x[i],x[j]))
                        continue;
                    double a=(y[i]/x[i]-y[j]/x[j])/(x[i]-x[j]),b=y[i]/x[i]-a*x[i];
                    if(cmp(a,0)>=0)
                        continue;
                    for(int k=0;k<n;k++)
                        g[i][j]+=!cmp(x[k]*x[k]*a+x[k]*b,y[k])<<k;
                }
                else
                    g[i][j]=1<<i;
        memset(f,0x3f,sizeof(f));
        f[0]=0;
        for(int i=0;i+1<1<<n;i++)
        {
            int sum;
            for(int j=0;j<n;j++)
                if(!(i>>j&1))
                {
                    sum=j;
                    break;
                }
            for(int j=0;j<n;j++)
                f[i|g[sum][j]]=min(f[i|g[sum][j]],f[i]+1);
        }
        printf("%d\n",f[(1<<n)-1]);
    }
}

AcWing 529

这个题不难发现,要想开发的代价最小,一定是一棵树,而且是最小生成树。但是本题加了一个条件,就是边权是从起点到这里经过的点数乘上该边的长度,起点是自选的,所以不能直接用最小生成树的板子做。考虑解决这个问题,发现问题的关键就在于距离,则设 f i , j f_{i,j} fi,j表示已经能到的点的压缩状态为 i i i,最远的点的距离是 j j j的一棵树。每次枚举该状态内的所有能到的点的一个子集,把子集中的点放在 j − 1 j-1 j1层之前,剩余的放在第 j j j层,计算这些点连接前面的点中的边最短的一条之和 r e s res res,就可以对于这个方案枚举 j j j,加上这一层的代价就是 j × r e s j\times res j×res。但是选出来的最短边有可能使得到该点的距离小于 j j j,则这里会多乘上很多次这条边,那么在之前就会有一种答案更小的方案在那一层就选了它,则不会出现问题。

#include<bits/stdc++.h>
using namespace std;
const int NN=16,MM=1<<NN;
int d[NN][NN],f[MM][NN],g[MM];
int main()
{
    int n,m;
    scanf("%d%d",&n,&m);
    memset(d,0x3f,sizeof(d));
    for(int i=0;i<n;i++)
        d[i][i]=0;
    for(int i=1;i<=m;i++)
    {
        int u,v,w;
        scanf("%d%d%d",&u,&v,&w);
        u--;
        v--;
        d[u][v]=d[v][u]=min(d[u][v],w);
    }
    for(int i=1;i<1<<n;i++)
        for(int j=0;j<n;j++)
            if(i>>j&1)
                for(int k=0;k<n;k++)
                    if(d[j][k]!=0x3f3f3f3f)
                        g[i]|=1<<k;
    memset(f,0x3f,sizeof(f));
    for(int i=0;i<n;i++)
        f[1<<i][0]=0;
    for(int i=0;i<1<<n;i++)
        for(int j=i-1;j;j=(j-1)&i)
            if((g[j]&i)==i)
            {
                int sum=i^j,res=0;
                for(int k=0;k<n;k++)
                    if(sum>>k&1)
                    {
                        int t=1e9;
                        for(int l=0;l<n;l++)
                            if((j>>l&1)&&t>d[k][l])
                                t=d[k][l];
                        res+=t;
                    }
                for(int k=1;k<n;k++)
                    f[i][k]=min(f[i][k],f[j][k-1]+res*k);
            }
    int ans=1e9;
    for(int i=0;i<n;i++)
        ans=min(ans,f[(1<<n)-1][i]);
    printf("%d\n",ans);
    return 0;
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值