【问题标题】:Trouble with finding floor(log2(int)) using binary search in O(log2(amount_bits))在 O(log2(amount_bits)) 中使用二进制搜索查找 floor(log2(int)) 时遇到问题
【发布时间】:2019-08-18 01:40:42
【问题描述】:

在我们的算法课上,教授在实验课上多了一道题。在 log2(n) 步中找到 n 位 int 的 floor(log2(x))(例如,当 T = uint64_t 时,n = 64)。

我们发现我们应该能够通过二分搜索来解决这个问题,但是在某些边缘情况下我们会得到一个关闭 1 的结果或无限循环。我们摸索了一段时间,但似乎无法做到这一点。我们如何最好地处理这个问题?我们试图用here 讨论的不变技巧来推理,但它似乎比它复杂一点。例如。对于十进制数,在第 7 位或第 6 位之间进行选择很困难,因为 128 大于 100,但 64 更小。不幸的是,在缓解这个问题时,我们打破了一些极端情况。

编辑:如下所述,这纯粹是一个学术问题,在现实生活中几乎没有可用性。

到目前为止,这是我们的代码:

//
//   h      l
//   76543210
// 0b01000001 = 65
//

using T = unsigned char;

int lgfloor(T value)
{
    assert(value > 0);

    int high = ((sizeof(value) * 8) - 1);
    int low = 0;
    int mid = 0;
    T guess = 0;

    while (high > low)
    {
        mid = (low + ((high - low) / 2));
        guess = static_cast<T>(1) << mid;

        printf("high: %d, mid: %d, low: %d\n", high, mid, low);

        if (value < guess)
        {
            high = mid - 1;
        }
        else
        {
            low = mid;
        }
    }

    return low;
}

我们创建了以下单元测试(使用 GoogleTest):

TEST(LgFloor, lgfloor)
{
    ASSERT_DEATH(lgfloor(-1), "Assertion `value > 0' failed.");
    ASSERT_DEATH(lgfloor(0), "Assertion `value > 0' failed.");

    ASSERT_EQ(lgfloor(1), 0);
    ASSERT_EQ(lgfloor(2), 1);
    ASSERT_EQ(lgfloor(64), 6);
    ASSERT_EQ(lgfloor(100), 6);
}

提前致谢, 亲切的问候,

【问题讨论】:

  • char 在您的平台上是否已签名或未签名?如果它没有签名,那么你的断言会很有趣。由于建议仅对无符号数量执行移位,但您希望使用有符号输入进行测试,因此您需要确定要使用哪一个。
  • @Max Langhof 对于using T = unsigned long long int,该算法同样失败,这是我们最初开发此方法时使用的类型。
  • 我只是想帮助您改进问题。我知道这些挑剔不是您的主要关注点,但是当这些问题不是您关心的问题时,其他人会被困在这些问题上,这很烦人。
  • 无论如何,你在调试这个的时候发现了什么? 哪个测试用例失败了,您的搜索采取了哪些步骤?

标签: c++ algorithm binary


【解决方案1】:

您需要一个适当的退出条件。假设y = floor(lg2(x))。当2^low &lt;= xx &lt; 2^(low+1) 时,您应该退出循环。但是如果high == low+1 则满足,但您当前没有退出。做吧:

