圖與最短路徑

問題

 給了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;
   }
}

圖結構

因爲把每一個字符串做爲圖中的一個頂點,因此我須要構造一個圖,而後把經過某種數據結構把圖存起來。在這裏,我使用鄰接鏈表。
 

頂點java

列表下標node

關聯頂點下標算法

hit數據結構

0ide

1函數

hot測試

1動畫

2,4this

dotspa

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+"\n");
    }
  }
 
  //求源頂點到每一個頂點的最小權值
  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)。所謂寬鬆就是不嚴格限制。
相關文章
相關標籤/搜索