位运算的应用与技巧:

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/consciousman/article/details/52825345

位运算的应用:

程序中的所有数在计算机内存中都是以二进制的形式储存的。所谓位运算,就是直接对整数在内存中的二进制位进行操作,一般解题时都用一个十进制整数来代表某个集合。
基本的位运算操作:
&(按位与)、|(按位或)、^(按位异或)、~ (按位取反)。
其中,按位取反运算符是单目运算符,其余均为双目运算符。
位运算符的优先级从高到低,依次为~、&、^、|,
其中~的结合方向自右至左,且优先级高于算术运算符,其余运算符的结合方向都是自左至右,且优先级低于关系运算符。

 

下面总结一下一些高效且实用的位运算技巧:

1、    枚举n个元素的所有子集:

这个就比较常用了,枚举出所有的二进制子集,总共2^n个集合

for (int i = 0; i< 1<<n; i++){
    for (int j = 0; j < n; j++)
        if (i & (1<<j))printf("%d ", j);
}
随便举一个例子:

hdu 4462- Scaringthe Birds

题意:有一个n*n的网格,有k个格子交点处可以放置稻草人,稻草人可以保护离它曼哈顿距离不超过d的范围内的点,求最少放几个稻草人可以保护所有的点。

分析:稻草人最多10个,即使不使用dlx,直接暴力效率也足够高,枚举所有的放置的情况,总共2^10-1种情况,注意题目有两个坑点(也不算是坑点吧,只是我被坑了而已),一个就是注意到距离是manhattan distance,而不是哈密尔顿距离,不要看漏了,还有一个就是可以放置稻草人的点是不要保护的,所以只要看是否覆盖了其他n*n-k个点即可…….

 

#include <cstdio>
#include <cstring>
#include <vector>
#include <algorithm>
#define INF 0x3f3f3f3f
#define LL long long
using namespace std;
int vis[55][55], r[15], x[15], y[15];
int n, k;
int dir[4][2] = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
struct po{
    int x, y;
    po(int x = 0, int y = 0):x(x), y(y){}
};
vector<po> G[15];

void dfs(int x, int y, int xx, int yy, int r, int c)
{
    vis[x][y] = 1;
    for (int i = 0; i < 4; i++){
        int nx = x + dir[i][0], ny = y + dir[i][1];
        if (nx < 1 || nx > n || ny < 1 || ny > n || vis[nx][ny]) continue;
        int d = abs(nx-xx) + abs(ny-yy);
        if (d <= r){
            G[c].push_back(po(nx, ny));
            dfs(nx, ny, xx, yy, r, c);
        }
    }
}

int main()
{
    while (scanf("%d", &n), n){
        scanf("%d", &k);
        for (int i = 1; i <= k; i++) G[i].clear();
        for (int i = 1; i <= k; i++) scanf("%d %d", &x[i], &y[i]);
        for (int i = 1; i <= k; i++) scanf("%d", &r[i]);
        for (int i = 1; i <= k; i++){
            memset(vis, 0, sizeof(vis));
            dfs(x[i], y[i], x[i], y[i], r[i], i);
        }
        int a = (1<<k), ans = INF;
        if (k == n*n) { puts("0");continue;}
        for (int i = 1; i < a; i++){
            memset(vis, 0, sizeof(vis));
            int cnt = k, c = 0;
            for (int j = 1; j <= k; j++) vis[x[j]][y[j]] = 1;
            for (int j = 1; j <= k; j++){
                if (i & (1 << (j-1))){
                    c++;
                    for (int l = 0; l < G[j].size(); l++){
                        int f = G[j][l].x, s = G[j][l].y;
                        if (!vis[f][s]){
                            vis[f][s] = 1;
                            cnt++;
                        }
                    }
                }
            }
            if (cnt == n*n) ans = min(ans, c);
        }
        if (ans == INF) ans = -1;
        printf("%d\n", ans);
    }
    return 0;
}

2、高效枚举某个集合所有的子集

就像是统计一个数的二进制位有多少个1一样:

int cnt = 0;
scanf("%d",&n);
for(; n; n &=(n-1), cnt++);

这样处理很快,循环次数为这个数当中二进制位为1的个数,n &= (n-1)的意思是清除n的二进制位当中最左边的1.

那么枚举某个集合Sup的子集的方法:

//如果不考虑空集的话,那么循环到0就可以结束了。

for (int sub = sup;sub; sub = (sub-1) & sup){
    //对子集sub的处理
}

考虑空集的话,使用dowhile 方便点

int t = sup;
do{
    //对子集的处理
    t = (t-1) & sup;
}while (t != sup);

例题:

Uva1354-MobileComputing

题意:给出一个房间的宽度r和s个挂坠的重量wi,设计一个尽量宽(但宽度不能超过房间宽度r)的天平,挂着所有挂坠,天平由长度为1的木棍组成,每一端要么挂一个挂坠,要么挂另一个天平。

