算法学习笔记:套碗取碗顺序合法性判断

77 阅读6分钟

题目分析

给定一组碗从1到n按编号从大到小叠放在木棍上,小F只能从最上面的碗开始取。需要判断是否存在一种取碗的合法操作顺序,使得目标顺序得以实现。


解题思路

此问题可以视为栈操作序列合法性验证问题,核心是通过模拟栈的操作(入栈、出栈)来检查目标取碗顺序是否符合规则。

规则回顾
  1. 碗从编号为1开始,按照顺序依次被放到木棍上(模拟入栈)。
  2. 每次取碗只能从木棍的顶部取出(模拟出栈)。
  3. 如果下一个目标碗是栈顶碗,则取出(出栈);否则,继续将新的碗放入栈,直到目标碗位于栈顶。
方法论

我们通过以下步骤模拟整个过程:

  1. 入栈操作:维护一个栈,表示当前木棍上的碗。如果目标碗不在栈顶,就按顺序将新的碗推入栈。
  2. 出栈操作:检查栈顶碗是否是目标碗,如果是则取出(弹栈),如果不是则操作失败。

通过这些操作,模拟出栈的整个流程。如果目标顺序可以完成,返回 1,否则返回 0


实现代码

def solution(M: int, a: list) -> int:
    stack = []  # 栈,用于模拟木棍
    next_to_push = 1  # 下一个待放入栈的碗编号

    for target in a:
        # 如果目标碗还未在栈中,继续放入栈直到找到目标碗
        while next_to_push <= M and (not stack or stack[-1] != target):
            stack.append(next_to_push)
            next_to_push += 1
        
        # 如果栈顶碗是目标碗,则取出
        if stack and stack[-1] == target:
            stack.pop()
        else:
            return 0  # 当前目标碗无法取出,顺序不合法

    return 1  # 所有目标碗都可以合法取出

复杂度分析

  1. 时间复杂度
    • 每个碗最多经历一次入栈和出栈操作,时间复杂度为 (O(n))。
  2. 空间复杂度
    • 需要一个栈存储中间状态,空间复杂度为 (O(n))。

测试用例

if __name__ == '__main__':
    print(solution(2, [1, 2]) == 1)
    print(solution(3, [3, 1, 2]) == 0)
    print(solution(4, [1, 3, 2, 4]) == 1)

难点与总结

  1. 难点
    • 如何将问题抽象为栈的操作序列问题。
    • 理解栈的特性(后进先出)对操作顺序的限制。
  2. 总结
    • 栈的经典应用之一是模拟操作序列,通过入栈、出栈验证某种顺序是否可能。
    • 本题的关键在于动态维护栈的状态,并在目标序列无法继续时及时终止。
    • 该算法思想可以拓展到验证括号匹配火车调度等问题中。

扩展思考

  1. 如果输入的碗编号顺序不连续(例如给定[1, 3, 5, 2]),如何判断合法性?
  2. 如果允许从其他位置取碗(非栈操作),问题如何变化?
  3. 是否能改进算法以支持并行取碗的操作?

拓展思考问题解答


1. 如果输入的碗编号顺序不连续(例如给定 [1, 3, 5, 2]),如何判断合法性?

问题分析:

碗编号不连续的情况下,问题的核心依然是验证出栈顺序是否合法。为了保持栈的特性,需要明确:

  • 编号之间的连续性对入栈过程没有影响。
  • 只需要验证目标序列是否符合合法的栈操作规则。
解法:
  • 按目标序列顺序,从 1 开始依次将碗放入栈,直到目标碗出现。
  • 如果目标碗编号小于当前栈顶编号,但未能从栈中取出,说明非法。
示例:

[1, 3, 5, 2] 为例:

  1. 按顺序推入栈:1 -> 3 -> 5
  2. 目标是 2,但 2 未被推入栈,因此目标顺序非法。
代码实现:
def solution_non_continuous(n: int, a: list) -> int:
    stack = []
    seen = set()  # 记录已出现的碗编号
    for target in a:
        if target in seen:
            # 如果目标碗已经在栈中,检查是否在栈顶
            if stack[-1] == target:
                stack.pop()
            else:
                return 0
        else:
            # 如果目标碗还没入栈,按顺序推入到目标碗
            for i in range(max(seen, default=0) + 1, target + 1):
                stack.append(i)
                seen.add(i)
            if stack[-1] == target:
                stack.pop()
            else:
                return 0
    return 1

2. 如果允许从其他位置取碗(非栈操作),问题如何变化?

问题分析:

若可以从任意位置取碗,则这不再是一个栈操作验证的问题,而是一个任意顺序问题:

  • 碗之间不再受“后进先出”规则限制。
  • 取碗顺序只需检查是否符合所有目标碗均能被一次性访问。
判断依据:

可以直接检查目标序列是否是木棍碗编号的一个合法排列(即是否包含每个编号且无重复)。

解法:
  • 检查目标序列是否包含所有碗编号。
  • 检查是否有重复或遗漏。
示例:
  • 给定 n=5, a=[4, 5, 3, 2, 1],目标序列为 [4, 3, 5, 1, 2]
    • 无需栈操作规则,只需验证目标序列是否包含 [1, 2, 3, 4, 5]
    • 若包含,则顺序合法。
代码实现:
def solution_no_stack(n: int, a: list) -> int:
    return int(sorted(a) == list(range(1, n + 1)))

3. 是否能改进算法以支持并行取碗的操作?

问题分析:

并行取碗意味着可以同时取走多个碗。为了支持这一操作:

  • 必须保证被取的碗满足连续性(在栈中顶部一段)。
  • 如果目标序列不符合连续性规则,操作非法。
解法:
  • 将目标序列分成多个连续段。
  • 验证每段是否能通过栈的合法操作完成。
示例:

目标序列为 [1, 2, 4, 3],可以分为 [1, 2][4, 3]

  1. [1, 2] 是连续段,合法。
  2. [4, 3] 也是连续段,合法。
代码实现:
def solution_parallel(n: int, a: list) -> int:
    stack = []
    next_to_push = 1

    for i in range(len(a)):
        while next_to_push <= n and (not stack or stack[-1] != a[i]):
            stack.append(next_to_push)
            next_to_push += 1

        # 检查并行连续段
        while stack and stack[-1] == a[i]:
            stack.pop()
            i += 1
            if i >= len(a):
                break

    return 1 if not stack else 0
局限性:

该算法的复杂性较高,具体实现中需要根据并行取碗的规则进行调整。


总结

  1. 非连续编号:核心仍是模拟栈规则,扩展到支持非连续编号序列。
  2. 非栈操作:问题简化为检查目标序列是否是碗编号的合法排列。
  3. 并行操作:需要分段验证每个连续段的合法性,但会增加复杂度。

拓展思考的重点在于理解问题本质:不同限制条件对操作规则的影响。通过模拟、分段处理等方式,可以解决类似问题的不同变体。