题目分析
给定一组碗从1到n按编号从大到小叠放在木棍上,小F只能从最上面的碗开始取。需要判断是否存在一种取碗的合法操作顺序,使得目标顺序得以实现。
解题思路
此问题可以视为栈操作序列合法性验证问题,核心是通过模拟栈的操作(入栈、出栈)来检查目标取碗顺序是否符合规则。
规则回顾
- 碗从编号为1开始,按照顺序依次被放到木棍上(模拟入栈)。
- 每次取碗只能从木棍的顶部取出(模拟出栈)。
- 如果下一个目标碗是栈顶碗,则取出(出栈);否则,继续将新的碗放入栈,直到目标碗位于栈顶。
方法论
我们通过以下步骤模拟整个过程:
- 入栈操作:维护一个栈,表示当前木棍上的碗。如果目标碗不在栈顶,就按顺序将新的碗推入栈。
- 出栈操作:检查栈顶碗是否是目标碗,如果是则取出(弹栈),如果不是则操作失败。
通过这些操作,模拟出栈的整个流程。如果目标顺序可以完成,返回 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 # 所有目标碗都可以合法取出
复杂度分析
- 时间复杂度:
- 每个碗最多经历一次入栈和出栈操作,时间复杂度为 (O(n))。
- 空间复杂度:
- 需要一个栈存储中间状态,空间复杂度为 (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, 3, 5, 2]),如何判断合法性?
- 如果允许从其他位置取碗(非栈操作),问题如何变化?
- 是否能改进算法以支持并行取碗的操作?
拓展思考问题解答
1. 如果输入的碗编号顺序不连续(例如给定 [1, 3, 5, 2]),如何判断合法性?
问题分析:
碗编号不连续的情况下,问题的核心依然是验证出栈顺序是否合法。为了保持栈的特性,需要明确:
- 编号之间的连续性对入栈过程没有影响。
- 只需要验证目标序列是否符合合法的栈操作规则。
解法:
- 按目标序列顺序,从
1开始依次将碗放入栈,直到目标碗出现。 - 如果目标碗编号小于当前栈顶编号,但未能从栈中取出,说明非法。
示例:
以 [1, 3, 5, 2] 为例:
- 按顺序推入栈:
1 -> 3 -> 5。 - 目标是
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, 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
局限性:
该算法的复杂性较高,具体实现中需要根据并行取碗的规则进行调整。
总结
- 非连续编号:核心仍是模拟栈规则,扩展到支持非连续编号序列。
- 非栈操作:问题简化为检查目标序列是否是碗编号的合法排列。
- 并行操作:需要分段验证每个连续段的合法性,但会增加复杂度。
拓展思考的重点在于理解问题本质:不同限制条件对操作规则的影响。通过模拟、分段处理等方式,可以解决类似问题的不同变体。