问题
给了A、B两个单词和一个单词集合Dict,每个的长度都相同。我们希望通过若干次操作把单词A变成单词B,每次操作可以改变单词中的一个字母,同时,新产生的单词必须是在给定的单词集合Dict中。求所有行得通步数最少的修改方法。
举个例子如下:
Given:
A = "hit"
B = "cog"
Dict = ["hot","dot","dog","lot","log"]
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"。
答题说明
A和B相同的情况下不需要做转换,此时直接返回空集;
main函数是为方便你在提交代码之前进行在线编译测试,可不完成。
思路
把每个单词当做图上的一个顶点,而求最少的修改步骤,实际上就是求两个顶点之间的最短路径。
汉明距离
对两个单词进行编辑操作,从一个单词变成另外一个单词的步骤叫做编辑距离。而如果单词的长度是一样的,那么单词之间的编辑距离就是汉明距离。在信息论中,两个等长字符串之间的汉明距离是两个字符对应位置的不同字符的个数。换句话说,它就是将一个字符串变换成另外一个字符串所需要替换的字符的个数。比如题目中的单词集合中的单词Dict = ["hot","dot","dog","lot","log"],
dog log的汉明距离是1
hit dot的汉明距离是2
hit cog的汉明距离是3
有了汉明距离这个概念,我们就知道如果把单词集合中的单词作为顶点,那么两个顶点之间是否有连接就在于两个单词的汉明距离是1。于是,我们需要一个函数来判断两个单词是否汉明距离为1,
以下是判断汉明距离的静态方法
package art.programming.algorithm; public class HammingDistance { public static int getHammingDistance(String a, String b){ if (a.length() != b.length()) throw new IllegalArgumentException("The length of different string must be the same"); int len = a.length(); int sum = 0; for (int i=0; i< len; i++){ if (a.charAt(i) != b.charAt(i)) sum += 1; } return sum; } }
图结构
由于把每个字符串作为图中的一个顶点,所以我需要构造一个图,然后把通过某种数据结构把图存起来。在这里,我使用邻接链表。
顶点 |
列表下标 |
关联顶点下标 |
hit |
0 |
1 |
hot |
1 |
2,4 |
dot |
2 |
1,3,4 |
dog |
3 |
2,5,6 |
lot |
4 |
1,2,5 |
log |
5 |
3,4,6 |
cog |
6 |
3,5 |
用Java把上面的邻接链表 表示出了。在下列代码中,Graph代表图,Vertex代表顶点(它有名词和权值),Vertex[] vertexes代表图中所有的顶点的集合,Map<Integer, List<Integer>> edges表示所有的边。
package art.programming.algorithm; import java.util.ArrayList; import java.util.List; import java.util.PriorityQueue; import java.util.Queue; public class Graph { private Vertex[] vertexes; private Edge[] edges; public Graph(String[] vertexNames){ vertexes = new Vertex[vertexNames.length]; //初始化每个节点 for (int i=0; i< vertexNames.length; i++){ vertexes[i] = new Vertex(vertexNames[i], i, Integer.MAX_VALUE); } //初始化每条边 edges = new Edge[vertexNames.length]; int len = this.vertexes.length; for (int i=0; i < len; i++){ edges[i] = new Edge(); for(int j=0; j < len; j++){ boolean isHammingDistanceAs1 = HammingDistance.getHammingDistance(this.vertexes[i].getName(), this.vertexes[j].getName()) == 1; //如果i和j之间的汉明距离是1,那么说明他们之间有连接 if (isHammingDistanceAs1){ edges[i].addNeighbor(j); } } } } private static class Vertex { private int weight; private int index; //这个顶点在顶点集合中的下标 private String name; public Vertex(String name, int index, int weight){ this.index = index; this.weight = weight; this.name = name; } public int getWeight() { return weight; } public void setWeight(int weight) { this.weight = weight; } public int getIndex() { return index; } public String getName() { return name; } public void setName(String name) { this.name = name; } } private static class Edge{ private List<Integer> neighbors = new ArrayList<Integer>(); public void addNeighbor(int i){ neighbors.add(i); } public List<Integer> getNeighbors(){ return neighbors; } } }
最短路径
由于需要寻找图中两个顶点之间的最短距离,我需要用到Dijkstra算法。Dijkstra算法是由荷兰计算机科学家Dijkstra发明的。Dijkstra 算法使用了广度优先搜索算法。算法解决的是有向图中单个源点到其他顶点的最短路径问题。参考 http://zh.wikipedia.org/wiki/%E8%BF%AA%E7%A7%91%E6%96%AF%E5%BD%BB%E7%AE%97%E6%B3%95
下图的动画描述了Dijkstra算法
本图由Wikipedia上摘录而来
以上面的动态图为例,求a点到b点的最短路径的步骤如下:
1. 建立两个集合,一个用来存放已经访问过的顶点;另外一个用来存放未被访问过的顶点。
1.1 sDict{}来表示已经访问过的顶点的集合, 在这个集合里面的每个元素的权值都是距离源顶点的最小权值。
1.2 Unvisited{}来表示没有被访问过的节点的集合。
在初始化状态下,两个集合像这样子:
sDist为空
unvisited{[1,0], [2,Infinity], [3,Infinity], [4, Infinity], [5, Infinity], [6,Infinity]}
*其中unvisited中每个元素表示为[1,0], 1为顶点,0为权值。Infinity是无穷大权值。
*unvisited的元素按照权值排序
2. 在unvisited集合里找第一个元素[1,0],广度搜索与之相连的顶点,分别为6,3,2,而且他们的权和分别是14,9,7。这时候把第一个元素(也就是源顶点,因为它的权值为0,其他的权值都是无穷大,所以排序后它在第一个位置)从unvisited集合中取出,并放到sDist中
unvisited{[2,7], [3,9], [4, Infinity], [5, Infinity], [6,14]}
sDist{[1,0]}
3. 同样道理, 在unvisited集合里找第一个元素 [2,7], 将它从集合中取走。与2相连的是1,3,4。 由于顶点1已经不在unvisited集合中了,所以不必计算了。之后分别计算2到3的权为7+10=17,这时候1->2->3这条路比1->3这条路的权要大,所以unvisited集合中3这个顶点的权和依然是9。接着通过2访问4,4之前没有被访问过,所以4这个顶点的权和是7+15=22。这时候2这个顶点的搜索就完毕了。
unvisited{[[3,9],[5,Infinity], [6,14],[4,22]}
sDist{[1,0], [2,7]}
4. 同样道理, 在unvisited集合里找第一个元素 [3,9], 将它从集合中取走 。发现跟3这个顶点直接相关联的是2和6和4,由于2 已经不在unvisited集合中了,所以不必计算了 。现在计算6这个顶点,发现6的权和这时候是9+2=11,11比原来的权和14要小,于是把6的权和置为11。接着计算4这个顶点,同样道理,9+11=20比22小,于是把4这个顶点的权和变为20,同时将顶点3。于是两个集合变成这样,
unvisited{[6,11],[4,20],[5,Infinity]}
sDist{[1,0], [2,7],[3,9]}
5. 同样道理, 在unvisited集合里找第一个元素 [6,11], 将它从集合中取走。发现跟6这个顶点直接相连的是1,3和5。由,1,3 已经不在unvisited集合中了 ,所以不必再计算了。现在计算5这个顶点,发现5这个顶点的权和为14+9=23。
unvisited{[4,20],[5,23]}
sDist{[1,0], [2,7],[3,9],[6,11]}
6. 同样道理, 在unvisited集合里找第一个元素 [4,20], 将它从集合中取走。 发现跟4这个顶点直接相连的是2,3和5。由于2,3 已经不在unvisited集合中了 ,所以不必再计算了。现在计算5这个顶点,发现5这个顶点的权和为20+6=26,因为之前5这个顶点的权和是23比26小,所以权和不变。
于是两个集合变成
unvisited{[5,23]}
sDist{[1,0], [2,7],[3,9],[6,11],[4,20]}
7. 这时候visited集合里面只有一个元素了,所以。
unvisited{}
sDist{[1,0], [2,7],[3,9],[6,11],[4,20],[5,23]}
经过以上步骤知道从源点到终点的最短距离是23。在以上的每一步都调整每个顶点到源顶点的权值,这一个过程叫做Relaxation。
但是问题是要求打印出详细的路径啊。这简单,只要在relaxtion过程的时候加上前置顶点(predcessor),然后从终点一路找过去就好了,比如这样
sDist{[1,0,1], [2,7,1],[3,9,1],[6,11,3],[4,20,2],[5,23,6]}
于是得到路径 5->6->3->1,逆过来就是路径了。
根据以上的步骤抽象出来的Pesudocode就是这样的(来自算法导论)
根据以上的算法,在Graph这个类中添加findShortestPath这个方法,并在Vertex这个类中加上predcessor属性用来记录前置顶点,还把所有的顶点加入到PriorityQueue中。
等等,好像问题是可能有多条路径啊。这也简单,假如我知道终点顶点的权值是23,那么遍历所有与终点之间相连的顶点,看看他们的权值是否有相等的,如果有两个相等的,说明有两条路径,以此类推。最终代码是这样的,
package art.programming.algorithm; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.PriorityQueue; import java.util.Queue; import java.util.Set; import java.util.Stack; public class Graph { private Vertex[] vertexes; private Edge[] edges; public Graph(String[] vertexNames){ vertexes = new Vertex[vertexNames.length]; //初始化顶点 for (int i=0; i< vertexNames.length; i++){ vertexes[i] = new Vertex(vertexNames[i], i, Integer.MAX_VALUE); } //初始化边 edges = new Edge[vertexNames.length]; int len = this.vertexes.length; for (int i=0; i < len; i++){ edges[i] = new Edge(); for(int j=0; j < len; j++){ boolean isHammingDistanceAs1 = HammingDistance.getHammingDistance(this.vertexes[i].getName(), this.vertexes[j].getName()) == 1; //如果两个字符的汉明距离为1,说明它们相连接 if (isHammingDistanceAs1){ edges[i].addNeighbor(j); } } } } //打印 public void printShortestPath(String source, String destination){ Vertex[] sDist = getDistances(source, destination); Vertex distVertex = null; for (Vertex v : vertexes){ if (v.getName().equals(destination)) distVertex = v; } int minWeight=Integer.MAX_VALUE; Set<Vertex> minPathSet = new HashSet<Vertex>(); for (int i : edges[distVertex.getIndex()].getNeighbors()){ if ( sDist[i].getWeight() <= minWeight){ minPathSet.add(sDist[i]); minWeight = sDist[i].getWeight(); }else{ minPathSet.remove(sDist[i]); } } for (Vertex v : minPathSet){ Stack<Vertex> stack = new Stack<Vertex>(); stack.add(distVertex); Vertex temp = v; while(temp.predecessor!=null){ stack.add(temp); temp = temp.getPredecessor(); } for (Vertex v1 : stack){ System.out.print(v1.name + "->"); } System.out.print(source+" "); } } //求源顶点到每个顶点的最小权值 public Vertex[] getDistances(String source, String destination){ Queue<Vertex> unvisitedQueue = new PriorityQueue<Vertex>(); initialize(source, unvisitedQueue); //放源顶点到各个顶点的最小权值 Vertex[] sDist = new Vertex[vertexes.length]; while(!unvisitedQueue.isEmpty()){ //取出最小的 Vertex u = unvisitedQueue.poll(); //遍历它的邻居 List<Integer> neighbors = edges[u.index].getNeighbors(); for (int i : neighbors){ if (sDist[i] != null) continue; //¸Ã¶¥µãµ½Ô´µãµÄ×îС¾àÀëÒѾ¼ÆËã¹ýÁË¡£ relax(u, vertexes[i]); } sDist[u.index] = u; decreaseKey(unvisitedQueue); } return sDist; } private void decreaseKey(Queue<Vertex> unvisitedQueue){ Vertex temp = unvisitedQueue.poll(); if (temp!=null) unvisitedQueue.add(temp); } /** * 初始化 * @param sourceVertexIndex * @param unvisitedVertexes * @param visitedQueue */ private void initialize(String sourceVertexName, Queue<Vertex> visitedQueue){ for (int i=0; i<vertexes.length; i++){ //如果是源顶点,它的权值为0,否则为无穷大 if (vertexes[i].getName().equals(sourceVertexName)){ vertexes[i].setWeight(0); } visitedQueue.add(vertexes[i]); } } private void relax(Vertex u, Vertex v){ //如果新路径算出来的权值比原先的小,替换原先的 if (v.weight >= u.weight + 1){ v.weight = u.weight + 1; v.predecessor = u; } } private static class Vertex implements Comparable<Vertex>{ private int weight; private int index; private Vertex predecessor; private String name; public Vertex(String name, int index, int weight){ this.index = index; this.weight = weight; this.name = name; } public int getWeight() { return weight; } public void setWeight(int weight) { this.weight = weight; } public Vertex getPredecessor() { return predecessor; } public void setPredecessor(Vertex predecessor) { this.predecessor = predecessor; } public int getIndex() { return index; } public String getName() { return name; } public void setName(String name) { this.name = name; } @Override public int compareTo(Vertex o) { if (weight > o.weight) return 1; if (weight < o.weight) return -1; return 0; } public int hashCode(){ return index; } public String toString(){ return "Name: "+this.name +" Weight:"+weight + " Predessesor:"+ (this.predecessor == null ? "null" : this.predecessor.getName()); } } private static class Edge{ private List<Integer> neighbors = new ArrayList<Integer>(); public void addNeighbor(int i){ neighbors.add(i); } public List<Integer> getNeighbors(){ return neighbors; } } }
测试一下
package art.programming.algorithm; import org.junit.Test; public class GraphTest { @Test public void findShortestPath(){ String[] nodes = {"hit","hot","dot","dog","lot","log","cog"}; Graph graph = new Graph(nodes); graph.printShortestPath("hit", "cog"); } }
总结
Dijkstra算法初始把除源顶点外的所有顶点的权置为无穷大,然后不停地调整这些顶点的权值,直到每个权值达到最小。调整顶点权值的过程叫做张弛(Relaxation,有些翻译成松弛)。《算法导论》对这一奇怪的名字做了解释。原因是在几何学的三角不等式中,两边之和必大于第三边。但是,从上图中可以得知,两边权重之和可能比第三边的权重小,所以在这个算法中三角不等式是宽松的(Relaxed)。所谓宽松就是不严格限制。