状态压缩DP

状压dp:简单来说就是通过,枚举所有的状态,然后用当前的状态不断的更新其他的状态,从而最终得到最终状态的最优值。这个做题的关键就是在我们枚举到当前状态的时候,当前的状态已经到达了最优值。然后用本身的最优值去更新其他的值,感觉很像最短路中松弛操作。状压dp的问题感觉还是比较好判断的,基本上他的n<=20的,因为我们需要枚举2^n中状态,所以太大的枚举不了。
例题1:棋盘内的状态压缩DP
小国王
题意:
1:给定m个国王
2:给定n*n的矩阵
3:每个国王可以攻击到相邻的八个格子的位置
4:求最大的摆放方案数(要求国王间不能相互进攻)
分析:
1:三维状态压缩DP
2:状态表示:dp[i][j][k],i是表示对前i行进行处理,j表示当前已经安排了j个国王,k表示第i行的状态
3:转移方程:当前的状态可以由dp[i-1][j-c][k]转移过来,这里的c表示第i行的国王的数量,此方程表达的含义:摆放好了i-1行,已经摆放了j-c个棋子,其中c是第i行棋子的数量。我们发现这一行如果上下两个状态可以满足的话就可以转移(具体的转移要求在代码中标明)。
4:我们需要预处理当前行的状态是否满足情况即是否有两个相邻的1,然后还需要预处理上一个状态能否预处理到下一个状态。
下面是AC代码:

#include<iostream>
#include<cstring>
#include<cstdio>
#include<algorithm>
#include<vector>
using namespace std;
#define int long long
const int N=12,M=1<<10,K=110;
int n,m;
vector<int> state;
int cnt[M];
vector<int> head[M];
int f[N][K][M];
bool check(int state)//判断i中是否有相邻的1
{
    for(int i=0;i<n;i++)
    {
        if((state>>i&1)&&(state>>i+1&1))
            return false;
    }
    return true;
}
int count(int state)//统计i中1的数量
{
    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);
            cnt[i]=count(i);
        }
    }
    for(int i=0;i<state.size();i++)
    {
        for(int j=0;j<state.size();j++)
        {
            int a=state[i];
            int b=state[j];
            if((a&b)==0&&check(a|b))//这里前一个就是相同的列不能全为1,a|b就是在矩阵中看上下合并的话不能有相邻的1。
            {
                head[i].push_back(j);//将可以转移到i的状态的数量的下标存起来
            }
        }
    }
    f[0][0][0]=1;//初始化,表示已经摆了0行,摆了0个国王,状态为0
    for(int i=1;i<=n+1;i++)//枚举1-n行
    {
        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]];//最后一行的1的数量
                    if(j>=c) f[i][j][a]+=f[i-1][j-c][b];//转移方程,注意转移的时候用的是状态号
                }
            }
        }
    }
    cout<<f[n+1][m][0];//注意这里的0表示的是下标,但是state[0]就是0
    return 0;
}


注:这里用到了一个技巧就是我们可以枚举到i+1行,就不用最终枚举所有n的状态,这里f[n+1][m][0]就是到了n+1行,用了m个国王,最后一行状态为state[0]即0,可以包含所有的答案。还有就是第三维更新的时候用的是状态下表而不是具体的状态,具体的状态可能很大,用的话可能就会爆数组的内存。

