最短路径案例
- 词梯应用,在一个词梯中,每个单词均由前一个单词改变一个字母而得到。例如,我们通过一系列单字母替换而得到zero转换为five,如下:five:zero,hero,here,hire,fire,five
- 我们可以看成是一个无权最短路径问题,其中每一个单词都是一个顶点,如果两个单词可以通过单字母替换而互相转换,那么他们之间就有边
- 假设我们有一个词典,由于n个不同的单词组成,大部分单词在6~11个字母之间,我们原始数据将这些单词存储在一个字符串数组中。
算法分析
- 首先我们需要一个比较方法,用来对比两个单词直接是否只有一个字符的不同,在这我们同时考虑增加一个字符,删除一个字符的情况,如下代码:
/**
* 比较是否只有一个字符不同的单词
*/
public static boolean oneCharOff(String word1, String word2) {
if (word1 == null || word2 == null || word1.length() != word2.length()) {
return false;
}
if (word1 == word2) {
return false;
}
for (int i = 0; i < word1.length(); i++) {
if (word1.charAt(i) != word2.charAt(i)) {
if (word1.substring(i + 1, word1.length()) != word2.substring(i + 1, word2.length())) {
return false;
}
}
}
return true;
}
/**
* 比较是否只添加/删除一个字符得到
* */
public static boolean oneCharAdd(String word1, String word2){
if(word1 == null || word2 == null || Math.abs(word1.length() - word2.length()) != 1){
return false;
}
String largeWord = word1.length() > word2.length() ? word2 : word1;
String shortWord = word1.length() > word2.length() ? word1 : word2;
Integer count = 0;
Integer largePis = 0;
for (int i = 0; i < shortWord.length(); i++) {
if(shortWord.charAt(i) != largeWord.charAt(largePis)){
count ++;
if(count > 1){
return false;
}
if(shortWord.charAt(i) != largeWord.charAt(++largePis)){
return false;
}
}
largePis++;
}
return true;
}
- 首先我们需要对数据进行处理,得到一个Map,其中key是单词,value是该key单词通过1个字母替换能够从关键字变换成的一系列单词。我们用两个for循环来对数据进行一次遍历,得到一个单词的key对应的数组:
/**
* 修改Map中数组对象元素
*/
public static void update(Map<String, List<String>> map, String key, String value) {
List<String> list = map.get(key);
if (list == null) {
list = new ArrayList<>();
map.put(key, list);
}
list.add(value);
}
/**
* 方法一: 时间复杂度 O(N^2)
* 处理basicWords 数组,最终 产出Map<String, List<String>>
* 获取以单词为key, 值修改一个字符可组成的新的单词为数组的value值的Map对象
* base数组基数89000, 运行75秒
*/
public static Map<String, List<String>> computeAdjacentWords(List<String> basicWords) {
Map<String, List<String>> adjMap = new HashMap<>();
for (int i = 0; i < basicWords.size(); i++) {
for (int j = i + 1; j < basicWords.size(); j++) {
if (oneCharOff(basicWords.get(i), basicWords.get(j)) || oneCharAdd(basicWords.get(i), basicWords.get(j))) {
update(adjMap, basicWords.get(i), basicWords.get(j));
update(adjMap, basicWords.get(j), basicWords.get(i));
}
}
}
return adjMap;
}
- 以上算法可以得到我们需要的数据结构,基础词典转换成我们需要的邻接表,其中key相当于我们的所有节点,key对应的value值是邻接节点,这样我们就可以通过这个邻接表来基础上用最短路径算法得到我们需要的词梯信息
- 算法的缺点在于速度太慢,词语基数过大时候,费时比较多,时间复杂度O(N^2),我们可以对以上算法进行优化,
- 预处理词典信息,将转换为Map字典,key值是关键字的长度,value是该长度的词语数组,也就是我们先通过字符串长度对他进行归类,然后继续用之前的做法将他转成我们需要的邻接表Map,如下代码:
/**
* 方法二
* 处理basicWords 数组,最终 产出Map<String, List<String>>
* 获取以单词为key, 值修改一个字符可组成的新的单词为数组的value值的Map对象
* base数组基数89000, 运行16秒
*/
public static Map<String, List<String>> computeAdjacentWords_v1(List<String> basicWords) {
Map<String, List<String>> adjMap = new HashMap<>();
Map<String, List<String>> lengthMap = new HashMap<>();
//先分类,按字符串长度分类
for (String basicWord : basicWords) {
update(lengthMap, String.valueOf(basicWord.length()), basicWord);
}
for (List<String> strings : lengthMap.values()) {
for (int i = 0; i < strings.size(); i++) {
for (int j = i + 1; j < strings.size(); j++) {
if (oneCharOff(strings.get(i), strings.get(j)) || oneCharAdd(strings.get(i), strings.get(j))) {
update(adjMap, strings.get(i), strings.get(j));
update(adjMap, strings.get(j), strings.get(i));
}
}
}
}
return adjMap;
}
-
以上算法我们先按长度分组后对每个组进行运算。
-
我们将这个Map代表一个图的邻接表表示方法,在此基础上我们只需要编写一个案例用来运行单源最短路径算法,然后在输出单词序列
我们通过findChain方法利用上面Map邻接表信息和两个要被链接的单词,同时返回一个Map,改Map中,关键字是单词,而相应的值是从first开始的最短词梯上的关键字签名的那个单词 -
如上面举例中的那个,如果开始单词是zero,关键字five的值是fire, 关键字fire的值是hire, 关键字hire的值是here,等等,只要我们得到这样一个Map链的结构,我们就能从后向前回溯出我们需要的词梯信息,实现方法如下:
/**
* 有权重图最短路径算法 实现词梯
* @param first 词梯开始单词
* @Param second 词梯结束单词
*/
public static List<String> findChain(List<String> baseWords, String first, String second) {
//处理基础数据,得到对应 权重为1 的图基本数据结构 用Map表示的邻接表
Map<String, List<String>> adjacentWords = computeAdjacentWords_v1(baseWords);
LinkedList<String> q = new LinkedList<>();
Map<String, String> previousWords = new HashMap<>();
q.add(first);
while (!q.isEmpty()) {
String current = q.removeFirst();
List<String> currentArray = adjacentWords.get(current);
if (currentArray != null && currentArray.size() > 0) {
for (String adjWord : currentArray) {
//==null 相当于 之前know 的节点属性为以及遍历过,则无需在处理避免 有圈图
if(previousWords.get(adjWord) == null){
//通过map,一层及一层及剥离,最终得到 second--X--Y--Z--first的map链
previousWords.put(adjWord, current);
q.add(adjWord);
}
}
}
}
previousWords.put(first, null);
return getChainFromPreviousMap(previousWords, first, second);
}
//得到词梯队列信息
public static List<String> getChainFromPreviousMap( Map<String, String> previousWords, String first, String second){
LinkedList<String> q = null;
if(previousWords.get(second) != null){
q = new LinkedList<>();
for(String str = second; str != null ; str = previousWords.get(str)){
q.addFirst(str);
}
}
return q;
}
- 如上实现中findChain假设first是合法的单词,通过邻接表中的信息得到对应的邻接单词,将邻接单词都构造成指向first的一个Map,并且意见处理过的邻接单词不会再次处理,Map相同key存储一次。
- getChainFromPreviousMap使用prev Map和 second,他是Map中的一个关键字并返回用于形成词梯的那些单词,通过prev向后遍历map,使用linkedList插表头的形式得到正确的排序词梯。
- 如上得到最终的答案,关键步骤是我们在预处理的阶段,我们利用图论的最短路径算法解决问题首先需要建立一个邻接表模型,才能完成后面的Dijkstra算法求解最短路径问题