状压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\times n^2\times2^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\n",dp[(1<<n)-1]);
    }
    return 0;
}

玉米田

题目链接

题意:一块\(n\times 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\n",ans);
    return 0;
}

炮兵阵地

题目链接

题意:在\(n \times 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\n",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\n",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\n",ans);
    }
    return 0;
}

转载于:https://www.cnblogs.com/Akaina/p/11374131.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值