将数字减少到1的最小步骤数

时间:2016-09-20 07:52:27

标签: algorithm math dynamic-programming

给出n上的任意数字,以及n上的三个操作:

  1. 添加1
  2. 减1
  3. 如果数字均为,则
  4. 除以2

    我想找到上述操作的最小数量,将n减少到1.我尝试了动态编程方法,也修复了BFS,但是n可能非常大(10 ^ 300)而且我不知道如何使我的算法更快。贪婪的方法(如果偶数则除以2,如果是偶数则除1)也不能给出最佳结果。是否存在另一种解决方案?

9 个答案:

答案 0 :(得分:31)

有一种模式可以让您知道恒定时间内的最佳下一步。实际上,可能存在两种同样最佳选择的情况 - 在这种情况下,其中一种可以在恒定时间内导出。

如果你看一下 n 的二进制表示及其最低有效位,你可以得出一些关于哪个操作导致解决方案的结论。简而言之:

  • 如果最低有效位为零,则除以2
  • 如果 n 为3,或者2个最低有效位为01,则减去
  • 在所有其他情况下:添加。

证明

如果最低有效位为零,则下一个操作应该除以2.我们可以尝试2次加法然后除法,但是然后可以通过两个步骤实现相同的结果:除法和加法。类似地有2次减法。当然,我们可以忽略无用的后续添加&减去步骤(反之亦然)。因此,如果最后一位为0,则除法是可行的。

然后剩下的3位模式就像**1。其中有四个。让我们写a011来表示一个以位011结尾的数字,并且有一组前缀位代表值 a

  • a001:添加一个会给a010,之后应该进行分组:a01:采取两个步骤。我们现在不想减去一个,因为这将导致a00,我们可以从一开始就分两步到达(减1和除)。所以我们再次添加并除以得到a1,出于同样的原因,我们再次重复,给出:a+1。这需要6个步骤,但是会产生一个数字,可以通过5个步骤得出(减1,除3,加1),所以显然,我们不应该执行加法。减法总是更好。

  • a111:加法等于或优于减法。我们通过4个步骤得到a+1。减法和除法将给出a11。与初始加法路径相比,现在添加效率低,因此我们重复此减法/除法两次​​,并在6个步骤中得到a。如果a以0结尾,那么我们可以通过5个步骤完成此操作(添加,划分三次,减去),如果a以1结束,则即使在4中也是如此。所以加法总是更好

  • a101:减法和双重划分分三个步骤导致a1。加法和除法导致a11。与减法路径相比,现在减去和除法将是低效的,因此我们添加和除以两次以在5个步骤中获得a+1。但是通过减法路径,我们可以分4步完成。所以减法总是更好。

  • a011:加法和双重划分导致a1。获得a将需要2个步骤(5),以获得a+1:再增加一个(4)。减法,除法,减法,双除法导致a(5),得到a+1需要多一步(6)。所以加法至少和减法一样好。然而有一种情况不容忽视:如果 a 为0,则减法路径在两个步骤中到达解决方案,而加法路径需要3个步骤。所以添加总是导致解决方案,除非 n 是3:那么应该选择减法。

因此,对于奇数,倒数第二位决定下一步(3除外)。

Python代码

这导致了以下算法(Python),每个步骤需要一次迭代,因此应该具有 O(logn)复杂性:

def stepCount(n):
    count = 0
    while n > 1:
        if n % 2 == 0:             # bitmask: *0
            n = n // 2
        elif n == 3 or n % 4 == 1: # bitmask: 01
            n = n - 1
        else:                      # bitmask: 11
            n = n + 1
        count += 1
    return count

repl.it上看到它。

JavaScript代码段

这是一个版本,您可以在其中输入 n 的值,并让代码段生成步骤数:

function stepCount(n) {
    var count = 0
    while (n > 1) {
        if (n % 2 == 0)                // bitmask: *0
            n = n / 2
        else if (n == 3 || n % 4 == 1) // bitmask: 01
            n = n - 1
        else                           // bitmask: 11
            n = n + 1
        count += 1
    }
    return count
}

