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