zoukankan      html  css  js  c++  java
  • 二进制的妙用——状态压缩

    本文题目都是状压dp

    洛谷P1879 [USACO06NOV]玉米田Corn Fields

    首先来一道入门题目,题目大意是求一共有多少种种植方案?限制条件两个:一是只能在给定的一个图里面标记为1的那块田才能种草地;二是草地不能连续,即只能独立存在,上下左右都不能种草。

    我们可以看到n m 比较小的时候基本就是要用状压来做了。

    对于每一行的种草情况我们可以用二进制表示,比如一块长5×宽1的草地,如果草地状态是(11011),则表示只有第3块地没有种草。

    那么我们可以看到符合情况的只能是类似这种状态(00000)(00001)(10101)因为题目要求不能连续嘛。

    同时我们还要结合题目给定的那幅图里面可以哪块地可种,哪块地不能种,比如只有(1 0 1 0 1)能种,即只有第1 3 5 块地能种。所以如果枚举到(11000)类似这种情况时就不行,因为这个状态表示第2块地要种草,而题目要求表示第2块地不能种。

    显而易见的我们可以用十进制来代表二进制来表示每种种植情况,那我们对于仅仅独立的一行,只需考虑草地是否相邻的同时看有没有破坏题目的要求。

    而对于上下两行,我们也有可能两块草地相邻的对吧?这时候我们只能枚举两行各自的可行状态,然后看是否冲突相邻,不冲突就把情况累加到下一行,那么答案就是累加到最后一行了。

    #include <bits/stdc++.h>
    #define maxn (1<<13)
    using namespace std;
    int n,m,s[maxn],num[maxn][13],x,i,j,tot,op[13],f[13][maxn];
    void solve()
    {
        int i,j,k,q;
        for (i=0;i<(1<<m);i++)
        {
            if (i&(i<<1)) continue;
            s[++tot]=i;
    /*        for (q=1;q<=n;q++)
            {
                k=0;
                for (j=0;j<m;j++)
                    if (op[q]&(1<<j))
                    {
                        if (i&(1<<j)) k++;
                    }
                    else {k=0;break;}
                num[tot][q]=k;
            }*/
        }
    }
    void dp()
    {
        int i,j,t,sum=0;
        for (i=1;i<=tot;i++)
            if ((op[1]|s[i])==op[1]) f[1][i]=1;
        //for (i=1;i<=tot;i++) cout<<f[1][i]<<" ";
        //cout<<tot;
        for (i=2;i<=n;i++)
            for (j=1;j<=tot;j++)
                        for (t=1;t<=tot;t++)
                            if (!(s[t]&s[j]))
                                if ((s[j]|op[i])==op[i])
                                    f[i][j]+=f[i-1][t];
    //    for (i=1;i<=tot;i++) cout<<f[2][i]<<" ";
        for (j=1;j<=tot;j++) sum=(sum+f[n][j])% 100000000;
        printf("%d
    ",sum);
    }
    int main()
    {
        cin>>n>>m;
        for (i=1;i<=n;i++)
            for (j=1;j<=m;j++)
            {
                scanf("%d",&x);
                if (x) op[i]+=(1<<(m-j));
            }
        //cout<<op[2]<<endl;
        solve();
        dp();
        return 0;
     } 
    View Code

    这次代码风格有点飘,请见谅。

    洛谷P4802 [CCO 2015]路短最

    我们看到n还是很小,考虑状压。

    还是很上一题一样,对于旅行情况我们可以用二进制表示,如状态是(11011),则表示只有第3个城市没有旅游过。

    那么我们现在只有第3个城市要旅游,我们怎么知道他是怎么从哪个城市来到第3个城市的呢?

    我们只能再加一维,表示最后去到的哪个城市。比如上面那种状态dp【(11011)2】【4】则表示我们旅行了1 2 4 5 城市,而且当前我们在第4个城市,而我们要去到第3个城市,就要从第4个城市去。

    怎么实现这个过程呢?首先我们还是要先枚举状态,然后再枚举当前处于的那个城市,和没有去而且想要下一步去的城市。每次做这个过程时还是要用到一些巧妙的位操作,这里不再过多阐述。

    #include <bits/stdc++.h>
    #define maxn (1<<19)
    using namespace std;
    int x,y,n,v,m,first[20],next[maxn],zhi[maxn],value[maxn],i,j,k,u,tot,ans,f[maxn][20];
    void add(int x,int y,int v)
    {
        tot++;
        next[tot]=first[x];
        first[x]=tot;
        zhi[tot]=y;
        value[tot]=v;
    }
    int main()
    {
        cin>>n>>m;
        memset(first,-1,sizeof(first));
        for (i=1;i<=m;i++)
        {
            scanf("%d%d%d",&x,&y,&v);
            add(x+1,y+1,v);
        }
        for (i=1;i<(1<<n);i++)
            for (j=1;j<=n;j++) f[i][j]=-0x3f3f3f3f;
        f[1][1]=0;
        for (i=1;i<(1<<n);i++)
        {
            for (j=1;j<=n;j++)
            {
                if (!(i&(1<<(j-1)))) continue;
                k=first[j];
                while (k!=-1)
                {
                    u=zhi[k];
                    
                    if ((i&(1<<(u-1))))
                    
                    {
                        f[i][u]=max(f[i][u],f[i^(1<<(u-1))][j]+value[k]);
                    }
                    
                    k=next[k];
                }
            }
        }
        for (i=1;i<(1<<n);i++) ans=max(ans,f[i][n]);
        printf("%d
    ",ans);
        return 0;
    }
    View Code

    洛谷P4772 灰化肥,会挥发

    我们还是看到n很小,考虑状压。(以后看到n之类的数很小基本就是状态压缩的题目了)

    和上一题一模一样,对于情况我们可以用二进制表示,如状态是(11011),则表示只有第3个字母没有去到。

    首先我们要最短路预处理掉每个点之间的距离(好久没写最短路了......)

    然后基本上就是上一题的做法了。

    这里题目还需要输出步骤。我们定义一个数组表示当前dp的那个状态的权值总和。什么意思呢?题目要求我们要输出字典序最小的步骤,我们可以对每个字母给予一个权值,A是1,B是2.......。每当一个步骤加进来时,我们对当前权值和乘以16再加上字母的权值,类似于十六进制,如果你字典序大的话,每次累计出来的值肯定大(秦九韶算法?)。最好倒回去算就行了。

    #include <bits/stdc++.h>
    #define maxn 505
    #define INF 0x3f3f3f3f
    using namespace std;
    struct node 
    {
        int x,y;
    };
    node zuobiao[20];
    const int dx[4]={0,1,0,-1};
    const int dy[4]={1,0,-1,0};
    int i,j,x,y,n,m,k,dis[maxn][maxn],vis[maxn][maxn],dp[(1<<17)][17],len[20][20];
    char a[maxn][maxn];
    long long state[(1<<17)][17];
    void dijas(int p)
    {
        int i,j,x,y;
        queue <pair<int,int> > q;
        for (i=1;i<=n;i++)
            for (j=1;j<=m;j++) 
            {
                dis[i][j]=INF;
                vis[i][j]=0;
            }
        dis[zuobiao[p].x][zuobiao[p].y]=0;
        q.push(make_pair(zuobiao[p].x,zuobiao[p].y));
        while (!q.empty())
        {
            x=q.front().first;
            y=q.front().second;
            q.pop();
            if (!vis[x][y])
            {
                vis[x][y]=1;
                for (i=0;i<=3;i++)
                {
                    if (x+dx[i]>=1 && x+dx[i]<=n && y+dy[i]>=1 && y+dy[i]<=m)
                    if (dis[x][y]+1<dis[x+dx[i]][y+dy[i]] && a[x+dx[i]][y+dy[i]]!='*')
                    {
                        dis[x+dx[i]][y+dy[i]]=dis[x][y]+1;
                        q.push(make_pair(x+dx[i],y+dy[i]));
                    }
                }
            }
        }
    }
    void work()
    {
        int minn=INF,k,i,tot=0,let[20];
        long long final;
        for (i=2;i<=x;i++) 
            {
                if (minn==dp[(1<<x)-1][i])
                {
                    final=min(final,state[(1<<x)-1][i]);
                }
                if (minn>dp[(1<<x)-1][i])
                {
                    minn=dp[(1<<x)-1][i];
                    final=state[(1<<x)-1][i];
                }
                
            }
        printf("%d
    A",minn);
        while (final) let[++tot]=final%17,final/=17;
        for (i=tot;i;i--) printf("%c",let[i]+'A');
    }
    int main()
    {
        cin>>n>>m>>x;
        for (i=1;i<=n;i++)
            for (j=1;j<=m;j++) 
            {
                cin>>a[i][j];
                if (a[i][j]>='A' && a[i][j]<='Z') 
                {
                    zuobiao[a[i][j]-64].x=i;
                    zuobiao[a[i][j]-64].y=j;
                }
            }
        for (i=1;i<=x;i++) 
        {
            dijas(i);
            for (j=1;j<=x;j++)
                if (i!=j)
                {
                    len[i][j]=dis[zuobiao[j].x][zuobiao[j].y];
                }
        }
    //    cout<<len[1][2]<<" "<<len[1][3]<<endl;
    //        cout<<len[2][1]<<" "<<len[2][3]<<endl;
    //            cout<<len[3][1]<<" "<<len[3][2]<<endl;
    //    cout<<zuobiao[3].x<<" "<<zuobiao[3].y<<endl;
        for (i=1;i<(1<<x);i++)
            for (j=1;j<=x;j++) dp[i][j]=INF;
        for (i=1;i<(1<<x);i++)
            for (j=1;j<=x;j++) state[i][j]=INF;
    //    for (i=0;i<x;i++) dp[(1<<i)^1][i]=dis[1][i]; 
        state[1][1]=0;
        dp[1][1]=0;
        for (i=1;i<(1<<x);i++)
        {
            for (j=0;j<x;j++)
                if (i&(1<<j))
                {
                    for (k=0;k<x;k++)
                        if (k!=j && i&(1<<k))
                        {
                            if (dp[i][k+1]==dp[i^(1<<k)][j+1]+len[j+1][k+1])
                                state[i][k+1]=min(state[i][k+1],state[i^(1<<k)][j+1]*17+k);
                            if (dp[i][k+1]>dp[i^(1<<k)][j+1]+len[j+1][k+1])
                            {
                                dp[i][k+1]=dp[i^(1<<k)][j+1]+len[j+1][k+1];
                                state[i][k+1]=state[i^(1<<k)][j+1]*17+k;
                            }
                        }
                }
        }
        //cout<<state[(1<<x)-1][1]<<endl;
        work();
        return 0;
    } 
    View Code

    本想找几题不是dp的来做的,但是这种题目既可以练状压,又可以练dp,还是很有可做性的。

    新加一题

    wannafly camp day3 C 无向图定向(k染色问题)

    其实这题在wannafly camp 的时候听到过要用状压做了,但是陆续找了许多篇博客,都没有显而易见的把解法给说出来,但是功夫不负有心人,我终于在今天找到。(原来是搜索方式不对……哭笑)

    网上有些人做这题是直接dfs暴力去搜索的,本着想了解一下k染色究竟如何用状压dp来做的好奇,时隔多日,我终于想通了!

    令dp[s] 为染 s 这个状态图最小的颜色数,其中 s 里 1代表要染色的 0 代表不需要染色! 

    那么显然dp[0] = 0  dp[s] = min{dp[s^s0] + 1}  其中s0是s 的子集并且里面的1 在图中不直接相连!  

    判断状态s里面的1是否不直接相连可以预处理! 

    找s 的子集可以减1进行&运算

    最后由于题目的要求,最小染色数-1就是答案了。

    #include <bits/stdc++.h>
    #define inf 0x3f3f3f3f
    using namespace std;
    int n,m,i,x,y,a[20][20],dp[(1<<17)],ok[(1<<17)],j;
    bool judge(int x)
    {
        for (int i=0;i<n;++i)
        {
            if (x&(1<<i))
                for (int j=0;j<n;++j)
                    if (i!=j && (x &(1<<j)))
                    {
                        if (a[i+1][j+1]) return false;
                    }
        }
        return true;
    }
    int main()
    {
        cin>>n>>m;
        for (i=1;i<=m;i++) 
        {
            cin>>x>>y;
            a[x][y]=1;
            a[y][x]=1;
        }
        for (i=0;i<(1<<n);i++) 
            if (judge(i)) ok[i]=1;    
        dp[0]=0;
        for (i=1;i<(1<<n);++i)
        {
            dp[i]=inf;
            for (j=i;j;j=(j-1) & i)
                if (ok[j]) 
                    dp[i]=min(dp[i],dp[i^j]+1);
        }
        cout<<dp[(1<<n)-1]-1;
        return 0;
     } 
    View Code
  • 相关阅读:
    多边形游戏
    大整数乘法
    矩阵连乘
    最长公共子序列
    动态规划
    快速排序
    二分搜索技术
    动态规划基本要素
    合并排序
    最大子段和
  • 原文地址:https://www.cnblogs.com/Y-Knightqin/p/12299790.html
Copyright © 2011-2022 走看看