// I/O
var input = document.getElementById('input')
var output = document.getElementById('output')
var calc = document.getElementById('calc')

calc.onclick = function () {
  var n = +input.value
  if (n > 9007199254740991) { // 2^53-1
    alert('Number too large for JavaScript')
  } else {
    var res = stepCount(n)
    output.textContent = res
  }
}
<input id="input" value="123549811245">
<button id="calc">Caluclate steps</button><br>
Result: <span id="output"></span>

请注意,JavaScript的准确性仅限于10 16 ,因此对于较大的数字,结果将是错误的。请使用Python脚本来获得准确的结果。

答案 1 :(得分:1)

我喜欢贪婪地寻找(对于奇数的情况)的狡猾的ossifrage的想法是否 n + 1 n - 1 看起来更有希望,但是想想决定看起来更有希望的东西可以比查看设置位的总数好一点。

对于数字x

bin(x)[:: -1].index('1')

表示直到第一个0之前的最低有效0的数量。然后,想法是查看 n + 1 n - 1 ,并选择两者中的较高者(许多连续的最低有效0表示连续减半)。

这导致

def min_steps_back(n):
    count_to_1 = lambda x: bin(x)[:: -1].index('1')

    if n in [0, 1]:
        return 1 - n

    if n % 2 == 0:
        return 1 + min_steps_back(n / 2)

    return 1 + (min_steps_back(n + 1) if count_to_1(n + 1) > count_to_1(n - 1) else min_steps_back(n - 1))

为了比较两者,我跑了

num = 10000
ms, msb = 0., 0.
for i in range(1000):
    n =  random.randint(1, 99999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999)

    ms += min_steps(n)
    msb += min_steps_back(n)

print ms / num, msb / num

哪个输出

57.4797 56.5844

显示,平均而言,这确实使用较少的操作(尽管不是那么多)。

答案 2 :(得分:1)

要解决上述问题,您可以使用递归或循环 已经提供了一个递归的答案,所以我会试着给出一个while循环方法。

逻辑:我们应该记住,2的数字倍数总是比不能被2整除的那些设置位数少。

要解决您的问题,我正在使用Java代码。我用很少的数字尝试过它,如果没有添加评论或编辑答案,它可以正常工作

while(n!=1)
    {
        steps++;
        if(n%2 == 0)
        {
            n=n/2;

        }
        else
        {
            if(Integer.bitCount(n-1) > Integer.bitCount(n+1))
            {
                n += 1;
            }
            else
            {
                n -=1;
            }
        }
    }

    System.out.println(steps);

代码以非常简单的形式编写,以便每个人都能理解。此处 n 是输入的数字,步骤是达到1所需的步骤

答案 3 :(得分:0)

如果考虑3,Ami Tavoy提供的解决方案有效(加4会产生0b100count_to_1等于2,大于0b10和{{减去2 1}}等于1)。当我们下来没有n = 3时,你可以添加两个步骤来完成解决方案:

count_to_1

抱歉,我知道会发表更好的评论,但我刚刚开始。

答案 4 :(得分:0)

摘要:

  • 如果n是偶数,则除以2
  • 如果n是3或它的最低有效位是01,请减去。
  • 如果n的最低有效位是11,则加。

在n上重复这些操作,直到达到1,计算已执行的操作数。这样可以保证给出正确的答案。

作为the proof from @trincot的替代方案,以下案例较少,希望更加明确:

证明:

案例1:n是偶数

让y为对该数字执行一些操作后的值。首先,y = n。

  1. 假设将n除以2不是最佳方法。
  2. 然后加或减偶数次
    1. 加法和减法混合会导致不必要的操作,因此只能执行其中一个。
    2. 必须加上/减去偶数,因为停止使用奇数会强制继续加减。
  3. 让2k(其中k是某个整数)是执行的加法或减法的次数
    1. 在减去k时将k限制为n-2k> = 2。
  4. 加/减后,y = n + 2k,或y = n-2k。
  5. 现在分开。除后,y = n / 2 + k,或y = n / 2-k
  6. 已执行2k +1项操作。但是,通过先除以k然后加或减k次,在1 + k次运算中也可以达到相同的结果。
  7. 因此,划分不是最佳方法的假设是错误的,而划分是最佳方法。

