状压dp

状压dp

当题目中的某一项的值极小(小于等于(20))时考虑使用状压(dp)

状压(dp)是二维时一般是

(dp[i][s])表示进行到第(i)步且第(i)步的状态为(s)时的(dp)

状压(dp)由于是二进制,多与位运算的操作有关

以下是一些常见的位运算操作:

(1.s&(1<<i)) 判断第(i)位是否为(1)

(2.s=s | (1<<i))把第i位设为(1)

(3.s=s&() ~ ((1<<i)))把第(i)位设为(0)

(4.s=s)^((1<<i))把第(i)位的值取反

(5.s=s&(s-1))(s)二进制下最靠右的一个(1)去掉

(6.for(s0=s;s0;s0=(s0-1)&s))枚举(s)的子集

(7.x&-x)取出最低位的(1)

例题

互不侵犯 洛谷P1896

(f[i][k][s])表示前(i)行放了(k)个国王,当前行状态为s的方案数

#include <iostream>
#include <cstdio>
using namespace std;
int n,King,tot=0;
int s[1050],pd[1050];
int num[15];
long long ans=0;
long long f[10][105][1050];
void Deal_first()
{
    for(int i=0;i<(1<<n);i++)
    {
        int cnt=0,x=i;
        pd[i]=1;
        while(x!=0)
        {
            num[++cnt]=x%2;
            x/=2;
        }
        for(int j=1;j<cnt;j++)
        {
            if(num[j]==num[j+1]&&num[j]==1)
            {
                pd[i]=0;break;
            }
            if(num[j]==1)s[i]++;
        }
        if(pd[i]==1)if(num[cnt]==1)s[i]++;
    }
}
int main()
{
    scanf("%d%d",&n,&King);tot=(1<<n)-1;
    Deal_first();
    for(int i=0;i<=tot;i++)if(pd[i]==1)f[1][s[i]][i]=1;
    for(int i=1;i<=n;i++)
    {
        for(int j=0;j<=tot;j++)
        {
            for(int k=0;k<=tot;k++)
            {
                if(!pd[j])continue;if(!pd[k])continue;
                if(((j|(j<<1)|(j>>1))&k)!=0)continue;
                for(int t=King;t>=s[j];t--)f[i][t][j]+=f[i-1][t-s[j]][k];
            }
        }
    }
    for(int i=0;i<=tot;i++)
    {
        if(pd[i])ans+=f[n][King][i];
    }
    printf("%lld",ans);
    return 0;
}

愤怒的小鸟 洛谷P2831

显然,因为原点加两个点即可确定抛物线,故抛物线的数量最多为(n^2)条,又因为(n<=18),

故考虑用状压(dp),枚举抛物线,复杂度为(O(T imes n^2 imes2^n))

但考虑到每个猪都要被打,所以可以从小到大枚举(S),每次每次打掉编号最小的还没消灭的猪,
由于包含该猪的抛物线只有(O(n))种,所以时间复杂度为(O(n*2^n))

