最短路径算法-Dijkstra算法的应用之单词转换(词梯问题)(转)

一,问题描述

在英文单词表中,有一些单词非常相似,它们可以通过只变换一个字符而得到另一个单词。比如:hive-->five;wine-->line;line-->nine;nine-->mine.....

那么,就存在这样一个问题:给定一个单词作为起始单词(相当于图的源点),给定另一个单词作为终点,求从起点单词经过的最少变换(每次变换只会变换一个字符),变成终点单词。

这个问题,其实就是最短路径问题。

由于最短路径问题中,求解源点到终点的最短路径与求解源点到图中所有顶点的最短路径复杂度差不多,故求解两个单词之间的最短路径相当于求解源点单词到所有单词之间的最短路径。

 

给定所有的英文单词,大约有89000个,我们需要找出通过单个字母的替换可以变成至少15个其他单词的单词?程序如何实现?

给定两个单词,一个作为源点,另一个作为终点,需要找出从源点开始,经过最少次单个字母替换,变成终点单词,这条变换路径中经过了哪些单词?

比如:(zero-->five):(zero-->hero-->here-->hire-->five)

 

二,算法分析

假设所有的单词存储在一个txt文件中,每行一个单词。

现在的问题主要有两个:①从文件中读取单词,并构造一个图;②图的最短路径算法--Dijkstra算法实现。

由于单词A替换一个字符变成单词B,那么反过来单词B替换一个字符也可以变成单词A(自反性)【wine-->fine; fine-->wine】。故图是一个无向图。

构造图的算法分析:

现在更进一步,假设单词已经读取到一个List<String>中,图采用邻接表形式存储,构造图其实就是:如何根据List<String> 构造一个Map<String,List<String>>

其中Map中的Key是某个单词,Value则是该单词的“邻接单词”列表,邻接单词即:该单词经过一个字符的替换变成另一个单词。

如:wine的邻接单词有:fine、line、nine.....

一个最直接的想法就是:

由于单词都在List<String>中存储,那么从第1个单词开始,依次扫描第2个至第N个单词,判断第1个单词是否与第 2,3,.....N个单词只差一个字符。这样一遍扫描,找出了List<String>中第1个单词的邻接表。

继续,对于第2个单词,依次扫描第3,4,....N个单词,找出List<String>中第2个单词的邻接表。

.......

上述过程可描述成如下循环:

    for(int i = 0; i < N; i++)
        for(int j = i+1; j < N; j++)//N 表示单词表中所有单词个数            //do something....

显然,上述构造图的算法的时间复杂度为O(N^2)。具体代码如下:

 1     public static Map<String, List<String>> computeAdjacentWords2(List<String> theWords){
 2         Map<String, List<String>> adjWords = new TreeMap<>();
 3         String[] words = new String[theWords.size()];
 4         words = theWords.toArray(words);
 5
 6         for(int i = 0; i < words.length; i++)
 7             for(int j = i+1; j < words.length; j++)//在整个单词表中的所有单词之间进行比较
 8                 if(oneCharOff(words[i], words[j]))
 9                 {
10                     update(adjWords, words[i], words[j]);//无向图,i--j
11                     update(adjWords, words[j], words[i]);//j--i
12                 }
13         return adjWords;
14     }

注意第4行,它将List转换成了数组,这样可以提高程序的执行效率。因为,若不转换成数组,在随后的第6、7行for循环中,在执行时泛型擦除,将频繁向下转型(Object转型成String)

另外两个工具方法如下:

//判断两个单词 只替换一个字符变成另一单词
    private static boolean oneCharOff(String word1, String word2) {
        if (word1.length() != word2.length())//单词长度不相等,肯定不符合条件.
            return false;
        int diffs = 0;
        for (int i = 0; i < word1.length(); i++)
            if (word1.charAt(i) != word2.charAt(i))
                if (++diffs > 1)
                    return false;
        return diffs == 1;
    }

    //将单词添加到邻接表中
    private static <T> void update(Map<T, List<String>> m, T key, String value) {
        List<String> lst = m.get(key);
        if (lst == null) {//该 Key是第一次出现
            lst = new ArrayList<String>();
            m.put(key, lst);
        }
        lst.add(value);
    }

 

Dijkstra算法分析:

 上面已经提到,这是一个无向图,无向图的最短路径问题,无向图的Dijkstra算法实现要比带权的有向图简单得多。简单的原因在于:无向图的Dijkstra实现只需要一个队列,采用“广度”遍历的思想从源点开始向外扩散求解图中其他顶点到源点的距离,之所以这样,是因为无向图一旦访问到某个顶点,更新它的前驱顶点后,它的前驱顶点以后都不会再变了(参考博文)。而对于有向图,某个顶点的前驱顶点可能会被多次更新。因此,需要更复杂的数据结构来”贪心“选择下一个距离最短的顶点。

 1 /**
 2  * 使用Dijkstra算法求解无向图 从 start 到 end 的最短路径
 3  * @param adjcentWords 保存单词Map,Map<String, List<string>>key:表示某个单词, Value:与该单词只差一个字符的单词
 4  * @param start 起始单词
 5  * @param end 结束单词
 6  * @return 从start 转换成 end 经过的中间单词
 7  */
 8     public static List<String> findChain(Map<String, List<String>> adjcentWords, String start, String end){
 9         Map<String, String> previousWord = new HashMap<String, String>();//Key:某个单词,Value:该单词的前驱单词
10         Queue<String> queue = new LinkedList<>();
11
12         queue.offer(start);
13         while(!queue.isEmpty()){
14             String preWord = queue.poll();
15             List<String> adj = adjcentWords.get(preWord);
16
17             for (String word : adj) {
18                 //代表这个word的'距离'(前驱单词)没有被更新过.(第一次遍历到该word),每个word的'距离'只会被更新一次.
19                 if(previousWord.get(word) == null){//理解为什么需要if判断
20                     previousWord.put(word, preWord);
21                     queue.offer(word);
22                 }
23
24             }
25         }
26         previousWord.put(start, null);//记得把源点的前驱顶点添加进去
27         return geChainFromPreviousMap(previousWord, start, end);
28     }

第19行进行if判断的原因是:还是前面提到的,每个顶点的前驱只会更新一次。当第一次遍历到 'word'时,它的前驱顶点'preWord'就被永久确定下来了。

当在后面可能再次从另外一个顶点遍历到该'word'时,这个顶点不可能是'word'的前驱顶点了。因为:这条到'word'的路径不可能是最短的了。这就是”广度“ 搜索的思想!

 

三,构造图的算法改进

这里将构造图的算法改进单独作为一节,是因为它很好地用到了“分类的思想”,在处理大量的数据时,先将相关的数据分类,然后以类为单位,一个一个地处理类中的所有数据。

分类要覆盖所有的数据,相当于概率论中的对 数据集合S的一个全划分。

将列表List<String>中的单词构造图,本质上查找每个单词的所有邻接单词。显然如果两个单词的长度不相等,它们就不可能构成邻接关系。

因此,可以把单词表中所有的单词先按单词的长度进行分类,分成长度为1的单词、长度为2的单词....长度为N的单词。分成了N个集合,这N个集合就是单词表的一个全划分,因为对于单词表中的任何一个单词,它一定属于这N个集合中的某一个。

因此,先将按长度进行分类。然后再对每一类中的单词进行判断。改进后的代码如下:

 1     /**
 2      * 根据单词构造邻接表
 3      * @param theWords 包含所有单词List
 4      * @return Map<String, List<string>>key:表示某个单词, Value:与该单词只差一个字符的单词
 5      */
 6     public static Map<String, List<String>> computeAdjacentWords(
 7             List<String> theWords) {
 8         Map<String, List<String>> adjWords = new TreeMap<>();
 9         Map<Integer, List<String>> wordsByLength = new TreeMap<>();//单词分类,Key表示单词长度,Value表示长度相同的单词集合
10
11         for (String word : theWords)
12             update(wordsByLength, word.length(), word);
13
14         for (List<String> groupWords : wordsByLength.values()) {//分组处理单词
15             String[] words = new String[groupWords.size()];
16             groupWords.toArray(words);
17
18             for (int i = 0; i < words.length; i++)
19                 for (int j = i + 1; j < words.length; j++)//只在一个组内所有的单词之间进行比较
20                     if (oneCharOff(words[i], words[j])) {
21                         update(adjWords, words[i], words[j]);
22                         update(adjWords, words[j], words[i]);
23                     }
24
25         }
26         return adjWords;
27     }

第11行至12行,完成单词分类,将单词按长度分类保存在一个Map中。Map的Key表示单词长度,Value表示所有长度相同的单词集合。如: <4, five,line,good,high....>

