状压 DP 简介
状压 dp 是动态规划的一种,通过将状态压缩为整数来达到优化转移的目的。
需要有位运算的基础
原博客:
https://www.cnblogs.com/ljy-endl/p/11627018.html
位运算的注释与代码如下
#include<iostream>
using namespace std;
int main(){
int x = 0b1001;
// x = x >> 2;
// >>> x*4;
// x = x << 3;
// >>> x//8;
// 第二个 是不是 1:
int i = 2;
if( ( (1<< (i-1)) &x) > 0) {
cout << 1 << endl;
}else cout << 0 << endl;
//0b10110 & 00100 >>> 100 > 0;
//0b10010 & 00100 >>> 0;
// & : 都为1 才是1
x = 0b1001;
//将一个数字x二进制下第i位更改成1
i = 2;
x = (1 << (i-1)) | x;
cout << x << endl;
//0b10110 | 00100 >>> 10110;
//0b10010 | 00100 >>> 10110;
// | :有零 补为 1
x = 0b1001;
//将一个数字x二进制下第i位更改成0
i = 4;
x = x & ~(1 << (i-1));
cout << x << endl;
//0b10110 & 11011 >>> 10010;
//0b10111 & 11111 >>> 10111;
//只有原来是1的 补反码 才是1,那么把要变的地方 换成0;
x = 0b1010;
// x = 10;
//把一个数字二进制下最靠右的第一个1去掉。
x = x & (x-1);
// x-1 = 1001;
//减去1 后 最靠右的第一个1 与 原来的错位 ,再与运算后 变成了0;
// 1001 & 1010 = 1000;
cout << x << endl;
}
**
原话
**
实际状压dp顾名思义,就是采用位运算,来记录更多的必须记录的状态来做dp有了比较深的dp功底后只要对位运算有了解就可以解决问题。。。
考虑到每行每列之间都有互相的约束关系。因此,我们可以用行和列作为另一个状态的部分。用一个新的方法表示行和列的状态:数字。考虑任何一个十进制数都可以转化成一个二进制数,而一行的状态就可以表示成这样——例如:1010(2)
个人的理解
原来是数组 : 第i行 用dp[ i ] = { 1 , 0 , 1 , 0 };
现在只需要一个数 10 就可以了,10的二进制为 1010;
【例题1】骑士(P1896 [SCOI2005]互不侵犯)
题目描述
在 n×n(1<=n<=10) 的棋盘上放 k(0<=k<n×n)个国王,国王可攻击相邻的 8 个格子,求使它们无法互相攻击的方案总数。
输入格式
输入有多组方案,每组数据只有一行,包含两个整数 n 和 k。
输出格式
每组数据一行为方案总数,若不能够放置则输出 0。
输入样例
3 2
4 4
样例输出
16
79
原题解释:
我们的三个状态就有了:第几行(用i表示)、此行放什么状态(用j表示)、包括这一行已经使用了的国王数(用s表示)。
考虑状态转移方程。我们预先处理出每一个状态(s[x])其中包含二进制下1的个数,及此状态下这一行放的国王个数(num[x]),于是就有:
f[ i ][ j ][ s ] = sum ( f [ i−1 ][ k ][ s − num [ j ] ] ) , f[ i ][ j ][ s ]就表示在只考虑前i行时,在前i行(包括第i行)有且仅有s个国王,且第i行国王的情况是编号为j的状态时情况的总数。而k就代表第i-1行的国王情况的状态编号
个人的理解
首先,i是表示行数,j是表示编号记录用,s是表示运行状态,有多少个国王,进程如何用
他写的f[ i ][ j ][ s ] = sum ( f [ i−1 ][ k ][ s − num [ j ] ] )
看的太糊了
实际上就是这个:
f[ i ][ j ][ k ] += f [ i-1 ][ j’ ] [ k’ ]
状压DP 就是将一个状态 转化成 一个数,用一个数表示出来状态,然后再用位运算去进行对状态的处理
对于相邻边上下是否重合,作这样的位运算
1010 & 0010 >>> 0010 != 0 不符合要求
10001 & 00100 >>> 00000 == 0 符合要求
对左上,右下是否重合
10001000 >> 1 == 01000100
01000000 & 01000100 >>> 01000000 != 0 不符合要求
10010010 >> 1 == 01001001
00000001 & 01001001 >>> 00000001 != 0 不符合要求
100010 >> 1 == 010001
001000 & 010001 == 0 符合要求
对右上,左下是否重合
把 >> 换成 << 就ok
对自己一行是否符合条件,位运算如下
1010 >> 1 == 0101
1010 & 0101 == 0 符合要求
1100 >> 1 == 0110
1100 & 0110 == 0100 != 0 不符合要求
原题原博客的那个图的代码:
#include<bits/stdc++.h>
using namespace std;
long long f[11][155][155], ans; int num[155],s[155],N,K,s0;
void pre(){
int i,j,k;
s0 = 0;
ans = 0;
memset(f,0,sizeof f);
for(i=0; i<(1<<N) ;i++){
if(i& (i<<1))continue;
k = 0;
for(j=0; j<N ;j++) if(1& (1<<j)) k++;
s[++ s0] = i;
num [s0] = k;
}
}
void DP(){
int i,j,k,t;
f[0][1][0] = 1;
for(i=1 ; i<=N ;i++)
for(j=1 ; j<=s0 ; j++)
for(k=0 ; k<=K; k++)
if(k > num[j])
for(t=1 ; t<= s0 ;t++)
if( !(s[t] & s[j] && !(s[t] & s[j]<<1)) &&!(s[t] & (s[j] >> 1)))
f[i][j][k] += f[i-1][t][k-num[j]];
for(i=1 ; i<=s0 ;i++) ans += f[N][i][K];
cout << ans << endl;
}
int main(){
while(cin >> N >> K)per(),DP();
return 0;
}