最短路径算法-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文件格式如下:

时间: 2024-10-27 04:51:33

最短路径算法-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算法实现

本文介绍的是图的非负权值的单源最短路径问题.问题的提出是,对于有权图D,t提供源点v,要找到从v到其他所有点的最短路径,即单源最短路径问题,在本文中,解决这一问题,是普遍比较熟悉的Dijkstra算法. 算法核心思想参见维基.简而言之,设集合S存放已经求出了最短路径的点.初始状态S中只有一个点v0,之后每求得v0到vn的最短路径,就会更新v0到所有vn邻接的点的一致的最短路径(不一定是最终的最短路径),如此重复,每次会确定v0到一个点的最短路径,确定好的点加入S中,直至所有点进入S结束.在本文中

单源最短路径(dijkstra算法)php实现

做一个医学项目,其中在病例评分时会用到单源最短路径的算法.单源最短路径的dijkstra算法的思路如下: 如果存在一条从i到j的最短路径(Vi.....Vk,Vj),Vk是Vj前面的一顶点.那么(Vi...Vk)也必定是从i到k的最短路径.Dijkstra是以最短路径长度递增,逐次生成最短路径的算法.例如:对于源顶点V0,首先选择其直接相邻的顶点中长度最短的顶点Vi,那么当前已知可得从V0到达Vj顶点的最短距离dist[j]=min{dist[j],dist[i]+cost[i][j]}.假设G

hdu 2680 最短路径(dijkstra算法+多源最短路径单源化求最小值)

Choose the best route Time Limit: 2000/1000 MS (Java/Others)    Memory Limit: 32768/32768 K (Java/Others)Total Submission(s): 7062    Accepted Submission(s): 2301 Problem Description One day , Kiki wants to visit one of her friends. As she is liable

最短路算法 :Bellman-ford算法 &amp; Dijkstra算法 &amp; floyd算法 &amp; SPFA算法 详解

 本人QQ :2319411771   邮箱 : [email protected] 若您发现本文有什么错误,请联系我,我会及时改正的,谢谢您的合作! 本文为原创文章,转载请注明出处 本文链接   :http://www.cnblogs.com/Yan-C/p/3916281.html . 很早就想写一下最短路的总结了,但是一直懒,就没有写,这几天又在看最短路,岁没什么长进,但还是加深了点理解. 于是就想写一个大点的总结,要写一个全的. 在本文中因为邻接表在比赛中不如前向星好写,而且前向星效率并

ACM: HDU 3790 最短路径问题-Dijkstra算法

HDU 3790 最短路径问题 Time Limit:1000MS     Memory Limit:32768KB     64bit IO Format:%I64d & %I64u Description 给你n个点,m条无向边,每条边都有长度d和花费p,给你起点s终点t,要求输出起点到终点的最短距离及其花费,如果最短距离有多条路线,则输出花费最少的. Input 输入n,m,点的编号是1~n,然后是m行,每行4个数 a,b,d,p,表示a和b之间有一条边,且其长度为d,花费为p.最后一行是

Java数据结构 最短路径解法Dijkstra算法

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

单源点最短路径的Dijkstra算法

在带权图(网)里,点A到点B所有路径中边的权值之和为最短的那一条路径,称为A,B两点之间的最短路径;并称路径上的第一个顶点为源点(Source),最后一个顶点为终点(Destination).在无权图中,最短路径则是两点之间经历的边数最少的路径.实际上,只要把无权图上的每条边都看成是权值为1的边,那么无权图和带权图的最短路径是一致的. 给定一个带权有向图G=(V,E),指定图G中的某一个顶点的V为源点,求出从V到其他各顶点之间的最短路径,这个问题称为单源点最短路径问题. 迪杰斯特拉(Dijkst

有向有权图的最短路径算法--Dijkstra算法

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