状压dp之位运算


## 一.知识
1.我们知道计算机中数据由二进制数存储,一个二进制数的一位就是计算机中数据的最小单位bit,我们有一种运算符可直接对二进制数进行位运算,所以它的速度很快。

2.C++中的位运算符有6

        &     与运算        同1得1
        |       或运算       有1得1   
        ^      异或运算    不同得1
        ~      非运算       取反
       >>     右移运算   
      <<      左移运算

3. 按位取反举例
补码的补码就是原码,在计算机中存储全是补码,
十进制数1的二进制补码为:00000001
取反后补码为:11111110
求这个补码的原码:10000010(屏幕上显示都是原码-2) 所以1取反后等于-2

4. 在补码下,每个数值都有唯一的表示方式,并且任意两个数值做加减法运算,都等价于在32位补码下做**最高位不进位**的二进制加减法运算。发生算术溢出时,相当于自动对2^32^取模。所以要注意两个大整数相加有可能是负数。

## 二.成对变换
我们通过计算可以发现,对于非负整数n:
当n为偶数时,n ^ 1 等于 n+1
当n为奇数时,n ^ 1 等于 n-1
因此0和1,2和3,4和5,这些数对都是关于异或“成对变换”,这个性质经常用于图论,可以通过异或运算获得当前边的反向边编号。但为了安全起见,最好边的编号最好从2开始。

## 三.lowbit运算
lowbit(n) 取出非负整数n在二进制表示下最低位的1以及后边的0构成的数值。
lowbit(n) = n & (~n+1) = n & -n
lowbit 是树状数组的核心操作。
lowbit 配合hash可以找到整数二进制表示下所有是1的位,所花费的时间与1的个数同级。

## 四.二进制状态压缩

二进制状态压缩,是指将一个长度为m的bool数组用一个m位二进制整数表示并存储的方法。利用下列位运算操作可以实现原bool数组中对应下标元素的存取。

操作| 运算
---|---
取出整数n在二进制表示下的第k位                          (n>>k) & 1
取出整数n在二进制表示下第0~k-1位(后k位)       n & ((1<<k)-1)
把整数n在二进制表示下的第k位取反                       n ^ (1<<k)
对整数n在二进制表示下的第k位赋值为1                 n \| (1<<k)
对整数n在二进制表示下的第k位赋值为0                 n & (~(1<<k))

这种方法运算简便,并且节省了程序运行的时间和空间。当m不大时,可以直接使用一个整数类型存储。当m较大时,可以使用若干个整数类型(int数组),也可以直接利用bitset实现。

#### 例题
给定一张n(n<=20)个点的带权无向图,点从0~n-1标号,求起点0到终点n-1的最短hamilton路径。hamilton路径的定义是从0到n-1不重不漏地经过每个点恰好一次。([洛谷1171]

扩展题目:[POJ2288 Islands and Bridges]

思路点拨: 
很容易想到一种朴素的做法,就是枚举n个点的全排列,计算路径长度,取最小值,时间复杂度为O(n*n!),可以使用状态压缩dp将其优化到O(n^2^*2^n)

在任意时刻,要表示出哪些点被走过,哪些点没有被走过,可以用一个n位二进制数,若其第i位(0<=i<n)为1,则表示第i个点已经被经过,反之未被经过。在任意时刻还需要知道当前所处的位置,因此使用F[i,j](0<=i<2^n,0<=j<n)表示点被经过的状态对应的二进制数为i,且目前处于点j时的最短路径。

初始值F[1,0],即只经过了点0(i只有第0位是1),目前处于起点0,最短路径长度为0。方便起见,将F的其他值设为极大值,目标状态为F[(1<<n)-1,n-1],即经过所有点(i的所有位都是1),处于终点n-1的最短路。

在任意时刻,有公式F[i,j]=min{F[i ^ (1<<j),k]+weight(k,j)},0<=k<n并且((i>>j)&1)=1,即当前时刻“被经过的点的状态”对应的二进制数为i,处于点j。因为j只能恰被经过1次,所以一定是刚刚经过的,故在上一时刻“被经过的点的状态”对应的二进制数的第j位应该赋值为0,也就是i^(1<<j)。另外,上一时刻所处的位置可能是i^(1<<j)中任意一个是1的位置k,从k走到j需要weight(k,j),可以考虑取所有这样的k取最小值。
int f[1<<20][20];
int main()
{
    memset(f,0x3f,sizeof f);
    f[1][0]=0;
    for(int i=1;i<(1<<n);i+=2)
    {
        for(int j=0;j<n;j++)
        {
            if(!((i >> j) & 1)) continue; //当前i状态下,根本没有走过j
            for(int k=0;k<n;k++)
            {
                if(j==k) continue;
                if(!(i>>k & 1)) continue; //上一次状态,根本没有走过k,就更不可能从k转移到j
                f[i][j]=min(f[i][j],f[i^(1<<j)][k]+weight[k][j]);
            }
        }
    }
    cout << f[(1<<n)-1][n-1]; // 所有点走完,最后停在n-1点上
}

posted @ 2018-11-18 10:59  南柯一场  阅读(333)  评论(0编辑  收藏  举报