案例2:n为奇数

这里的目标是表明面对奇数n时,加法或减法将导致达到给定状态的操作减少。我们可以利用这样的事实:当遇到偶数时,除法是最佳的。

我们将用表示最低有效位的部分位串来表示n:X1或X01等,其中X代表其余位,并且非零。当X为0时,正确答案是明确的:1,就完成了; 2(0b10)除对于3(0b11),减去并除。

尝试1:使用一点信息检查加法或减法是否更好:

  1. 开始:X1
    1. 添加:(X + 1)0,除:X + 1(2个操作)
    2. 减:X0,除:X(2个操作)

我们陷入了僵局:如果X或X + 1是偶数,则最佳移动是除法。但是我们不知道X或X + 1是偶数,所以我们不能继续。

尝试2:使用两个信息检查加法或减法是否更好:

  1. 开始:X01
    1. 添加:X10,划分:X1
      1. 添加:(X + 1)0,除以:X + 1(4个操作)
      2. 减:X0,除:X(4个操作)
    2. 减:X00,除:X0,除:X(3个运算)
      1. 添加:X + 1(可能不是最佳选择)(4个操作)

结论:对于X01,减法运算将至少导致与加法运算一样少的运算:3和4运算与4和4运算达到X和X + 1。

  1. 开始:X11
    1. 加:(X + 1)00,除:(X + 1)0,除:X + 1(3个操作)
      1. 减去:X(可能不是最佳)(4个操作)
    2. 减:X10,除:X1
      1. 添加:(X + 1)0,除以:X + 1(4个操作)
      2. 减:X0,除:X(4个操作)

结论:对于X11,加法运算将至少导致与减法运算一样少:3和4运算与4和4运算达到X + 1和X。

因此,如果n的最低有效位是01,则减去。如果n的最低有效位是11,请添加。

答案 5 :(得分:0)

我真的很擅长二进制文件,所以不算lsb或msb。 那么下面的程序-

public class ReduceNto1 {
    public static void main(String[] args) {
        int count1 = count(59);//input number
        System.out.println("total min steps - " + count1);
    }
    static int count(int n){
        System.out.println(n + " > ");
        if(n==1){
            return 0;
        }
        else if(n %2 ==0){

            return 1 + count(n/2);
        }else{
            return 1 + Math.min(count(n-1), count(n+1));
        }
    }
}

答案 6 :(得分:0)

正如@trincot 指出的那样,我们应该始终尝试将数字除以 2,但有一种简单的方法可以了解为什么如果数字是奇数,我们应该在 3 时减 1 或以“01”结尾,然后加上1 在另一种情况下是这样的。如果 n 是奇数,n % 4 将是 1 或 3,那么 n+1 或 n-1 将是 4 的倍数,这意味着我们将能够将数字的两倍除以 2。

答案 7 :(得分:0)

根据@trincot 的回答,验证 2 个 LSB 的另一种方法是简单地使用 bin(n)[-2:],瞧,对于那些不想处理二进制文件的人!

答案 8 :(得分:0)

虽然大家已经回答的很深入了,但我还是想给读者分享一个直觉。 (注:我的回答中没有正式的证明)

  • 我们同意,当数字为偶数时最好除以 2。
  • 现在对于奇数情况,考虑 n 的最后 2 个 LSB。
  • 案例 1:01 -> 如果我们减去 1,它们将变为 00,允许我们在后续步骤中除以 2。 (而不是加 1 使它们变成 10)
  • 案例 2:11 -> 如果我们加 1,它们将变成 00,允许我们在后续步骤中除以 2。 (与减 1 使它们成为 10 相对)。正如其他答案中已经讨论过的那样,特殊情况是 3。