while (high > low+1)
{

看看你的循环中的不变量是很好的。例如,我们可以尝试维护x &lt; 2^high(这需要从sizeof(T)*8 开始,而不是sizeof(T)*8 - 1)。然后你需要做的就是平分直到low == high-1,你就完成了。

如果x &lt; 2^mid,即如果value &lt; guess,我们可以通过仅将high 更改为mid 来保持这个不变量。这是第一种情况:

if (value < guess)
  high = mid;

我们还必须维护2^low &lt;= x = value。所以,在 else 分支中(需要2^mid == guess &lt; value,我们可以安全地设置low = mid

else
  low = mid;

剩下的就是证明循环总是在进行。由于high &gt; low+1,我们有high - low &gt;= 2,因此有mid != lowmid != high。显然,我们正在将每次迭代的时间间隔(减半)。

所以你去:

int lgfloor(T value)
{
    assert(value > 0);

    int high = (sizeof(value) * 8);
    int low = 0;

    while (high > low+1)
    {
        int mid = (low + ((high - low) / 2));
        T guess = static_cast<T>(1) << mid;

        printf("high: %d, mid: %d, low: %d\n", high, mid, low);

        if (value < guess)
            high = mid;
        else
            low = mid;
    }

    return low;
}

我当然应该注意,在现代硬件中,有专门用于此目的的内部函数。例如,在 Intel's intrinsics guide 中搜索 _BitScanReverse,这将在上述代码所需周期的一小部分内完成。

在处理固定宽度类型(例如 C++ 的整数类型)时,依赖于位宽的渐近运行时无论如何都是毫无意义的(尽管这个问题仍然具有教育价值)。

【讨论】:

  • if (value &lt; guess) { high = mid; } else { low = mid + 1; } 导致 lgfloor(1) 失败。 if (value &lt; guess) { high = mid - 1; } else { low = mid; } 导致 lgfloor(2) 中出现无限循环:high: 2, mid: 1, low: 1。修复一个案例会导致其他案例失败。我们怀疑这不是普通的二分搜索应用程序,而是有一个我们似乎经常忽略的额外困难。我们调试了这种方法的各种变体,但似乎总是在一项或多项测试中失败。
  • @MartenBE 在这种情况下,只需添加检查 guess == value 并返回 mid。请注意,low 在您的实现中永远无法达到high,因此如果在任何时候high 是正确的猜测,那么您目前无法返回high(您只能返回low)。或者,您尝试过high = midlow = mid 了吗?同样,如果您坐下来用一张纸手动完成一次(我承认自己没有这样做),那么正确的解决方案应该非常明显。
  • 我们已经尝试过了,但是这样的解决方案失败了 lgfloor(100):if (value == guess) { return mid; } else if (value &lt; guess) { high = mid - 1; } else { low = mid + 1; }。此外:if (value &lt; guess) { high = mid; } else { low = mid; } 还提供了一个无限循环:high: 7, mid: 6, low: 6。这真的很棘手:p
  • 好吧value == guess当然是错的。你需要检查的是你是否在最左边。即if ((guess ^ value) &lt; guess) return mid;
  • 另请注意,有一个内在函数可以为您完成整个工作指令(基本上是 O(1)):_BitScanReverse。不符合分配条件(我的意思是,您可能会争辩说,用于加法、除法移位和比较的内置操作都是 O(numBits) 一样的),但以防万一您在实践中必须这样做,您知道。
【解决方案2】:

无限循环是由于这一行:

 mid = (low + ((high - low) / 2));

如果highlow 相差1,则结果可能是mid == low,然后在while 循环中导致low = mid 的条件下,您将永远检查相同的条件。我的建议是,如果你在循环中有low = mid,那么在这种情况下你必须确保你的mid != low。因此,只需在分配之前检查此内容,如果发生这种情况,请执行 low = mid+1

【讨论】:

    【解决方案3】:

    解决方案必须在lg(n) 步骤中找到,这意味着诸如low= 0high= 32 之类的初始化将不起作用,因为它在每种情况下都需要5 步骤并且不适用于x 大于 2^32。正确的解决方案必须结合第一个 geometric 搜索,其中您将指数加倍,然后是标准二分搜索。

    # Geometric search
    low= 0
    high= 1
    while (1 << high) <= x:
        low= high
        high+= high
    
    # Dichotomic search
    while high - low > 1:
        mid= (high + low) >> 1
        if x < mid:
            high= mid
        else:
            low= mid
    

    【讨论】:

      【解决方案4】:

      似乎您只需将 if 转移到正确的“日志”时间,直到您有一个“1”。

      using T = unsigned char;
      
      int lgfloor(T value)
      {
        assert(value > 0);
      
        int log = 0;
        while(value != 1) {
          value >> 1;
          log++;
        }
        return log;
      }
      

      【讨论】:

      • 这个解决方案是 O(amount_bits) 而不是 O(log2(amount_bits))。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2019-09-12
      • 1970-01-01
      • 2010-10-19
      • 1970-01-01
      • 1970-01-01
      • 2017-12-25
      • 2021-08-06
      相关资源
      最近更新 更多