计算二进制表示完全要求数字1的数字

时间:2018-02-15 15:08:31

标签: algorithm binary dynamic-programming

好的问题是找到一个正整数n,使得在n + 1到2n(包括两个端点)中恰好有m个数,其二进制表示恰好为k 1s。 约束:m <= 10 ^ 18且k <= 64。答案也不到10 ^ 18。

现在我无法想出一种有效的解决方法,而不是遍历每个整数并计算每个整数所需时间间隔内的二进制1计数,但这需要太长时间。那么还有其他方法吗?

1 个答案:

答案 0 :(得分:3)

你怀疑是否有更有效的方式。

让我们从稍微简单的子问题开始。缺席一些非常聪明 洞察力,我们需要能够找到整数的数量 [n+1, 2n]在其二进制表示中设置了k位。至 保持简短,让我们称之为整数&#34;体重 - k&#34;整数(为了这个术语的动机,查找Hamming weight)。我们可以 立即简化我们的计数问题:如果我们可以计算k中的所有权重 - [0, 2n]整数 我们可以计算k中的所有权重 - [0, n]整数,我们可以减去一个计数 从另一个获取权重数 - k中的[n+1, 2n]整数。

因此,一个明显的子问题是计算有多少权重 - k整数 对于给定的非负整数[0, n]k,在n区间内。

这种问题的标准技术是寻找破解的方法 它归结为较小的同类子问题;这是一个方面 通常称为dynamic programming的是什么。在这种情况下,这是一种简单的方法 这样做:考虑[0, n]中的偶数和[0, n]中的奇数 分别。 m中的每个偶数[0, n]的权重与其完全相同 m/2(因为除以2,我们所做的就是删除一个零 位)。同样地,每个奇数m的权重恰好都是一个 重量(m-1)/2。考虑到一些适当的基础案例,这个 导致以下递归算法(在本例中用Python实现, 但它应该很容易翻译成任何其他主流语言。)

