问题描述
小S玩起了多米诺骨牌,他排列了一行骨牌,并可能将某些骨牌向左或向右推倒。随着骨牌连锁反应的进行,一些骨牌可能因为左右两侧受力平衡而保持竖立。现在小S想要知道在所有动作完成后,哪些骨牌保持竖立。
给定一个表示骨牌初始状态的字符串,其中:
- "L" 表示该位置的骨牌将向左倒。
- "R" 表示该位置的骨牌将向右倒。
- "." 表示该位置的骨牌初始时保持竖立。
模拟整个骨牌倒下的过程,求出最终仍然保持竖立的骨牌的数目和位置。
问题分析
这道题的本质是通过模拟多米诺骨牌的倒下过程,找到最终仍然保持竖立的骨牌的位置和数量。骨牌的倒下遵循以下规则:
- 向左倒(
L)的骨牌会推动其左侧相邻的骨牌倒向左边。 - 向右倒(
R)的骨牌会推动其右侧相邻的骨牌倒向右边。 - 若一个竖立的骨牌同时受到
L和R的作用,由于受力平衡,它将保持竖立。 - 如果一个竖立的骨牌仅受到一个方向的作用,它会被推倒并传播这种倒向。
根据题意,我们需要反复模拟骨牌的倒下过程,直到所有骨牌都稳定为止。最后统计剩下的竖立骨牌。
模拟思路
我们可以通过逐轮更新骨牌状态的方式,来模拟整个倒下过程,直到状态不再发生变化。以下是详细的模拟步骤:
思路解析
1. 初始化状态
我们将输入字符串转化为一个字符数组,分别用 prev 和 now 表示上一轮和当前轮的骨牌状态:
prev:上一轮骨牌的状态。now:当前轮骨牌的状态(用于计算新状态)。
初始状态直接从输入中获取。
2. 模拟骨牌倒下的规则
我们逐个遍历数组 now 中的骨牌状态,依据规则进行处理:
-
当前骨牌为
L:- 它会尝试向左推倒左侧的骨牌。如果左侧骨牌是
.(竖立),则变为L(倒向左)。 - 如果左侧骨牌是
R,说明两股力量平衡,左侧骨牌恢复为.(竖立)。 - 当前骨牌本轮处理完毕,标记为
X,表示已完成传播。
- 它会尝试向左推倒左侧的骨牌。如果左侧骨牌是
-
当前骨牌为
R:- 它会尝试向右推倒右侧的骨牌。如果右侧骨牌是
.,则变为R(倒向右)。 - 如果右侧骨牌是
L,说明两股力量平衡,当前骨牌与右侧骨牌形成三角,因此标记下一张骨牌为X。 - 当前骨牌本轮处理完毕,标记为
X。
- 它会尝试向右推倒右侧的骨牌。如果右侧骨牌是
-
当前骨牌为
.或X:- 若为
.,说明该骨牌本轮不受影响,继续保持竖立。 - 若为
X,说明该骨牌已经完成本轮传播,跳过。
- 若为
-
边界处理:
- 如果当前骨牌为最左侧(
i == 0),则无法向左传播。 - 如果当前骨牌为最右侧(
i == num-1),则无法向右传播。
- 如果当前骨牌为最左侧(
3. 判断结束条件
每次更新完成后,我们检查:
- 如果本轮更新后的状态与上一轮状态完全相同,说明所有骨牌已经稳定,退出循环。
- 否则,将
now的状态赋值给prev,并开始下一轮更新。
4. 统计竖立骨牌
当状态稳定后,我们统计最终仍然竖立的骨牌:
- 遍历数组,检查状态为
.的位置,将其索引加入结果列表。 - 统计竖立骨牌的数量和索引,并返回结果。
代码详解
以下是代码的逐步解释:
def solution(num, data):
# 初始化 prev 和 now
prev = list(data) # 上一轮状态
now = list(data) # 当前轮状态
while True: # 模拟循环
i = 0
while i < num: # 遍历所有骨牌
if now[i] == ".": # 如果当前骨牌是竖立状态
i += 1 # 跳过
elif now[i] == "L": # 如果当前骨牌向左倒
if i != 0: # 检查左侧是否有骨牌
if now[i-1] == ".": # 如果左侧骨牌竖立
now[i-1] = "L" # 推倒左侧骨牌
elif now[i-1] == "R": # 如果左侧骨牌向右倒
now[i-1] = "." # 受力平衡,恢复竖立
now[i] = "X" # 标记当前骨牌处理完成
i += 1
elif now[i] == "R": # 如果当前骨牌向右倒
now[i] = "X" # 标记当前骨牌处理完成
if i != num - 1: # 检查右侧是否有骨牌
if now[i+1] == ".": # 如果右侧骨牌竖立
now[i+1] = "R" # 推倒右侧骨牌
i += 2 # 跳过下一骨牌(已处理)
continue
elif now[i+1] == "L": # 如果右侧骨牌向左倒
now[i+1] = "X" # 受力平衡,但并非竖立
i += 1
else: # 如果当前骨牌已标记为 X
i += 1
# 检查是否稳定
if prev == now:
break
else: # 否则更新 prev 和 now
prev = now
now = list(now)
# 统计竖立的骨牌
c = 0
index = []
for i in range(num):
if prev[i] == ".": # 找到竖立的骨牌
c += 1
index.append(str(i + 1)) # 记录位置
if c != 0:
return f"{c}:{','.join(index)}"
else:
return "0"
为什么这样实现
-
分离状态更新与稳定检查:
- 使用两个状态数组
prev和now,确保上一轮状态不受本轮更新的影响。 - 避免因为实时更新导致的连锁反应错误。
- 使用两个状态数组
-
逐步遍历,规则简单清晰:
- 将骨牌倒下的规则按照
L、R和.分别处理,逻辑清晰。
- 将骨牌倒下的规则按照
-
状态稳定后退出:
- 通过对比
prev和now的状态,判断是否稳定,避免无限循环。
- 通过对比
复杂度分析
-
时间复杂度:
- 外层
while循环最多执行 次(每次至少有一部分骨牌状态更新)。 - 内层
for遍历 。 - 总体复杂度为 。
- 外层
-
空间复杂度:
- 主要存储两个数组
prev和now,空间复杂度为 。
- 主要存储两个数组
优化思路:双向扫描
对于这类涉及方向性传播的模拟问题,暴力法虽然直观,但会多次重复更新状态。可以通过一次性处理所有的力量传播,减少不必要的循环。一个更优的解决方案是双向扫描法,它通过记录每个骨牌受力的最终方向来快速得出结果。
核心思想
-
左向扫描:
- 从左到右遍历骨牌字符串,记录每个骨牌受右侧
R推力的影响。 - 如果遇到
R,开始施加右推力,推力逐渐减弱。 - 如果遇到
L,右推力终止。
- 从左到右遍历骨牌字符串,记录每个骨牌受右侧
-
右向扫描:
- 从右到左遍历骨牌字符串,记录每个骨牌受左侧
L推力的影响。 - 如果遇到
L,开始施加左推力,推力逐渐减弱。 - 如果遇到
R,左推力终止。
- 从右到左遍历骨牌字符串,记录每个骨牌受左侧
-
合并结果:
-
对于每个骨牌位置,比较左右两方向的推力:
- 如果左推力强于右推力,骨牌向左倒(
L)。 - 如果右推力强于左推力,骨牌向右倒(
R)。 - 如果推力相等或没有推力,骨牌保持竖立(
.)。
- 如果左推力强于右推力,骨牌向左倒(
-
这种方法只需要两次扫描(线性时间),因此效率更高。
代码实现
def solution(num, data):
# 初始化推力数组
left_force = [0] * num # 左向推力
right_force = [0] * num # 右向推力
# 从左到右计算右向推力
force = 0 # 当前右向推力值
for i in range(num):
if data[i] == "R":
force = num # 最大推力
elif data[i] == "L":
force = 0 # 遇到 L,右推力归零
elif force > 0:
force -= 1 # 推力随距离递减
right_force[i] = force
# 从右到左计算左向推力
force = 0 # 当前左向推力值
for i in range(num - 1, -1, -1):
if data[i] == "L":
force = num # 最大推力
elif data[i] == "R":
force = 0 # 遇到 R,左推力归零
elif force > 0:
force -= 1 # 推力随距离递减
left_force[i] = force
# 合并左右推力,确定最终状态
result = []
for i in range(num):
if left_force[i] > right_force[i]:
result.append("L")
elif right_force[i] > left_force[i]:
result.append("R")
else:
result.append(".")
# 统计竖立的骨牌
upright_count = 0
upright_positions = []
for i in range(num):
if result[i] == ".":
upright_count += 1
upright_positions.append(str(i + 1)) # 索引从 1 开始
# 返回结果
if upright_count > 0:
return f"{upright_count}:{','.join(upright_positions)}"
else:
return "0"
复杂度分析
-
时间复杂度:
- 左右两次扫描的时间复杂度均为 ,合并结果也是 ,总复杂度为 。
-
空间复杂度:
- 需要两个额外的数组
left_force和right_force,空间复杂度为 。
- 需要两个额外的数组
总结
这道题如果使用暴力模拟法,核心是正确模拟规则,同时在代码实现中保证状态更新的正确性和高效性。时间复杂度为 ,但对于中等规模的数据足够有效。
如果骨牌数目很大,可以用双向扫描法进行优化。相比暴力模拟法,双向扫描法更加高效:
- 只需两次线性扫描即可完成所有推力计算,无需反复迭代更新。
- 推力计算逻辑清晰易懂,适用于类似的力传播问题。