例题2:棋盘内的状态压缩DP
玉米田
题意:
1:N*M的玉米地
2:a[i][j]为1表示此块土地肥沃,a[i][j]=0表示此块土地不育
3:每种一个玉米,与其有一个相邻边的格子不能种玉米
4:问种玉米的最多方案数
题解:此题与上一题是很类似的,只不过状态数变少了而已,但是我们依旧是处理所有可能的状态,然后在转移的时候按照一定的条件转移即可,转移过程中如果可转移的上个状态不存在,加上0即可,不会对最终答案造成影响。
下面是AC代码:

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<vector>
using namespace std;
const int N = 14, M = 1 << 12, mod = 1e8;
int n, m;
int w[N];
vector<int> state;
vector<int> head[M];
int f[N][M];//这里存的是状态
bool check(int state)
{
    for(int i=0;i+1<m;i++)
        if((state>>i&1)&&(state>>i+1&1))
            return false;
    return true;
}
int main()
{
    cin>>n>>m;
    for(int i=1;i<=n;i++)
    {
        for(int j=1;j<=m;j++)
        {
            int x;
            cin>>x;
            w[i]=w[i]*2+x;//计算当前的值
        }
    }
    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++)
            if((state[i]&state[j])==0)
               head[i].push_back(j);
    f[0][0]=1;
    for(int i=1;i<=n+1;i++)
    {
        for(int j=0;j<state.size();j++)
        {
            if((state[j]|w[i])<=w[i])//保证不育的土地不会种树,假设上面的土地的值为110那么如果0上面有1的话或起来的值一定是大于w[i]的。
            {
                for(int b:head[j])
                {
                    f[i][j]=(f[i][j]%mod+f[i-1][b]%mod)%mod;
                }
            }
        }
    }
    cout<<f[n+1][0]<<endl;
    return 0;
}

我们也可以变形一下求总方案中玉米的最大数量:
此题代码如下:(测了几个样例过了)

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<vector>
using namespace std;
#define int long long
const int N = 14, M = 1 << 12, mod = 1e8;
int n, m;
int w[N];
vector<int> state;
vector<int> head[M];
int f[N][M];//这里存的是状态
int cnt[M];
bool check(int state)
{
    for(int i=0;i+1<m;i++)
        if((state>>i&1)&&(state>>i+1&1))
            return false;
    return true;
}
int count(int state)
{
    int res=0;
    for(int i=0;i<m;i++) res+=state>>i&1;
    return res;
}
signed main()
{
    cin>>n>>m;
    for(int i=1;i<=n;i++)
    {
        for(int j=1;j<=m;j++)
        {
            int x;
            cin>>x;
            w[i]=w[i]*2+x;
        }
    }
    for(int i=0;i<1<<m;i++)
        if(check(i))
          state.push_back(i),cnt[i]=count(i);
    for(int i=0;i<state.size();i++)
        for(int j=0;j<state.size();j++)
            if((state[i]&state[j])==0)
               head[i].push_back(j);
    for(int i=1;i<=n+1;i++)
    {
        for(int j=0;j<state.size();j++)
        {
            if((state[j]|w[i])<=w[i])
            {
                for(int b:head[j])
                {
                    f[i][j]=max(f[i][j],f[i-1][b]+cnt[state[j]]);
                    //cout<<state[j]<<"-->"<<cnt[state[j]]<<endl;
                }
            }
        }
    }
    cout<<f[n+1][0]<<endl;
    return 0;
}

状态压缩dp除了上面说的棋盘类的,还有就是普通的二进制形式。我们下面举几个例题来理解:
例题1:Rectangular Covering
题意:
1:平面上给出n个点(二维坐标)
2:要求用平行于轴的矩形来覆盖这些点
3:问覆盖所有点最小的总面积
注:假设是同一行或同一列的点需要将变化为宽为1的长方形
题解:
这个题的话还是比较简单的,我们按照输入的顺序,将一个点看作一个状态,然后预处理任意两个点的矩形面积和之间的点,求到相应的状态,然后存入一个vector里面最后在将所有的状态状压一下即可,下面是AC代码:

