状压DP

状压 $ DP $ 学习笔记

状压,状态压缩,好像很nb的样子

实际上,它就是利用二进制将只有 $ 0 $ 和 $ 1 $ 两种状态的一个序列压缩成一个数来存储。还是不好理解?举个例子,如果在一行棋盘上摆棋子,棋子只有摆与不摆两种状态,则 \((1011)_2\)\((11)_{10}\) 就表示,棋盘的第 $ 1,3,4 $ 个位置摆了棋子,这就是这一行棋子的状压。

做状压 $ DP $ ,首要的是明确状压什么。如果二进制状态有 $ n $ 位,那么实际存状态的十进制数大小就是 $ 2^n-1 $ ,显然, $ n $ 不能很大,所以可以根据数据范围来初步判断要状压谁

对于 $ DP $ 来说,还是要具体题目具体分析,进行状态转移

另外,由于状压用的是二进制,位运算就很重要了……


题单 (学校 $ OJ $ )


$ T_A $ 特殊方格棋盘

题意

在 $ n \times n (n \leqslant 20) $ 的方格棋盘上放置 $ n $ 个车,某些格子不能放,求使它们不能互相攻击的方案总数。

输入

输入文件第一行,有两个数 $ n $ 、 $ m $ , $ n $ 表示方格棋盘大小, $ m $ 表示不能放的格子数量
下面有 $ m $ 行,每行两个整数,为不能放的格子的位置。

2 1
1 1

输出 $ \ \ \ \ \ \ $ 方案总数

1

思路分析

