mthoutai

  博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

你好呀!我是 山顶风景独好
欢迎踏入我的博客世界,能与您在此邂逅,真是缘分使然!
愿您在此停留的每一刻,都沐浴在轻松愉悦的氛围中。
这里不仅有丰富的知识和趣味横生的内容等您来探索,更是一个自由交流的平台,期待您留下独特的思考与见解。
让我们一起踏上这段探索与成长的旅程,携手挖掘更多可能,共同进步!✨

题目一:最小好进制(LeetCode 483,困难)

题目分析

对于给定的整数 nn >= 2),如果 k^m + k^(m-1) + ... + k^0 = nk >= 2m >= 1),则称 kn 的一个好进制。例如,13 的好进制有 33² + 3 + 1 = 13)和 1212 + 1 = 13)。请返回 n最小好进制。示例:输入 n = 13,输出 "3";输入 n = 4681,输出 "8";输入 n = 2,输出 "1"(特殊情况:2 = 1+1)。

解题思路(数学推导+二分查找)

核心是利用“等比数列求和公式”转化问题,结合数学边界缩小枚举范围,再通过二分查找验证候选进制:

  1. 数学公式转化
    • 好进制满足等比数列求和:n = (k^(m+1) - 1) / (k - 1)k 为进制,m 为项数-1);
    • 推导关键结论:
      • m 的最大值:由 2^m < nm < log2(n)(因 k>=2 时,2^m + ... +1 < 2^(m+1)-1 < n),故 m 范围为 [1, log2(n)]
      • k 的范围:对固定 mk 满足 k^m < n < (k+1)^m,即 k = floor(n^(1/m))(可通过二分查找验证)。
  2. 枚举与验证步骤
    • 从最大可能的 m 开始枚举(m 越大,k 越小,优先找到最小进制);
    • 对每个 m,用二分查找计算 k 并验证是否满足求和公式;
    • 若找到合法 k,直接返回(因 m 从大到小枚举,首次找到的 k 最小);
    • 若所有 m 均不合法,返回 n-1(此时 m=1k=n-1 必满足 (n-1)+1 =n)。

示例代码

import math
def smallestGoodBase(n: str) -> str:
n = int(n)
max_m = int(math.log2(n))  # m的最大可能值(2^m <n)
# 从最大m开始枚举,优先找最小k
for m in range(max_m, 0, -1):
# 二分查找k:k^m <n <(k+1)^m
left, right = 2, n-1
while left <= right:
mid = (left + right) // 2
# 计算sum = mid^m + ... +1 = (mid^(m+1)-1)/(mid-1)
# 避免溢出:用乘法替代幂运算,逐步累加
sum_k = 1
overflow = False
for _ in range(m):
sum_k = sum_k * mid + 1
if sum_k > n:
overflow = True
break
if overflow:
right = mid -1
elif sum_k == n:
return str(mid)
else:
left = mid +1
# 所有m均不匹配,返回n-1(m=1的情况)
return str(n-1)
# 测试示例
print("最小好进制1:", smallestGoodBase("13"))  # 输出:"3"
print("最小好进制2:", smallestGoodBase("4681"))  # 输出:"8"
print("最小好进制3:", smallestGoodBase("2"))  # 输出:"1"

代码解析

  • 数学推导的核心价值:将“枚举所有可能进制”的暴力思路,转化为“枚举项数 m + 二分找进制 k”,时间复杂度从 O(n) 降至 O(log²n),避免超时;
  • 溢出处理技巧:通过循环累加 sum_k = sum_k * mid +1 替代 pow(mid, m+1),防止大数值幂运算导致的溢出,确保计算准确性;
  • 枚举顺序的优化:从最大 m 开始枚举,因 m 越大,k 越小(如 13m=2 对应 k=3m=1 对应 k=12),首次找到的 k 即为最小解,无需后续判断。

题目二:最小移动次数使数组元素相等 II(LeetCode 462,困难)

题目分析

给你一个长度为 n 的整数数组 nums,返回使所有数组元素相等所需的最小移动次数。每次移动可以使某个元素增加或减少 1。例如:输入 nums = [1,2,3],输出 2(将 1 变为 23 变为 2,共移动 1+1=2 次);输入 nums = [1,10,2,9],输出 16(将所有元素变为 56,移动次数最少);输入 nums = [1,0,0,8,6],输出 14

解题思路(中位数最优+快速选择)

