简体   繁体   English

计算 Python 中的最小二元运算

[英]Count Min Binary Operations in Python

Problem: Given a positive integer n , in a single operation, choose any i >= 0 and convert n to n + 2^i or n - 2^i .问题:给定一个正数 integer n ,在一次操作中,选择任何i >= 0并将n转换为n + 2^in - 2^i Find the minimum number of operations required to convert n to 0. Example, n = 5 .找出将n转换为 0 所需的最少操作次数。例如, n = 5 n can be reduced to 0 in two operations: 5 - 2^0 - 2^2 = 0. n可以通过两个操作减少为 0:5 - 2^0 - 2^2 = 0。

My solution that works for every case I've tested by hand:我的解决方案适用于我手动测试过的每个案例:

def get_min_operations(n: int) -> int:
    binary_representation = "{0:b}".format(n)
    # reverse the string so i = 0 is the least significant bit
    binary_representation = binary_representation[::-1]
    for i, bit in enumerate(binary_representation):
        if bit == "0":
            continue
        if (i == len(binary_representation) - 1) or binary_representation[i + 1] == "0": # isolated 1
            return 1 + get_min_operations(n - 2 ** i)
        else: # neighboring 1s
            return 1 + get_min_operations(n + 2 ** i)
    return 0

This iteratively applies operations to flip the 1s in the binary representation of the number until it is all 0s.这迭代地应用操作来翻转数字的二进制表示中的 1,直到它全部为 0。 It's not necessary for this to be recursive.这没有必要递归。 We iterate from least to most significant bit.我们从最低位到最高位进行迭代。 If a 1 is by itself, such as 01 , then we apply n - 2^i to turn it to 0. If we have neighboring 1s, such as 011 , then we apply n + 2^i to push the 1 up to a more significant bit: 100 .如果 1 是单独的,例如01 ,那么我们应用n - 2^i将其变为 0。如果我们有相邻的 1,例如011 ,那么我们应用n + 2^i将 1 推到 a更重要的位: 100 This is repeated until all bits are 0.重复此操作,直到所有位都为 0。

Here are my test cases:这是我的测试用例:

    assert(get_min_operations(1) == 1)
    assert(get_min_operations(2) == 1)
    assert(get_min_operations(3) == 2)
    # 4 -2^2 == 0
    assert(get_min_operations(4) == 1)
    # 5 - 2^0 + 2^2 == 0
    assert(get_min_operations(5) == 2)
    # 6 + 2^1 - 2^3 == 0
    assert(get_min_operations(6) == 2)
    # 7 + 2^0 - 2^3 == 0
    assert(get_min_operations(7) == 2)
    assert(get_min_operations(8) == 1)
    # 9 - 2^0 - 2^3 == 0
    assert(get_min_operations(9) == 2)
    # 10 - 2^1 - 2^3 == 0
    assert(get_min_operations(10) == 2)
    # 11 - 2^0 - 2^1 - 2^3 == 0
    assert(get_min_operations(11) == 3)
    # 12 - 2^2 - 2^3 == 0
    assert(get_min_operations(12) == 2)
    # 13 - 2^0 - 2^2 - 2^3 == 0
    assert(get_min_operations(13) == 3)
    # 14 + 2^2 - 2^4 == 0
    assert(get_min_operations(14) == 2)
    assert(get_min_operations(16) == 1)
    # 18 - 2 - 16 == 0
    assert(get_min_operations(18) == 2)
    assert(get_min_operations(21) == 3)
    # 24 + 8 - 32 == 0
    assert(get_min_operations(24) == 2)
    # 26 + 2 + 4 - 32 == 0
    assert(get_min_operations(26) == 3)
    assert(get_min_operations(27) == 3)
    # Add 2^2 == 4, subtract 2^5 == 32
    assert(get_min_operations(28) == 2)

Many programmers on first inspection of this problem, think it can be solved using dynamic programming with just the -2^i operations, or, even simpler, they think the solution is the Hamming weight , but see how these approaches fail:许多程序员在第一次检查这个问题时,认为它可以使用仅 -2^i 操作的动态规划来解决,或者,甚至更简单,他们认为解决方案是汉明权重,但看看这些方法是如何失败的:

Base 10基数 10 Base 2基地 2 Example Operations示例操作 Min Operations最小操作
1 1个 1 1个 1 - 2^0 1 - 2^0 1 1个
2 2个 10 10 2 - 2^1 2 - 2^1 1 1个
3 3个 11 11 3 - 2^1 - 2^0 3 - 2^1 - 2^0 2 2个
4 4个 100 100 4 - 2^2 4 - 2^2 1 1个
5 5个 101 101 5 - 2^2 - 2^0 5 - 2^2 - 2^0 2 2个
6 6个 110 110 6 - 2^2 - 2^1 6 - 2^2 - 2^1 2 2个
7 7 111 111 7 + 2^1 - 2^3 7 + 2^1 - 2^3 2 2个

