1. 问题背景与需求分析
1321删数问题是一个经典的贪心算法练习题,主要考察选手对数字特性和贪心策略的理解。题目通常给出一个由数字组成的字符串和一个正整数k,要求从中删除k个数字后,使剩下的数字组成的新数最小。
这个问题在实际中有很多应用场景,比如:
- 优化存储空间时选择删除哪些冗余数据
- 金融交易中最小化手续费的计算
- 资源分配时如何做出最优取舍
我第一次接触这个问题是在准备NOIP比赛时,当时就被它简洁描述背后隐藏的巧妙解法所吸引。经过多次练习和教学实践,我总结出了一套系统的解题思路和常见错误防范方法。
2. 算法思路解析
2.1 贪心算法选择
解决这个问题的核心在于理解贪心算法的适用性。我们需要在每一步做出局部最优选择,最终达到全局最优解。具体到这个题目:
- 从左到右遍历数字字符串
- 当发现当前数字比下一个数字大时,删除当前数字
- 重复这个过程直到删除k个数字
这种策略之所以有效,是因为数字的高位对数值大小的影响更大。比如数字"132",删除'3'得到的"12"比删除'1'得到的"32"更小。
2.2 数据结构选择
为了实现这个算法,我们需要选择合适的数据结构。经过实践比较,我推荐使用栈(Stack)来实现:
- 栈可以帮助我们方便地比较当前元素和前一个元素
- 栈的LIFO特性正好符合我们需要回溯检查的需求
- 栈操作的时间复杂度为O(1),整体算法可以达到O(n)的时间复杂度
以下是使用栈的伪代码框架:
stack = [] for digit in num_str: while k > 0 and stack and stack[-1] > digit: stack.pop() k -= 1 stack.append(digit)3. 完整实现与代码解析
3.1 Python实现代码
def removeKDigits(num: str, k: int) -> str: stack = [] for digit in num: while k > 0 and stack and stack[-1] > digit: stack.pop() k -= 1 stack.append(digit) # 如果还有剩余的k需要删除 if k > 0: stack = stack[:-k] # 处理前导零 result = ''.join(stack).lstrip('0') return result if result else '0'3.2 关键点解析
循环条件:内层while循环的三个条件缺一不可
k > 0:确保不会删除超过k个数字stack:确保栈不为空stack[-1] > digit:当前数字比下一个数字大时才删除
剩余k处理:如果遍历完字符串后k仍然大于0,说明数字是递增的,直接从末尾删除剩余k个数字
前导零处理:使用
lstrip('0')去除前导零,同时注意全零情况要返回'0'
4. 边界情况与测试用例
4.1 常见边界情况
- 全部数字相同:"11111", k=2 → "111"
- 递增序列:"12345", k=2 → "123"
- 递减序列:"54321", k=2 → "321"
- 包含前导零:"100200", k=1 → "00200" → "200"
- 删除所有数字:"123", k=3 → "0"
4.2 测试用例设计
test_cases = [ ("1432219", 3, "1219"), ("10200", 1, "200"), ("10", 2, "0"), ("1234567890", 9, "0"), ("4321", 2, "21"), ("100", 1, "0"), ("112", 1, "11") ]5. 算法优化与变种
5.1 时间复杂度分析
原始算法的时间复杂度是O(n),因为每个数字最多入栈和出栈一次。空间复杂度也是O(n),用于存储栈。
5.2 实际应用中的优化
- 提前终止:当k减到0时可以直接将剩余数字加入结果,无需继续比较
- 双端队列:可以使用collections.deque代替list实现的栈,在某些情况下性能更好
- 字符串操作:对于小规模数据,直接操作字符串可能更简单直观
5.3 问题变种
- 删除数字使结果最大:只需将比较条件改为stack[-1] < digit
- 删除特定数字:如删除所有'0'或特定模式的数字
- 带权重的删数:每个数字有不同的删除成本,需要综合考虑
6. 常见错误与调试技巧
6.1 新手常见错误
- 忽略前导零:没有正确处理像"0200"这样的情况
- 剩余k处理不当:忘记处理遍历结束后k>0的情况
- 边界条件遗漏:没有考虑k=0或k=字符串长度的情况
- 数据类型混淆:将数字字符串当作数值处理,导致前导零丢失
6.2 调试建议
- 打印中间结果:在循环中加入print语句观察栈的变化
- 小规模测试:先用简单例子如"123", k=1手动验证
- 逐步增加复杂度:从简单情况开始,逐步增加数字长度和k值
- 使用断言:在代码中添加assert检查不变量
重要提示:在处理字符串数字时,始终记住保持字符串类型直到最后,避免过早转换为整数导致前导零丢失。
7. 教学实践心得
在教学过程中,我发现学生最容易困惑的是为什么贪心算法在这里适用。我通常用以下方式来解释:
想象数字字符串是一座山峰,我们要删除的是山峰而不是山谷。因为山峰(局部最大值)的存在会让整体数值变大,而我们要的是尽可能小的数。所以应该优先删除这些"山峰"。
另一个实用的教学技巧是让学生手动模拟算法过程。例如对于"1432219", k=3:
- 1 → [1]
- 4 (1<4) → [1,4]
- 3 (4>3,删除4) → [1,3], k=2
- 2 (3>2,删除3) → [1,2], k=1
- 2 → [1,2,2]
- 1 (2>1,删除2) → [1,2,1], k=0
- 9 → [1,2,1,9] 结果:"1219"
这种逐步演示能帮助学生直观理解算法的工作原理。