状态压缩dp模型整理

状态压缩dp的核心在于将每一层视为一个状态,这些状态用二进制来表示,每一层由上一层转移而来,那么这么来看的话,我们只用预处理本层的每一个状态可以由上一层的哪些状态转移而来,然后由此来更新本层每个状态即可。这种一般由于状态不能选满,所以看似时间复杂度很高,但实际循环跑不满。

我们结合实际的例子来看一下这种题比较套路的解法:

基于连通的状态压缩dp

291. 蒙德里安的梦想(291. 蒙德里安的梦想 - AcWing题库)

思路:这里既要考虑横向的小方块,又要考虑纵向的小方块,显然太麻烦了,很容易发现,一旦横向的小方块放好了,那么纵向的小方块就只有一种放法。所以我们就来考虑横向的小方块怎么放。

有一个思路就是,我们一列一列来看,用一个二进制数来表示当前列的状态,0表示该该位对应的行的格子什么都没填,1表示有填,这里的填,我们定义成从上一列转移而来。那么我们更新当前列的每一个状态时,只用看一下上一列的情况即可。

假设当前列的状态为j,上一列的状态为k,那么j&k==0,因为我们当前这个j为1的位置,另一端是要延伸到上一列的,故而上一列的对应位置不能有从上上列延伸来的东西。另外j|k的值中不能有连续奇数个0,因为剩下的部分我们要用竖着的小方格来填满,如果为奇数,那么竖着的小方格填不了。对于这个我们只需要预处理一下每个状态中不合法的部分,有两种思路,一种是将不合法的状态标记一下,循环的时候,循环所有的状态,另一种是统计所有合法的状态,只循环合法状态,两种都可以,但是第二种很明显快一些,第二种的实现是在第一种的基础上做优化,我们这里先实现第一种思路,第二种加个数组就行。

另外,为了简化结果的统计,我们设定列从0-(m-1),但是我们状态计算计算到m,然后输出
dp[m][0],因为最后一列中肯定不能有越界的。

#include<bits/stdc++.h>
using namespace std;
long long dp[12][1<<12];
int st[1<<12];
int main()
{
    int n,m;
    while(cin>>n>>m)
    {
        if(!n&&!m) break;
        for(int i=0;i<1<<n;i++)
        {
            st[i]=1;
            int cnt=0;
            for(int j=0;j<n;j++)
            {
                if(i>>j&1)
                {
                    if(cnt&1) st[i]=0;
                    cnt=0;
                }
                else cnt++;
            }
            if(cnt&1) st[i]=0;
        }
        memset(dp,0,sizeof dp);
        dp[0][0]=1;
        for(int i=1;i<=m;i++)
        {
            for(int j=0;j<1<<n;j++)
            {
                for(int k=0;k<1<<n;k++)
                {
                    if(st[j|k] && (j&k)==0)
                    {
                        dp[i][j]+=dp[i-1][k];
                    }
                }
            }
        }
        cout<<dp[m][0]<<endl;
    }
}

 