#include <iostream>
#include <cstring>
#include <cstdio>
#include <bitset>
#include <vector>
using namespace std;
const int N = 20;
int dp[1 << N];
struct node
{
    int x, y;
} a[1 << N];
int main()
{
    int n;
    while (~scanf("%d", &n) && n)
    {
        vector<pair<int, int> > vec(1<<N);
        
        for (int i = 0; i < n; i++)
        {
            int x, y;
            scanf("%d%d", &x, &y);
            a[i].x = x;
            a[i].y = y;
        }
        int cnt = 0;
        for (int i = 0; i < n; i++)
        {
            for (int j = i + 1; j < n; j++)
            {
                bitset<15> bit;
                int maxx = max(a[i].x, a[j].x);
                int minx = min(a[i].x, a[j].x);
                int maxy = max(a[i].y, a[j].y);
                int miny = min(a[i].y, a[j].y);
                for (int k = 0; k < n; k++)
                {
                    if (a[k].x >= minx && a[k].x <= maxx && a[k].y >= miny && a[k].y <= maxy)
                    {
                        bit.set(k, 1);
                    }
                }
                int v = 0;
                for (int k = bit.size() - 1; k >= 0; k--)
                {
                    v = v * 2 + bit[k];
                }
                if (minx == maxx)
                    vec[cnt].first = v, vec[cnt++].second = maxy - miny;
                else if (miny == maxy)
                    vec[cnt].first = v, vec[cnt++]. second = maxx - minx;
                else
                    vec[cnt].first = v, vec[cnt++].second = (maxx - minx) * (maxy - miny);
            }
        }
        for(int i=0;i<(1<<n);i++) dp[i]=0x3f3f3f3f;
        dp[0] = 0;
        for (int i = 0; i < (1 << n); i++)
        {
            for (int j = 0; j < cnt; j++)
            {
                int v1 = i;
                int v2 = vec[j].first;
                int num = vec[j].second;
                int t = v1 | v2;
                dp[t] = min(dp[t], dp[i] + num);
            }
        }
        printf("%d\n", dp[(1 << n) - 1]);
    }
    return 0;
}

例题2:DNA Laboratory
题意:给出n个字符串,构造求一个最小长度的串,该串包含给出的所有字符串。注意该字符串在长度最小的同时还必须是字典序最小
题解:感觉这个还是有一定的难度的,状压的过程还可以,但是回溯寻找原串的过程比较麻烦。

这个题的本质就是一个状压dp中顺序的问题,感觉这种问题还是比较常见的,我们设置dp[i][j]表示状态为j的情况下, i i i放到最前面的情况。

这样我们的状压方程也就很容易表示了,但是,我们还要预处理一个东西就是,任意一个串放到另一个串的前面给整体的串带来的长度贡献是多少。这两项知道了,方程也就很容易求了。

我们通过上面的东西可以得到的是最小长度,题目还有的要求是要将整个串的字典序最小输出出来,这个通用的做法就是dfs回溯找出,具体的戳我
下面是AC代码:

#include <iostream>
#include <cstring>
#include <cstdio>
#include <algorithm>
#include <bitset>
using namespace std;
const int N = 20;
const int INF = 0x3f3f3f3f;
//思路:先求最小长度,然后再dfs回滚求字典序最小
int dp[20][(1 << 20)]; // dp[i][j]表示状态j,第i个字符串放在最前面的最小长度
int cost[20][20];      // cost[i][j]表示第i个字符串放到第j个字符串前的花费长度
string s[20], ans;
int m;
void dfs(int id, int cur) //从id开始,cur为当前的状态
{
    if (cur == 0)
        return;
    int flag = -1;
    string tmp = "zzzz";
    for (int i = 0; i < m; i++) //从前往后枚举字符串
    {
        if (i == id)
            continue;
        if ((cur & (1 << i)) && (dp[id][cur] == dp[i][cur & ~(1 << id)] + cost[id][i]))
        {
            if (s[i].substr(s[id].size() - cost[id][i]) < tmp)
            {
                tmp = s[i].substr(s[id].size() - cost[id][i]);
                flag = i;
            }
        }
    }
    if (flag != -1)
        ans += tmp;
    dfs(flag, cur & ~(1 << id));
}
int main()
{
    int t;
    scanf("%d", &t);
    int Case=0;
    while (t--)
    {
        ans="";
        int n;
        scanf("%d", &n);
        for (int i = 0; i < n; i++)
            cin >> s[i];
        //去重
        for (int i = 0; i < n; i++)
        {
            for (int j = 0; j < n; j++)
            {
                if (s[i].find(s[j]) != -1)
                    s[j] = s[i];
                cost[i][i]=0;
            }
        }
        sort(s, s + n); //从小到大
        m = unique(s, s + n) - s;
        memset(cost, 0, sizeof(cost));
        //找到cost[i][j]//第i个字符串放在最前面的最小长度
        for (int i = 0; i < m; i++)
        {
            for (int j = 0; j < m; j++)
            {
                if (i == j)
                    continue;
                int len = min(s[i].size(), s[j].size());
                int mx = 0; //最大的相同长度
                for (int k = s[i].size() - len; k < s[i].size(); k++)
                {
                    string s1 = s[i].substr(k); //从i开始到最后的子串
                    int midl = s1.size();
                    string s2 = s[j].substr(0, midl);
                    if (s1 == s2)
                    {
                        mx = max(mx, midl);
                    }
                }
                cost[i][j] = s[i].size() - mx;
            }
        }
        //进行状压dp
        for (int i = 0; i < m; i++)
        {
            for (int j = 0; j < (1 << m); j++)
                dp[i][j] = 0x3f3f3f3f;
        }
        for (int i = 0; i < m; i++)
        {
            dp[i][(1 << i)] = s[i].size();
        }
        for (int i = 0; i < (1 << m); i++)
        {
            for (int j = 0; j < m; j++)
            {
                if ((i & (1 << j)) && dp[j][i] != INF)
                {
                    for (int k = 0; k < m; k++)
                    {
                        dp[k][i | (1 << k)] = min(dp[k][i | (1 << k)], dp[j][i] + cost[k][j]); //用当前的状态去更新其他的状态
                    }
                }
            }
        }
        int id = 0;
        for (int i = 0; i < m; i++)
        {
            if (dp[id][(1 << m) - 1] > dp[i][(1 << m) - 1])
            {
                id = i;
            }
        }
        ans += s[id];
        dfs(id, (1 << m) - 1);
        cout<<"Scenario #"<<++Case<<":"<<endl;
        cout << ans << endl<<endl;//记得多加一个endl,否则会PE
    }
}

