一.前言
二进制位运算符:&(与),|(或),^(异或)
0&0=0; 0&1=0; 1&1=1;(与运算): &
0$|0=0;0|1=1;1|$1=1; (或运算): |
0^0=0; 1^=1; 1^1=0;(异或运算): ^
与操作:同为1才为1
或操作:有1就为1
或与操作:有1有0才为1
常用操作:
1.二进制意义下的去掉最后一位(相当于x/2):x>>1
2.二进制意义下的最后一位加零(相当于x*2):x<<1
3.二进制意义下的把最后一位变为1(xxx—>xx1):x|1
4.二进制意义下的把最后一位变为0(xxx—>xx0):x|1-1
5.二进制意义下的最后一位取反:x^1
稍微复杂一点的操作:
6.判断一个数字x二进制下第i位是不是等于1:(也可以理解成判断第i个点在不在集合中)
if((1<<(i-1))&x>0) 为真即说明等于1
7.将一个数字x二进制下第i位更改成1:
x=x|(1<<(i-1))
8.把一个数字二进制下最靠右的第一个1去掉:
x=x&(x-1)
9.枚举s的子集:
for(int i=s;i;i=(i-1)&s){}
10.若s是u的子集,那么s对于u的补集v:
v=s^u
二.状压DP
1.适用情景
题目设计图/物品的状态只有两种(是/不是),用二进制表示状态(比如地图中一整行的0010000…)
即二进制存状态,判断状态的合法性,最后输出合法状态的方案数目。
2.状压DP常用格式
一般来讲,状压DP处理的数据范围很小,但是一般比爆搜的范围稍微大一些,就是在两位数的范围内。
int maxn=1<<n; //规定状态的上界
for (int i=0;i<maxn;i++){
if (i&(i<<1)) continue;//如果i情况不成立就忽略
Type[++top]=i;//记录情况i到Type数组中
}
for (int i=1;i<=top;i++){
if (fit(situation[1],Type[i]))
dp[1][Type[i]]=1;//初始化第一层
}
for (int i=2;i<=层数(dp上界);i++){
for (int l=1;l<=top;l++)//穷举本层情况
for (int j=1;j<=top;j++)//穷举上一层情况(上一层对本层有影响时)
if (situation[i],Type[l]和Type[j]符合题意)
dp[i][l]=dp[i][l]+dp[i-1][j];//改变当前层(i)的状态(l)的方案种数
}
for (int i=1;i<=top;i++) ans+=dp[上界][Type[i]];
3.题目:
注意:
1.状压DP的题目涉及二进制运算符,所以一定要注意二进制运算时加括号,否则很容易错。
2.另外定义数组大小的时候最好不要用二进制表示,如:mp[2][1>>12](这样很容易发生错误),就用一个数值表示.
1.HRBUST - 1823 铺地砖
状压方程为dp[j][nxt]=sum(dp[j-1][state])
//状压DP例题 HRBUST - 2186 铺地砖
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <iostream>
using namespace std;
int N, M;
long long dp[42][8];
//第i列,搜索的当前行数, 第i列的状态,第j+1列的状态
void dfs(int i,int j,int state,int nex)
{
if(j==N)//遍历第i列结束
{
dp[i+1][nex]+=dp[i][state];
return;
}
//如果这个位置已经被上一列所占用,直接跳过
if (((1<<j)&state)>0)
dfs(i,j+1,state,nex);
//如果这个位置是空的,尝试放一个1*2的
if (((1<<j)&state)==0)
dfs(i,j+1,state,nex|(1<<j));//改变了下一列的状态
//如果这个位置以及下一个位置都是空的,尝试放一个2*1的
if (j+1<N&& ((1<<j)&state)==0 && ((1<<(j+1))&state)==0)
dfs(i,j+2,state,nex);
return;
}
int main()
{
N=3;
while (cin>>M)
{
memset(dp,0,sizeof(dp));
dp[1][0]=1;
for (int i=1;i<=M;i++){ //dp第i列
for (int j=0;j<(1<<N);j++) //遍历第i列的状态
if (dp[i][j]){
dfs(i,0,j,0);
}
}
cout<<dp[M+1][0]<<endl;
}
}
2.P1879 [USACO06NOV]玉米田Corn Fields
#include<iostream>
using namespace std;
#define mod 100000000
int mp[15][15],dp[15][4096];
int all_state[4096],now_state[15];
int main(){
int N,M;
scanf("%d%d",&N,&M);
//mp[i][j]地图
for(int i=1;i<=N;i++)
for(int j=1;j<=M;j++)
cin>>mp[i][j];
//now_state[i]存第i行的初始状态
for(int i=1;i<=N;i++)
for(int j=1;j<=M;j++){
now_state[i]=(now_state[i]<<1)+mp[i][j];//now_state[i]=1说明第i行的状态不存在有两个临近的1(即合法)
}
int all=1<<M;
//预处理所有可能状态i的合法性
for(int i=0;i<all;i++){
if((i&(i<<1))==0&&((i&(i>>1))==0))
all_state[i]=1;
}
dp[0][0]=1;
//开始DP
for(int i=1;i<=N;i++){//第i行
for(int j=0;j<all;j++){//遍历第i行所有可能的状态j
if((all_state[j]==1)&&((j&now_state[i])==j)){ //状态j合法且 now_state[i]在某位值是0的情况下j的值也必须是0
for(int k=0;k<all;k++){//第i-1行的状态
if((k&j)==0){//状态合法---说明不存在同一位上都为1
dp[i][j]=(dp[i][j]+dp[i-1][k])%mod;
}
}
}
}
}
int ans=0;
for(int i=0;i<all;i++){ //所有合法状态个数
ans=(ans+dp[N][i])%mod;
}
cout<<ans<<endl;
}
3.OpenJ_Bailian - 4124 海贼王之伟大航路
当题目给出可种范围时,学状压dp之前的我的第一想法肯定是暴力将范围与我选择的情况一一对比,然而我们有‘&’这种神奇的运算符——我们发现,在‘&’之后,如果结果与原先选择的情况相等,代表可种。(只要一个不可种,即check位是0,结果也是0,则不同了)
#include <bits/stdc++.h>
using namespace std;
#define maxn 65540
int n;
int dp[20][maxn];
int mp[20][20];
#define inf 0x3f3f3f3f
int main()
{
while(~scanf("%d",&n)){
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++){
cin>>mp[i][j];
}
}
memset(dp,inf,sizeof(dp));
dp[1][1]=0;//从岛1出发
int i,j,k,fi,fj;
for(int k = 1;k<(1<<n);k++){//k表示状态,k中第v位为0表示没去过,为1表示去过;
for(i = 1,fi = 1;i <= n;i++,fi <<= 1){//到达第i个岛
if(!(k & fi)) continue;//没去过第i个岛
for(j = 1,fj = 1;j <= n;j++,fj <<= 1){//途径第j个岛
if(i == j || !(k & fj)) continue;//岛i与j相等,或者没去过j岛
dp[i][k] = min(dp[i][k],dp[j][k^fi]+mp[j][i]);
}
}
}
int f=(1<<n)-1;
cout<<dp[n][f]<<endl;
}
return 0;
}
4.最小总代价(Vijos-1456)
5.胜利大逃亡(续)(Hdoj-1429)
参考博客:https://blog.csdn.net/u011077606/article/details/43487421
https://blog.csdn.net/LBCLZMB/article/details/96574249
题目列表:https://blog.csdn.net/u011077606/article/details/43487421