1064. 小国王(活动 - AcWing

思路:

如图,在中间格子放上一个国王之后,周围八个位置都不能再放了,由于我们这里划分是一层一层划分的,所以本层是由上一层转移而来,那么先只看相邻的两行,那就是图1,然后我们保证每行行内是有效的,那就是2,3,4,然后我们就可以据此找出行与行之间可以成立的条件。

总结一下就是满足三个个条件:

1.每行内部不能有两个相邻的

2.两行之间对应位置求&结果是0

3.两行之间对应位置求异或,得到的结果也不能有两个相邻位置都是1

会发现1和3都要判断二进制数的相邻位置的情况,所以我们可以单独写一个函数来实现。

然后整体思路就是,我们先预处理出来每行可以放的合法方案,然后再预处理一下每个状态可以由哪些状态转移而来。

定义dp[i][j][a]为已经填完i行,用了j个国王,第i行的状态为a

据此dp[i-1][j-count(a)][b]则为已经填完i-1行,用了j-count(a)个国王,第i-1行的状态为b

很显然dp[i][j][a]可以由dp[i-1][j-count(a)][b]转移而来,而dp[i-1][j-count(a)][b]的前两维都确定了,只有第三维是不定的,第三维的所有合法情况都累加到dp[i][j][a],则为dp[i][j][a]的结果。那么就是判断状态a和b是否可以共存的问题。如果可以就能累加,否则就不行。而经过预处理后,我们可以直接枚举可以使用的b,进一步优化。最后为了再优化一下,不然就要循环a的值来找结果,我们直接计算到dp[n+1][m][0],那么第n层的所有方案都可以累加给它,直接输出这个值即可。另外记得给初态赋值,dp[0][0][0]=1,因为一个都不选也是一种方案。

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
ll dp[12][120][1<<10];
vector<int>sta;
vector<int>h[1<<10];
int n,m;

bool check(int s)
{
    for(int i=0;i<n;i++)
    {
        if((s>>i&1) && (s>>i+1&1)) return false;
    }
    return true;
}

int count(int x)
{
    int ans=0;
    for(int i=0;i<n;i++) ans += x>>i&1;
        //if(x>>i&1) ans++;
    return ans;
}

int cnt[1<<10];
int main()
{
    
    scanf("%d%d",&n,&m);//n是行数和列数
    for(int i=0 ; i<1<<n ; i++)
    {
        if(check(i)) 
        {
            sta.push_back(i);
            cnt[i]=count(i);
        }
    }
    

    for(int i=0;i<sta.size();i++)
    {
        for(int j=0;j<sta.size();j++)
        {
            int a=sta[i],b=sta[j];
            if(a&b) continue;
            if(check(a|b)) h[i].push_back(j);
        }
    }
    
    dp[0][0][0]=1;//
    for(int i=1;i<=n+1;i++)
    {
        for(int j=0;j<=m;j++)
        {
            for(int k=0;k<sta.size();k++)
            {
                for(auto it:h[k])
                {
                    int a=sta[k],b=sta[it];
                    if(j>=cnt[a])dp[i][j][a]+=dp[i-1][j-cnt[a]][b];
                }
            }
        }
    }
    cout<<dp[n+1][m][0];
}

327. 玉米田(327. 玉米田 - AcWing题库) 

思路:这里还是一行一行种,所以我们要看每行内部以及行与行之间的合法情况。

这个画出来就很清楚了,而且比上一种实际简单一点,因为每行内部考虑一下,然后行与行之间只要取&结果不为1即可。

那么就类似上面的思路,先将合法的状态预处理出来,再将合法转移预处理出来,然后推出来的表达式更新每一层每个状态时的值,而且由于这里没有数量上限,比上面还少一维。不过这里有一点要注意,每一行中有些地方不能种,那么其实我们也可以用把土地状况转化成一个二进制的数,然后在计算的时候与我们选择的状态取&来判断是否合法,如果合法再转移。

定义dp[i][a]表示填完第i行且第i行的状态为a时,能种的最大玉米数

dp[i-1][b]为填完i-1行且第i-1行状态为b时,能种的最大玉米数,

g[i]表示第i行的土地状态,如果不能种则对应的二进制位为1

if(!(a&g[i]))dp[i][a]+=dp[i-1][b]),枚举a,b(b可以直接用预处理出来的值)

也同上面一样,我们多处理一行,输出dp[n+1][0]的值

另外因为求和,所以需要对dp[0][0]预处理,dp[0][0]=1;

#include<bits/stdc++.h>
using namespace std;
const int M=1<<12;
vector<int>h[M];
vector<int>sta;
int g[20],dp[20][M];
int n,m;
const int mod=1e8;
int check(int sta)
{
    for(int i=0;i<m;i++)
    {
        if((sta>>i&1) && (sta>>i+1&1)) return 0;
    }
    return 1;
}
int main()
{
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++)
    {
        for(int j=0;j<m;j++)//二进制从第0位开始
        {
            int c;
            scanf("%d",&c);
            if(!c) g[i] += (1<<j); 
        }
    }
    for(int i=0;i < 1<<m;i++)
    {
        if(check(i))
        {
            sta.push_back(i);
        }
    }
    for(int i=0;i<sta.size();i++)
    {
        for(int j=0;j<sta.size();j++)
        {
            int a=sta[i],b=sta[j];
            if(a&b) continue;
            h[i].push_back(j);
        }
    }
    dp[0][0]=1;
    for(int i=1;i<=n+1;i++)
    {
        for(int j=0;j<sta.size();j++)
        {
            for(auto it:h[j])
            {
                int a=sta[j],b=sta[it];
                if(!(g[i]&a)) dp[i][a] = (dp[i][a]+dp[i-1][b])%mod;
            }
        }
    }
    cout<<dp[n+1][0];
}

 292. 炮兵阵地(292. 炮兵阵地 - AcWing题库)

这个题很像上面两个题的综合,或是说玉米田的进阶版,因为它的范围扩大了,我们不能只考虑上一行,需要考虑前两行,另外有些地方不能放。对于不能放的位置的处理和玉米田的处理一样,关键在于前两行这个转移该怎么看。

这里再引入一种集合定义方式,dp[i][a][b]表示填完第i行,第i-1行的状态为a,第i行的状态为b的情况。

那么它可以由什么转移而来呢,很显然是dp[i-1][c][b],只要a,b,c三者关系是合法的即可。

合法即(a&b)|(a&c)|(b&c) != 1

那么实际上就可以写了,我们只需要预处理每一行即可。  

而且我们注意到这里是求最多能放多少个,c有很多个状态,但我们实际上只能取使结果最大的一种,而且我们要注意到我们实际是用下标来表示状态的,而不是直接用a,b,c