例题3:Paid Roads
题意:
在这里插入图片描述
题解:这个题的主要的问题就是那个可以提前支付的问题,也就是说这个题的最小值可能是重复走一些边才会得到。

所以,我们要暴力所有的状态,判断当前的状态的相应位置是不是1,假设是1的话就更新与他相连的所有位置,但是这个更新的时候类似于dijistra更新,只要是有状态更新了,就一直更新,只要没状态更新,就跳出。大致除了用了dijistra的思想外,其余基本和模板类似,具体看代码:

#include<iostream>
#include<cstring>
#include<algorithm>
#include<cstdio>
#include<queue>
using namespace std;
#define int long long
const int inf=0x3f3f3f3f;
int dp[1<<11][11];//dp[i][j]表示在i的状态下,最后一个遍历到j所可以的最小值
struct node
{
    int b,c,p,r;//分别表示目的城市,提前付费的城市,付费多少,不提前付费付多少
};
vector<node> ma[20];
signed main()
{
    int n,m;
    scanf("%lld%lld",&n,&m);
    while(m--)
    {
        int a,b,c,p,r;
        node t;
        scanf("%lld%lld%lld%lld%lld",&a,&t.b,&t.c,&t.p,&t.r);
        a--;
        t.b--;
        t.c--;
        ma[a].push_back(t);
    }
    for(int i=0;i<(1<<n);i++)
    {
        for(int j=0;j<n;j++)
        {
            dp[i][j]=0x3f3f3f3f;
        }
    }
    dp[1][0]=0;
    for(int i=0;i<(1<<n);i++)
    {
        int flag=1;
        while(flag)//松弛操作,假设有可以更新的就更新
        {
            flag=0;
            for(int j=0;j<n;j++)
            {
                if(!(i&1<<j)) continue;
                for(int k=0;k<ma[j].size();k++)
                {
                    int tmp=inf;
                    int v=ma[j][k].b;
                    if(i&1<<ma[j][k].c) tmp=min(tmp,dp[i][j]+ma[j][k].p);
                    tmp=min(tmp,dp[i][j]+ma[j][k].r);
                    if(tmp<dp[i|1<<v][v])
                    {
                        dp[i|1<<v][v]=tmp;
                        flag=1;
                    }
                }
            }
        }
    }
    int ans=inf;
    for(int i=0;i<(1<<n);i++)
    {
        if(i&1) ans=min(ans,dp[i][n-1]);//是奇数,因为必须要有1
    }
    if(ans==inf) printf("impossible\n");
    else printf("%lld\n",ans);
    return 0;
}

持续更新中~~~~~~~~

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值