• 状压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点上
    }

  • 相关阅读:
    Builder与Factory,殊途同归!
    IIS中的身份验证
    如何给项目选择合适语言(转)
    动态行转列
    ORACLE系统表大全(转)
    C# 操作Word文档(转)
    产品化思维之分层的思想
    开发管理目前开发工作的问题分析和诊断
    MongoDB数据插入、删除、更新、批量更新某个字段
    学习正则表达式
  • 原文地址:https://www.cnblogs.com/719666a/p/9977239.html
Copyright © 2020-2023  润新知