第18行至19行的for循环,现在只需要对一个分类里面的所有单词进行比较了。而上面第2点(算法分析)中贴出的computeAdjacentWords2()方法中的第6、7行for循环则是对所有的单词进行遍历。

可以看出,改进后的算法比较的次数少了。但是从时间复杂度的角度来看,仍是O(N^2)。且额外用了一个Map<Integer, List<String>>来保存每个分类。

 

四,总结

 这个单词转换问题让我认识到了图论算法的重要性。以前觉得图的算法高大上,遥不可及,原来它的应用如此实在。

Dijkstra算法是一个典型的贪心算法。对于带权的有向图的Dijkstra算法实现需要用到最小堆。最小堆的DelMin操作最坏情况下的复杂度为O(logN),很符合Dijkstra中贪心选取下一个距离最小的顶点。其次,要注意的是:当选取了某个顶点之后,该顶点的所有邻接点的距离都可能被更新,这里需要进行堆调整,可视为将这些邻接点执行decreaseKey(weight)操作。但是,有个问题,我们需要找到该顶点的所有邻接点!而对最小堆中的某个元素进行查找操作是低效的!(为什么网上大部分的基于最小堆实现的Dijkstra算法都没有考虑查找邻接点且对它执行decreaseKey操作????)因此,Dijkstra算法的实现才会借助对查找效率更好的斐波拉契堆或者配对堆来实现。

 

其次,对待求解的大问题进行分类,将大问题分解成若干小的类别的问题,这是一种分治的思想。只”比较“(处理)相关的元素而不是”比较“所有的元素,有效地减少了程序的时间复杂度。

 

五,完整代码实现

  1 import java.io.BufferedReader;
  2 import java.io.File;
  3 import java.io.FileReader;
  4 import java.io.IOException;
  5 import java.util.ArrayList;
  6 import java.util.HashMap;
  7 import java.util.LinkedList;
  8 import java.util.List;
  9 import java.util.Map;
 10 import java.util.Queue;
 11 import java.util.TreeMap;
 12
 13 public class WordLadder {
 14
 15     /*
 16      * 从文件中将单词读入到List<String>. 假设一行一个单词,单词没有重复
 17      */
 18     public static List<String> read(final String filepath) {
 19         List<String> wordList = new ArrayList<String>();
 20
 21         File file = new File(filepath);
 22         FileReader fr = null;
 23         BufferedReader br = null;
 24         String lines = null;
 25         String word = null;
 26         try {
 27             fr = new FileReader(file);
 28             br = new BufferedReader(fr);
 29             String line = null;
 30             int index = -1;
 31             while ((lines = br.readLine()) != null) {
 32                 // word = line.substring(0, line.indexOf(" ")).trim();
 33                 line = lines.trim();
 34                 index = line.indexOf(" ");
 35                 if (index == -1)
 36                     continue;
 37                 word = line.substring(0, line.indexOf(" "));
 38                 wordList.add(word);
 39             }
 40         } catch (IOException e) {
 41             e.printStackTrace();
 42         } finally {
 43             try {
 44                 fr.close();
 45                 br.close();
 46             } catch (IOException e) {
 47
 48             }
 49         }
 50
 51         return wordList;
 52     }
 53
 54     /**
 55      * 根据单词构造邻接表
 56      * @param theWords 包含所有单词List
 57      * @return Map<String, List<string>>key:表示某个单词, Value:与该单词只差一个字符的单词
 58      */
 59     public static Map<String, List<String>> computeAdjacentWords(
 60             List<String> theWords) {
 61         Map<String, List<String>> adjWords = new TreeMap<>();
 62         Map<Integer, List<String>> wordsByLength = new TreeMap<>();
 63
 64         for (String word : theWords)
 65             update(wordsByLength, word.length(), word);
 66
 67         for (List<String> groupWords : wordsByLength.values()) {
 68             String[] words = new String[groupWords.size()];
 69             groupWords.toArray(words);
 70
 71             for (int i = 0; i < words.length; i++)
 72                 for (int j = i + 1; j < words.length; j++)
 73                     if (oneCharOff(words[i], words[j])) {
 74                         update(adjWords, words[i], words[j]);
 75                         update(adjWords, words[j], words[i]);
 76                     }
 77
 78         }
 79         return adjWords;
 80     }
 81
 82     public static Map<String, List<String>> computeAdjacentWords2(List<String> theWords){
 83         Map<String, List<String>> adjWords = new TreeMap<>();
 84         String[] words = new String[theWords.size()];
 85         words = theWords.toArray(words);
 86
 87         for(int i = 0; i < words.length; i++)
 88             for(int j = i+1; j < words.length; j++)
 89                 if(oneCharOff(words[i], words[j]))
 90                 {
 91                     update(adjWords, words[i], words[j]);//无向图,i--j
 92                     update(adjWords, words[j], words[i]);//j--i
 93                 }
 94         return adjWords;
 95     }
 96
 97
 98     //判断两个单词 只替换一个字符变成另一单词
 99     private static boolean oneCharOff(String word1, String word2) {
100         if (word1.length() != word2.length())//单词长度不相等,肯定不符合条件.
101             return false;
102         int diffs = 0;
103         for (int i = 0; i < word1.length(); i++)
104             if (word1.charAt(i) != word2.charAt(i))
105                 if (++diffs > 1)
106                     return false;
107         return diffs == 1;
108     }
109
110     //将单词添加到邻接表中
111     private static <T> void update(Map<T, List<String>> m, T key, String value) {
112         List<String> lst = m.get(key);
113         if (lst == null) {//该 Key是第一次出现
114             lst = new ArrayList<String>();
115             m.put(key, lst);
116         }
117         lst.add(value);
118     }
119
120
121 /**
122  * 使用Dijkstra算法求解从 start 到 end 的最短路径
123  * @param adjcentWords 保存单词Map,Map<String, List<string>>key:表示某个单词, Value:与该单词只差一个字符的单词
124  * @param start 起始单词
125  * @param end 结束单词
126  * @return 从start 转换成 end 经过的中间单词
127  */
128     public static List<String> findChain(Map<String, List<String>> adjcentWords, String start, String end){
129         Map<String, String> previousWord = new HashMap<String, String>();//Key:某个单词,Value:该单词的前驱单词
130         Queue<String> queue = new LinkedList<>();
131
132         queue.offer(start);
133         while(!queue.isEmpty()){
134             String preWord = queue.poll();
135             List<String> adj = adjcentWords.get(preWord);
136
137             for (String word : adj) {
138                 //代表这个word的'距离'(前驱单词)没有被更新过.(第一次遍历到该word),每个word的'距离'只会被更新一次.
139                 if(previousWord.get(word) == null){//理解为什么需要if判断
140                     previousWord.put(word, preWord);
141                     queue.offer(word);
142                 }
143
144             }
145         }
146         previousWord.put(start, null);//记得把源点的前驱顶点添加进去
147         return geChainFromPreviousMap(previousWord, start, end);
148     }
149
150     private static List<String> geChainFromPreviousMap(Map<String, String> previousWord, String start, String end){
151         LinkedList<String> result = null;
152
153         if(previousWord.get(end) != null){
154             result = new LinkedList<>();
155             for(String pre = end; pre != null; pre = previousWord.get(pre))
156                 result.addFirst(pre);
157         }
158         return result;
159     }
160 }