因为我们求的不是方案数,所以不能相加。

#include<bits/stdc++.h>
using namespace std;
const int M=1<<10;
vector<int>sta;
int g[120];
int dp[2][M][M];
int n,m;
int cnt[M];
int check(int sta)
{
    for(int i=0;i<m;i++)
    {
        if( (sta>>i&1) && ( (sta>>i+1&1)||(sta>>i+2&1) ) ) return 0;
    }
    return 1;
}
int count(int x)
{
    int ans=0;
    for(int i=0;i<m;i++) ans+= (x>>i)&1;
    return ans;
}
int main()
{
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++)
    {
        string s;
        cin>>s;
        for(int j=0;j<m;j++)
        {
            if(s[j]=='H') g[i] += 1<<j;
        }
    }
    
    for(int i=0;i< 1<<m;i++)
        if(check(i))
        {    
            sta.push_back(i);
            cnt[i]=count(i);
        }   
    for(int i=1;i<=n;i++)
    {
        for(int x=0;x<sta.size();x++)//i
        {
            for(int y=0;y<sta.size();y++)//i-1
            {
                for(int z=0;z<sta.size();z++)//i-2
                {
                    int a=sta[x],b=sta[y],c=sta[z];
                    if(a&b | b&c | a&c ) continue;
                    if(g[i]&a | g[i-1]&b) continue;
                    dp[i&1][y][x]=max(dp[i&1][y][x],dp[i-1&1][z][y]+cnt[a]);
                }
            }
        }
    }
    int ans=0;
    for(int x=0;x<sta.size();x++)//i
    {
        for(int y=0;y<sta.size();y++)//i-1
        {
            ans = max(ans,dp[n&1][x][y]);
        }
    }
    cout<<ans;
}

基于集合的状态压缩dp

524. 愤怒的小鸟(524. 愤怒的小鸟 - AcWing题库

思路:我们先来理解下题目的意思,在平面直角坐标系的第一象限有若干个点,我们要用尽可能少的过原点且开口向下的抛物线去覆盖它们,问最少需要多少条。

所以首先我们要将所有有效的抛物线算出来,根据抛物线的式子,可以发现两点就能算出来一条抛物线,所以总共有n*n条抛物线。然后算出来的抛物线可能不仅仅经过这两个点,还能经过其他点,所以我们固定两个点,去计算经过它们的抛物线可以经过哪些点,作为状态记录下来。

然后我们用第i位上的0或者1表示第i个点有没有选上。那么状态就从0->111...1(n个1,转成十进制数就是2^n-1)。

这里我们定义dp[i]表示当前n个点的状态,然后从中找到一个没有被覆盖的点,然后去找经过这个点的抛物线,通过那条抛物线的状态得到当前的状态。然后去更新。 

#include<bits/stdc++.h>
using namespace std;
#define x first 
#define y second
typedef pair<double,double> pdd;
pdd q[20];//如果定义vector<pdd>记得清空
const int N=1<<18;
int f[N];
int p[20][20];
const double eps=1e-8;
int cmp(double a,double b)
{
    if(fabs(a-b)<eps) return 0;
    if(a<b) return -1;
    else return 1;
}
int main()
{
    int t;
    scanf("%d",&t);
    while(t--)
    {
        int n,m;
        scanf("%d%d",&n,&m);
        for(int i=0;i<n;i++) 
        {
            double x,y;
            scanf("%lf%lf",&x,&y);
            q[i].x=x,q[i].y=y;
        }
        memset(p,0,sizeof p);
        for(int i=0;i<n;i++)
        {
            p[i][i]=1<<i;
            for(int j=0;j<n;j++)
            {
                double x1=q[i].x,y1=q[i].y;
                double x2=q[j].x,y2=q[j].y;
                if(!cmp(x1,x2)) continue;
                double a=(y1/x1-y2/x2)/(x1-x2);
                double b=y1/x1-a*x1;
                if(cmp(a,0)>=0) continue;
                int s=0;
                for(int k=0;k<n;k++)
                {
                    double x=q[k].x,y=q[k].y;
                    if(!cmp(a*x*x+b*x,y))
                        s += 1<<k;
                }
                p[i][j]=s;
            }
        }
        memset(f,0x3f,sizeof f);
        f[0]=0;
        for(int i=0;i+1 < 1<<n;i++)//11..11不用循环
        {
            int x=0;
            for(int j=0;j<n;j++)
                if(!(i>>j&1))
                {
                    x=j;
                    break;
                }

            for(int j=0;j<n;j++)
                f[i|p[x][j]]=min(f[i]+1,f[i|p[x][j]]);
        }
        cout<<f[(1<<n)-1]<<endl;
    }
}

ps:另外这里有一点要注意,因为浮点数储存可能会出现误差,所以判断等于不等需要加上容错 

  • 22
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值