SCAU 2019级寒假训练 E - Secret Origins 【位运算方法】

在这里插入图片描述
题意:找到一个最小的,比N大,且二进制中1的个数与N二进制中1的个数相同,的数。
思路:

  1. 在N的二进制中,从低位到高位,找到第一个出现01的位置,然后将01置为10,然后后面的全部变成0,这一步是控制比原来的数N大,相当于进了一位。比如N的二进制10111->11000,这是第一步,并且将原来的连续1区间存下来,用于等下后面添1,即是10111->00111;
  2. 然后我们现在是想控制数尽量的小,那么肯定是将剩下的1放在低位是最好的,因为我们现在的11000相当于已经有两个1了,我只用再进两个1就行了,那么我就将00111右移,直到只剩两个1,这里就只用右移一次就完事了。 00111->00011
  3. 好,现在我们得到了两部分,一部分控制了比原来的数大(11000),另一部分控制了1的个数和且尽量小(00011),现在,我们只需要把这两部分“拼”起来,就达到了我们的目的了。相当于我们想得到11011,怎么实现呢?我们只需要进行按位或运算00011|11000即是11011啦。
  4. 正常思路实现上述步骤需要用到循环,即是贪心思想。但是看了大佬的博客发现,巧用位运算可以实现O(1)的算法。
  5. 比如我想让10111->11000,相当于10111+00001(x)->11000,那么怎么把00001弄出来呢,换句话说,它和N有什么关系?这里巧用了补码,对于一个正数N,-N的补码相当于N取反后+1,那么原来的N&(-N)的话就得到了含1的最低位,并以十进制的方式输出,举个例子01110的lowbit就是第二位(低位开始数),那么采取上面操作就会得到2,也就是00010,而这个00010加上01110不就是我要实现的进位吗->10000,一步实现我的步骤1。
  6. 下面我们就要解决原来的连续1区间怎么求出来,还是拿前三个步骤里面的10111,我想得到00111,等下通过移位操作就可以和步骤五结果进行按位或运算得到结果了。这里采用了((n & ~y) / x >> 1) ,n按位与y的取反,这里的y=N+x。因为y是N+x,那么加上x后没有变化的位,都取了相反值,而连续1区间在 ~y 中也还是1,所以这样进行按位与操作后便得到了原来的连续1区间(00111),然后因为x是最低位1,那么只需要除以x,就能将连续的1的末尾卡在最后一位,最后再右移一下,就少了一个1,达到1总数与N的相同。
  7. 最后这两部分进行按位或运算就完事了
#include <cstdio>
using namespace std;

int main()
{
    int kase, num = 0;
    scanf("%d", &kase);
    while(kase--) {
        int n;
        scanf("%d", &n);
        //思路: 1.先找到最先出现01的位置,然后将其改成10,后面全置为0
        //2.这样目的是保证最小且大于
        //3.然后为了保持1的数目一样,那么将原来的后面的1的数量-1全丢到低位去。这一步也是保证了数尽可能的小
        int x = n & -n, y = n + x;
        //x用来求出n加上多少可以得到去除最长连续1后的数y

        n = ((n & ~y) / x >> 1) | y;       //n与y取反可以得到连续1区间,除以x(lowbit)再右移一位其实就得到了3中的结果。
        printf("Case %d: %d\n", ++num, n);
    }
    return 0;
}

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值