一.题目描述(LeetCode126题):
给定一个单词集合Dict,其中每个单词的长度都相同。现从此单词集合Dict中抽取两个单词A、B。希望通过若干次操作把单词A变成单词B,每次操作可以改变单词的一个字母,同时,每次操作后,新产生的单词必须是在给定的单词集合Dict中。求所有行得通步数最少的修改方法。
举个例子如下:
Given:
A ="hit"
B ="cog"
Dict =["hit","hot","dot","dog","lot","log","cog"]
Return
[
["hit","hot","dot","dog","cog"],
["hit","hot","lot","log","cog"]
]
即把字符串A ="hit"转变成字符串B = "cog",有以下两种可能:
"hit"-> "hot" -> "dot" -> "dog"-> "cog";
"hit"-> "hot" -> "lot" -> "log" ->"cog"。
二.分析:
方法一:构造图+求两点之间的最短路径
常见的方法就是将Dict中的字符串作为图中的节点,如果两个字符串通过一次操作就可以互相转换,则这两个字符串的节点之间有边。图的起点就是字符串A,终点就是字符串B。题目就可以转换为求解这个图的最短路径问题。
判断两个字符串是否可以互相转换的方法时间复杂度分析:
思路一:由于需要对两两字符串进行比较,假设Dict的长度为n,两个字符串的最短长度为length,对于两个字符串,我们需要遍历其中一个字符串中的每个字符,判断这个字符被改变(也就是替换)后两个字符串是否相等。这样得到某个节点相邻节点的时间复杂度为:O(n*length)
思路二:将每个字符串的每个字符分别用26个字母替换,然后在Dict里面查询有没有出现,查询操作使用hashmap的话复杂度为O(1),这样时间复杂度为O(26*length),length为当前节点的长度。很明显这个时间复杂度当n>26时小于思路一
关于得到相邻节点的思路分析,这个的实现下面的链接讲解的很好:
https://wenku.baidu.com/view/510763fafd0a79563d1e7203.html
后面还有求解最短路径的时间复杂度,比如使用迪杰斯特拉算法,时间复杂度为O(N^2),N表示的是图中节点的个数。在这个问题中最坏的情况是所有的节点都存在于构造的图中,所以时间复杂度为O(n^2)。这个时间复杂度在LeetCode上面是通不过的。
方法二:带有剪枝的BFS+DFS、BFS或者双向BFS
(1)首先是BFS+DFS
首先使用BFS得到从起始点出发的每层每个节点的相邻节点,求相邻节点的方法使用分析里面的思路二,直到遇到结束节点就结束遍历。
然后使用DFS得到可能的路径。
时间复杂度分析:假设起始点到结束点的层数距离为d,每个节点最大可能的相邻节点是k,那么BFS的时间复杂度为O(k^d)
因为求解可能的相邻节点时,前面层中出现的节点不再考虑作为当前层某个节点的相邻下一层节点(因为如何前面层出现的节点也可以当做下一层的节点,那么可能是出现在前面层的节点最先到达结束节点,所以再当做下一层的节点没有意义),这样的话每个节点至多出现一次,也就是k^d<n,时间复杂度为O(n)
DFS时时间复杂度也为O(n),所以综合来说总的时间复杂度为O(n)
(2)BFS:
在遍历的时候将可能的路径找到,后面不再进行DFS遍历,时间复杂度跟上面的一样,为O(n)
过程为:
(1)从当前层出发,寻找下一个可能的节点
(2)下一个可能的节点在上面层没有遍历过的节点里面找
(3)当碰到源点时,输出,终止向下一层探索。
上面的例子建立的树如下所示:
(3)双向BFS
从起始点和结束点同时进行BFS,这样时间复杂度为O(k^(d/2))+O(k^(d/2))<O(n^0.5),显然比上面的要快
三. 代码实现
(1)BFS+DFS
import copy
class Solution:
def findLadders(self, beginWord, endWord, wordList):
def BFS(beginWord,endWord,wordList):#得到直到endWord的广度搜索结果,用于DFS得到所有的路径
curList=[beginWord]#curList存储每层的节点
DisDict={}#存储在前面层出现的节点
DisDict[beginWord]=0
Distance=0
Found=False#当某层出现结束的字符,遍历完这层就不再向下一层遍历
while len(curList)>0:
Distance+=1
curL_tp=[]
for word in curList:
neighs=getNeigh(word,wordList)#求当前节点所有可能的下一个节点
neighs_tmp=[]
#NextDict[word]=neighs
for neigh in neighs:
if neigh==endWord:#碰到结束字符
Found=True
neighs_tmp.append(neigh)
#NextDict[word].append(neigh)
elif neigh not in DisDict or Distance==DisDict[neigh]:#正常的就加入下一个需要层序遍历的队列中
DisDict[neigh]=Distance
#curL_tp.append(neigh)
#NextDict[word].append(neigh)
neighs_tmp.append(neigh)
else:
#NextDict[word].remove(neigh)
continue
NextDict[word]=neighs_tmp#当前节点下一层的可能节点
curL_tp.extend(neighs_tmp)#下一层的节点
if Found:
break
curList=curL_tp#赋给下一层节点的遍历队列
def getNeigh(Node,wordList):#得到每个节点可能的下一层的相邻节点
res=[]
for i in range(len(Node)):
Char=Node[i]
for Chr in range(ord("a"),ord("z")+1):
Node_tmp=Node[:i]+chr(Chr)+Node[i+1:]
if Node_tmp in wordList and chr(Chr)!=Char:
res.append(Node_tmp)
return res
def DFS(beginWord, endWord):#进行DFS回溯:满足条件在结果里面加上,不满足继续递归,然后回退一步,
#回溯和递归的区别是:回溯等于递归加回退一步
if beginWord not in NextDict:
return
Neighs=NextDict[beginWord]
for i in range(len(Neighs)):
temp.append(Neighs[i])
if Neighs[i]==endWord:
ans.append(copy.deepcopy(temp))
else:
DFS(Neighs[i], endWord)
temp.pop(-1)
wordList=set(wordList)
if endWord not in wordList:
return []
ans=[]
NextDict={}
temp=[beginWord]
start = time.clock()
BFS(beginWord,endWord,wordList)
DFS(beginWord, endWord)
return ans
(2)BFS:
使用Python编写的线索树实现的,
#最小操作数
#求所有通行数最少的方法
class Node:
def __init__(self,x):
self.val=x
self.father=None
def Min(A,B,Dict):
FNode=Node(A)
FList=[FNode]
res=[]
sign=0
while FList!=[] and sign==0:
SList=[]
for FList_i in FList:
if FList_i.val in Dict:
Dict.remove(FList_i.val)
for FList_i in FList:
for i in range(len(FList_i.val)):
for CNum in range(ord('a'),ord('z')+1,1):
word=chr(CNum)
if FList_i.val[:i]+word+FList_i.val[i+1:] in Dict and FList_i.val[:i]+word+FList_i.val[i+1:]!=FList_i.val:
TNode=Node(FList_i.val[:i]+word+FList_i.val[i+1:])
TNode.father=FList_i
if FList_i.val[:i]+word+FList_i.val[i+1:]==B:
Tres=[B]
while TNode.father!=None:
Tres.insert(0,TNode.father.val)
TNode=TNode.father
res.append(Tres)
sign=1
break
else:
SList.append(TNode)
else:
continue
FList=SList
return res
不用线索树,直接列表实现的:
import copy
class Solution:
def findLadders(self, beginWord, endWord, wordList):
def BFS(beginWord,endWord,wordList):#得到直到endWord的广度搜索结果,用于DFS得到所有的路径
wordList=set(wordList)
queue=[[beginWord]]
ans=[]
Visited=set(beginWord)
isFound=False
while queue!=[] and not isFound:
subVisited=set()
subQueue=[]
for i in range(len(queue)):
path=queue[i]
temp=path[-1]
neighs=getNeigh(temp,wordList)
for neigh in neighs:
if neigh not in Visited:
subVisited=subVisited|{neigh}
path.append(neigh)
if neigh==endWord:
ans.append(copy.deepcopy(path))
isFound=True
subQueue.append(copy.deepcopy(path))
path.pop(-1)
queue=copy.deepcopy(subQueue)
Visited=Visited|subVisited
return ans
def getNeigh(Node,wordList):#得到每个节点可能的下一层的相邻节点
res=[]
for i in range(len(Node)):
Char=Node[i]
for Chr in range(ord("a"),ord("z")+1):
Node_tmp=Node[:i]+chr(Chr)+Node[i+1:]
if Node_tmp in wordList and chr(Chr)!=Char:
res.append(Node_tmp)
return res
return BFS(beginWord,endWord,wordList)
(3)双向BFS
class Solution:
def findLadders(self, beginWord, endWord, wordList):
if endWord not in wordList:
return []
wordList=set(wordList)
#分别存放两个方向的中间节点和对应的从起始点/结尾节点到中间节点的路径,value值是多维列表,因为到一个中间节点的路径可能有多条
forWard={beginWord:[[beginWord]]}
backWard={endWord:[[endWord]]}
vsSet=set()#保存已经访问过的节点,每一层保存一次,这样对于每个节点在同一层可以出现多次,也就是可以有多条路径
Len=2
res=[]
while forWard:
if len(forWard)>len(backWard):
forWard,backWard=backWard,forWard
tmp={}
while forWard:#对上一层中间节点进行遍历
word,paths=forWard.popitem()
vsSet.add(word)
for i in range(len(word)):
for j in range(ord('a'),ord('z')+1):
new=word[:i]+chr(j)+word[i+1:]
if new in backWard:#如果在另外一个方向的中间节点出现过,说明两个方向接上了,遍历完这一层之后接结束循环
if paths[0][0]==beginWord:
res.extend(fpath+bpath[::-1] for fpath in paths for bpath in backWard[new])
else:
res.extend(bpath+fpath[::-1] for fpath in paths for bpath in backWard[new])
if new in wordList and new not in vsSet:#如果没有访问过而且存在于原始给出的列表中,就可以当做当前节点的下一层相邻节点
tmp[new]=tmp.get(new,[])+[path+[new] for path in paths]
Len+=1
if res and Len>len(res[0]):
break
forWard=tmp
return res
参考链接: