非线性数据结构--图的最短路径问题与动态规划问题的区别(二)
前情摘要:记得之前同学拿一道例题问起我关于最短路径问题与动态规划是否有关,当时其实就有思考,不过随后并没有将想法做记录,现在也很后悔。在此不立flag,一定以后要对当前的思考做好即时记录,一定以后要落地到书面上!!!以下简单介绍一下我对这道例题的做法,明天再写一篇更详尽的随笔说清楚我对这个问题的看法。
题目:
给定两个单词(beginWord 和 endWord)和一个字典,找到从 beginWord 到 endWord 的最短转换序列的长度。转换需遵循如下规则:
每次转换只能改变一个字母。
转换过程中的中间单词必须是字典中的单词。
说明:
如果不存在这样的转换序列,返回 0。
所有单词具有相同的长度。
所有单词只由小写字母组成。
字典中不存在重复的单词。
你可以假设 beginWord 和 endWord 是非空的,且二者不相同。
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/word-ladder
示例 1:
输入:
beginWord = "hit",
endWord = "cog",
wordList = ["hot","dot","dog","lot","log","cog"]
输出: 5
解释: 一个最短转换序列是 "hit" -> "hot" -> "dot" -> "dog" -> "cog",
返回它的长度 5。
我的做法是:利用常规的单源最短路径问题的解决算法(迪杰斯特拉算法求单源路径问题),得到最短路径数组再反向递归得到最短转换序列集合。以下是例程,有不懂之处可以看注释或者交流。不是最佳算法,时空复杂度都不太好,看官方有更好的做法,思路大都是在广度优先同时收集最短路径到集合的算法上做不同优化的多个版本。关于例程,也曾优化过两次,初版是穷举遍历得出阶段性的最短路径,亲试超时,第二版是用堆优化得到阶段性最短路径的步骤,还是超时,最后一版优化收集符合题意的最短路径的步骤,简化了判断是否符合题意的条件,虽然最后通过了,但时间和空间都不太理想。后面会根据官方提供的思路再尝试优化一遍。另外,本随笔之所以命名为(二),是笔者认为本系列的主题是研讨图的最短路径问题与动态规划问题的区别,而该篇只是引发该主题思考的一篇题解,明天写的首篇要更容易理解且更详尽更切合该主题,先抛砖引玉一下。
package algorithm;
import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;
/**
* @project: study
* @author: Sac
* @create: 2020-06-23 22:23
* @description:
**/
public class Word2WordSolution {
private static final int MAX_VALUE = Integer.MAX_VALUE;
/**
* 寻找所有以beginWord为起点、以endWord为终点的最短转换序列
* @param beginWord
* @param endWord
* @param wordList
* @return
*/
private static List<List<String>> findLadders(String beginWord, String endWord, List<String> wordList) {
//原词->单源节点,最终词->目的节点,字典集->节点集和点边关系,建模->构造图
if (!wordList.contains(endWord)) {
return new ArrayList<>();
}
ArrayList<String> list = new ArrayList<>();
if (!wordList.contains(beginWord)) {
list.add(beginWord);
}
list.addAll(wordList);
Long[] nodes = new Long[list.size()];
int[] srcNodes = srcNodes(nodes, list, beginWord, endWord);
int[][] edges = edges(nodes, beginWord.length());
return shortestPath(edges, list, srcNodes[0], srcNodes[1]);
}
/**
* 构造图的邻边矩阵
* @param dict
* @param srcNodeLen
* @return
*/
private static int[][] edges(Long[] dict, int srcNodeLen) {
int[][] edges = new int[dict.length][dict.length];
long[] powers = getPowers(srcNodeLen);
for (int i = 0; i < dict.length; i++) {
for (int j = i + 1; j < dict.length; j++) {
boolean isLink = isLink(dict[i], dict[j], powers);
edges[i][j] = isLink ? 1 : MAX_VALUE;
edges[j][i] = isLink ? 1 : MAX_VALUE;
}
}
return edges;
}
/**
* 返回以edges为边集、以nodes为点集的构造的图以srcNodeIndex为源点索引、以targetNodeIndex为终点索引的最短路径集合
* @param edges
* @param nodes
* @param srcNodeIndex
* @param targetNodeIndex
* @return
*/
private static List<List<String>> shortestPath(int[][] edges, List<String> nodes, int srcNodeIndex, int targetNodeIndex) {
ArrayList<List<String>> res = new ArrayList<>();
if(nodes.size() <= 1) {
return res;
}
boolean[] sureSet = new boolean[nodes.size()];
//srcNode到其他节点的目标路径值数组
int[] dis = new int[nodes.size()];
//初始化
System.arraycopy(edges[srcNodeIndex], 0, dis, 0, dis.length);
Heap heap = new Heap(nodes.size(), dis);
while (!heap.isEmpty()) {
int min = heap.pop();
sureSet[min] = true;
//基于已确定最小值的当前节点min更新其他节点最小路径值
for (int i = 0; i < nodes.size(); i++) {
if (!sureSet[i]
&& (edges[min][i] != MAX_VALUE && dis[min]!= MAX_VALUE)
&& (dis[min] + edges[min][i] < dis[i])) {
dis[i] = dis[min] + edges[min][i];
heap.shiftUpByElem(i);
}
}
}
return shortestPathList(dis, edges, nodes, srcNodeIndex, targetNodeIndex, new ArrayList<>());
}
/**
* 寻找所有以beginWord为起点、以endWord为终点的最短转换序列长度
* @param beginWord
* @param endWord
* @param wordList
* @return
*/
public int ladderLength(String beginWord, String endWord, List<String> wordList) {
if (!wordList.contains(endWord)) {
return 0;
}
ArrayList<String> list = new ArrayList<>();
if (!wordList.contains(beginWord)) {
list.add(beginWord);
}
list.addAll(wordList);
Long[] nodes = new Long[list.size()];
int[] srcNodes = srcNodes(nodes, list, beginWord, endWord);
int[][] edges = edges(nodes, beginWord.length());
return shortestPathLen(edges, list, srcNodes[0], srcNodes[1]);
}
private static int shortestPathLen(int[][] edges, List<String> nodes, int srcNodeIndex, int targetNodeIndex) {
if (nodes.isEmpty()) {
return 0;
}
ArrayList<List<String>> res = new ArrayList<>();
if(nodes.size() <= 1) {
return 1;
}
boolean[] sureSet = new boolean[nodes.size()];
//srcNode到其他节点的目标路径值数组
int[] dis = new int[nodes.size()];
//初始化
System.arraycopy(edges[srcNodeIndex], 0, dis, 0, dis.length);
Heap heap = new Heap(nodes.size(), dis);
while (!heap.isEmpty()) {
int min = heap.pop();
sureSet[min] = true;
//基于已确定最小值的当前节点min更新其他节点最小路径值
for (int i = 0; i < nodes.size(); i++) {
if (!sureSet[i]
&& (edges[min][i] != MAX_VALUE && dis[min]!= MAX_VALUE)
&& (dis[min] + edges[min][i] < dis[i])) {
dis[i] = dis[min] + edges[min][i];
heap.shiftUpByElem(i);
}
}
}
return dis[targetNodeIndex];
}
/**
* 第一版求最短路径的算法,区别在于寻找阶段性最短路经时使用穷举遍历,并非用堆的方法
* @param edges
* @param nodes
* @param srcNodeIndex
* @param targetNodeIndex
* @return
*/
private static List<List<String>> shortestPathO(int[][] edges, List<String> nodes, int srcNodeIndex, int targetNodeIndex) {
ArrayList<List<String>> res = new ArrayList<>();
if(nodes.size() <= 1) {
return res;
}
boolean[] sureSet = new boolean[nodes.size()];
sureSet[srcNodeIndex] = true;
//srcNode到其他节点的目标路径值数组
int[] dis = new int[nodes.size()];
//初始化
System.arraycopy(edges[srcNodeIndex], 0, dis, 0, dis.length);
int unSure = srcNodeIndex == nodes.size() - 1 ? srcNodeIndex - 1 : srcNodeIndex + 1;
boolean isAllPass = false;
while (!isAllPass) {
int min = unSure;
for (int i = 0; i < dis.length; i++) {
if (!sureSet[i] && dis[min] > dis[i]) {
min = i;
}
}
sureSet[min] = true;
//基于已确定最小值的当前节点min更新其他节点最小路径值
isAllPass = true;
for (int i = 0; i < nodes.size(); i++) {
if (!sureSet[i]) {
unSure = i;
isAllPass = false;
}
if (!sureSet[i] && (edges[min][i] != MAX_VALUE && dis[min]!= MAX_VALUE) && dis[min] + edges[min][i] < dis[i]) {
dis[i] = dis[min] + edges[min][i];
System.out.println("" + min + "->" + i);
}
}
}
return shortestPathList(dis, edges, nodes, srcNodeIndex, targetNodeIndex, new ArrayList<>());
}
/**
* 自定义的契合题意场景的堆
*/
private static class Heap {
private int[] elems;
private int end;
private final int maxSize;
private int[] powers;
private int[] hashs;
public Heap(int maxSize, int[] powers) {
this.maxSize = maxSize;
this.elems = new int[maxSize + 1];
this.end = 0;
this.powers = powers;
this.hashs = new int[powers.length];
for (int i = 0; i < powers.length; i++) {
this.push(i);
}
}
void shiftUp(int idx) {
int c = idx;
int p = c / 2;
int t = this.elems[c];
while (p > 0) {
if (this.isLessThan(this.elems[p], t)) {
break;
}
this.elems[c] = this.elems[p];
updateHashs(c);
c = p;
p = c / 2;
}
this.elems[c] = t;
this.updateHashs(c);
}
private void updateHashs(int c) {
this.hashs[this.elems[c]] = c;
}
void shiftDown(int idx) {
int p = idx;
int c = 2 * p;
int t = this.elems[p];
while (c <= this.end) {
if ((c + 1 <= this.end) && this.isLessThan(this.elems[c + 1], this.elems[c])) {
c ++;
}
if (this.isLessThan(t, this.elems[c])) {
break;
}
this.elems[p] = this.elems[c];
updateHashs(p);
p = c;
c = 2 * p;
}
this.elems[p] = t;
this.updateHashs(p);
}
private boolean isLessThan(int a1, int a2) {
return this.powers[a1] <= this.powers[a2];
}
private void push(int elem) {
if (this.isFull()) {
return;
}
this.elems[ ++ this.end] = elem;
shiftUp(this.end);
}
public boolean isHeap(int root) {
if (root == this.end || 2 * root > this.end) {
return true;
}
if (this.isLessThan(this.elems[root], this.elems[root * 2]) && (root * 2 + 1 > this.end || this.isLessThan(this.elems[root], this.elems[root * 2 + 1]))) {
return isHeap(root * 2) && isHeap(root *2 + 1);
}
return false;
}
public int isHeapVal(int root) {
if (root == this.end || 2 * root > this.end) {
return -1;
}
if (this.isLessThan(this.elems[root], this.elems[root * 2]) && (root * 2 + 1 > this.end || this.isLessThan(this.elems[root], this.elems[root * 2 + 1]))) {
int heapVal = isHeapVal(root * 2);
int heapVal2 = isHeapVal(root * 2 + 1);
if (heapVal == -1 && heapVal2 == -1) {
return -1;
}
if (heapVal == -1) {
return heapVal2;
}else {
return heapVal;
}
}
return root;
}
public int pop() {
if (this.isEmpty()) {
return -1;
}
return this.pop(1);
}
public void shiftUpByElem(int elem) {
int idx = this.hashs[elem];
this.shiftUp(idx);
}
public int pop(int idx) {
if (idx > this.end) {
return -1;
}
int t = this.elems[idx];
this.elems[idx] = this.elems[this.end];
this.hashs[this.elems[this.end --]] = -1;
shiftDown(idx);
return t;
}
public boolean isFull() {
return this.maxSize == this.end;
}
public boolean isEmpty() {
return this.end == 0;
}
}
/**
* 递归寻找最短路径集合
* @param dis
* @param edges
* @param nodes
* @param srcIndex
* @param targetIndex
* @param appendList
* @return
*/
private static List<List<String>> shortestPathList(int[] dis, int[][] edges, List<String> nodes, int srcIndex, int targetIndex, List<String> appendList) {
ArrayList<String> res = new ArrayList<>();
res.add(nodes.get(targetIndex));
res.addAll(appendList);
if (srcIndex == targetIndex) {
ArrayList<List<String>> lists = new ArrayList<>();
lists.add(res);
return lists;
}
int[] edge = edges[targetIndex];
//收集与nodes[targetIndex]关联且与srcIndex距离最小的点集合
ArrayList<Integer> linkedMinNodes = new ArrayList<>();
for (int i = 0; i < edge.length; i++) {
if (edge[i] != MAX_VALUE && edge[i] != 0) {
if (edge[i] == dis[targetIndex] - dis[i]) {
linkedMinNodes.add(i);
}
}
}
List<List<String>> finalRes = new ArrayList<>();
for (Integer i : linkedMinNodes) {
ArrayList<String> rs = new ArrayList<>(res);
List<List<String>> shortestChangeList = shortestPathList(dis, edges, nodes, srcIndex, i, rs);
finalRes.addAll(shortestChangeList);
}
return finalRes;
}
/**
* 权值一定要大于单位位数上的数值,最好是素数,因为小写字母的运算中字母最大的映射整数值减去最小的映射值不超过25('z' - 'a'),故25以上的任意素数即可
*/
private static int POW = 53;
/**
* 获取每位的单位权值数组
* @param length
* @return
*/
private static long[] getPowers(int length) {
long[] res = new long[length];
long power = 1;
for (int i = res.length - 1; i >= 0; i--) {
res[i] = power;
power *= POW;
}
return res;
}
/**
* 字母映射值的最大差值
*/
private static int SUB = 26;
/**
* a与b对应节点是否有边
* @param a
* @param b
* @param powers
* @return
*/
private static boolean isLink(Long a, Long b, long[] powers) {
long elem = Math.abs(b - a);
long elemByBinarySearch = getMaxValueNoGreatThanElemByBinarySearch(powers, elem);
long div = elem / elemByBinarySearch;
return (elem) % elemByBinarySearch == 0 && div <= SUB && div > 0;
}
/**
* 构造节点映射用于计算点间关系的整数值
* @param nodes
* @param initNodes
* @param srcNode
* @param targetNode
* @return
*/
private static int[] srcNodes(Long[] nodes, List<String> initNodes, String srcNode, String targetNode) {
int srcIndex = -1;
int desIndex = -1;
int size = initNodes.size();
for (int i = 0; i < size; i++) {
String node = initNodes.get(i);
if (srcNode.equals(node)) {
srcIndex = i;
}
if (targetNode.equals(node)) {
desIndex = i;
}
nodes[i] = stringHashCode(node);
}
return new int[]{srcIndex, desIndex};
}
/**
* 字符串到整数值的映射运算
* @param s
* @return
*/
private static long stringHashCode(String s) {
char[] chars = s.toCharArray();
long h = 0;
for (int i = 0; i < chars.length; i++) {
h = POW * h + (chars[i] - 'a');
}
return h;
}
/**
* 寻找不超过elem的单位权值
* @param a
* @param elem
* @return
*/
private static long getMaxValueNoGreatThanElemByBinarySearch(long[] a, long elem) {
int sp = 0, ep = a.length - 1;
while (sp < ep) {
int mid = sp + ((ep - sp) >>> 1);
if (a[mid] < elem) {
ep = mid;
}else if (a[mid] > elem) {
sp = mid + 1;
}else {
return a[mid];
}
}
return a[sp];
}
public static void main(String[] strings) {
ArrayList<String> words = new ArrayList<>();
Scanner scanner = new Scanner(System.in);
String srcWord = scanner.next();
String endWord = scanner.next();
int i = scanner.nextInt();
for (int j = 0; j < i; j++) {
words.add(scanner.next());
}
System.out.println(findLadders(srcWord, endWord, words));
}
}

浙公网安备 33010602011771号