将数字减少到 1 的最小步骤数
Posted
技术标签:
【中文标题】将数字减少到 1 的最小步骤数【英文标题】:minimum number of steps to reduce number to 1 【发布时间】:2017-01-28 01:13:53 【问题描述】:给定任意数 n,并对 n 进行三个操作:
-
加1
减1
如果是偶数,则除以 2
我想找到将 n 减少到 1 的上述操作的最小数量。我尝试了动态编程方法,也尝试了带有修剪的 BFS,但 n 可能非常大(10^300),我不知道如何让我的算法更快。贪心方法(如果是偶数则除以 2,如果奇数则减去 1)也不会给出最佳结果。是否存在其他解决方案?
【问题讨论】:
The greedy approach ... does not give the optimal result
...你能给出一个不是最优的数字吗?
15,贪心会给出 6 (14 -> 7 -> 6 -> 3 -> 2 -> 1) 但最佳是 5 (16 -> 8 -> 4 -> 2 -> 1 )
听起来你想尽可能接近 2 的幂。
做一个贪心方法的变种,但在每一步检查是否更快地得到最接近的 2 的幂并除以 1。
问题陈述需要更清楚。您想要上述操作的最小数量,但我可以使用其他操作(例如,相乘、相加)来计算最小步数吗?
【参考方案1】:
如果您考虑任何正整数的二进制表示和允许的操作,您将得出以下结论:
任何1的序列都将通过加1来处理
任何不属于序列的 1 将通过减法处理 1
所需的分区总数将是 二进制位数或二进制位数减 1 取决于 最后一个操作是否是加 1 导致 数字上的额外位(例如 1111 将变为 10000 需要额外的除法,而 1000 将需要总共 3 个除法)
数字 3 (11) 有一种特殊情况,减一比加一要快,需要 2 步、减法和除法,而不是 3 步、加法和 2 次除法。
要点是您实际上不需要执行任何操作来计算步数。您需要做的就是遍历数字的各个位,并确定您遇到了多少上述情况。虽然每次加一时,1 序列中剩下的位都需要切换为 1。
这是上述概念的草率 python 实现:
def countSteps(n):
count = 0
k = bin(n)[2:]
i = len(k)-1
le = len(k)
k = list(k)
subs = 0
adds = 0
divs = 0
if n == 1:
return 0
while i>=0:
ones=0
while k[i] == '1' and i >=0:
ones+=1
i-=1
if ones == 1 and i > 0:
subs+=1
if ones >1:
#special case for 3
if i < 0 and ones == 2:
subs+=1
divs-=1
else:
adds+=1
k[i]='1'
i+=1
i-=1
if k[1] == '1':
divs = divs+le
else:
divs = divs+le-1
return divs + subs + adds
这种方法可能非常快。比任何需要模数来确定下一步的方法都快得多。
【讨论】:
【参考方案2】:虽然大家已经用深入的分析回答了这个问题,但我想与读者分享一个直觉。 (注:我的回答中没有正式的证明)
我们可以同意,当数字是偶数时,最好除以 2。 现在对于奇数情况,请考虑 n 的最后 2 个 LSB。 案例 1:01 -> 如果我们减去 1,它们将变为 00,允许我们在后续步骤中除以 2。 (而不是加 1 会使它们变成 10) 案例 2:11 -> 如果我们加 1,它们将变为 00,从而允许我们在后续步骤中除以 2。 (而不是减去 1 会使它们变为 10)。特殊情况是 3,正如其他答案中已经讨论的那样。【讨论】:
【参考方案3】:根据@trincot 的回答,验证 2 LSB 的另一种方法是简单地使用 bin(n)[-2:]
,瞧,对于那些不想处理二进制文件的人来说!
【讨论】:
【参考方案4】:正如@trincot 指出的那样,我们应该始终尝试将数字除以 2,但是一种简单的方法可以了解为什么如果数字是奇数,我们应该减少 1,如果它是 3 或以“01”结尾,然后添加1 在另一种情况下是这样的。如果 n 是奇数,则 n % 4 将是 1 或 3,那么 n+1 或 n-1 将是 4 的倍数,这意味着我们将能够将这个数字的两倍除以二。
【讨论】:
【参考方案5】:我真的不擅长二进制,所以不计算 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));
【讨论】:
它返回 8 表示 59。它返回 5 表示 15 我认为你无法解决大量问题【参考方案6】:总结:
如果 n 是偶数,则除以 2 如果 n 为 3 或其最低有效位为 01,则减法。 如果 n 的最低有效位为 11,则相加。在 n 上重复这些操作,直到达到 1,计算执行的操作数。这保证给出正确的答案。
作为the proof from @trincot 的替代方案,这里有一个案例较少且希望更清晰的案例:
证明:
案例 1:n 是偶数
令 y 为对它执行一些操作后的数字的值。首先,y = n。
-
假设将 n 除以 2 不是最佳方法。
然后加或减偶数次
-
混合加减法会导致不必要的运算,所以只做其中一个。
必须加/减偶数,因为停在奇数上会强制继续加减。
-
减法时限制 k,使 n - 2k >= 2。
案例 2:n 为奇数
这里的目标是表明,当面对奇数 n 时,无论是加法还是减法,都会减少达到给定状态的操作。我们可以利用这样一个事实,即在遇到偶数时除法是最佳的。
我们将用显示最低有效位的部分位串表示 n:X1 或 X01 等,其中 X 表示剩余位,并且非零。当 X 为 0 时,正确答案很明确:为 1,您完成了;对于 2 (0b10),除以;对于 3 (0b11),进行减法和除法。
尝试1:用一点信息判断加法还是减法更好:
-
开始:X1
-
加:(X+1)0,除:X+1(2 次操作)
减:X0,除:X(2 次操作)
我们遇到了一个僵局:如果 X 或 X+1 是偶数,那么最佳的做法是除法。但是我们不知道 X 还是 X+1 是偶数,所以我们不能继续。
尝试2:用两位信息判断加法还是减法更好:
-
开始:X01
-
加:X10,除:X1
-
加:(X+1)0,除:X+1(4 次操作)
减:X0,除:X(4 次操作)
-
添加:X+1(可能不是最优)(4 次操作)
结论:对于 X01,减法至少与加法一样少:3 次和 4 次运算与 4 次和 4 次运算得到 X 和 X+1。
-
开始:X11
-
加:(X+1)00,除:(X+1)0,除:X+1(3次操作)
-
减法:X(可能不是最优的)(4 次操作)
-
加:(X+1)0,除:X+1(4 次操作)
减:X0,除:X(4 次操作)
结论:对于 X11,加法至少与减法一样少:3 次和 4 次运算与 4 次和 4 次运算得到 X+1 和 X。
因此,如果 n 的最低有效位为 01,则减去。如果 n 的最低有效位为 11,则相加。
【讨论】:
【参考方案7】:有一种模式可以让您在恒定时间内知道最佳下一步。事实上,在某些情况下,可能存在两个同样最优的选择——在这种情况下,其中一个可以在恒定时间内推导出来。
如果您查看 n 的二进制表示及其最低有效位,您可以得出一些关于哪个操作导致解决方案的结论。简而言之:
如果最低有效位为零,则除以 2 如果n为3,或2个最低有效位为01,则减法 在所有其他情况下:添加。证明
如果最低有效位为零,则下一个操作应该是除以 2。我们可以尝试 2 加法然后除法,但同样的结果可以通过两步实现:除法和加法。与 2 减法类似。当然,我们可以忽略无用的后续加减步骤(反之亦然)。所以如果最后一位是0,除法就是要走的路。
那么剩下的 3 位模式就像**1
。其中有四个。让我们写a011
来表示一个以位011
结尾的数字,并且有一组前缀位代表值a:
a001
:加一将得到a010
,之后应进行除法:a01
:已执行 2 步。我们现在不想减去一个,因为这会导致a00
,我们可以从一开始就分两步得到它(减去 1 和除法)。所以我们再次相加和除以得到a1
,出于同样的原因,我们再次重复,给出:a+1
。这需要 6 步,但导致的数字可以通过 5 步得到(减 1,除 3 次,加 1),所以很明显,我们不应该执行加法。减法总是更好。
a111
:加法等于或优于减法。通过 4 个步骤,我们得到a+1
。减法和除法将给出a11
。与初始添加路径相比,现在添加效率低下,因此我们重复此减法/除法两次并分 6 步得到a
。如果a
以 0 结尾,那么我们可以分 5 步完成(加、除 3、减),如果 a
以 1 结尾,那么即使是 4。所以加法总是更好。
a101
:减法和双除法分三步得到a1
。加法和除法导致a11
。与减法路径相比,现在进行减法和除法效率低下,因此我们加法和除法两次以分 5 步得到a+1
。但是使用减法路径,我们可以通过 4 个步骤来实现。所以减法总是更好。
a011
:加法和双除法导致a1
。要获得 a
需要多走 2 个步骤 (5),要获得 a+1
:还要多走一步 (6)。减法、除法、减法、双除法导致a
(5),要得到a+1
需要多一步(6)。所以加法至少和减法一样好。然而,有一种情况不容忽视:如果 a 为 0,则减法路径在 2 步中到达解的一半,而加法路径需要 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 的值并让 sn-p 生成步数:
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 的准确度被限制在 1016 左右,因此对于更大的数字,结果将是错误的。请改用 Python 脚本以获得准确的结果。
【讨论】:
这似乎需要一个非常大的缓存。数字可以大到 10^300 我完全重写了我的答案。我相信这是现在发布的最快的解决方案。它不需要缓存,不需要递归。 算法不错,避免了不必要的“尾递归”。次要编辑建议:从您的证明中删除“显然”。具有直观意义,但并不明显,事实上,需要证明(您已经这样做了)。 首先,@trincot 这是一个很好的答案,非常感谢!我想知道您能否谈谈是什么导致您将范围限制为仅三位? 我进行了广度优先搜索以确认此答案,它适用于前 1400 个整数。检查后有道理。对于实物的 2^x 行,我们应该选择 1(第一位)、2(前两位)、4(三位)还是 8(四)或 2 的更高幂作为过滤器?选择 1 或 2 不会过滤任何内容。 4(3 sig bits)是第一个过滤任何东西的,任何更高的功率只是多余的。很好的答案。【参考方案8】:如果考虑 3,Ami Tavoy 提供的解决方案有效(加到 4 将产生 0b100
和 count_to_1
等于 2,这大于减去 2 的 0b10
和 count_to_1
等于 1)。当我们得到 no n = 3 时,您可以添加两个步骤来完成解决方案:
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 == 3:
return 2
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))
抱歉,我知道会做出更好的评论,但我才刚刚开始。
【讨论】:
欢迎来到 SO!这看起来像是对问题的评论而不是答案。如果您打算发表评论,您需要在任何帖子上拥有足够的reputation 至comment。还要检查这个what can I do instead。【参考方案9】:要解决上述问题,您可以使用递归或循环 已经提供了一个递归答案,所以我会尝试给出一个 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 是输入的数字,steps 是达到 1 所需的步数
【讨论】:
这个函数给出59的错误结果。它返回9步,而正确答案是8。它对59执行的第一步是-1,而它应该选择+1。因此,设置位的计数不是找到最短路径的可靠基础。另外:“逻辑”段落中的语句不正确:5(奇数)有 2 个设置位,而 14(偶数)有 3。该语句需要进一步限定以使其正确。【参考方案10】:我喜欢贪婪地寻找(对于奇数的情况)n + 1 还是 n - 1 看起来更有希望,但想想决定看起来更有希望的事情可以比查看设置的总位数做得更好。
对于一个号码x
,
bin(x)[:: -1].index('1')
表示直到第一个 1 之前最不重要的 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
表明,平均而言,这确实使用了更少的操作(尽管没有那么多)。
【讨论】:
应该是if n in [0, 1]: return 1-n
,否则这看起来不错:-) +1
@squeamishossifrage 谢谢!再一次,真的很喜欢你的回答(不能多次投票)。
除法应该是整数除法(//
而不是/
)。另外:这个函数对 3、6、11、12、13 和许多其他函数给出了错误的结果:在所有这些情况下,它返回的结果比最优解多 1 步。
@trincot 谢谢,我会检查的。无论如何,我的回答只是一个启发式的,而不是最佳的解决方案。以上是关于将数字减少到 1 的最小步骤数的主要内容,如果未能解决你的问题,请参考以下文章
C语言将一组数从大到小排序 只能移动相邻的数 并且要求步骤最小 怎么设计逻辑