在棋盘上放车,显然每行每列最多都只能放一个。那么我们用 $ dp_i $ 来表示 当前棋盘上车的状态为 $ i $ 时(若该列已放了车,二进制状态为 $ 1 $ 的方案总数

$AC\ \ CODE$
#include<bits/stdc++.h>
using namespace std;
const int N=(1<<20)+10;
#define ll long long
#define endl '\n'
ll n,m,f[N];
ll a[25];
int count(int x)
{
    int res=0;
    while(x){
        if(x&1)    res++;
        x>>=1;
    }
    return res;
}
int lowbit(int x){
    return x&(-x);
}
int main()
{
    ios::sync_with_stdio(false);
    cin.tie(0);cout.tie(0);
    cin>>n>>m;
    for(int i=1;i<=m;i++)
    {
        int u,v;
        cin>>u>>v;
        a[u]+=(1<<(v-1));//状压存储不能放的位置
    }
    f[0]=1;
    for(int i=1;i<=(1<<n)-1;i++)
    {
        int cnt=count(i);//这里cnt为放车的数量,巧的是,这刚好也是当前行数
        for(int j=i;j;j-=lowbit(j))//枚举每一个车
        {
            if(!(a[cnt]&lowbit(j)))//该车位置合法
            {
                int k=i^lowbit(j);
                f[i]+=f[k];
            }
        }
    }
    cout<<f[(1<<n)-1];
    return 0;
}

$ T_B $ 互不侵犯

题意

在 $ N \times N $ 的棋盘里面放 $ K $ 个国王,使他们互不攻击,求共有多少种摆放方案。国王能攻击到它上下左右,以及左上左下右上右下八个方向上附近的各一个格子,共 $ 8 $ 个格子。

$ N \leqslant 9 , k \leqslant N $

输入 $ {\ \ \ \ \ \ \ \ \ }n $ , $ k $

3 2

输出 $ {\ \ \ \ \ \ \ \ \ } $ 方案数

16

思路分析

$ N \leqslant 9 $ ,这么好的范围,必须状压每一行的国王状态啊。国王会攻击左右,因此并不是所有状态都合法,于是我们可以预处理出合法状态。利用一个 $ check() $ 函数检查是否合法,设当前检查的状态为 $ a $ ,由于国王会攻击左一位和右一位,所以只要检验 \(a \&\) $ (a>>1) $ 以及 \(a \&\) \((a<<1)\) 是否有冲突,如果没有,显然是合法的

下一步,设定 $ dp $ 的状态,令 $ dp_{i,k,j} $ 表示当前枚举到第 $ i $ 行,已经摆了 $ k $ 个国王,最后一行状态为 $ j $

接下来进行 $ DP $ ,由于国王会攻击上下,所以枚举第 $ i $ 行和第 $ i-1 $ 行的状态 $ a,b $ ,检查一下 $ a,b $ 是否冲突,同理。

最后推出转移方程,显然第 $ i $ 行的 $ dp $ 只与第 $ i-1 $ 行有关

第 $ i $ 行,状态为 $ \ i\ ,\ k\ ,\ a\ ,\ \ \ dp_{i,k,a} $

第 $ i-1 $ 行,状态为 $ \ i-1\ ,\ k-cnt(a)\ ,\ b\ \ \ dp_{i-1,k-cnt(a),b} $

加法原理,转移方程则为:

\[dp_{i,k,a}=\sum_{k=1}^m dp_{i-1,k-cnt(a),b} \ \ (k \geqslant cnt(a)) \]

$AC\ \ CODE$
#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define endl '\n'
ll n,m;
ll dp[10][100][1<<10];
ll king[1000],cnt[1000],sum;
bool check_self(ll a)
{
    if(a&(a>>1))  return 0;
    if(a&(a<<1))  return 0;
    return 1;
}

bool check_ab(ll a,ll b)
{
    if(a&(b>>1))  return 0;
    if(a&(b<<1))  return 0;
    if(a&b)  return 0;
    return 1;
}
ll count(ll x)
{
    ll res=0;
    while(x){
        if(x&1)  res++;
        x>>=1;
    }
    return res;
}

int main()
{
    ios::sync_with_stdio(false);
    cin.tie(0);cout.tie(0);
    cin>>n>>m;
    ll MAXN=(1<<n)-1;
    for(ll i=0;i<=MAXN;i++)
        if(check_self(i))//预处理合法状态
        {
            king[++sum]=i;
            cnt[sum]=count(i);
            dp[1][cnt[sum]][i]=1;//初始化第一行
        }
    for(ll i=2;i<=n;i++)
        for(ll ja=1;ja<=sum;ja++)
            for(ll jb=1;jb<=sum;jb++)
                for(ll k=0;k<=m;k++)
                {
                    ll a=king[ja];
                    ll b=king[jb];
                    if(check_ab(a,b)&&k>=cnt[ja])
                    {
                        dp[i][k][a]+=dp[i-1][k-cnt[ja]][b];
                    }
                }
    ll ans = 0;
    for (ll i=0;i<=MAXN;++i)
        ans+=dp[n][m][i];
    cout<<ans;
    return 0;
}

$ T_C $ 炮兵阵地

题意

司令部的将军们打算在 的网格地图上部署他们的炮兵部队。一个 $ N \times M $ 的地图由 $ N $ 行 $ M $ 列组成,地图的每一格可能是山地(用 $ H $ 表示),也可能是平原(用 $ P $ 表示),如下图。在每一格平原地形上最多可以布置一支炮兵部队(山地上不能够部署炮兵部队);一支炮兵部队在地图上的攻击范围如图中黑色区域所示:

如果在地图中的灰色所标识的平原上部署一支炮兵部队,则图中的黑色的网格表示它能够攻击到的区域:沿横向左右各两格,沿纵向上下各两格。图上其它白色网格均攻击不到。从图上可见炮兵的攻击范围不受地形的影响。
现在,将军们规划如何部署炮兵部队,在防止误伤的前提下(保证任何两支炮兵部队之间不能互相攻击,即任何一支炮兵部队都不在其他支炮兵部队的攻击范围内),在整个地图区域内最多能够摆放多少我军的炮兵部队。

输入

第一行包含两个由空格分割开的正整数,分别表示 $ N $ 和 $ M $ ;
接下来的 $ N $ 行,每一行含有连续的 $ M $ 个字符( $ P $ 或者 $ H $ ),中间没有空格。按顺序表示地图中每一行的数据。

5 4
PHPP
PPHH
PPPP
PHPP
PHHP

输出 $ {\ \ \ \ \ } $ 最多能摆放的炮兵队数

6

思路分析

有了前面两个题的经验,我们肯定要状压存储每一行山地的状态 $ hill[i] $ 。此题就是 $ T_B.plus $ ,道理差不多,只要躲判断一个山地的影响就好了,自己根据 $ T_B $ 推一下,不一样的是,要用 $ dp_{i,j,k} $ 表示到第 $ i $ 行,本行状态为 $ j $ ,上一行状态为 $ k $
转移方程去看\(shadow\)的吧,相信如果你能看懂你就不需要看我的题解了

$AC\ \ CODE$
#include<bits/stdc++.h>
using namespace std;
#define endl '\n'
#define MAXN (1<<m)-1
int n,m;
int hill[110];
int dp[110][110][110];
int p[1000],cnt[1000],sum;
int ans;
bool check_self(int a)
{
    if(a&(a<<1) || a&(a<<2) || a&(a>>1) || a&(a>>2))
        return 0;
    return 1;
}

int count(int x){
    int res=0;
    while(x)
    {
        if(x&1) res++;
        x>>=1;
    }
    return res;
}
int main()
{
    ios::sync_with_stdio(false);
    cin.tie(0);cout.tie(0);
    cin>>n>>m;
    char c;
    for(int i=1;i<=n;i++)
        for(int j=1;j<=m;j++)
        {
            cin>>c;
            if(c=='H')
                hill[i]+=(1<<(j-1));
        }
    for(int i=0;i<=MAXN;i++)
        if(check_self(i))
        {
            p[++sum]=i;
            cnt[sum]=count(i);
            if(!(i & hill[1]))//初始化第1行,与地形不冲突
                dp[1][sum][0]=cnt[sum];
        }
    for(int i=2;i<=n;i++)
        for(int j=1;j<=sum;j++)//第i行状态->j
        {
            if(p[j] & hill[i])
                continue;
            for(int k=1;k<=sum;k++)//第i-1状态->k
            {
                if(p[k] & hill[i-1])
                    continue;
                if(p[k] & p[j])
                    continue;
                if( i == 2 )//代码出了点小bug,特判一下
                    dp[i][j][k]=max(dp[i][j][k],dp[i-1][k][0]+cnt[j]);
                for(int l=1;l<=sum;l++)//第i-2行状态->l
                {
                    if(p[l] & hill[i-2]) 
                        continue;
                    if(p[k] & p[l])
                        continue;
                    if(p[j] & p[l]))
                        continue;
                    //保证都不会冲突
                    dp[i][j][k]=max(dp[i][j][k],dp[i-1][k][l]+cnt[j]);
                }
            }
        }
        //枚举i代替p[i],节省空间
    for(int i=1;i<=sum;i++)
        for(int j=1;j<=sum;j++)
            ans=max(ans,dp[n][i][j]);
    cout<<ans;
    return 0;
}