Notice that 7 can be reduced to 0 in only two operations, not 3. Adding 1 makes it a power of 2 and can then be reduced to 0 in one additional operation for a total of 2. The idea of using the Hamming weight led to the working solution I have above, However.请注意,7 只能在两次操作中减少到 0,而不是 3。加 1 使其成为 2 的幂,然后可以在一个额外的操作中减少到 0,总共 2。使用汉明权重的想法导致但是,我上面的工作解决方案。 I don't have an intuition for it or what types of problems this bit manipulation would broadly apply to.我对此没有直觉,也不知道这种位操作将广泛应用于哪些类型的问题。 I would greatly prefer if I could come up with some dynamic programming or number theory solution to this problem.如果我能想出一些动态规划或数论解决方案来解决这个问题,我会非常愿意。

I believe this can be achieved using string manipulations on the bit representation of the number.我相信这可以通过对数字的位表示进行字符串操作来实现。

Removing a stand-alone "1" can be done in one operation.删除一个独立的“1”可以在一次操作中完成。 Any streak of 1s can be removed in two operations, no matter the length of the streak (ie adding 1 at the lowest power and then removing 1 at the next higher power).任何连续的 1 都可以在两次操作中删除,无论连续的长度如何(即在最低功率时加 1,然后在下一个更高的功率时删除 1)。

The only exception to this would be lone 0s between 1s in 4 bits or more (ie ..1011.. or..1101..) where flipping the 0 (in one operation) allows the removal of the 3 or more 1 bits in exactly 3 moves which is equal or better than the 3-4 operations to remove two separate streaks of 1s.唯一的例外是 4 位或更多位(即 ..1011.. 或 ..1101..)中 1 之间的单独 0,其中翻转 0(在一次操作中)允许删除 3 或更多 1 位恰好 3 次移动等于或优于 3-4 次操作以删除两个独立的 1 条纹。

Any streak of multiple zeros isolates the 1s sufficiently to be processed as separate problems.多个零的任何连串都足以将 1 隔离开来作为单独的问题进行处理。

from itertools import groupby
def get_min_operations(n):
    result = 0
    bits = f"{n:b}"
    for shortcut in ("1011","1101","1011"):
        result += bits.count(shortcut) 
        bits    = bits.replace(shortcut,"1111")
    result += sum( 1+(len(g)>1) for b,(*g,) in groupby(bits) if b=="1")
    return result

[EDIT] [编辑]

Giving it a little more thought, I came up with a recursive approach that doesn't use string representations by processing the last 2 bits with a carry from upper recursion:再考虑一下,我提出了一种不使用字符串表示的递归方法,方法是使用上层递归的进位处理最后 2 位:

def get_min_operations(n,carry=0):
    if not n:
        return carry
    # 1s streaks take max 2 operations
    if n&1 == 1:            
        return get_min_operations(n//2,min(2,carry+1))
    # 10 with carry extend streaks at cost of 1 oper.
    if n&3 == 2 and carry:  
        return 1 + get_min_operations(n//4,carry)
    # 00 ends streaks of 1s costing current carry
    return carry + get_min_operations(n//2)

You want to convert to a "signed" binary representation where instead of just 0's and 1's, you also include -1's (which I represent as '-')您想要转换为“带符号”的二进制表示形式,其中不仅包括 0 和 1,还包括 -1(我表示为“-”)

from math import log, ceil, floor

def signed_binary(num, current_digit = None):
    if num == 0:
        return '0' if current_digit is None else '0'*current_digit + '0'
    elif num == 1:
        return '1' if current_digit is None else '0'*current_digit + '1'
    elif num == -1:
        return '-' if current_digit is None else '0'*current_digit + '-'
    else:
        if num > 0:
        
            # Got log base 2
            power = log(num)/log(2)

            # Look to represent as either 2^ceil(power) - remainder
            # or 2^floor(power) - remainder
            # and use representations with lower number of '1' and '-'
            
            # Check if we need to prepend zeros
            if current_digit is not None:
                ceil_start = '0'*max(0, current_digit - ceil(power)) + '1'
                floor_start = '0'*max(0, current_digit - floor(power)) + '1'
                
            else:
                ceil_start = '1'
                floor_start = '1'
                
            return shorter_rep(ceil_start + signed_binary(num - 2**ceil(power), ceil(power)-1), floor_start + signed_binary(num - 2**floor(power), floor(power)-1))
        
        else:
            return flip_signed_binary(signed_binary(-num))
    
def shorter_rep(sbin1, sbin2):
    
    if sbin1.count('1') + sbin1.count('-') < sbin2.count('1') + sbin2.count('-'):
        return sbin1
    else:
        return sbin2
    
def flip_signed_binary(sbin):
    return ''.join(['-' if char == '1' else '1' if char == '-' else char for char in sbin])


for i in range(30):
    rep = signed_binary(i)
    ops = rep.count('1') + rep.count('-')
    print(f'{i}: \'{rep}\', {ops} operations')

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM