题目描述
字典 wordList 中从单词 beginWord 和 endWord 的 转换序列 是一个按下述规格形成的序列 beginWord -> s1 -> s2 -> … -> sk:
每一对相邻的单词只差一个字母。
对于 1 <= i <= k 时,每个 si 都在 wordList 中。注意, beginWord 不需要在 wordList 中。
sk == endWord
给你两个单词 beginWord 和 endWord 和一个字典 wordList ,返回 从 beginWord 到 endWord 的 最短转换序列 中的 单词数目 。如果不存在这样的转换序列,返回 0 。
https://leetcode.cn/problems/word-ladder/
朴素BFS
本题如果用朴素的BFS也是可以直接通过的,但耗时1s以上在其他的平台中(ACM格式的)基本上是过不了的
using PII = pair<int,int>;
const int N = 5010;
class Solution {
private:
int dist[N];// 储存到每个点的最短距离
bool st[N];// 储存每个点的最短距离是否已经确定
int n;
public:
// 判断两个单词是否只有一个地方不同
bool isOnlyOneLetterDiff(string a, string b) {
int count = 0;
for (int i = 0; i < a.size(); ++ i) {
if (a[i] != b[i]) count++;
}
// cout<< a<<" "<<b<<" "<<count<<endl;
return count <= 1;
}
int ladderLength(string beginWord, string endWord, vector<string>& wordList) {
n = wordList.size();
memset(dist, 0x3f, sizeof dist);
dist[0] = 0;
queue<PII> q; // first 存距离,second存编号
for (int i = 0; i < n; ++ i) {
if (isOnlyOneLetterDiff(beginWord, wordList[i])) {
// cout<<" wefwef";
q.push({1, i});
dist[i] = 1;
st[i] = true;
}
}
while(q.size()) {
auto [distance, ver] = q.front();
q.pop();
// cout<< wordList[ver]<<endl;
// if (st[ver]) continue;
// st[ver] = true;
for (int i = 0; i < n; ++ i) {
if (st[i]) continue;
if (isOnlyOneLetterDiff(wordList[ver],wordList[i])) {
q.push({distance + 1, i});
dist[i] = distance + 1;
st[i] = true;
}
}
}
for (int i = 0; i < n; ++ i) {
if (endWord == wordList[i] && dist[i] < 0x3f3f3f3f) {
return dist[i] + 1;
}
}
return 0;
}
};
时间复杂度不是很好
![在这里插入图片描述](https://img-blog.csdnimg.cn/68e3be8fc68d4c8c9cb401a9f26ea6e3.png
双端BFS
因为本题存在唯一的最优解,因此如果使用双端bfs则可以有效的缩小单向bfs巨大的搜索空间
从两个方向开始搜索,一旦搜索到相同的值则代表找到了最短路径
基本思路:
创建两个队列分别从起点和终点进行bfs
创建两个哈希表解决重复搜索,并探索次数
如果某一次中两个搜索同时到了同一个点,则代表最短路径
代码如下:
class Solution {
private:
string beginWord, endWord;
unordered_set<string> hashset;
int update(queue<string> &q, unordered_map<string,int> &cur, unordered_map<string,int> &other) {
int m = q.size();
while (m --) {
auto top = q.front();
// cout<<top<<endl;
q.pop();
int n = top.size();
// 枚举替换原字符串中的字符位置
for (int i = 0; i < n; ++ i) {
for (int j = 0; j < 26; ++ j) {
string tempstr = top.substr(0, i) + (char)('a' + j) + top.substr(i + 1);
if (hashset.count(tempstr)) {
// cout << tempstr<<endl;
// 在当前的方向上如果已经记录过这个字符,则跳过
if(cur.count(tempstr) && cur[tempstr] <= cur[top] + 1) continue;
// 如果该字符在另一个方向上已经找到了,则说明找到了最短路
if (other.count(tempstr)) return cur[top] + 1 + other[tempstr];
else {
// cout<<"wefwef"<<endl;
q.push(tempstr);
cur[tempstr] = cur[top] + 1;
}
}
}
}
}
return -1;
}
int doublebfs() {
queue<string> q1; // 用于正向搜索
queue<string> q2; // 用于反向搜索
unordered_map<string, int> hashmap1; // 记录正向搜索次数
unordered_map<string, int> hashmap2; // 记录反向搜索次数
q1.push(beginWord);
hashmap1[beginWord] = 0;
q2.push(endWord);
hashmap2[endWord] = 0;
int i = 10;
while (q1.size() && q2.size()) {
int turn = -1;
// 平均两个搜索方向,优先扩展内部容量少的队列
// cout<< q1.size()<<" "<<q2.size()<<endl;
if (q1.size() <= q2.size()) {
turn = update(q1, hashmap1, hashmap2);
} else {
turn = update(q2, hashmap2, hashmap1);
}
if (turn != -1) return turn;
}
return -1;
}
public:
int ladderLength(string _beginWord, string _endWord, vector<string>& wordList) {
beginWord = _beginWord;
endWord = _endWord;
for(auto item : wordList) {
hashset.insert(item);
}
if (!hashset.count(endWord)) return 0;
int ans = doublebfs();
return ans == -1 ? 0 : ans + 1;
}
};
最终的时间和空间结果如下:
Astar算法
本题也可以使用Astar算法来进行启发式求解,启发函数可以定为 与目标单词有多少位不同
Astar算法的思路:
维护一个优先队列,使用代价函数来进行启发式搜索,每次走下一步都错所有备选的结果中找到代价最低的方案来进行下一步行走,本题中的代价函数设计为:当前单词与目标单词之间的差距有多大。
与图论中的Astar的区别在于这里的代价函数并没有计算当前已经走了多远
struct Node {
string str;
int val;
Node(string _str, int _val) {
str = _str;
val = _val;
}
};
class Solution {
private:
string beginWord, endWord;
int INF = 0x3f3f3f3f;
unordered_set<string> hashset;
// 代价函数,当前单词与目标单词差距多大
int valuation(string str) {
if (str.size() != endWord.size()) return INF;
int n = str.size();
int ans = 0;
for (int i = 0; i < n; ++ i) {
ans += str[i] == endWord[i] ? 0 : 1;
}
return ans;
}
int astar() {
// 自定义比较函数,比较谁的代价更大,作为小顶堆
auto cmp = [] (Node &nodea, Node &nodeb){
return nodea.val > nodeb.val;
};
priority_queue<Node, vector<Node>, decltype(cmp)> heap(cmp); // astar 算法的关键,使用优先队列来计算下一步的选择
unordered_map<string, int> dist; // 使用哈希表来保存距离
dist[beginWord] = 0; // 自己到自己的距离为0
heap.push(* new Node(beginWord, valuation(beginWord)));
while(heap.size()) {
auto topNode = heap.top();
heap.pop();
string str = topNode.str;
// cout<<str<<endl;
int distance = dist[str];
if (str == endWord) {
break;
}
for (int i = 0; i < str.size(); ++ i) {
for (int j = 0; j < 26; ++ j) {
string tempstr = str.substr(0, i) + (char)('a' + j) + str.substr(i + 1);
if (!hashset.count(tempstr)) continue;
if (!dist.count(tempstr) || dist[tempstr] > distance + 1) {
dist[tempstr] = distance + 1;
Node *newNode = new Node(tempstr, dist[tempstr] + valuation(tempstr));
heap.push(*newNode);
}
}
}
}
// cout<<dist[endWord]<<endl;
return dist.count(endWord) ? dist[endWord] : -1;
}
public:
int ladderLength(string _beginWord, string _endWord, vector<string>& wordList) {
beginWord = _beginWord;
endWord = _endWord;
for (auto item : wordList) hashset.insert(item);
if (!hashset.count(endWord)) return 0;
int ans = astar();
return ans == - 1 ? 0 : ans + 1;
}
};
因为这种题的数据量比较小,因此上Astar有些大材小用,并且只有在确保有解的情况下Astar算法的启发式搜索才能真正发挥,本题则没有更好的办法来判断是否有解,因此并不能带来空间上的优化,所以结果反而不如双向bfs