#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cmath>
#include <cstring>
using namespace std;
#define lld long double
#define eps 1e-8
int dp[1<<19],line[20][20];
lld x[20],y[20];
int n,m;
int lowbit(int x)
{
    return x&(-x);
}
int main()
{
    int t;scanf("%d",&t);
    while(t--)
    {
    scanf("%d%d",&n,&m);
    lld z=eps;
    memset(dp,0x3f3f3f3f,sizeof(dp));
    memset(line,0,sizeof(line));
    dp[0]=0;
    for(int i=1;i<=n;i++)
    {
        //scanf("%lf%lf",&x[i],&y[i]);//double用这个
        cin>>x[i]>>y[i];//long double用这个
    }
    for(int i=1;i<=n;i++)
    {
        for(int j=1;j<=n;j++)
        {
            if(fabs(x[i]-x[j])<eps)continue;
            lld a=y[j]/(x[j]*(x[j]-x[i]))-y[i]/(x[i]*(x[j]-x[i]));
            lld b=(y[i]-a*x[i]*x[i])/x[i];
            if(a>-eps)continue;
            for(int k=1;k<=n;k++)
                if(fabs(a*x[k]*x[k]+b*x[k]-y[k])<eps)line[i][j]|=(1<<(k-1));
        }
    }
   for(int i=0;i<(1<<n);i++)
    {
        int x=-1;
        for(int j=1;j<=18;j++)
        {
            if((i&(1<<(j-1)))==0)
            {
                x=j;break;
            }
        }
        if(x==-1)continue;
        dp[i|(1<<(x-1))]=min(dp[i|(1<<(x-1))],dp[i]+1);
        for(int j=1;j<=n;j++)dp[i|line[x][j]]=min(dp[i|line[x][j]],dp[i]+1);
    }
    printf("%d
",dp[(1<<n)-1]);
    }
    return 0;
}

玉米田

题目链接

题意:一块(n imes m)的牧场,有的方格可以种草,有的不能种,且任意种草的方格不能有公共边,求方案数

(1<=n,m<=12)

(m)极小,故可以用状压(dp)
(f[i][j])表示前i行符合条件且第(i)行的状态为(j)的方案数
预处理(cnt[i])表示在不管其它行的情况下第(i)行符合条件的状态数,(g[i][j])表示第i行符合条件的第j个状态
在转移时需满足(g[i-1][j]&g[i][k]==0)

#include <iostream>
#include <cstdio>
using namespace std;
#define mol 100000000
int n,m;
int a[15][15];
int h[15],cnt[15];
int g[15][1<<15],f[15][1<<15];
void read()
{
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++)
    {
        for(int j=1;j<=m;j++)
        {
            scanf("%d",&a[i][j]);
        }
    }
    for(int i=1;i<=n;i++)
    {
        int sum=0;
        for(int j=1;j<=m;j++)
        {
            sum=sum*2+a[i][j];
        }
        h[i]=sum;
    }
}
void Deal_first()
{
    for(int i=1;i<=n;i++)
    {
        for(int j=0;j<=(1<<m)-1;j++)
        {
              int pd=1;
              if((j|h[i])==h[i])
              {
                  if(((j&(j<<1))==0)&&((j&(j>>1))==0))pd=0;
                  if(!pd)g[i][++cnt[i]]=j;
              }
        }
    }
    for(int i=1;i<=cnt[1];i++)f[1][g[1][i]]=1;
}
int main()
{
    read();
    Deal_first();
    for(int i=2;i<=n;i++)
    {
        for(int j=1;j<=cnt[i-1];j++)
        {
            for(int k=1;k<=cnt[i];k++)
            {
                if((g[i-1][j]&g[i][k])==0)
                {
                    f[i][g[i][k]]=(f[i][g[i][k]]+f[i-1][g[i-1][j]])%mol;
                }
            }
        }
    }
    int ans=0;
    for(int i=1;i<=cnt[n];i++)ans=(ans+f[n][g[n][i]])%mol;
    printf("%d
",ans);
    return 0;
}

炮兵阵地

题目链接

题意:在(n imes m)的网格地图中部署炮兵部队,只能在平原地形上部署,每支炮兵部队能攻击沿横向左右各两格,沿纵向上下各两格,求最多能部署多少支炮兵部队。

玉米田的变式题。

因为前面两行都会对当前行产生影响,故(dp)要多记录一维,但是(f[110][1024][1024])会炸空间

但发现每一行合法的状态最多(59)种,故只需用(f[110][70][70])来记录

预处理(cnt[i])表示在不管其它行的情况下第i行符合条件的状态数,(g[i][j])表示第i行符合条件的第j个状态

我一开始把状态编号和状态搞混了,调错调了半天。

#include <iostream>
#include <cstdio>
using namespace std;
int n,m;
char ch[110][15];
int h[110],cnt[110];
int g[110][1<<12];
int num[1<<12]={};
int f[110][70][70];
void read()
{
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++)scanf("%s",ch[i]+1);
}
void Deal_first()
{
    for(int i=1;i<=n;i++)
    {
        int sum=0;
        for(int j=1;j<=m;j++)
        {
            if(ch[i][j]=='P')sum=sum*2+1;
            else sum=sum*2;
        }
        h[i]=sum;
    }
    for(int i=1;i<=(1<<m)-1;i++)
    {
        int cnt=0;
        for(int j=1;j<=m;j++)
        {
            if((i&(1<<(j-1)))!=0)cnt++;
        }
        num[i]=cnt;
    }
    for(int i=1;i<=n;i++)
    {
        for(int j=0;j<=(1<<m)-1;j++)
        {
            if((j|h[i])==h[i])//
            {
                if((((j<<2)&j)==0)&&(((j<<1)&j)==0)&&(((j>>2)&j)==0)&&(((j>>1)&j)==0))g[i][++cnt[i]]=j;
            }
        }
    }
    for(int i=1;i<=cnt[1];i++)
    {
        for(int j=1;j<=cnt[2];j++)
        {
            if((g[1][i]&g[2][j])==0)f[2][j][i]=num[g[1][i]]+num[g[2][j]];
        }
    }
}
int main()
{
    read();
    Deal_first();
    for(int i=3;i<=n;i++)
    {
        for(int j=1;j<=cnt[i];j++)
        {
            for(int k=1;k<=cnt[i-1];k++)
            {
                if((g[i][j]&g[i-1][k])==0)
                {
                    for(int t=1;t<=cnt[i-2];t++)
                    {
                        if(((g[i][j]&g[i-2][t])==0)&&((g[i-1][k]&g[i-2][t])==0))
                        {
                            f[i][j][k]=max(f[i][j][k],f[i-1][k][t]+num[g[i][j]]);
                        }
                    }
                }
            }
        }
    }
    int ans=0;
    for(int j=1;j<=cnt[n];j++)
    {
        for(int k=1;k<=cnt[n-1];k++)
        {
            if((g[n][j]&g[n-1][k])==0)
            {
                ans=max(ans,f[n][j][k]);
            }
        }
    }
    printf("%d
",ans);
    return 0;
}

动物园

题目链接

题意:

动物园的围栏呈环状排列

每个小朋友可以看到从他所在位置开始数的连续5个围栏,每个小朋友有他害怕或喜欢的动物。

当下面两处情况之一发生时,小朋友就会高兴:

  • 至少有一个他害怕的动物被移走
  • 至少有一个他喜欢的动物没被移走

你可以选择去移走一些围栏,使得最多的小朋友高兴

围栏数为(N (1≤N≤10 000)),小朋友的个数为(C (1≤C≤50 000))

经过观察发现围栏数极小,故可以用状压(dp)

(f[i][j])表示第i个小朋友可以看到的动物被移走的状态为(j)

预处理(s[i][j])表示第i个位置的所有小朋友可以看到的动物被移走的状态为j时会高兴的小朋友数量

先枚举第n个小朋友的状态,再进行(dp)

(dp)方程为

        for(int j=1;j<=n;j++)
        {
            for(int k=0;k<32;k++)
            f[j][k]=max(f[j-1][(k&15)<<1],f[j-1][((k&15)<<1)|1])+s[j][k];
        }

整道题的代码:

#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
int n,m;
int s[10100][35];
int f[10100][35];
int ans=0;
int main()
{
    scanf("%d%d",&n,&m);
    for(int i=1;i<=m;i++)
    {
        int e,f,l;scanf("%d%d%d",&e,&f,&l);
        int x=0,y=0;
        for(int j=1;j<=f;j++)
        {
            int xx;scanf("%d",&xx);
            x=x|(1<<((xx-e+n)%n));
        }
        for(int j=1;j<=l;j++)
        {
            int xx;scanf("%d",&xx);
            y=y|(1<<((xx-e+n)%n));
        }
        for(int j=0;j<32;j++)
        {
            s[e][j]+=((x&j)||(y&((~j)&31)));
        }
    }
    for(int i=0;i<32;i++)
    {
        memset(f,-0x3f3f3f3f,sizeof(f));
        f[0][i]=0;
        for(int j=1;j<=n;j++)
        {
            for(int k=0;k<32;k++)f[j][k]=max(f[j-1][(k&15)<<1],f[j-1][((k&15)<<1)|1])+s[j][k];
        }
        ans=max(ans,f[n][i]);
    }
    printf("%d
",ans);getchar();
    return 0;
}

学校食堂

题目链接

同学们排队在食堂吃饭,每个同学想吃的菜都有一个特定的口味值T[i],每个同学的菜所需的时间为T[i]^T[j](T[j]表示食堂做的前一道菜的口味值),每个同学有一定的忍耐值B[i],表示他最多允许他身后的B[i]个人先拿到饭菜。

求在满足所有人的忍耐值的前提下,学校食堂做完这些菜最少需要多少时间。

对于100%的数据,满足(1 ≤ N ≤ 1,000,0 ≤ Ti ≤ 1,000,0 ≤ Bi ≤ 7。)

我们通过观察发现(B[i])的值极小,考虑状压(dp)

这道题最大的一个难点就是(dp)方程怎么列

首先我们考虑至少有一维是[i]且前i-1个人的菜已经做完了,肯定还有一维[j]用于状态压缩表示这个人和他身后的B[i]个人拿到菜的情况,又因为做菜时间与上一个拿到饭菜的人有关,所以再记录一维([k])表示上一个拿到饭菜的人相对第i个人的位置,经过分析可得k的范围是([-8,7])。(作为数组下标时需整体加8)

所以(f[i][j][k])表示前i-1个人已经拿到饭菜了,第(i)个人到第(i+B[i])个人拿到饭菜的情况为(j),上一个拿到饭菜的人为第(i+k)个人

考虑状态方程如何转移:

如果(j&1==1),那么(f[i][j][k])可以等价地转移到(f[i+1][j>>1][k-1])

如果(j&1==0),枚举下一个拿到饭菜的人,注意用(B[i])来限制枚举的范围

#include <iostream>
#include <cstdio>
#include <cstring>
using namespace std;
int n;
int T[1010],B[1010];
int f[1070][1<<10][20];
int main()
{
    int Case;scanf("%d",&Case);
    while(Case--)
    {
    scanf("%d",&n);
    for(int i=1;i<=n;i++)scanf("%d%d",&T[i],&B[i]);
    memset(f,0x3f3f3f3f,sizeof(f));
    f[1][0][7]=0;
    for(int i=1;i<=n;i++)
    {
        for(int j=0;j<=(1<<8)-1;j++)
        {
            for(int k=-8;k<=7;k++)
            {
                if(f[i][j][k+8]>=1e9)continue;
                if(j&1)f[i+1][j>>1][k+7]=min(f[i+1][j>>1][k+7],f[i][j][k+8]);
                else
                {
                    int cnt=1e9;
                    for(int t=0;t<=7;t++)
                    {
                        if(i+t>cnt)break;
                        if((j>>t)&1)continue;
                        cnt=min(cnt,i+t+B[i+t]);
                        f[i][j|(1<<t)][t+8]=min(f[i][j|(1<<t)][t+8],f[i][j][k+8]+((i+k>0)?(T[i+k]^T[i+t]):0));
                    }
                }
            }
        }
    }
    int ans=1e9;
    for(int i=-8;i<=0;i++)ans=min(ans,f[n+1][0][i+8]);
    printf("%d
",ans);
    }
    return 0;
}

原文地址:https://www.cnblogs.com/Akaina/p/11374131.html