Given two words (start and end), and a dictionary, find all shortest transformation sequence(s) from start to end, such that:
Only one letter can be changed at a time
Each intermediate word must exist in the dictionary
For example,
Given:
start = “hit”
end = “cog”
dict = [“hot”,”dot”,”dog”,”lot”,”log”]
Return
[
[“hit”,”hot”,”dot”,”dog”,”cog”],
[“hit”,”hot”,”lot”,”log”,”cog”]
]
Note:
All words have the same length.
All words contain only lowercase alphabetic characters.
这道题,如果从实现功能的角度来说,感觉应该不难,但是,这道题对时间的要求太严格。。。。先后尝试了3次,都华丽丽地超时了,我只想说。。。。我的心好累。。。。
刚看到这道题,第一个天真的想法是,递归(其实就是深度遍历,但当时没想那么多),在找到结果时,记录一下当前结果长度,如果长度比结果集中的长度小,清空所有结果集。在寻找结果的时候,如果当前长度超过了最小结果长度,则停止寻找,程序如下:
private int length=Integer.MAX_VALUE;
private List<List<String>> rst=new ArrayList<List<String>>();
public List<List<String>> findLadders3(String start, String end, Set<String> dict) {
List<String> curRst=new ArrayList<String>();
curRst.add(start);
helper(start, end, dict, curRst);
return rst;
}
private void helper(String start, String end, Set<String> dict,List<String> preRst){
if(preRst.size()>=length)
return;
if(cantransform(start, end)){
List<String> curRst=new ArrayList<String>(preRst);
curRst.add(end);
int curLength=curRst.size();
if(curLength<length){
rst.clear();
length=curLength;
}
rst.add(curRst);
return;
}
for(String s:dict){
if(!preRst.contains(s)&&cantransform(start, s)){
preRst.add(s);
helper(s, end, dict, preRst);
preRst.remove(preRst.size()-1);
}
}
}
private boolean cantransform(String a,String b){
if(a.equals(b)||a.length()!=b.length())
return false;
int counter=0;
for(int i=0;i<a.length();i++){
if(a.charAt(i)!=b.charAt(i))
counter++;
}
return counter==1;
}
结果华丽丽地超时了,然后分析了一下代码,不难发现,以上代码采用了深度优先的算法,但是因为要求的是最短的结果,所以应该使用广度优先的算法,但是广度优先算法需要额外的存储空间,但是既然对时间的要求比较高,也只有牺牲空间了,于是有了第二版代码:
public List<List<String>> findLadders2(String start, String end, Set<String> dict){
List<List<String>> rst=new LinkedList<List<String>>();
boolean done=false;
List<String> singleRst=new ArrayList<String>();
singleRst.add(start);
rst.add(singleRst);
Set<String> set=new HashSet<String>();
while(!done&&rst.size()!=0){
List<List<String>> curRst=new LinkedList<List<String>>();
for(List<String> list:rst){
String s=list.get(list.size()-1);
if(cantransform(s, end)){
done=true;
list.add(end);
curRst.add(list);
}else {
for(String e:dict){
if(!set.contains(e)&&cantransform(s, e)){
List<String> curList=new ArrayList<String>(list);
curList.add(e);
curRst.add(curList);
set.add(e);
}
}
}
}
rst=curRst;
}
List<List<String>> removeList=new LinkedList<List<String>>();
for(List<String> list:rst){
if(!list.get(list.size()-1).equals(end)){
removeList.add(list);
}
}
rst.removeAll(removeList);
return rst;
}
本以为这次的程序应该没问题了,但是结果还是给了我一个华丽的超时,我就不开心了。。。但是仔细分析了一下超时的测试用例,以及稍微看了看别人的博客,发现,超时的用例中,给的字典的长度特别长,而start和end的长度略微小一些,所以又有了第三个想法,在查找时,并不是直接遍历字典,看当前的string能否转换成字典中的特定string,而是,改变当前string中的一个char,看是否在字典中。然后就有了第三版代码(PS:因为心情比较暴躁,所有这版代码可能可读性不是很强,如果不想看可以不看,因为,这段代码还是没有通过。。。。。):
public List<List<String>> findLadders4(String start, String end, Set<String> dict){
List<List<String>> rst=new LinkedList<List<String>>();
boolean done=false;
List<String> singleRst=new ArrayList<String>();
singleRst.add(start);
rst.add(singleRst);
Set<String> set=new HashSet<String>();
Set<String> dictSet=new HashSet<String>(dict);
int wordLength=start.length();
while(!done&&rst.size()!=0){
List<List<String>> curRst=new LinkedList<List<String>>();
Set<String> curSet=new HashSet<String>();
for(List<String> list:rst){
for(int i=0;i<wordLength;i++){
for(char c='a';c<='z';c++){
StringBuilder s=new StringBuilder(list.get(list.size()-1));
if(s.charAt(i)!=c){
s.setCharAt(i, c);
String curString=s.toString();
if(curString.equals(end)){
done=true;
list.add(end);
curRst.add(list);
break;
}else if(dictSet.contains(curString)&&!set.contains(curString)){
List<String> curList=new ArrayList<String>(list);
curList.add(curString);
curRst.add(curList);
curSet.add(curString);
}
}
}
}
}
set.addAll(curSet);
dictSet.removeAll(set);
rst=curRst;
}
List<List<String>> removeList=new LinkedList<List<String>>();
for(List<String> list:rst){
if(!list.get(list.size()-1).equals(end)){
removeList.add(list);
}
}
rst.removeAll(removeList);
return rst;
}
在第三版的程序超时以后,真是累觉不爱了。。。。不知如何是好。。。于是我去了讨论区(最近特别喜欢讨论去,感觉讨论区某些代码比国内一些博客好多了,强烈推荐)。然后找到了一份比较喜欢的代码:
地址为:https://leetcode.com/discuss/9523/share-two-similar-java-solution-that-accpted-by-oj
List<List<String>> results;
List<String> list;
Map<String,List<String>> map;
public List<List<String>> findLadders(String start, String end, Set<String> dict) {
results= new ArrayList<List<String>>();
if(dict.size()==0)
return results;
int cuur=1,next=0;
boolean found =false;
list=new LinkedList<String>();
map=new HashMap<String, List<String>>();
Queue<String> queue = new ArrayDeque<String>();
Set<String> unvisited=new HashSet<String>(dict);
Set<String> visited=new HashSet<String>();
queue.add(start);
unvisited.add(end);
unvisited.remove(start);
while(!queue.isEmpty()){
String word=queue.poll();
cuur--;
for(int i=0;i<word.length();i++){
StringBuilder builder=new StringBuilder(word);
for(char c='a';c<='z';c++){
builder.setCharAt(i, c);
String newWord=builder.toString();
if(unvisited.contains(newWord)){
if(visited.add(newWord)){
next++;
queue.add(newWord);
}
if(map.containsKey(newWord))
map.get(newWord).add(word);
else {
List<String> list=new LinkedList<String>();
list.add(word);
map.put(newWord, list);
}
if(newWord.equals(end)&&!found) found=true;
}
}
}
if(cuur==0){
if(found) break;
cuur=next;
next=0;
unvisited.removeAll(visited);
visited.clear();
}
}
backTrace(end, start);
return results;
}
private void backTrace(String word,String start){
if(word.equals(start)){
list.add(0,start);
results.add(new ArrayList<String>(list));
list.remove(0);
return;
}
list.add(0,word);
if(map.get(word)!=null)
for(String s:map.get(word))
backTrace(s, start);
list.remove(0);
}
这段代码即可AP。
下面分析一下,这段代码和我的第三版程序的异同,以及这段程序的可取之处:
首先,大体的思路是一样的,都是广度优先遍历。
其次,不同之处在于,对细节的优化,在我的算法中,使用了两个set:记录访问过的set和记录没访问过的set,以及一个临时set。而他的算法中,也有两个set,visitedSet和unvisitedSet。差别之处在于,他的算法中没有临时set,而是将每次循环遍历的值都存在visitedSet中,并且在循环结束后,执行unvisited.removeAll(visited);和visited.clear();动作,明显提升了效率。
所以按照他的优化方法,我对我的程序进行了小优化以后,发现竟然AP了,细节决定成败啊。。。。:
以下是优化后的代码:
但是看了一下leetcode的结果,我优化后的程序,是1854ms,而他的程序是730ms。还是有很大差别的。当前的想法是,可能是他使用了map的原因,map的存取比较快,而我使用“蛮力”实现,还有很多存在优化的地方,这个问题留给以后再解决吧,或者有谁看到了,知道产生差距的原因,可以一起讨论一下。
好的,就这样吧,我只想说,我的心。。好累。。。。
public List<List<String>> findLadders4(String start, String end, Set<String> dict){
List<List<String>> rst=new LinkedList<List<String>>();
boolean done=false;
List<String> singleRst=new ArrayList<String>();
singleRst.add(start);
rst.add(singleRst);
Set<String> set=new HashSet<String>();
Set<String> dictSet=new HashSet<String>(dict);
int wordLength=start.length();
while(!done&&rst.size()!=0){
List<List<String>> curRst=new LinkedList<List<String>>();
for(List<String> list:rst){
for(int i=0;i<wordLength;i++){
for(char c='a';c<='z';c++){
StringBuilder s=new StringBuilder(list.get(list.size()-1));
if(s.charAt(i)!=c){
s.setCharAt(i, c);
String curString=s.toString();
if(curString.equals(end)){
done=true;
list.add(end);
curRst.add(list);
break;
}else if(dictSet.contains(curString)){
List<String> curList=new ArrayList<String>(list);
curList.add(curString);
curRst.add(curList);
set.add(curString);
}
}
}
}
}
dictSet.removeAll(set);
set.clear();
rst=curRst;
}
List<List<String>> removeList=new LinkedList<List<String>>();
for(List<String> list:rst){
if(!list.get(list.size()-1).equals(end)){
removeList.add(list);
}
}
rst.removeAll(removeList);
return rst;
}