当我们有时无法用普通的dp以及一些算法表示一些状态、解决一些问题的时候,我们就可以想到状态压缩动态规划。
状态压缩动态规划一般使用二进制来表示一个问题的子问题是否使用过。
例如:1 0 1(5)
这上面的二进制指的是第一个和第三个问题已经求解过或者已经用过,但是第二个还没用过。
当然存储时我们是不能用二进制存储的,所以我们一般都用二进制所对应的十进制存储
由于状压dp一般要用位运算,所以以下是一些位运算的常规操作:
1、 x > > n x>>n x>>n 将x右移n位,相当于x/2^n
2、 x < < n x<<n x<<n 将x左移n位,相当于x*2^n
3、 x & y x\&y x&y 将x和y进行与运算,都为1则1,否则为0
4、 x ∣ y x|y x∣y 将x和y进行或运算,有1则1,否则为0
5、 x ( x o r ) y x^ \ (xor)y x (xor)y 将x和y进行异或运算,相同为0,不同为1
还有一些组合操作:
1、 1 < < i 1<<i 1<<i 相当于2^i
2、 x ∣ ( 1 < < i − 1 ) x|(1<<i-1) x∣(1<<i−1) 将x的第i位变为1
3、 i f ( x & ( 1 < < i − 1 ) ) if (x\&(1<<i-1)) if(x&(1<<i−1)) 判断x的第i位是否为1
4、 f o r ( i n t i = x ; i ; i = ( i − 1 ) & x ) for (int\ i=x;i;i=(i-1)\&x) for(int i=x;i;i=(i−1)&x) 枚举x的子集
(PS:如果x|y==x,y为x的子集)
5、-x=(x)+1 x&(-x)是返回最后一个1的位置
6、a ^ =b ^ = a ^ = b 就是a与b交换。具体为何易证,自己去模拟
从网上摘来一个图,有错勿喷:
那么接下来上几道例题:
给定一张 n(n≤20) 个点的带权无向图,点从 0~n-1 标号,求起点 0 到终点 n-1 的最短Hamilton路径。 Hamilton路径的定义是从 0 到 n-1 不重不漏地经过每个点恰好一次。
输入和输出
Input
第一行一个整数n。 接下来n行每行n个整数,其中第i行第j个整数表示点i到j的距离(一个不超过10^7的正整数,记为a[i,j])。 对于任意的x,y,z,数据保证 a[x,x]=0,a[x,y]=a[y,x] 并且 a[x,y]+a[y,z]>=a[x,z]。
Output
一个整数,表示最短Hamilton路径的长度。
样例
Sample Input
4
0 2 1 3
2 0 2 1
1 2 0 1
3 1 1 0
Sample Output
4
分析:状压dp,设dp[i][j]表示当前在第i个点,当前状态为j(其实就是一串二进制,二进制数为1表示当前的星球已经到达过,0表示没到达过)。我们先枚举当前的状态i,找出当前状态中到达过的星球j(二进制所对应的位是1),然后在枚举终点k,如果发现k是当前状态中没到达的点,那么就进行转移。
具体的转移方程如下:
d p [ k ] [ i ∣ ( 1 < < k − 1 ) ] = m i n ( d p [ k ] [ i ∣ 1 < < k − 1 ] , d p [ j ] [ i ] + a [ j ] [ k ] ) ; dp[k][i|(1<<k-1)]=min(dp[k][i|1<<k-1],dp[j][i]+a[j][k]); dp[k][i∣(1<<k−1)]=min(dp[k][i∣1<<k−1],dp[j][i]+a[j][k]);
稍微注意一下空间。
那么具体代码如下:
#include
using namespace std;
int n;
int dp[51][1048580];
int a[1001][1001];
int main(){
freopen("test.in","r",stdin);
freopen("test.out","w",stdout);
scanf("%d",&n);
for (int i=1;i<=n;i++)
for (int j=1;j<=n;j++)
scanf("%d",&a[i][j]);
memset(dp,20,sizeof(dp));
dp[1][1]=0;
for (int i=1;i<(1<<n);i++)
for (int j=1;j<=n;j++)
if (i&(1<<j-1)){
for (int k=1;k<=n;k++)
if (!(i&(1<<k-1)))
dp[k][i|(1<<k-1)]=min(dp[k][i|1<<k-1],dp[j][i]+a[j][k]);
}
printf("%d",dp[n][(1<<n)-1]);
fclose(stdin);
fclose(stdout);
return 0;
}
农场主John新买了一块长方形的新牧场,这块牧场被划分成M行N列(1 ≤ M ≤ 12; 1 ≤ N ≤ 12),每一格都是一块正方形的土地。John打算在牧场上的某几格里种上美味的草,供他的奶牛们享用。
遗憾的是,有些土地相当贫瘠,不能用来种草。并且,奶牛们喜欢独占一块草地的感觉,于是John不会选择两块相邻的土地,也就是说,没有哪两块草地有公共边。
John想知道,如果不考虑草地的总块数,那么,一共有多少种种植方案可供他选择?(当然,把新牧场完全荒废也是一种方案)
输入输出格式
输入格式:
第一行:两个整数M和N,用空格隔开。
第2到第M+1行:每行包含N个用空格隔开的整数,描述了每块土地的状态。第i+1行描述了第i行的土地,所有整数均为0或1,是1的话,表示这块土地足够肥沃,0则表示这块土地不适合种草。
输出格式:
一个整数,即牧场分配总方案数除以100,000,000的余数。
分析:状压dp,设 d p [ i ] [ j ] dp[i][j] dp[i][j]为第i行状态为j的方案数。开始用一个数组f[i]
f[i]预处理出每一行的二进制所对应得十进制数。转移时,枚举上下两行,不难发现,如果当前种的田是合理的,那么就肯定满足 ( i & ( i > > 1 ) = = 0 ) & & ( i & ( i < < 1 ) = = 0 ) (i\&(i>>1)==0)\&\&(i\&(i<<1)==0) (i&(i>>1)==0)&&(i&(i<<1)==0),以及是放在农田不贫瘠的土上的 ( j ∣ f [ k ] = = j ) (j∣f[k]==j) (j∣f[k]==j)
(j∣f[k]==j)。转移时就从上层转移累加即可.
不难处理出预处理部分:
for (int i=1,x;i<=n;i++){
int sum=0;
for (int j=1;j<=m;j++) scanf("%d",&x),sum=sum*2+x;
f[i]=sum;
}
dp部分如下:
for (int i=1;i<=n;i++)
for (int j=0;j<=sum;j++)
if (vis[j]&&((j&f[i])==j))
for (int k=0;k<=sum;k++)
if (!(k&j))
dp[i][j]=(dp[i][j]+dp[i-1][k])%P;
这两部分是核心,需要注意,不过值得一提的是这道题的初值也比较特殊。
那么具体代码如下:
#include
using namespace std;
#define P 100000000
int f[100001];
int dp[13][16777217];
bool vis[16777217];
int n,m;
int main(){
scanf("%d %d",&n,&m);
for (int i=1,x;i<=n;i++){
int sum=0;
for (int j=1;j<=m;j++) scanf("%d",&x),sum=sum*2+x;
f[i]=sum;
}
// for (int i=1;i<=n;i++) cout<
int sum=(1<<m)-1;
for (int i=0;i<=sum;i++)
if (!(i&(i<<1))&&(i|f[1])==f[1]) dp[1][i]=1;
for (int i=0;i<=sum;i++)
vis[i]=(!(i&(i>>1)))&&(!(i&(i<<1)));
for (int i=1;i<=n;i++)
for (int j=0;j<=sum;j++)
if (vis[j]&&((j&f[i])==j))
for (int k=0;k<=sum;k++)
if (!(k&j))
dp[i][j]=(dp[i][j]+dp[i-1][k])%P;
int ans=0;
for (int i=0;i<=sum;i++) ans=(ans+dp[n][i])%P;
printf("%d",ans);
}
安利一下题解
现在这些灯都是开的,给出所有开关对所有灯的控制效果,求问最少要按几下按钮才能全部关掉。
输入输出格式
输入格式:
前两行两个数,n m
接下来m行,每行n个数,a[i][j]表示第i个开关对第j个灯的效果。
输出格式:
一个整数,表示最少按按钮次数。如果没有任何办法使其全部关闭,输出-1
输入输出样例
输入样例#1:
3
2
1 0 1
-1 1 0
输出样例#1:
2
说明
对于20%数据,输出无解可以得分。
对于20%数据,n<=5
对于20%数据,m<=20
上面的数据点可能会重叠。
对于100%数据 n<=10,m<=100
很多人看到这道题的取值范围后,第一时间的想法就是——搜索??
接下来看怎么做:
预处理部分如下:
for (int i=0;i<(1<<n);i++)
for (int j=1;j<=m;j++){
int ans=0;
for (int k=1;k<=n;k++){
if (a[j][k]==1) if (i&(1<<n-k)) ans=ans;else;//如果当前的操作是1,如果当前的灯是亮着的,把它关上,否则不管
else if (a[j][k]==-1) ans=ans+(1<<n-k);//如果当前的操作是-1,直接加上
else if (a[j][k]==0) if (i&(1<<n-k))ans+=(1<<n-k);else;else;//如果当前的操作是0,如果当前的灯是开着的,累加;否则不管
}
t[j][i]=ans;
}
那么具体代码如下:
#include
using namespace std;
int n,m;
int a[110][110];
queue < int > q;
int t[2001][2001];
int dp[2001];
int main(){
scanf("%d %d",&n,&m);
for (int i=1;i<=m;i++)
for (int j=1;j<=n;j++) scanf("%d",&a[i][j]);
for (int i=0;i<(1<<n);i++)
for (int j=1;j<=m;j++){
int ans=0;
for (int k=1;k<=n;k++){
if (a[j][k]==1) if (i&(1<<n-k)) ans=ans;else;//如果当前的操作是1,如果当前的灯是亮着的,把它关上,否则不管
else if (a[j][k]==-1) ans=ans+(1<<n-k);//如果当前的操作是-1,直接加上
else if (a[j][k]==0) if (i&(1<<n-k))ans+=(1<<n-k);else;else;//如果当前的操作是0,如果当前的灯是开着的,累加;否则不管
}
t[j][i]=ans;
}
// for (int i=0;i<(1<
// for (int j=1;j<=m;j++) cout<
// cout<
// }
memset(dp,-1,sizeof(dp));
dp[(1<<n)-1]=0;
q.push((1<<n)-1);
while (!q.empty()){
int x=q.front();q.pop();
for (int i=1;i<=m;i++){
int y=t[i][x];
if (dp[y]==-1) dp[y]=dp[x]+1,q.push(y);
}
}
// for (int i=(1<=0;i--) cout<
// cout<
printf("%d",dp[0]);
return 0;
}
其实跟我的题解也差不多
状态压缩具体介绍到这里,有兴趣的人可以去继续看一看我其它的博客