$ T_D $ 旅游景点

题意

题面太 $ TM $ 长了,概括一下题意:有一个 $ n $ 个节点的带权图, $ 1 $ 为起点, $ n $ 为终点,图中的 $ 2...k+1 $ 为必经点,同时有 $ q $ 组限制,每组包含一个 $ a,b $ ,要求 $ a $ 点必须在 $ b $ 点前经过,求最短路径

输入

第一行包含3个整数 $ N(2\leqslant N\leqslant 20000),M(1\leqslant M\leqslant 200000),K(0\leqslant K\leqslant 20) $ ,意义如上所述。以下 $ M $ 行,每行包含 $ 3 $ 个整数 $ X,Y,Z,(1\leqslant X,Y\leqslant n,0 < Z\leqslant 1000) $ ;

接下来一行,包含一个整数 $ q $ ,表示有 $ q $ 个限制条件 $ (0\leqslant q < n) $ 。以下 $ q $ 行,每行两个整数 $ f,l(1\leqslant l,f\leqslant n) $ ,表示在 $ f $ 停留的时候要在 $ l $ 之前。

8 15 4
1 2 3
1 3 4
1 4 4
1 6 2
1 7 3
2 3 6
2 4 2
2 5 2
3 4 3
3 6 3
3 8 6
4 5 2
4 8 6
5 7 4
5 8 6
3
2 3
3 4
3 5