处理的单词TXT文件格式如下:

 

 

http://www.cnblogs.com/hapjin/p/5445370.html

 

时间: 2024-08-30 02:16:32

最短路径算法-Dijkstra算法的应用之单词转换(词梯问题)(转)的相关文章

最短路径之Dijkstra算法

Dijkstra算法: 首先,引进一个辅助向量D,它的每个分量D[i]表示当前所找到的从始点v到每个终点vi的的长度:如D[3]=2表示从始点v到终点3的路径相对最小长度为2.这里强调相对就是说在算法过程中D的值是在不断逼近最终结果但在过程中不一定就等于长度.它的初始状态为:若从v到vi有弧,则D为弧上的权值:否则置D为∞.显然,长度为 D[j]=Min{D | vi∈V} 的路径就是从v出发的长度最短的一条.此路径为(v,vj). 那么,下一条长度次短的是哪一条呢?假设该次短路径的终点是vk,

【算法导论】单源最短路径之Dijkstra算法

        Dijkstra算法解决了有向图上带正权值的单源最短路径问题,其运行时间要比Bellman-Ford算法低,但适用范围比Bellman-Ford算法窄. 迪杰斯特拉提出的按路径长度递增次序来产生源点到各顶点的最短路径的算法思想是:对有n个顶点的有向连通网络G=(V, E),首先从V中取出源点u0放入最短路径顶点集合U中,这时的最短路径网络S=({u0}, {}); 然后从uU和vV-U中找一条代价最小的边(u*, v*)加入到S中去,此时S=({u0, v*}, {(u0,

[临时]单源最短路径(Dijkstra算法)

因为没有原创内容,相当于看书笔记,因此本打算发在QQZone,但因为QQ空间日志忽然服务器繁忙,大骂腾讯无奈还是把此日志临时发布在自己的博客上. 参考资料:<计算机算法设计与分析>(第三版). 条件: (1)带权有向图 G = (V, E); 任意边的权 >= 0; 算法: 贪心法.体现在从源节点开始,每次从集合S外"选一个最近的节点"添加到S中,然后对dist数组做更新.   参数说明: T:模板参数,权值类型.(计量长度的数据类型) int n; 图的节点数: i

单源最短路径算法--Dijkstra算法和Bellman-Ford算法

Dijkstra算法 算法流程: (a) 初始化:用起点v到该顶点w的直接边(弧)初始化最短路径,否则设为∞: (b) 从未求得最短路径的终点中选择路径长度最小的终点u:即求得v到u的最短路径: (c) 修改最短路径:计算u的邻接点的最短路径,若(v,-,u)+(u,w)<(v,-,w),则以(v,-,u,w)代替. (d) 重复(b)-(c),直到求得v到其余所有顶点的最短路径. 特点:总是按照从小到大的顺序求得最短路径. 假设一共有N个节点,出发结点为s,需要一个一维数组prev[N]来记录

单源最短路径算法-Dijkstra

描述: 1)算法思想原理:      Floyd算法是一个经典的动态规划算法.用通俗的语言来描述的话,首先我们的目标是寻找从点i到点j的最短路径.从动态规划的角度看问题,我们需要为这个目标重新做一个诠释(这个诠释正是动态规划最富创造力的精华所在)       从任意节点i到任意节点j的最短路径不外乎2种可能,1是直接从i到j,2是从i经过若干个节点k到j.所以,我们假设Dis(i,j)为节点u到节点v的最短路径的距离,对于每一个节点k,我们检查Dis(i,k) + Dis(k,j) < Dis(

网络最短路径Dijkstra算法

最近在学习算法,看到有人写过的这样一个算法,我决定摘抄过来作为我的学习笔记: <span style="font-size:18px;">/* * File: shortest.c * Description: 网络中两点最短路径 Dijkstra 算法 * Shortest Path Dijkstra Algorithm * Created: 2001/11/25 * Author: Justin Hou [mailto:justin_hou@hotmail.com] *

求两点之间最短路径-Dijkstra算法

 Dijkstra算法 1.定义概览 Dijkstra(迪杰斯特拉)算法是典型的单源最短路径算法,用于计算一个节点到其他所有节点的最短路径.主要特点是以起始点为中心向外层层扩展,直到扩展到终点为止.Dijkstra算法是很有代表性的最短路径算法,在很多专业课程中都作为基本内容有详细的介绍,如数据结构,图论,运筹学等等.注意该算法要求图中不存在负权边. 问题描述:在无向图 G=(V,E) 中,假设每条边 E[i] 的长度为 w[i],找到由顶点 V0 到其余各点的最短路径.(单源最短路径)   2

Dijkstra算法【模板】

1.定义概览 Dijkstra(迪杰斯特拉)算法是典型的单源最短路径算法,用于计算一个节点到其他所有节点的最短路径.主要特点是以起始点为中心向外层层扩展,直到扩展到终点为止.Dijkstra算法是很有代表性的最短路径算法,在很多专业课程中都作为基本内容有详细的介绍,如数据结构,图论,运筹学等等.注意该算法要求图中不存在负权边. 问题描述:在无向图 G=(V,E) 中,假设每条边 E[i] 的长度为 w[i],找到由顶点 V0 到其余各点的最短路径.(单源最短路径)   2.算法描述 1)算法思想

Dijkstra算法(三) Java详解

迪杰斯特拉算法介绍 迪杰斯特拉(Dijkstra)算法是典型最短路径算法,用于计算一个节点到其他节点的最短路径. 它的主要特点是以起始点为中心向外层层扩展(广度优先搜索思想),直到扩展到终点为止. 基本思想 通过Dijkstra计算图G中的最短路径时,需要指定起点s(即从顶点s开始计算). 此外,引进两个集合S和U.S的作用是记录已求出最短路径的顶点(以及相应的最短路径长度),而U则是记录还未求出最短路径的顶点(以及该顶点到起点s的距离). 初始时,S中只有起点s:U中是除s之外的顶点,并且U中