分析:悬挂吊坠的方式有很多种,挂坠最多6个,暴力枚举即可。一个合法的挂的方式可以看作是一颗二叉树,那么我们只需要枚举所有可能的二叉树?怎么枚举呢?正如紫书上面所说的:自顶向下构造,每次枚举左子树用到哪些子集,那么这棵树当中的挂坠就确定了,然后右子树就是剩下的挂坠。所以我们递归构造所有可能的二叉树,对于每一个子树的根节点,统计当前悬挂方式的左右臂长度,最后对于根节点,在所有可能的长度中选出不超过r的最长的宽度即可。

所以问题的一个关键是进行子树挂坠集合的枚举,然后程序实现过程中可以进行记忆化搜索,对于已经处理过的子集,直接返回即可。整体框架感觉类似于数位dp的实现,那里是枚举数位然后统计,而这里是枚举子集然后统计。剩下的都是些细节问题了,注意计算左右臂长度时要考虑全面,这题的一个条件是保证天平的宽度不会恰好在r-1e5到r+1e5之间,所以我们可以忽略精度问题,直接进行浮点数的比较。

感觉做过这题之后体会到了位运算的方便与实用。

 

#include<algorithm>
#include<cstdio>
#include<cstring>
#include <vector>
#define INF 0x3f3f3f3f
#define LL long long
#define EPS 1e-6

using namespace std;

const int N = 1<<6;
struct node{
    double ll, rl;
    node(double l = 0, double r = 0): ll(l), rl(r){}
};
vector<node> scale[N];
int vis[N], w[6], sw[N];
int n;
double r;

int bitcount(int S)
{
    int cnt = 0;
    for (; S; S &= (S-1), cnt++);
    return cnt;
}
void dfs(int S)
{
    if (vis[S]) return ;
    vis[S] = 1;
    if (bitcount(S) == 1){
        scale[S].push_back(node(0, 0));
        return ;
    }
    for (int l = S & (S-1), r; l; l = (l-1) & S){
        r = S^l;
        dfs(l); dfs(r);
        for (int i = 0; i < scale[l].size(); i++){
            for (int j = 0; j < scale[r].size(); j++){
                double llen = max(scale[l][i].ll + sw[r]*1.0/sw[S], scale[r][j].ll - sw[l]*1.0/sw[S]);
                double rlen = max(scale[r][j].rl + sw[l]*1.0/sw[S], scale[l][i].rl - sw[r]*1.0/sw[S]);
                scale[S].push_back(node(llen, rlen));
            }
        }
    }
}
double solve()
{
    int S = (1<<n)-1;
    double ans = -1;
    dfs(S);
    for (int i = 0; i < scale[S].size(); i++){
        double t = scale[S][i].ll + scale[S][i].rl;
        if (t <= r && t > ans) ans = t;
    }
    return ans;
}
int main()
{
    int t;
    scanf("%d", &t);
    while (t--){
        scanf("%lf %d", &r, &n);
        for (int i = 0; i < n; i++) scanf("%d", w+i);
        for (int i = 1; i < 1<<n; i++){
            vis[i] = sw[i] = 0;
            scale[i].clear();
            for (int j = 0; j < n; j++)
                if (i & (1<<j)) sw[i] += w[j];
        }
        printf("%.9f\n", solve());
    }
    return 0;
}


3、枚举大小为k的所有子集。

核心代码:

int t =(1<<k) - 1;
while (t <1<<n){
    //对子集的处理
    int x = t & -t, y = t + x;
    t = ((t & ~y) / x >> 1) |y;
}

例题:

Hdu5914- Triangle

题意:给定n,有n根长度分别为1、2…..n的木棍,问最少去掉几根可以使得剩下的木棍不能组成三角形。

分析:不用斐波那契数的话,考虑到n<=20,可以直接暴力,按去掉木棍根数从小到大开始枚举,直到不能组成三角形为止。

#include <cstdio>
#include <algorithm>
#include <cstring>
#define LL long long
#define  INF 0x3f3f3f3f
using namespace std;

int a[25] = {0, 0, 0, 0, 1, 1, 2};

int judge(int x, int n)
{
    int b[30];
    int k = 0, tag = 1;
    for (int i = 1; i <= n; i++)
        if (!(x & (1 << (i-1)))) b[k++] = i;
    for (int j = k-1; j >= 2 && tag; j--)
        if (b[j] < b[j-1]+b[j-2]) tag = 0;
    return tag;
}

int main()
{
    for (int i = 7; i <= 20; i++){
        int flag = 1;
        for (int k = a[i-1]; flag ; k++){
            int co = (1<<k)-1;
            while (co < 1 << i){
                int x = co & -co, y = co + x;
                co = ((co & ~y) / x >> 1) | y;
                if (judge(co, i)) {flag = 0; a[i] = k; break;}
            }
        }
    }
    int t, ca = 0, n;
    scanf("%d", &t);
    while (t--){
        scanf("%d", &n);
        printf("Case #%d: %d\n", ++ca, a[n]);
    }
    return 0;
}


4、枚举不相邻元素的集合。

