状压dp 学习笔记

"此刻发生的所有事,都是你过去选择的结果。"

最近打模拟赛在状压dp上总是没有一点思路。来重学一遍。

 

状态压缩:通过一串 01 码来清晰地表示一个集合的状态。同时,在确定了最低位的前提下,一串 01 码与一个二进制数一一对应。

 

其本质上是进行了两次操作:

  1. 给这个集合的每个状态一个编号。
  2. 通过这个编号,轻易地访问该状态。

 

状压dp常见题型:

  1. 棋盘式,求方案数。
  2.  集合,求最短路径。

 

首先,我们需要掌握一些位运算的小技巧:

1.判断一个数字x二进制下第i位是不是等于1。(最低第 1 位)

方法:if(((1<<(i−1))&x)>0)

将1左移 i -1 位,相当于制造了一个只有第 i 位上是 1,其他位上都是 0 的二进制数。然后与 x 做与运算,如果结果 > 0, 说明 x 第 i 位上是 1,反之则是 0。

2.将一个数字x二进制下第 i 位更改成 1。

方法:x=x|(1<<(i−1))

证明方法与 1 类似。

3.将一个数字x二进制下第 i 位更改成 0。

方法:x=x&~(1<<(i−1))

4.把一个数字二进制下最靠右的第一个1去掉。

方法:x=x&(x−1)

 

下面是例子。

1.P2622 关灯问题

题意略。

可以发现n的范围<=10。此时想到状压dp的时间复杂度为指数级别,刚好符合本题要求。

考虑从0开始不断向上枚举,遍历 m 个控制效果。通过某个控制效果可以达到状态 t ,那么设 fs 表示到达状态 s 时所需要的最小步数。

复制代码
#include<bits/stdc++.h>
using namespace std;
const int inf=0x3f3f3f3f;
const int maxn=12,maxm=1025;
int n,m,t,f[1<<maxn],a[maxm][maxn];
int main(){
    cin>>n>>m;
    for(int i=1;i<=m;i++)
        for(int j=1;j<=n;j++)
            cin>>a[i][j];
    fill(f+1,f+maxm+1,inf);
    for(int i=0;i<(1<<n);i++)
        for(int k=1;k<=m;k++){
            t=i;
            for(int j=0;j<n;j++)
                if((a[k][j+1]==1&&!(i&1<<j))||(a[k][j+1]==-1&&(i&1<<j)))
                    t^=(1<<j);
            f[t]=min(f[t],f[i]+1);
        }
    if(f[(1<<n)-1]==inf) cout<<"-1"<<endl;
    else cout<<f[(1<<n)-1]<<endl;
    return 0;
}
复制代码

 

#注意,其实本题的正解应当为记忆化搜索或者状压最短路。状态是有后效性的。状压dp的做法在洛谷上已经被hack。但这种做法不乏为一种很好的思路。所以还是放这了。

 

 2.P1896 [SCOI2005] 互不侵犯

很好的一道状压dp入门题。

 

设状态 f[i][j][s] 定义为所有只摆在前 i 行,已经摆了 j 个国王,并且第 i 行摆放的状态是 s 的所有方案的集合。

限制:

1.第 i-1 行内部不能有两个 1 相邻。

2.第 i-1 行和第 i 行之间也不能相互攻击到。

再给出两个定义:

已经摆完前 i 行,且第 i 行的状态为 a,第 i-1 行的状态为 b,已经摆了 j 个国王的所有方案。

已经摆完前 i-1 行,并且第 i-1 排的状态是 b ,已经摆了 j-count(a) 个国王的所有方案。

其中 count 计算的是这一行有多少个1。

接下来贴代码。

 