输出 $ {\ \ \ \ \ \ } $ 最短路径

19

思路分析

$ 2...k+1 $ 节点必经,可以 $ Dijk $ 预处理 $ 2...k+1 $ 之间的的最短路,然后状压已经经过的必经点状态,即 $ dp_{i,j} $ 表示跑到 $ i $ 节点,经过的必经点状态为 $ j $ 的最短路径长度。
转移方程见代码。

$AC\ \ CODE$
#include<bits/stdc++.h>
using namespace std;
const int N=2000010;
#define ll long long
#define endl '\n'
#define MAXN (1<<k)-1
int n,m,k;
struct EDGE{
    int next,to,w;
}e[N];
int tot,head[N];
void add(int u,int v,int w)
{
    tot++;
    e[tot].to=v;
    e[tot].next=head[u];
    head[u]=tot;
    e[tot].w=w;
}
int s[25][25];
int dis[100010];
bool f[100010];
int dp[25][1<<20];
void Dijk(int u)//朴素Dijk会T,要用堆优化
{
    memset(dis,0x3f,sizeof(dis));
    memset(f,0,sizeof(f));
    priority_queue<pair<int,int> > q;
    q.push(make_pair(0,u));
    dis[u]=0;
    while(q.size()){
        int x=q.top().second;
        q.pop();
        if(!f[x])
        {
            f[x]=1;
            for(int i=head[x];i;i=e[i].next){
                int y=e[i].to;
                if(dis[y]>dis[x]+e[i].w){
                    dis[y]=dis[x]+e[i].w;
                    q.push(make_pair(-dis[y],y));
                }
            }
        }
        
    }
    for(int i=1;i<=k+1;i++)
         s[i][u]=s[u][i]=dis[i];
    s[u][0]=dis[n];

}
int front[25];
int main()
{
    ios::sync_with_stdio(false);
    cin.tie(0);cout.tie(0);
    cin>>n>>m>>k;
    memset(dp,-1,sizeof(dp));
    dp[1][0]=0;
    for(int i=1;i<=m;i++)
    {
        int a,b,c;
        cin>>a>>b>>c;
        add(a,b,c);
        add(b,a,c);
    }
    for(int i=1;i<=k+1;i++)
        Dijk(i);
    int q;cin>>q;
    while(q--)
    {
        int a,b;
        cin>>a>>b;
        front[b]+=(1<<(a-2));//记录必须在b之前经过的点
    }
    for(int now=0;now<=MAXN;now++)//现在的状态
        for(int i=1;i<=k+1;i++)//枚举当前在的点
            if(dp[i][now]!=-1)
            {
                for(int j=2;j<=k+1;j++)//枚举下一个跑的点
                {
                    int reach=(now|(1 << (j-2)));
                    if((now & front[j])==front[j])//已经符合要求
                    //位运算一定要加括号……呜呜呜
                        if(dp[j][reach]>dp[i][now]+s[i][j] || dp[j][reach]==-1)
                           dp[j][reach]=dp[i][now]+s[i][j];
                }
            }
    int ans=1e9;
    for(int i=1;i<=k+1;i++)
    {
        if(dp[i][MAXN]!=-1)
            ans=min(ans,dp[i][MAXN]+s[i][0]);
    }
    cout<<ans;
    return 0;
}

学校OJ空间限350MB,但是某LOJ和某谷都限64MB,交上去直接MLE,乐,我也懒得改了


$ T_E $ 愤怒的小鸟

题意

Kiana 最近沉迷于一款神奇的游戏无法自拔。
简单来说,这款游戏是在一个平面上进行的。有一架弹弓位于 \((0,0)\) 处,每次 Kiana 可以用它向第一象限发射一只小鸟,小鸟们的飞行轨迹均为形如 \(y=ax^2+bx\) 的曲线,其中 \(a,b\) 是 Kiana 指定的参数,且必须满足 \(a<0\) 。当小鸟落回地面(即\(x\)轴)时,它就会瞬间消失。

