最小步数归零问题
问题描述
小R拿到了一个长度为n的数组,其中每个元素都是一个正整数。小R发现每次可以删除某个数组中某个数的一位数字,这样可以逐步将所有数字变为0。他想知道,要将数组中所有数字都变为0,最少需要多少步?
例如:对于数字 103,小R可以选择删除第1位数字,将其变为 3;或者删除第2位数字,变为 13,又或者删除第3位数字,将其变为 10。最终目标是将所有数字都删除为0。
测试样例
样例1:
输入:
n = 5,a = [10, 13, 22, 100, 30]
输出:7
样例2:
输入:
n = 3,a = [5, 50, 505]
输出:4
样例3:
输入:
n = 4,a = [1000, 1, 10, 100]
输出:4
递归解法
从题目描述来看,我们需要解决的是一个最小步数归零问题,这个问题涉及到对每个数字进行操作,以最少的步骤将其变为0。这个问题具有以下几个特点,这些特点决定了使用递归加记忆化(Memoization)的方法是合适的:
-
重叠子问题:在尝试将一个数字变为0的过程中,我们会遇到许多重复的子问题。例如,当我们处理数字
100时,删除1后得到的子问题是0,这是一个不需要进一步操作的简单情况。但是,如果我们处理数字101,删除1后得到的子问题同样是01,这与处理100时的子问题相同。动态规划和递归加记忆化都是用来解决这类具有重叠子问题的问题。 -
最优子结构:这个问题的最优解可以通过其子问题的最优解来构建。也就是说,一个数字变为0的最少步数取决于删除每一位后得到的子问题的最少步数。这个特性使得问题可以通过递归的方式来解决,因为我们可以不断地将问题分解为更小的子问题。
-
状态可存储:每个数字变为0的最少步数是一个确定的值,这意味着我们可以将这些值存储起来,以避免在递归过程中重复计算。这就是记忆化的作用,它可以显著减少计算量,特别是在处理大数字时。
-
递归自然适合:对于这个问题,递归是一种直观的解决方案,因为我们可以通过删除每一位数字来自然地分解问题。递归方法可以很好地模拟这个过程,并且通过记忆化可以避免重复计算,提高效率。
-
复杂度考虑:如果不使用记忆化,递归方法可能会导致指数级别的复杂度,因为每个数字都可能产生大量的重复计算。通过记忆化,我们可以将复杂度降低到多项式级别,这对于实际的编程问题来说是非常重要的。
-
实现简单:相比于动态规划,递归加记忆化的实现通常更简单,更直观。我们不需要显式地定义状态转移方程,也不需要管理一个DP数组,只需要在递归的基础上添加一个检查已经计算过的结果的步骤。
综上所述,使用递归加记忆化的方法来解决最小步数归零问题是因为这种方法能够有效地处理重叠子问题,利用最优子结构,并且能够通过存储中间结果来避免重复计算,从而提高算法的效率。同时,这种方法的实现相对简单直观,适合解决这类问题。
代码实现
def deleteSteps(num):
# 将数字转换为字符串,方便操作每一位数字
num_str = str(num)
if num == 0:
return 0
if len(num_str) == 1:
return 1
# 删除每一位数字后,剩余数字变为0所需的最少步数
res = float('inf')
for i in range(len(num_str)):
# 删除当前位数字后,剩余数字
new_num = int(num_str[:i] + num_str[i+1:])
# 计算删除当前位数字后,剩余数字变为0所需的最少步数
res = min(res, deleteSteps(new_num) + 1)
return res
def solution(n: int, a: list) -> int:
total_steps = 0
for num in a:
total_steps += deleteSteps(num)
return total_steps
if __name__ == '__main__':
print(solution(5, [10, 13, 22, 100, 30]) == 7)
print(solution(3, [5, 50, 505]) == 4)
print(solution(4, [1000, 1, 10, 100]) == 4)
复杂度
- 时间复杂度:对于每个数字,我们最多需要O(k)的时间来遍历它的每一位,其中k是数字的位数。对于整个数组,时间复杂度为O(n*k),其中n是数组的长度,k是数组中数字的最大位数。
- 空间复杂度:由于我们使用了递归,最坏情况下递归的深度可以达到数组中所有数字的位数之和,因此空间复杂度为O(n*k)。如果使用动态规划,空间复杂度可以降低到O(1),因为我们只需要一个数组来存储中间结果。
动态规划解法
为了降低空间复杂度 我们尝试使用动态规划完成这道题 将复杂问题分解成更简单的子问题并存储这些子问题的解
代码实现
def solution(n: int, nums: list) -> int:
# 将数字转换为字符串,方便操作每一位数字
# 使用字典来存储每个数字对应的最少步数,避免重复计算
dp = {}
def dp_step(num):
if num == 0:
return 0
if len(str(num)) == 1:
return 1
if num in dp:
return dp[num]
res = float('inf')
for i in range(len(str(num))):
new_num = int(str(num)[:i] + str(num)[i+1:])
res = min(res, dp_step(new_num) + 1)
# 存储结果,并返回
dp[num] = res
return res
total_steps = 0
# 对于数组中的每个数字,计算变为0所需的最少步数,并累加
for num in nums:
total_steps += dp_step(num)
return total_steps
# 测试样例
print(minStepsToZero(5, [10, 13, 22, 100, 30]) == 7)
print(minStepsToZero(3, [5, 50, 505]) == 4)
print(minStepsToZero(4, [1000, 1, 10, 100]) == 4)
复杂度
- 时间复杂度:对于每个数字,我们最多需要O(k)的时间来遍历它的每一位,其中k是数字的位数。由于我们使用了记忆化,每个数字只被计算一次,所以整体时间复杂度为O(n * k),其中n是数组的长度,k是数组中数字的最大位数。
- 空间复杂度:由于我们使用了字典来存储每个数字的最少步数,最坏情况下,如果所有数字都不相同,空间复杂度为O(m),其中m是所有数字的总数。但是,由于数字的范围是有限的(每个数字都是正整数),实际的空间复杂度会小于O(m),并且远小于递归方法的空间复杂度,后者在最坏情况下可以达到O(n * k)。