二进制的妙用——状态压缩
本文题目都是状压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\n",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; }
这次代码风格有点飘,请见谅。
洛谷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\n",ans); return 0; }
洛谷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\nA",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; }
本想找几题不是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; }