在游戏的某个关卡里,平面的第一象限中有\(n\)只猪,其中第\(i\)只猪所在的坐标为 \((x_i,y_i)\) 。如果某只小鸟的飞行轨迹经过了 \((x_i,y_i)\) ,那么第\(i\)只猪就会被消灭掉,同时小鸟将会沿着原先的轨迹继续飞行;否则这只小鸟飞行的全过程就不会对第\(i\)只猪产生任何影响。

而这个游戏的目的,就是通过发射小鸟消灭所有的猪。

这款神奇游戏的每个关卡对来说都很难,所以 Kiana 还输入了一些神秘的指令,使得自己能更轻松地完成这个游戏。这些指令将在「输入格式」中详述(没用)。
假设这款游戏一共有\(T\)个关卡,现在 Kiana 想知道,对于每一个关卡,至少需要发射多少只小鸟才能消灭所有的猪。由于她不会算,所以希望由你告诉她。

输入

第一行包含一个正整数\(T\),表示游戏的关卡总数。
下面依次输入这\(T\)个关卡的信息。每个关卡第一行包含两个非负整数\(n,m\),分别表示该关卡中的猪数量和 Kiana 输入的神秘指令类型。(m没用)
接下来的\(n\)行中,第\(n\)行包含两个正实数,表示第\(i\)只猪坐标为 \((x_i,y_i)\)。数据保证同一个关卡中不存在两只坐标完全相同的猪。

3
2 0
1.41 2.00
1.73 3.00
3 0
1.11 1.41
2.34 1.79
2.98 1.49
5 0
2.72 2.72
2.72 3.14
3.14 2.72
3.14 3.14
5.00 5.00

\(n∈[1,18],m∈[0,2],x_i,y_i∈(0,10)\)

输出 \({\ \ \ \ \ \ \ }\) 一个正整数,表示相应的关卡中,消灭所有猪最少需要的小鸟数量。

2
2
3

思路分析

首先预处理出一条抛物线能打下的小鸟状态,用 \(L_{i,j}\) 表示过\(i,j\)两鸟的抛物线能打下的鸟,状压存储。枚举\(spx\)为已打下的鸟的状态,枚举\(i,j\),那么

\[dp_{spx | L_{i,j}}=min(dp_{spx | L_{i,j}},dp_{spx}+1) \]

怎样预处理\(L\)?枚举\(i,j\),解二元一次方程,解出\(a,b\),再枚举\(k\),如果\(a,b\)分别相等,\(L_{i,j}++\)

$AC\ \ CODE$
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define endl '\n'
#define MAXN (1<<n)-1
double x[20],y[20];
int L[20][20];
int dp[1<<20];
int n,m;
double e=1e-10;
double get_a(int i,int j)
{
    return (x[j]*y[i]-x[i]*y[j])/(x[i]*x[i]*x[j]-x[i]*x[j]*x[j]);
}
double get_b(int i,int j)
{
    return (x[j]*x[j]*y[i]-x[i]*x[i]*y[j])/(x[i]*x[j]*x[j]-x[i]*x[i]*x[j]);
}

signed main()
{
    ios::sync_with_stdio(false);
    cin.tie(0);cout.tie(0);
    int T;cin>>T;
    while(T--)
    {
        memset(dp,0x3f,sizeof(dp));
        memset(L,0,sizeof(L));
        cin>>n>>m;
        for(int i=1;i<=n;i++)
        {
            cin>>x[i]>>y[i];
        }
        for(int i=1;i<=n;i++)
        {
            for(int j=1;j<=n;j++)
            {
                if(x[i]==x[j])  continue;
                double a=get_a(i,j);
                double b=get_b(i,j);
                if(a>0)  continue;
                L[i][j]=L[i][j] | (1<<(i-1));
                L[i][j]=L[i][j] | (1<<(j-1));
                for(int k=1;k<=n;k++)
                {
                    if(x[k]==x[i]||x[k]==x[j]) continue;
                    double a1=get_a(i,k);
                    double b1=get_b(i,k);
                    if(a>0)  continue;
                    // if(a==a1 && b==b1)//显然这样写不行
                    if(fabs(a-a1)<=e && fabs(b-b1)<=e)
                        L[i][j]=L[i][j] | (1<<(k-1));
                }
            }
        }
        dp[0]=0;
        for(int spx=0;spx<MAXN;spx++)
        {
            for(int i=1;i<=n;i++)
            {
                if(spx & (1<<(i-1)))
                    continue;
                for(int j=1;j<=n;j++)
                {
                    if(spx & (1<<(j-1)))
                        continue;
                    if(!L[i][j])
                        continue;
                    dp[spx | L[i][j]]=min(dp[spx | L[i][j]],dp[spx]+1);
                }
                dp[spx | (1<<(i-1))]=min(dp[spx | (1<<(i-1))],dp[spx]+1);
            }
        }
        cout<<dp[MAXN]<<endl;
    }

    return 0;
}

