编程:将二进制数转换为零所需的最小步骤

19
我在解决一个编程练习时卡住了,无法找到正确的算法。问题如下:

给定一个十进制数,将其转换为零需要多少最小步骤:

  1. 如果下一位i+1为'1'且所有其他位i+2及之后都为0,则更改位i
  2. 不受限制地更改最后一位
例如:
如果输入为(8)Base10 =(1000)Base2,则所需的步骤为:
1000100110111010111011111101110001000101011101100010001100010000

需要完成15个步骤。

请完成以下定义:

int minStepsRequired(long number)

获取伪代码或算法是可以的。这不是一项作业或任务。


1
嗨,roger_that,你也想在这里发布这个谜题。 - Fernando Bravo Diaz
1
请澄清您的过程,即什么是“set”(= 1?)和“rest”? - Trunk
1
@Fureeish 那里的人可能会认为 StackOverflow 是一个自动作业机器。我不怪他们。它可以欺骗任何人。 :) - Torben
4
不行。你需要先自己尝试一下。编写一些代码,如果遇到困难再回来问。 - Torben
2
这是一个简单的图的广度优先搜索。节点是二进制值;边是每个节点允许移动的位置。网上有很多这样搜索的例子。编写您的转换,测试您的结果。如果遇到困难,请在此处发布。 - Prune
显示剩余12条评论
7个回答

16

这是一个递归算法很好的问题。

如果二进制数的长度为0,你已经可以得出答案。或者如果长度不允许为0,则如果长度为1,则根据那个位是0还是1来确定答案。

如果长度大于1:

  • 如果第一位是0,则答案与没有该0位时相同。删除它并调用递归以获得答案。
  • 如果第一位是1,则分成三个子问题并找到每个步骤的步数:
    1. 建立一种情况,在该情况下您被允许将前导1更改为0。这意味着它应该后跟一个1,然后是所有的0。为此编写一个递归辅助算法。它将非常类似于主算法,并且可能它们可以共享一些逻辑。
    2. 将1翻转为0(1步)
    3. 将其余的位转换为0。另一个递归调用。

该算法可能需要很长时间。实际上,它正在计算步骤,因此需要的时间与步骤数成正比,我认为这大约与输入数字成正比。您的方法需要一个long参数,但对于我的算法而言,在大的long值上它可能不会在运行它的计算机寿命内终止。此外,步骤数可能会溢出一个int甚至是一个long(如果输入是负的long值)。

快速方法

以下解决方案不需要递归,而且运行时间是常量级别的。我无法很好地解释它的工作方式,这是一个严重的问题,如果我们想要将其用于某些东西。我尝试了一些示例,看到了一些模式并加以推广。相比之下,我认为上面的递归解决方案之美在于,如果你懂得递归,它就很容易理解。

例如:输入8或1000二进制。结果是15或1111二进制。其中的规律是:结果的每个位都是前一个结果位和相同位置的输入位的异或。因此从1000中只需复制前一个位1。后面的位是1 XOR 0 = 1,其中1是结果的前一个位,0来自输入。剩余的两位以相同的方式计算。

下面是一个更长的例子,以便您可以检查是否理解:

Input:  115 = 1110011
Result:       1011101 = 93

或者在代码中:

static BigInteger calculateStepsRequired(long number) {
    // Take sign bit
    int bit = number < 0 ? 1 : 0;
    BigInteger result = BigInteger.valueOf(bit);
    for (int i = 0; i < 63; i++) {
        number = number << 1;
        int sign = number < 0 ? 1 : 0;
        bit = (bit + sign) % 2;
        result = result.shiftLeft(1).add(BigInteger.valueOf(bit));
    }
    return result;
}

我已经针对自己实现的上述第一种算法使用各种输入进行了检查,包括高达100,000,000的输入,它们总是一致的,因此我相信快速方法也是正确的。我仍然建议您编写、运行和测试代码,以验证我是否做得正确。


