【问题标题】:Efficient way to find Next Smaller number with same number of 1 bits找到具有相同数量 1 位的下一个较小数字的有效方法
【发布时间】:2013-09-21 05:35:36
【问题描述】:

我正在研究这个问题并想出一个解决方案(可能需要添加一个或两个条件),但不确定这是否是正确的方法,并且发现使用两个循环很麻烦,而不是确定这是否是有效的方法。如果有人有一些不错的技巧可以做到这一点,或者任何更好的方法都会受到欢迎:)。 (语言不是障碍)

我的算法:

  • 首先找到数字中的第一个'0' lsb位
  • 然后找到与这个“0”位相邻的下一个设置位
  • 将您找到的“0”位更改为 1,将“1”位更改为“0”
  • 您将获得的数字更小
  • 如果所有位都已设置,那么您没有任何比相同数量的“1”位更小的数字。
void nextSmaller(int number) {
    int firstZeroBitHelper = 1, nextOneBitHelper;
    while (firstZeroBitHelper < number) {
        // when we find first lsb zero bit we'll stop
        bool bit = number & firstZeroBitHelper;
        if (bit == false)
            break;
        firstZeroBitHelper = firstZeroBitHelper << 1;
    }

    if (firstZeroBitHelper >= number) {
        cout << "No minimum number exists" << endl;
        return;
    }

    nextOneBitHelper = firstZeroBitHelper;
    nextOneBitHelper = nextOneBitHelper << 1;

    while (nextOneBitHelper < number) {
        // when we get '1' after the previous zero we stop
        bool bit = number & nextOneBitHelper;
        if (bit == true)
            break;
        nextOneBitHelper = nextOneBitHelper << 1;
    }

    // change the first zero to 1
    number = number | firstZeroBitHelper;
    // change the next set bit to zero
    number = number & ~nextOneBitHelper;

    cout << number << endl;
}

【问题讨论】:

  • 如果数字是 31 怎么办?您如何找到具有相同数量 1 位的下一个最小的?或者我应该问,根据您的要求,下一个最小的是什么?
  • @Smac89:虽然,我在练习按位运算问题时在网上找到了这个问题,但是在所有位设置的情况下,如果我找不到任何零位,我会在两者之间放置一个 if 条件在两者之间,然后返回一条消息。
  • 我认为在这种情况下我们找不到具有相同数量“1”位的最小数字。
  • 既然有一个很好的方法来做相反的事情(即找到下一个 更高 具有相同数量的 1 的数字),我怀疑有一个(几乎?)同样好的方法来找到下一个较小的。我去找找。。

标签: algorithm binary


【解决方案1】:

继续我的评论..

嗯,我找到了,而且很快。请参阅计算机编程的艺术第 7.1.3 章(第 4A 卷),回答问题 21:“Gosper 黑客的反面”。

看起来像这样:

t = y + 1;
u = t ^ y;
v = t & y;
x = v - (v & -v) / (u + 1);

y 是输入,x 是结果。与 Gosper 的 hack 中相同的优化适用于该部门。

【讨论】:

  • 感谢您的回答,这看起来是一个很棒的简洁解决方案。我会试着理解它到底发生了什么:)
【解决方案2】:

往上走:

  • 在数字中找到最右边的“01”并将其设为“10”。
  • 使后面的所有 1 位尽可能靠右对齐。

往下走:

  • 在数字中找到最右边的“10”并将其设为“01”。
  • 左对齐所有跟随 1 位(即,如果您刚刚设置的位后面已经跟着一个 1,则不要执行任何操作)。

一个说明向下大小写的例子:

  • 225 = 0b11100001
  • 交换:0b11010001
  • 左对齐:0b11011000 = 216

我将首先解释向上的情况,因为我觉得它不那么棘手。我们希望找到可以将 1 位向左移动一位的最不重要的位置(换句话说,最右边的 0 右边有一个 1)。应该清楚的是,这是我们可以设置的最右边的位,因为我们需要为我们设置的每个位在其他地方清除一个位,并且我们需要在 right 的某个地方清除一个位我们设置的位,否则数字会变小而不是变大。

现在我们已经设置了这个位,我们想要清除一个位(以恢复设置位的总数),并重新洗牌剩余的位,使数字尽可能小(这使它成为 next 具有相同设置位数的最大数)。我们可以清除我们刚刚设置的那个位的右边的位,我们可以将任何剩余的 1 位尽可能推到最右边,而不必担心低于我们的原始数字,因为所有不太重要的位加起来仍然是小于我们刚刚设置的单个位。