$ T_F $ 动物园

题意

简要概括就是,有一圈动物,还有一圈小朋友,小朋友能看见五个动物,有喜欢的有不喜欢的。我们做一些调整,当下面两处情况之一发生时,小朋友就会高兴:

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

输入输出 \({ \ \ \ \ \ \ \ }\)

思路分析

\(N,C\)显然不能状压,唯一可以考虑状压的就是小朋友看到的\(5\)个围栏的状态,用\(1\)表示移走,\(0\)表示未移走。预处理一个 \(happy_{i,spx}\) 表示在第\(i\)个位置到第\(i+5\)个位置移走动物状态为\(spx\)时能够让多少小朋友开心。怎样处理?看上面两个条件:

  • 至少有一个他害怕的动物被移走;$ \Leftrightarrow spx$ $&\ $ \(fear\) 为真
  • 至少有一个他喜欢的动物没被移走。$ \Leftrightarrow \ \(~\)spx$ $&\ $ \(love\) 为真

这个位运算还是相当好推的, 满足时将\(happy_{i,spx}\)++

接下来就是\(DP\)了,枚举当前状态为\(k\),再枚举所在位置\(i\),接着枚举移走动物的状态为\(spx\),那么\(i-1\)位置的移走状态为 \(spx \& \ 15\) 或者 \((spx \& \ 15)|1\),因为,\(spx \& \ 15\) 显然是取出\(spx\)的后\(4\)位。另外这里由于是一个环,所以我们把\(n\)映射到\(0\)。转移方程:

\[dp_{i,spx}=\max( dp_{i-1(spx \& 15)<<1,} , dp_{i-1,(spx \& 15)<<1|1})+happy_{i,spx} \]

$AC\ \ CODE$
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const int N=1e4+10;
#define MAXN (1<<5)-1
#define endl '\n'
int n,m;
int e,fear,love,f,l;
int happy[N][35];
int dp[N][35];
int ans;
int main()
{
    ios::sync_with_stdio(false);
    cin.tie(0);cout.tie(0);
    cin>>n>>m;
    for(int i=1;i<=m;i++)
    {
        cin>>e>>f>>l;
        fear=love=0;
        int t;
        for(int j=1;j<=f;j++)
        {
            cin>>t;
            t=(t-e+n)%n;
            fear=fear | (1<<t);
        }
        for(int j=1;j<=l;j++)
        {
            cin>>t;
            t=(t-e+n)%n;
            love=love | (1<<t);
        }
        for(int spx=0;spx<=MAXN;spx++)
        {
            if((spx & fear)||((~spx) & love))
            {
                happy[e][spx]++;
            }
        }
    }
    for(int k=0;k<=MAXN;k++)
    {
        memset(dp,-0x3f,sizeof(dp));
        dp[0][k]=0;//这个还是很巧妙的,因为第n位的后四位恰好为前四位,所以考虑把n映射到0上
        for(int i=1;i<=n;i++)
            for(int spx=0;spx<=MAXN;spx++)
                dp[i][spx]=max(dp[i-1][(spx&15)<<1],dp[i-1][(spx&15)<<1|1])+happy[i][spx];
        ans=max(ans,dp[n][k]);
    }
    cout<<ans;
    return 0;
}
---
posted @ 2024-03-03 17:00  lty_ylzsx  阅读(22)  评论(2编辑  收藏  举报