复制代码
//学习状压dp!
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int N=12,M=1<<10,K=110;
int n,m;
vector<int>state,head[M];
int id[M],cnt[M];
int f[N][K][M];
//判断同一行是否存在相邻的1
bool check(int state){
    for(int i=0;i<n;i++){
        if((state>>i&1)&&(state>>i+1)&1) return 0;
    }
    return 1;
}
//计算这个状态有多少个1
int count(int state){
    int res=0;
    for(int i=0;i<n;i++) res+=state>>i&1;
    return res;
}
signed main(){
    cin>>n>>m;
    for(int i=0;i<(1<<n);i++){
        if(check(i)) state.push_back(i);
        id[i]=state.size()-1;
        cnt[i]=count(i);
    }
    for(int i=0;i<state.size();i++){
        for(int j=0;j<state.size();j++){
            int a=state[i],b=state[j];
            if((a&b)==0&&check(a|b))
                head[i].push_back(j);
        }
    }
    f[0][0][0]=1;
    for(int i=1;i<=n+1;i++)
        for(int j=0;j<=m;j++)
            for(int a=0;a<state.size();a++)
                for(int b:head[a]){
                    int c=cnt[state[a]];
                    if(j>=c)
                        f[i][j][a]+=f[i-1][j-c][b];
                }
    cout<<f[n+1][m][0]<<endl;
    return 0;
}
复制代码

3.P1879 [USACO06NOV] Corn Fields G


与上题比较类似。只是因为不需要记录个数,所以可以删去一维。

复制代码
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int N=14,M=1<<12,mod=1e8;
int n,m,g[1<<N];
//g表示第i行是否可用。
vector<int>state,head[M];
int f[N][1<<N];
bool check(int state){
    for(int i=0;i<m;i++)
            if((state>>i&1)&&(state>>i+1&1))
                return 0;
    return 1;
}

signed main(){
    cin>>n>>m;
    
    for(int i=1;i<=n;i++)
        for(int j=0;j<m;j++){
            int t;
            cin>>t;
            g[i]+=!t*(1<<j);
    }
    
    for(int i=0;i<1<<m;i++)
        if(check(i)) state.push_back(i);
    
    
    for(int i=0;i<state.size();i++)
    {
        for(int j=0;j<state.size();j++)
        {
            int a=state[i],b=state[j];
            if(!(a&b)) head[i].push_back(j);
        }
    }
    f[0][0]=1;
     for (int i = 1;i <= n + 1;i++) {
        for (int a = 0;a < state.size ();a++) {
            if (!(state[a] & g[i])) {
                for (int b : head[a]) f[i][a] = (f[i][a] + f[i - 1][b]) % mod;
            }
        }
    }
    cout<<f[n+1][0];
    return 0;
}
复制代码


4.P2704 [NOI2001] 炮兵阵地

这题不同于上面题目的点在于,状态会受到上面两行的影响。

复制代码
    
#include<bits/stdc++.h>
using namespace std;

const int N = 110, M = 1 << 10;
int n, m;
int g[N], cnt[M];
int f[2][M][M];
vector<int> state;
vector<int> head[M];

bool check(int st)
{
    return !(st & st >> 1 || st & st >> 2);
}
int count(int st)
{
    int res = 0;
    while (st) res += st & 1, st >>= 1;
    return res;
}
int main()
{

    cin >> n >> m;
    for (int i = 1, j = 0; i <= n; ++ i, j = 0)
        for (char c; j < m && cin >> c; ++ j)   
            g[i] += (c == 'H') << j;

    for (int st = 0; st < 1 << m; ++ st)
        if (check(st))
            state.push_back(st), cnt[st] = count(st);

    for (int cur_st: state)
        for (int pre_st: state)
            if (!(cur_st & pre_st))
                head[cur_st].push_back(pre_st);

    for (int i = 1; i <= n; ++ i)
        for (int st: state)
            if (!(g[i] & st))
                for (int p1: head[st])
                    for (int p2: head[p1])
                        if (!(st & p2))
                            f[i&1][st][p1] = max(f[i&1][st][p1], f[i-1&1][p1][p2] + cnt[st]);
    int res = 0;
    for (int st: state)
        for (int pre: head[st])
            res = max(res, f[n&1][st][pre]);
    cout << res << endl;
    return 0;
}
复制代码

本题可以滚动数组优化。

for (int i = 1; i <= n + 2; ++ i)
        for (int st: state)
            if (!(g[i] & st))
                for (int p1: head[st])
                    for (int p2: head[p1])
                        if (!(st & p2))
                            f[i&1][st][p1] = max(f[i&1][st][p1], f[i-1&1][p1][p2] + cnt[st]);
    cout << f[n+2&1][0][0] << endl;

 

posted @   Miya555  阅读(31)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· 三行代码完成国际化适配,妙~啊~
· .NET Core 中如何实现缓存的预热?
· 如何调用 DeepSeek 的自然语言处理 API 接口并集成到在线客服系统
点击右上角即可分享
微信分享提示
主题色彩