你好呀!我是 山顶风景独好
欢迎踏入我的博客世界,能与您在此邂逅,真是缘分使然!
愿您在此停留的每一刻,都沐浴在轻松愉悦的氛围中。
这里不仅有丰富的知识和趣味横生的内容等您来探索,更是一个自由交流的平台,期待您留下独特的思考与见解。
让我们一起踏上这段探索与成长的旅程,携手挖掘更多可能,共同进步!✨
题目一:最小好进制(LeetCode 483,困难)
题目分析
对于给定的整数 n(n >= 2),如果 k^m + k^(m-1) + ... + k^0 = n(k >= 2,m >= 1),则称 k 是 n 的一个好进制。例如,13 的好进制有 3(3² + 3 + 1 = 13)和 12(12 + 1 = 13)。请返回 n 的最小好进制。示例:输入 n = 13,输出 "3";输入 n = 4681,输出 "8";输入 n = 2,输出 "1"(特殊情况:2 = 1+1)。
解题思路(数学推导+二分查找)
核心是利用“等比数列求和公式”转化问题,结合数学边界缩小枚举范围,再通过二分查找验证候选进制:
- 数学公式转化:
- 好进制满足等比数列求和:
n = (k^(m+1) - 1) / (k - 1)(k为进制,m为项数-1); - 推导关键结论:
m的最大值:由2^m < n得m < log2(n)(因k>=2时,2^m + ... +1 < 2^(m+1)-1 < n),故m范围为[1, log2(n)];k的范围:对固定m,k满足k^m < n < (k+1)^m,即k = floor(n^(1/m))(可通过二分查找验证)。
- 好进制满足等比数列求和:
- 枚举与验证步骤:
- 从最大可能的
m开始枚举(m越大,k越小,优先找到最小进制); - 对每个
m,用二分查找计算k并验证是否满足求和公式; - 若找到合法
k,直接返回(因m从大到小枚举,首次找到的k最小); - 若所有
m均不合法,返回n-1(此时m=1,k=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越小(如13中m=2对应k=3,m=1对应k=12),首次找到的k即为最小解,无需后续判断。
题目二:最小移动次数使数组元素相等 II(LeetCode 462,困难)
题目分析
给你一个长度为 n 的整数数组 nums,返回使所有数组元素相等所需的最小移动次数。每次移动可以使某个元素增加或减少 1。例如:输入 nums = [1,2,3],输出 2(将 1 变为 2,3 变为 2,共移动 1+1=2 次);输入 nums = [1,10,2,9],输出 16(将所有元素变为 5 或 6,移动次数最少);输入 nums = [1,0,0,8,6],输出 14。
解题思路(中位数最优+快速选择)
核心是证明“将所有元素调整为中位数时,移动次数最少”,再通过快速选择算法高效找到中位数(避免全排序):
- 数学证明核心:
- 设目标值为
x,移动次数总和为sum(|nums[i] -x|); - 当
n为奇数时,x取中位数,总和最小(若x偏离中位数,总和会增加); - 当
n为偶数时,x取任意两个中位数之间的整数,总和相同且最小(如[1,10,2,9]中x=5或6,总和均为16)。
- 设目标值为
- 算法步骤:
- 找到数组的中位数(无需全排序,用快速选择算法找第
n//2小的元素); - 计算所有元素与中位数的绝对值之和,即为最小移动次数。
- 找到数组的中位数(无需全排序,用快速选择算法找第
- 快速选择算法:
- 类似快速排序的分区过程,每次选择基准元素,将数组分为“小于基准”“等于基准”“大于基准”三部分;
- 若基准位置等于
k(k=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(交换 2 和 7);输入 num = 9973,输出 9973(无需交换,已最大);输入 num = 1993,输出 9913(交换第一个 1 和最后一个 9)。
解题思路(贪心+位置记录)
核心是通过“记录每个数字最后出现的位置”,从高位到低位找到“可替换的最大数字”,实现一次交换得到最大值:
- 预处理:记录数字最后出现位置:
- 将数字转为字符数组
digits(便于修改); - 建立数组
last_pos,last_pos[d]表示数字d在digits中最后一次出现的索引(优先选择最后出现的相同数字,避免低位小数字被提前替换)。
- 将数字转为字符数组
- 贪心替换逻辑:
- 从左到右遍历每一位(高位优先):
- 对当前位
digits[i],尝试找到比它大的数字d(从9到digits[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中选择最后一个9与1交换,得到9913,而非1993交换第一个9得到的9193); - 位置记录的高效性:
last_pos数组预处理时间O(n),遍历判断时间O(n*10)(每个位最多枚举9个数字),整体时间复杂度O(n),空间复杂度O(n)(存储字符数组),效率极高; - 边界处理的完整性:覆盖“无需交换”(如
9973)、“多位相同数字”(如1993)、“首位最小”(如2736)等场景,逻辑无遗漏。
✨ 本次分享的3道题均为LeetCode中“思维冷门但实用性强”的困难题,覆盖“数学推导+二分(最小好进制)”“中位数最优+快速选择(最小移动次数)”“贪心+位置记录(最大交换)”,突破传统算法框架,侧重“数学分析”与“贪心策略”的结合。若对某题的拓展场景(如多进制好进制判断、带权重的移动次数计算)或其他冷门困难题有需求,随时告诉我!
更多算法解析欢迎到CSDN主页交流:山顶风景独好
浙公网安备 33010602011771号