for (int i = 1; i< (1<<n); i++){
   if ((i >> 1) & i) continue;
   //对子集的处理
}

刷的题太少了,暂时没有遇到用过这个的题….

 

5、    枚举第k位一定为1的所有子集。

可以从第k位为1的最小整数开始循环,直到取遍所有的子集。

//从满足条件的最小的整数m开始,枚举所有第k位一定为1的所有子集:
for (int t = m; t< (1 << n); t = (t+1) | m){
   //对子集的处理
}

6、    当然还有很多有用的位运算,在此借用Matrix67大牛的总结:

下面列举了一些常见的二进制位的变换操作。

功能 | 示例 | 位运算
去掉最后一位 | (101101->10110) | x>> 1
在最后加一个0 | (101101->1011010) | x<< 1
在最后加一个1 | (101101->1011011) | x<< 1+1
把最后一位变成1 | (101100->101101) | x| 1
把最后一位变成0 | (101101->101100) | x| 1-1
最后一位取反 | (101101->101100) | x^ 1
把右数第k位变成1 |(101001->101101,k=3) | x | (1<< (k-1))
把右数第k位变成0 |(101101->101001,k=3) | x & ~(1 << (k-1))
右数第k位取反 |(101001->101101,k=3) | x ^ (1<< (k-1))
取末k位 |(1101101->1101,k=5) | x &((1 << k)-1)
取右数第k位 |(1101101->1,k=4) | x >>(k-1) & 1
把末k位变成1 |(101001->101111,k=4) | x | ((1<< k)-1)
末k位取反 |(101001->100110,k=4) | x ^ ((1<< k)-1)
把右边连续的1变成0 |(100101111->100100000) | x &(x+1)
把右起第一个0变成1 |(100101111->100111111) | x | (x+1)
把右边连续的0变成1 |(11011000->11011111) | x | (x-1)
取右边连续的1 | (100101111->1111) |(x ^ (x+1)) >> 1
去掉右起第一个1的左边 | (100101000->1000) | x & (x ^ (x-1))
最后一个在树状数组中会用到。

 

7、    关于位运算的一点总结:

很久之前借了学校图书馆的一本叫做算法心得的书,看过之后感觉作者简直神奇,不愧是大师,里面是各种优化程序的奇淫技巧,不过那时什么都不懂,看了觉得没用,现在懂了一点了,然而之前看过的都忘了,里面关于位运算的内容特别多,值得一看。

下面就是一些常见的技巧,效率都比普通运算要高很多

/*1、交换两个整数的值:
int a, b;
a = a ^ b;
b = a ^ b;
a = a ^ b;
与下面的等价,不过利用的是^的重要性质,效率当然比算术运算高
a = a + b;
b = a - b;
a = a - b;

2、与2的x次方的运算
int a;
a <<= x;//乘2^x
a >= x;//除2^x
a & (1 << x) - 1 //取a%(2^x)的余数
a & 1//判断奇偶性,为0则a为偶数,否则为奇数
a & (a-1)//判断a是否为2的幂或者0,结果为0代表是,否则代表不是

3、其他常用技巧
int a, b;
a = ~a + 1//取相反数
a = (a ^ (a >> 31)) - (a >> 31) //取绝对值
(a & b) + ((a ^ b) >> 1) //取平均值
a ^ b//判断a、b符号是否相同,如果结果>0则相同,否则不同
*/

(1)、位运算优先级较低,如果结合其他的运算符,那么最好在使用时加上括号,当然如果很清楚优先级就另当别论了。
(2)、位运算虽说高效,但是很多枚举子集的技巧数据量大点就无法使用了,所以还是得慎用,根据题目的数据范围而定吧。
(3)、使用位运算注意细节的处理,比如说枚举子集时的起点,终点等等。
(4)、使用位运算关键是理解每一个运算的特点,灵活运用它们的性质,并且找到问题与位之间的联系,其实上面的几个枚举集合的技巧都是根据位之间的联系然后运用相应的运算符得出的,一些位运算符也有一些非常重要的特点,比如说异或运算具有交换律,结合律,自反性等等。
(5)、位运算应用很广,体现在很多算法和数据结构上,比如说状态压缩,树状数组等等,在状态压缩中的使用通常是最常见的,很多算法都设计到状态之间的转移,比如说搜索,dp等等。有时候很难表示当前的这个状态,但是通过二进制位便可以解决了,所以位运算还是很方便和实用的,比如说一些在棋盘,网格中要表示某一行/列的状态时的问题。
在数据结构中的应用最常见的是树状数组,借用论文上面的话:树状数组的思想核心在于运用了十进制数与二进制数之间的联系,通过数的二进制形式来决定储存信息,把复杂的问题简单化,方法简单巧妙。
树状数组的优势在于代码长度短,不易出错,思想巧妙,算法复杂度低,维护简单,易推广到二维甚至三维等等。对于数据结构要求操作不复杂的题目,是上佳的选择。
而且线段树也涉及到了简单的右移位运算,或运算等等。

 

展开阅读全文

没有更多推荐了,返回首页