3
原文:The actual question in the OP is "what's the index of this gray-code-number" (https://en.wikipedia.org/wiki/Gray_code#Gray_code_counters_and_arithmetic). And this is pretty much what the faster version in your code does.翻译:原帖中实际问题是“这个格雷码数的索引是什么”(https://en.wikipedia.org/wiki/Gray_code#Gray_code_counters_and_arithmetic)。而您代码中更快的版本就是在解决这个问题。 - user4668606

12

起初,我尝试使用递归深度优先函数(在NodeJS中)来解决它,但是只能处理小数字——例如输入值为10^5将生成运行时错误,因为递归调用的数量超出了堆栈的限制。

然后我试图看看如何将问题简化为较小问题的总和,并发现对于N的步数,其中N是2的幂,为

规则#1

N * 2 - 1

(例如:2的步数为3,32的步数为63,256的步数为511,依此类推)。

然后,我需要找到其他任何非2的幂次方的数字该怎么做,由于任何整数都是不同2的幂次方的总和(因此是二进制表示),因此我只需要查看步数是否也会累加……但情况并非如此。但是,我发现不仅要从每个2的幂次方添加步骤,还要

规则#2

以交替方式从最高阶位开始减去和添加步骤

演示

给定数字 42 (二进制为101010

让我们首先应用规则#1

1 0 1 0 1 0
^ ^ ^ ^ ^ ^
| | | | | |_           0 steps
| | | | |___  2*2-1 =  3 steps
| | | |_____           0 steps
| | |_______  2*8-1 = 15 steps
| |_________           0 steps
|___________ 2*32-1 = 63 steps

其次,应用规则#2

63 - 15 + 3 = 51

步骤总数为51

实现(JavaScript)

function minOperations(n) {
  const bin = n.toString(2);
  const digitCount = bin.length;
  let accumulator = 0;
  let sign = 1;

  for (let i = 0; i < digitCount; ++i) {
    const digit = Number.parseInt(bin.charAt(i));
    const power = digit > 0 ? Math.pow(2, digitCount - (i + 1)) : 0;
    const steps = digit * (power * 2 - 1);

    accumulator += steps * sign;
    sign = sign * (digit == 0 ? 1 : -1);
  }
  
  return accumulator;
}

这种情况发生的原因是在每一步中,您基本上都否定了整个计算的符号。因此,您可以将其视为63 -(15-3)= 51,而不是考虑它是加法和减法之间的交替。因此,正如其他人指出的那样,这是一个递归解决方案的好问题。 - hasancc
1
正如我在帖子开头提到的那样,递归对于大数并不起作用,因此在这里解释了解决方法。 - nicolasdij

9
下面是一个递归的PHP函数用于计算所需的步骤数。它操作时会注意到有两种可能的要求:
  1. 将字符串转换为 0s(总体要求);和
  2. 将字符串转换为 1后跟一串 0s(以允许翻转前一个数字)
第二个要求显然是第一个要求的扩展,因此可以编写一个递归函数来同时执行这两个操作。它有一个单个数字长度字符串的特殊情况,只需检查是否需要翻转即可。
function reduce($bits, $value = '0') {
    if (strlen($bits) == 1) {
        // a single bit can be flipped as needed
        return ($bits[0] == $value) ? 0 : 1;
    }
    if ($bits[0] == $value) {
        // nothing to do with this bit, flip the remainder
        return reduce(substr($bits, 1));
    }
    // need to convert balance of string to 1 followed by 0's 
    // then we can flip this bit, and then reduce the new string to 0
    return reduce(substr($bits, 1), '1') + 1 + reduce(str_pad('1', strlen($bits) - 1, '0'));
}

在3v4l.org上的演示

这个函数可以被改进,以存储实际采取的步骤,那么步骤数就是该数组的计数(减1,因为我们也将原始值放入了数组中)。为了存储步骤,我们需要跟踪字符串的前半部分(下面代码中的$prefix)以及我们正在缩短的部分:

function reduce($bits, $prefix, $value = '0') {
    if (strlen($bits) == 1) {
        // a single bit can be flipped as needed
        return array($prefix . ($bits[0] == '0' ? '1' : '0'));
    }
    if ($bits[0] == $value) {
        // nothing to do with this bit, flip the remainder
        $prefix .= $bits[0];
        return reduce(substr($bits, 1), $prefix);
    }
    // need to convert balance of string to 1 followed by 0's 
    $prefix .= $bits[0];
    $steps = reduce(substr($bits, 1), $prefix, '1');
    // now we can flip this bit
    $prefix = substr($prefix, 0, -1) . ($bits[0] == '0' ? '1' : '0');
    $steps[] = $prefix . str_pad('1', strlen($bits) - 1, '0');
    // now reduce the new string to 0
    $steps = array_merge($steps, reduce(str_pad('1', strlen($bits) - 1, '0'), $prefix));
    return $steps;
}

您可以这样运行:

$bin = decbin($i);
$steps = array_merge(array($bin), reduce($bin, ''));
echo "$i ($bin) takes " . (count($steps) - 1) . " steps\n";
print_r($steps);

输入 8 的输出为:
8 (1000) takes 15 steps
Array
(
    [0] => 1000
    [1] => 1001
    [2] => 1011
    [3] => 1010
    [4] => 1110
    [5] => 1111
    [6] => 1101
    [7] => 1100
    [8] => 0100
    [9] => 0101
    [10] => 0111
    [11] => 0110
    [12] => 0010
    [13] => 0011
    [14] => 0001
    [15] => 0000
)

3v4l.org的演示

格雷码

观察这些步骤,我们可以看出这实际上是一种从原始值倒数到0的格雷码(反射二进制码)。因此,如果我们生成一个足够覆盖起始值的代码列表,我们可以简单地在该列表中查找起始值的二进制表示,这将给出我们回到0所需的步骤数:

function gray_code($bits) {
    if ($bits == 1) {
        return array('0', '1');
    }
    else {
        $codes = gray_code($bits - 1);
        return array_merge(array_map(function ($v) { return '0' . $v; }, $codes),
                           array_map(function ($v) { return '1' . $v; }, array_reverse($codes))
        );
    }
}

$value = 8;
$bin = decbin($value);
// get sufficient gray codes to cover the input
$gray_codes = gray_code(strlen($bin));
$codes = array_flip($gray_codes);
echo "$bin takes {$codes[$bin]} steps to reduce to 0\n";
// echo the steps
for ($i = $codes[$bin]; $i >= 0; $i--) {
    echo $gray_codes[$i] . PHP_EOL;
}

点击此处查看3v4l.org上的演示

如果您不需要单独的步骤,可以使用Gray码转二进制转换器来查找步骤数量。这非常快:

function gray_to_binary($value) {
    $dec = $value;
    for ($i = 1; $i < strlen($value); $i++) {
        $dec[$i] = (int)$dec[$i-1] ^ (int)$value[$i];
    }
    return $dec;
}

echo bindec(gray_to_binary(decbin(115)));

输出:

93

3v4l.org上的演示

格雷码生成器

我们可以使用迭代的格雷码生成器来从原始代码中递减步骤。优点是它不会消耗任何内存来存储代码,因此可以处理非常大的数字。这个版本使用一个将格雷码转换为二进制的整数转换器,而不像上面那个使用字符串。

function gray_to_binary($value) {
    $dec = 0;
    $bits = floor(log($value, 2));
    for ($i = $bits; $i >= 0; $i--) {
        $dec = $dec | (((($dec >> ($i + 1)) ^ ($value >> $i)) & 1) << $i);
    }
    return $dec;
}

function iterate_gray($value) {
    // get the equivalent starting binary value
    $code = decbin($value);
    yield $code;
    $len = strlen($code);
    $count = gray_to_binary($value);
    while ($count > 0) {
        // flip the bit which corresponds to the least significant 1 bit in $count
        $xor = 1;
        while (($count & $xor) == 0) $xor <<= 1;
        $value ^= $xor;
        yield sprintf("%0{$len}b", $value);
        $count--;
    }
}

foreach (iterate_gray(8) as $code) {
    echo $code . PHP_EOL;
}

输出:

1000
1001
1011
1010
1110
1111
1101
1100
0100
0101
0111
0110
0010
0011
0001
0000

在 3v4l.org 上的演示


很棒的答案。我也通过这个网站找到了与格雷码的联系(你可以输入一个序列的开头,它会告诉你这是什么)。 - Olivier
@Olivier 谢谢。我最初是从EE开始的,不得不为异步接口设计其中之一,因此一旦开始编写代码就能识别出模式。你提供的那个链接网站看起来非常有用,谢谢 - 我会将其添加到我的参考列表中。 - Nick

3
以下是Ole的“快速方式”算法的PHP实现。思路相同:
  • 用数字的第一个位(从左边开始)初始化结果
  • 对于数字中接下来的每个位,将其与结果的前一个位进行异或运算,得到结果的新位
function getBit($number, $i)
{
    // Extracts bit i from number
    return ($number & (1<<$i)) == 0 ? 0 : 1;
}

function minStepsRequired($number)
{
    $i = 30; // Enough to handle all positive 32-bit integers
    $bit = getBit($number, $i); // First bit
    $res = $bit;
    do
    {
        $i--;
        $bit ^= getBit($number, $i); // Computes XOR between previous bit of the result and current bit of the number
        $res = ($res<<1) + $bit; // Shifts the result to the left by 1 position and adds the new bit
    }
    while($i>0);
    return $res;
}

var_dump(minStepsRequired(8)); // Outputs 15
var_dump(minStepsRequired(115)); // Outputs 93

请问您能否解释一下步骤? - Developerium
1
@Developerium 我添加了一些更多的细节。 - Olivier

2
重要的是要注意,前k位为零的情况可以保持不变,只需从第(k + 1)位开始,并忽略所有起始位的值为零的位,始终旨在增加它们的数量。这是我们的首要目标,因此,我们总是将问题空间缩小到类似但较小的问题空间。现在,假设您的数字如下:
1b1b2b3...bn
你需要确保b1为1,而b2、b3、......bn为0,以便能够修改b1之前的位为0。在发生这种情况之后,你知道b2为1,所有后续位都为0,然后新的目标是将b2更改为0,知道所有后续位都为0。
您可以使用堆栈跟踪您的进度,堆栈可以有与您正在处理的位数相同的元素,其顶部始终表示您当前的目标。
因此,当您想将第一位归零时,实现后续位的正确序列对您来说代表着子任务,然后才能更改您的位,一旦成功完成,您需要类似地继续,但忽略第一位。您可以对步骤进行编号。
假设一个数字:
1...0000000
可以在2^n - 1步内被清零,则所需的总步数等于2^n - 1加上达到上述组合所需的步数。我没有检查它是否为2^n - 1。

0
private static int reduce(String s) {
    int count = 0;
    char[] ch = s.toCharArray();
    for (int i = ch.length - 1; i >= 0; i--) {

        if (i == 0 && ch[i] == '0') {
            return count - 1;
        }

        if (i == 0 && ch[i] == '1') {
            return count + 1;
        }

        if (ch[i] == '0') {
            count++;
        } else {
            count++;
            ch[i] = '0';
            i++;
        }
    }
    return count;
}

1
你的回答可以通过提供更多支持信息来改进。请编辑以添加进一步的细节,例如引用或文档,以便他人可以确认你的答案是正确的。您可以在帮助中心中找到有关如何编写良好答案的更多信息。 - Community

-1
def minOperations(n):
    number = []
    while n>0:
        number.append(n%2)
        n = n // 2

    ans = 0
    sign = 1
    for i in range(len(number)):
        if number[i] == 1:
            ans = (2**(i+1) - 1) - ans
    return ans

1
虽然这段代码可能解决了问题,但是包括解释它如何以及为什么解决了问题将有助于提高您的帖子质量,并可能导致更多的赞。请记住,您正在回答未来读者的问题,而不仅仅是现在提问的人。请编辑您的答案以添加解释并指出适用的限制和假设。 - jasie

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接