找到下一个较低的数字而不是下一个较高的数字基本上是相同的,除了我们正在寻找最右边的位置,我们可以将一组位移动到一个位置right,然后这样做我们希望将所有不太重要的位尽可能向左移动。

看起来其他人已经掌握了这个很好的版本,但我想看看我是否可以很好地解释算法的逻辑/数学方面。

【讨论】:

【解决方案3】:

anatolyg 很好地涵盖了您的算法,但有一个更有效的解决方案。

您可以使用Gosper's hack 巧妙地意识到,如果您翻转位,那么 Gosper 会按降序生成值。

类似这样的伪代码可以工作:

let k := number
let n := num bits in k (log base 2)
k = k ^ ((1 << n) - 1)
k = gosper(k)
k = k ^ ((1 << n) - 1)
return k

这为您提供了一个不错的 O(1)(如果您认为 xor 是线性时间,则为 O(log n))算法。 :)

有些情况你必须考虑,比如如果k=2^x-1 换成x,但这很容易发现。

【讨论】:

  • 感谢您的回答 :) 这个 Gosper 的 hack 很有趣!
【解决方案4】:

你描述的算法不太正确;除了一个细节,它做的一切都是正确的。任何二进制数都具有以下形式,可在您的算法中间找到:

xxxxx...10000...1111...
         ---n---// f // 

这里xxx...是任意位,连续0和1的个数由firstZeroBitHelpernextOneBitHelperfn)决定。

现在你必须减少这个数字,保留相同数量的设置位,这必然会将最重要的1 变为0

xxxxx...0????...????...
         -----n+f------ 

请注意,??? 位的任何值都会使新数字小于原始数字,并且您确实希望选择这些位以使结果数字具有最大值:

xxxxx...011111...0000...
         ---f+1--//n-1// 

所以你不仅要翻转 2 位,还要翻转 f+2 位(一位从 10f+1 其他位从 01)。

一种方法如下。

首先关闭所有相关位:

number &= ~nextOneBitHelper;
number &= ~(nextOneBitHelper - 1);

现在打开需要的位,从 MSB 开始:

nextOneBitHelper >>= 1;
while (firstZeroBitHelper != 0)
{
    number |= nextOneBitHelper;
    nextOneBitHelper >>= 1;
    firstZeroBitHelper >>= 1;
}

可以在没有循环的情况下实现上面描述的位旋转;为此,您需要计算 nf。完成后:

unsigned mask = (1 << (f + 1)) - 1; // has f+1 bits set to 1
mask <<= n - 1; // now has these bits at correct positions
number |= mask; // now the number has these bits set

【讨论】:

  • 感谢您的精彩解释 :)
【解决方案5】:
#include <iostream>

bool AlmostdivBy2(int num) {
    return (-~num & (num)) == 0;
}

void toggleright(int &num) {
    int t;
    for (t = -1; num & 1; t++)
        num >>= 1;
    ++num = (num << t) | ~-(1 << t);
}

void toggleleft(int &num) {
    while (~num & 1)
        num >>= 1; //Simply keep chopping off zeros
    //~num & 1 checks if the number is even
    //Even numbers have a zero at bit at the rightmost spot
}

int main() {

    int value;
    std::cin >> value;
    if (!AlmostdivBy2(value)) {
        (~value & 1) ? toggleleft(value) : toggleright(value);
    }
    std::cout << value << "\n";
    return 0;
}

我想我可能想多了,但这是我的解释:

  • 如果数字接近 2 的幂,即 1、3、7、15、31 等值,则没有比它小的值可以具有相同数量的 1在他们的二进制表示中。因此我们不用担心这些数字。

  • 如果数字是偶数,那是另一个简单的解决方法,我们只需从末尾不断删除零,直到数字为奇数

  • 奇数是最具挑战性的,这就是为什么它是递归的。首先,您必须从数字的右侧开始找到第一个零位。找到后,您将在该数字上加一,这会将最后一位变为 1。随着递归展开,您不断将这些位向左移动并加一。完成后,您将拥有下一个最小的。

希望我没有把你弄糊涂

编辑

做了更多的工作,这里是toggleright的非递归版本

void toggleright(int &num) {    
    int t = 1;
    while ( (num >>= 1) & 1 && t++ );
    num = (-~num << ~-t) | ~-(1 << t);    
}

【讨论】:

  • -~num 顺便说一句是num + 1
  • 感谢您的解释! @Smac89 :)
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-05-24
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多