def count_weights(n, k):
    """
    Return number of weight-k integers in [0, n] (for n >= 0, k >= 0)
    """
    if k == 0:
        return 1  # 0 is the only weight-0 value
    elif n == 0:
        return 0  # only considering 0, which doesn't have positive weight
    else:
        from_even = count_weights(n//2, k)
        from_odd = count_weights((n-1)//2, k-1)
        return from_even + from_odd

这里有很多错误,所以让我们测试一下我们的花哨递归 对低效但更直接的东西的算法(我希望,更多 显然是正确的):

def weight(n):
    """
    Number of 1 bits in the binary representation of n (for n >= 0).
    """
    return bin(n).count('1')

def count_weights_slow(n, k):
    """
    Return number of weight-k integers in [0, n] (for n >= 0, k >= 0)
    """
    return sum(weight(m) == k for m in range(n+1))

比较两种算法的结果看起来很有说服力:

>>> count_weights(100, 5)
11
>>> count_weights_slow(100, 5)
11
>>> all(count_weights(n, k) == count_weights_slow(n, k)
...     for n in range(1000) for k in range(10))
True

然而,我们所谓的快速count_weights功能并不能很好地扩展到。{ 您需要的尺码:

>>> count_weights(2**64, 5)  # takes a few seconds on my machine
7624512
>>> count_weights(2**64, 6)  # minutes ...
74974368
>>> count_weights(2**64, 10)  # gave up waiting ...

但是,这里有动态编程的第二个关键想法:memoize! 也就是说,记录以前调用的结果,以防我们需要使用 他们又来了。事实证明,的递归调用链倾向于 重复大量的电话,所以在记忆中有价值。在Python中,这是 通过functools.lru_cache装饰器,很容易做到。这是我们的新事物 版本count_weights。所有改变的都是顶部的额外线:

@lru_cache(maxsize=None)
def count_weights(n, k):
    """
    Return number of weight-k integers in [0, n] (for n >= 0, k >= 0)
    """
    if k == 0:
        return 1  # 0 is the only weight-0 value
    elif n == 0:
        return 0  # only considering 0, which doesn't have positive weight
    else:
        from_even = count_weights(n//2, k)
        from_odd = count_weights((n-1)//2, k-1)
        return from_even + from_odd

现在再次对这些较大的示例进行测试,我们可以更快地获得很多的结果, 没有任何明显的延迟。

>>> count_weights(2**64, 10)
151473214816
>>> count_weights(2**64, 32)
1832624140942590534
>>> count_weights(5853459801720308837, 27)
356506415596813420

所以现在我们有一种有效的计算方法,我们遇到了一个反问题 求解:给定km,找ncount_weights(2*n, k) - count_weights(n, k) == m。事实证明这一点特别容易,因为 数量count_weights(2*n, k) - count_weights(n, k)是单调的 随n增加(对于固定k),更具体地说,增加任何一个 01每次n增加1。我会留下那些证据 事实给你,但这是一个演示:

>>> for n in range(10, 30): print(n, count_weights(n, 3))
... 
10 1
11 2
12 2
13 3
14 4
15 4
16 4
17 4
18 4
19 5
20 5
21 6
22 7
23 7
24 7
25 8
26 9
27 9
28 10
29 10

这意味着我们保证能够找到解决方案。可能存在多种解决方案,因此我们的目标是找到最小的解决方案(尽管找到最大的解决方案同样容易)。二分搜索为我们提供了一种粗略但有效的方法。这是代码:

def solve(m, k):
    """
    Find the smallest n >= 0 such that [n+1, 2n] contains exactly
    m weight-k integers.

    Assumes that m >= 1 (for m = 0, the answer is trivially n = 0).
    """
    def big_enough(n):
        """
        Target function for our bisection search solver.
        """
        diff = count_weights(2*n, k) - count_weights(n, k)
        return diff >= m

    low = 0
    assert not big_enough(low)

    # Initial phase: expand interval to identify an upper bound.
    high = 1
    while not big_enough(high):
        high *= 2

    # Bisection phase.
    # Loop invariant: big_enough(high) is True and big_enough(low) is False
    while high - low > 1:
        mid = (high + low) // 2
        if big_enough(mid):
            high = mid
        else:
            low = mid
    return high

测试解决方案:

>>> n = solve(5853459801720308837, 27)
>>> n
407324170440003813446

让我们仔细检查n

>>> count_weights(2*n, 27) - count_weights(n, 27)
5853459801720308837

看起来不错。如果我们的搜索正确,这应该是最小的 有效的n

>>> count_weights(2*(n-1), 27) - count_weights(n-1, 27)
5853459801720308836

还有很多其他的优化和清理机会 上面的代码,以及解决问题的其他方法,但我希望这给你一个 起点。

OP评论说他们需要在C中执行此操作,其中无需使用外部库即可立即使用memoization。这是count_weights的变体,不需要记忆。它是通过以下方式实现的:(a)在count_weights中调整递归,以便在递归调用中使用相同的n,然后(b)返回给定的n ,答案为非零的所有 count_weights(n, k)的{​​{1}}值。实际上,我们只是将备忘录移动到一个明确的列表中。

注意:如上所述,下面的代码需要Python 3。

k

示例电话:

def count_all_weights(n):
    """
    Return frequencies of weights of all integers in [0, n],
    as a list. The kth entry in the list gives the count
    of weight-k integers in [0, n].

    Example
    -------
    >>> count_all_weights(16)
    [1, 5, 6, 4, 1]

    """
    if n == 0:
        return [1]
    else:
        wm = count_all_weights((n-1)//2)
        weights = [wm[0], *(wm[i]+wm[i+1] for i in range(len(wm)-1)), wm[-1]]
        if n % 2 == 0:
            weights[bin(n).count('1')] += 1
        return weights

即使对于较大的>>> count_all_weights(7590) [1, 13, 78, 286, 714, 1278, 1679, 1624, 1139, 559, 182, 35, 3] ,此函数也应该足够好:n在我的机器上只需不到0.5毫秒。

现在,二分搜索将像以前一样工作,将count_all_weights(10**18)的调用替换为count_weights(n, k)(同样适用于count_all_weights(n)[k])。

最后,另一种可能性是将区间count_weights(2*n, k)分解成一系列越来越小的子区间,其中每个子区间的长度为2。例如,我们将时间间隔[0, n]分为[0, 101][0, 63][64, 95][96, 99]。这样做的好处是我们可以通过计算组合轻松计算这些子区间中任何一个子区间的权重 - [100, 101]整数。例如,在k中我们有所有可能的6位组合,所以如果我们在权重-3整数之后,我们知道它们必须恰好有6选择3(即20)。在[0, 63]中,我们知道每个整数都以[64, 95]位开头,然后在排除1位之后我们有所有可能的5位组合,所以我们再次知道有多少在这个区间内有任何给定重量的整数。

应用这个想法,这是一个完整,快速,一体化的功能,可以解决您的原始问题。它没有递归也没有记忆。

1