4つの丸いダイヤルが付いたターンテーブルロックがあります。各ダイヤルには、「0」、「1」、「2」、「3」、「4」、「5」、「6」、「7」、「8」、「9」の10個の番号があります。各ダイヤルは自由に回転できます。たとえば、「9」を「0」に、「0」を「9」に変更します。各回転は、ダイヤルの1桁しか回転できません。
ロックの初期番号は「0000」で、4つのダイヤルの番号を表す文字列です。
行き止まりリストには、一連の死亡番号が含まれています。ホイールの番号がリスト内の要素と同じになると、ロックは永続的にロックされ、回転できなくなります。
文字列ターゲットは、ロックを解除できる数を表します。最小回転数を指定する必要があります。とにかくロックを解除できない場合は、-1を返します。
ソース:LeetCode
リンク:https ://leetcode-cn.com/problems/open-the-lock
方法1:双方向BFSアルゴリズム
従来のBFSフレームワークは、開始点から始まり、終点に到達すると広がり、停止します。一方、双方向BFSは、始点と終点から同時に広がり始め、2つの側面が交差するときに停止します。従来のBFSアルゴリズムの戦略に従って、「エンドポイント」(ターゲット値)がバイナリツリーの最下部にある場合、ツリー全体のノードが検索され、最終的に検出されtarget
ます。一方、双方向BFSは、実際には、交差が表示される前にツリーの半分のみを通過します。つまり、最短距離が見つかりました。直感的には、双方向BFSは従来のBFSよりもはるかに効率的です。ただし、双方向BFSにも制限があります。つまり、「エンドポイント」(目標値)がどこにあるかを知る必要があります。終点がどこから始まっているかわからないため、双方向BFSを使用できません。ただし、パスワードロックの2番目の問題は、双方向BFSアルゴリズムを使用して効率を向上できることです。
class Solution:
def openLock(self, deadends: List[str], target: str) -> int:
def onePlus(s:str,j: int)-> str:
# 将s[i]向上拨动一次
strList=list(s)
if strList[j]=='9':
strList[j]='0'
else:
strList[j]=str(int(strList[j])+1)
return ''.join(strList)
def oneMinus(s,j):
# 将s[i]向下拨动一次
strList=list(s)
if strList[j]=='0':
strList[j]='9'
else:
strList[j]=str(int(strList[j])-1)
return ''.join(strList)
# 利用set提高查询速度
deadendSet=set(deadends)
visited=set()
queue=set()
# 从0000开始
queue.add('0000')
visited.add('0000')
# 从目标值开始
queue2=set()
queue2.add(target)
step=0
while queue and queue2 :
tmp=set()
if (len(queue) > len(queue2)):
#按照 BFS 的逻辑,队列(集合)中的元素越多,扩散之后新的队列(集合)中的元素就越多;
# 在双向 BFS 算法中,如果我们每次都选择一个较小的集合进行扩散,那么占用的空间增长速度就会慢一些,效率就会高一些。所以这里进行一次交换
queue,queue2 = queue2,queue
for cur in queue:
if cur in deadendSet:
continue
if cur in queue2:
return step
# 记录已经使用过的组合
visited.add(cur)
# 上下拨动的可能值放入循环队列
for j in range(4):
up=onePlus(cur,j)
if (up not in visited):
tmp.add(up)
down=oneMinus(cur,j)
if down not in visited:
tmp.add(down)
# 步数加一
step+=1
# temp 相当于 queue,交换queue,queue2,下一轮 while 就是扩散 q2
queue=queue2
queue2=tmp
return -1
方法2:
class Solution:
def openLock(self, deadends: List[str], target: str) -> int:
# 定义一个距离函数,计算一个code直接拨需要多少下,比如0004就是4下,0009就是1下。
def dist(code):
return sum([min(int(c), 10-int(c)) for c in code])
if "0000" in deadends or target in deadends :
return -1
# 由target反推能够到达target的所有前一个节点,放在 new_codes 里返回。
new_codes = []
for i in range(4):
pre, x, sur = target[:i], int(target[i]), target[i+1:]
new_codes.append(pre + str((x+1)%10) + sur)
new_codes.append(pre + str((x-1)%10) + sur)
# 利用集合(set)是可以做减法,得出不在deadends里面的new_codes,放在 moves 里。
# set的减法:例如{'0001','0003','0015'} - {'0003','0007'} = {'0001', '0015'}
moves = set(new_codes) - set(deadends)
# 如果moves为空,那就说明没有路径可以到达target,那就返回 -1
if not moves:
return -1
# 计算直接播到目标值需要多少次
result = dist(target) # 例如 result = dist('0088') ,那么 result就等于4
# 遍历所有排除掉deadends的选项,计算这里面的值
# 判断是否有大于直接到达目标值的选项
for move in moves:
if dist(move) < result: # 如果存在任一个到达target的前一节点,则直接到达target就是可能的
return result
return result + 2 # 如果所有target的前一节点的到达距离都比直接到达target大,则就返回 result + 2
方法3:この方法は方法2に似ていますが、方法2よりもスペースの複雑さが少なくなります。
class Solution:
def openLock(self, deadends: List[str], target: str) -> int:
def distance(string):
return sum(min(int(num), 10 - int(num)) for num in string)
if '0000' in deadends or target in deadends:
return -1
moves = {str(i): (str((i - 1) % 10), str((i + 1) % 10)) for i in range(10)}
adjacent = set()
for i in range(len(target)):
start = target[:i]
end = target[i + 1:]
for j in moves[target[i]]:
adjacent.add(start + j + end)
possibleMoves = adjacent - set(deadends)
if not possibleMoves:
return -1
return min(distance(move) for move in possibleMoves) + 1