核心是证明“将所有元素调整为中位数时,移动次数最少”,再通过快速选择算法高效找到中位数(避免全排序):

  1. 数学证明核心
    • 设目标值为 x,移动次数总和为 sum(|nums[i] -x|)
    • n 为奇数时,x 取中位数,总和最小(若 x 偏离中位数,总和会增加);
    • n 为偶数时,x 取任意两个中位数之间的整数,总和相同且最小(如 [1,10,2,9]x=56,总和均为 16)。
  2. 算法步骤
    • 找到数组的中位数(无需全排序,用快速选择算法找第 n//2 小的元素);
    • 计算所有元素与中位数的绝对值之和,即为最小移动次数。
  3. 快速选择算法
    • 类似快速排序的分区过程,每次选择基准元素,将数组分为“小于基准”“等于基准”“大于基准”三部分;
    • 若基准位置等于 kk=n//2),则基准即为中位数;否则递归在对应分区查找,时间复杂度平均 O(n),最坏 O(n²)(可通过随机基准优化)。

示例代码(快速选择实现)

import random
def minMoves2(nums) -> int:
def quick_select(arr, k):
"""找到数组中第k小的元素(0-based)"""
if len(arr) == 1:
return arr[0]
# 随机选择基准,避免最坏情况
pivot = random.choice(arr)
left = [x for x in arr if x < pivot]
mid = [x for x in arr if x == pivot]
right = [x for x in arr if x > pivot]
if k < len(left):
return quick_select(left, k)
elif k < len(left) + len(mid):
return pivot
else:
return quick_select(right, k - len(left) - len(mid))
n = len(nums)
median = quick_select(nums, n // 2)
# 计算所有元素到中位数的绝对值之和
return sum(abs(num - median) for num in nums)
# 测试示例
print("最小移动次数1:", minMoves2([1,2,3]))  # 输出:2
print("最小移动次数2:", minMoves2([1,10,2,9]))  # 输出:16
print("最小移动次数3:", minMoves2([1,0,0,8,6]))  # 输出:14

代码解析

  • 中位数最优的本质:绝对值函数是凸函数,根据凸函数的“ Jensen 不等式”,其和的最小值在中位数处取得,这是算法的理论基础;
  • 快速选择的优势:相比全排序(O(n log n)),快速选择平均 O(n) 的时间复杂度在大数据量下更高效,尤其适合 n 较大的场景;
  • 随机基准的必要性:若数组已排序或存在大量重复元素,固定基准会导致快速选择最坏 O(n²),随机基准可将最坏情况概率降至极低。

题目三:最大交换(LeetCode 670,困难)

题目分析

给定一个非负整数,你可以交换该数的任意两位数字(最多交换一次),返回能得到的最大值。例如:输入 num = 2736,输出 7236(交换 27);输入 num = 9973,输出 9973(无需交换,已最大);输入 num = 1993,输出 9913(交换第一个 1 和最后一个 9)。

解题思路(贪心+位置记录)

核心是通过“记录每个数字最后出现的位置”,从高位到低位找到“可替换的最大数字”,实现一次交换得到最大值:

  1. 预处理:记录数字最后出现位置
    • 将数字转为字符数组 digits(便于修改);
    • 建立数组 last_poslast_pos[d] 表示数字 ddigits 中最后一次出现的索引(优先选择最后出现的相同数字,避免低位小数字被提前替换)。
  2. 贪心替换逻辑
    • 从左到右遍历每一位(高位优先):
      • 对当前位 digits[i],尝试找到比它大的数字 d(从 9digits[i]+1 枚举);
      • d 的最后出现位置 last_pos[d] > i(该数字在当前位右侧,可交换):
        • 交换 digits[i]digits[last_pos[d]]
        • 转为整数返回,即为最大值(高位已优化,无需后续判断);
    • 若遍历结束未找到可替换数字,返回原数字(已最大)。

示例代码

def maximumSwap(num: int) -> int:
digits = list(str(num))
n = len(digits)
# 记录每个数字最后出现的位置(0-9)
last_pos = [0] * 10
for i in range(n):
last_pos[int(digits[i])] = i
# 从高位到低位遍历,寻找可替换的最大数字
for i in range(n):
current_d = int(digits[i])
# 尝试找比current_d大的数字(从9开始)
for d in range(9, current_d, -1):
if last_pos[d] > i:
# 交换当前位与d的最后位置
digits[i], digits[last_pos[d]] = digits[last_pos[d]], digits[i]
return int(''.join(digits))
# 无需交换
return num
# 测试示例
print("最大交换结果1:", maximumSwap(2736))  # 输出:7236
print("最大交换结果2:", maximumSwap(9973))  # 输出:9973
print("最大交换结果3:", maximumSwap(1993))  # 输出:9913

代码解析

  • 贪心策略的精准性:高位对数字大小的影响远大于低位,因此从左到右遍历;同时优先选择“最大且最靠右”的数字交换,确保交换后数值最大(如 1993 中选择最后一个 91 交换,得到 9913,而非 1993 交换第一个 9 得到的 9193);
  • 位置记录的高效性last_pos 数组预处理时间 O(n),遍历判断时间 O(n*10)(每个位最多枚举9个数字),整体时间复杂度 O(n),空间复杂度 O(n)(存储字符数组),效率极高;
  • 边界处理的完整性:覆盖“无需交换”(如 9973)、“多位相同数字”(如 1993)、“首位最小”(如 2736)等场景,逻辑无遗漏。

✨ 本次分享的3道题均为LeetCode中“思维冷门但实用性强”的困难题,覆盖“数学推导+二分(最小好进制)”“中位数最优+快速选择(最小移动次数)”“贪心+位置记录(最大交换)”,突破传统算法框架,侧重“数学分析”与“贪心策略”的结合。若对某题的拓展场景(如多进制好进制判断、带权重的移动次数计算)或其他冷门困难题有需求,随时告诉我!
更多算法解析欢迎到CSDN主页交流:山顶风景独好

posted on 2025-11-04 11:22  mthoutai  阅读(6)  评论(0)    收藏  举报