Skip to content

Commit d6e3b41

Browse files
committed
Added an solution for Word Ladder II
1 parent 242cf9d commit d6e3b41

File tree

2 files changed

+175
-6
lines changed

2 files changed

+175
-6
lines changed

C++/chapBFS.tex

+175-6
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ \subsubsection{单队列}
8787
for (size_t i = 0; i < s.word.size(); ++i) {
8888
state_t new_state(s.word, s.step + 1);
8989
for (char c = 'a'; c <= 'z'; c++) {
90+
// 防止同字母替换
9091
if (c == new_state.word[i]) continue;
9192

9293
swap(c, new_state.word[i]);
@@ -150,6 +151,7 @@ \subsubsection{双队列}
150151
for (size_t i = 0; i < s.size(); ++i) {
151152
string new_word(s);
152153
for (char c = 'a'; c <= 'z'; c++) {
154+
// 防止同字母替换
153155
if (c == new_word[i]) continue;
154156

155157
swap(c, new_word[i]);
@@ -235,12 +237,11 @@ \subsubsection{描述}
235237
\subsubsection{分析}
236238
跟 Word Ladder比,这题是求路径本身,不是路径长度,也是BFS,略微麻烦点。
237239

238-
这题跟普通的广搜有很大的不同,就是要输出所有路径,因此在记录前驱和判重地方与普通广搜略有不同
240+
求一条路径和求所有路径有很大的不同,求一条路径,每个状态节点只需要记录一个前驱即可;求所有路径时,有的状态节点可能有多个父节点,即要记录多个前驱
239241

242+
如果当前路径长度已经超过当前最短路径长度,可以中止对该路径的处理,因为我们要找的是最短路径。
240243

241-
\subsubsection{单队列}
242-
243-
单队列无法求解所有路径。因为 需要一个 \fn{queue} 和 \fn{unordered_set} 的综合体,这是不可能的。
244+
单队列无法求解所有路径。因为 需要一个 \fn{queue} 和 \fn{unordered_set} 的综合体,这是不可能的。本题可以用双队列来求解。
244245

245246

246247
\subsubsection{双队列}
@@ -259,13 +260,16 @@ \subsubsection{双队列}
259260
unordered_set<string> visited; // 判重
260261
unordered_map<string, vector<string> > father; // DAG
261262

263+
int level = 0; // 层次
264+
262265
auto state_is_target = [&](const string &s) {return s == end;};
263266
auto state_extend = [&](const string &s) {
264267
unordered_set<string> result;
265268

266269
for (size_t i = 0; i < s.size(); ++i) {
267270
string new_word(s);
268271
for (char c = 'a'; c <= 'z'; c++) {
272+
// 防止同字母替换
269273
if (c == new_word[i]) continue;
270274

271275
swap(c, new_word[i]);
@@ -284,6 +288,11 @@ \subsubsection{双队列}
284288
vector<vector<string> > result;
285289
current.insert(start);
286290
while (!current.empty()) {
291+
++ level;
292+
// 如果当前路径长度已经超过当前最短路径长度,可以中止对该路径的
293+
// 处理,因为我们要找的是最短路径
294+
if (!result.empty() && level > result[0].size()) break;
295+
287296
// 1. 延迟加入visited, 这样才能允许两个父节点指向同一个子节点
288297
// 2. 一股脑current 全部加入visited, 是防止本层前一个节点扩展
289298
// 节点时,指向了本层后面尚未处理的节点,这条路径必然不是最短的
@@ -314,7 +323,18 @@ \subsubsection{双队列}
314323
vector<vector<string> > &result) {
315324
path.push_back(word);
316325
if (word == start) {
317-
result.push_back(path);
326+
if (!result.empty()) {
327+
if (path.size() < result[0].size()) {
328+
result.clear();
329+
result.push_back(path);
330+
} else if(path.size() == result[0].size()) {
331+
result.push_back(path);
332+
} else {
333+
// throw exception
334+
}
335+
} else {
336+
result.push_back(path);
337+
}
318338
reverse(result.back().begin(), result.back().end());
319339
} else {
320340
for (const auto& f : father[word]) {
@@ -327,6 +347,137 @@ \subsubsection{双队列}
327347
\end{Code}
328348

329349

350+
\subsubsection{图的广搜}
351+
352+
本题还可以看做是图上的广搜。给定了字典 \fn{dict},可以基于它画出一个无向图,表示单词之间可以互相转换。本题的本质就是已知起点和终点,在图上找出所有最短路径。
353+
354+
\begin{Code}
355+
//LeetCode, Word Ladder II
356+
// 时间复杂度O(n),空间复杂度O(n)
357+
class Solution {
358+
public:
359+
vector<vector<string> > findLadders(const string& start,
360+
const string &end, const unordered_set<string> &dict) {
361+
const auto& g = build_graph(dict);
362+
vector<path_node_t*> pool;
363+
queue<path_node_t*> q; // 未处理的节点
364+
// value 是所在层次
365+
unordered_map<string, int> visited;
366+
367+
q.push(new_path_node(nullptr, start, 1, pool));
368+
visited[start] = 1;
369+
370+
vector<vector<string>> result;
371+
while (!q.empty()) {
372+
path_node_t* node = q.front();
373+
q.pop();
374+
375+
// 如果当前路径长度已经超过当前最短路径长度,
376+
// 可以中止对该路径的处理,因为我们要找的是最短路径
377+
if (!result.empty() && node->length > result[0].size()) break;
378+
379+
if (node->word == end) {
380+
result.push_back(gen_path(node));
381+
continue;
382+
}
383+
// 挪到了下面,只有尽早加入visited, 才能防止,同一层,
384+
// 前面一个子节点扩展时,指向同层后面的节点
385+
// visited[node->word] = node->length;
386+
387+
// 扩展节点
388+
auto iter = g.find(node->word);
389+
if (iter == g.end()) continue;
390+
391+
for (const auto& neighbor : iter->second) {
392+
auto visited_iter = visited.find(neighbor);
393+
394+
if (visited_iter != visited.end()) {
395+
if (visited_iter->second < node->length + 1) {
396+
continue;
397+
} else if (visited_iter->second > node->length + 1) {
398+
// not possible, throw exception
399+
// throw std::logic_error("not possible");
400+
} else {
401+
// do nothing
402+
}
403+
} else {
404+
visited[neighbor] = node->length + 1;
405+
}
406+
407+
q.push(new_path_node(node, neighbor, node->length + 1, pool));
408+
}
409+
}
410+
411+
// release path nodes
412+
for (auto node : pool) {
413+
delete node;
414+
}
415+
return result;
416+
}
417+
418+
private:
419+
struct path_node_t {
420+
path_node_t* father;
421+
string word;
422+
int length; // 路径长度
423+
424+
path_node_t(path_node_t* father_, const string& word_, int length_) :
425+
father(father_), word(word_), length(length_) {}
426+
};
427+
428+
path_node_t* new_path_node(path_node_t* parent, const string& value,
429+
int length, vector<path_node_t*>& pool) {
430+
path_node_t* node = new path_node_t(parent, value, length);
431+
pool.push_back(node);
432+
433+
return node;
434+
}
435+
vector<string> gen_path(const path_node_t* node) {
436+
vector<string> path;
437+
438+
while(node != nullptr) {
439+
path.push_back(node->word);
440+
node = node->father;
441+
}
442+
443+
reverse(path.begin(), path.end());
444+
return path;
445+
}
446+
447+
unordered_map<string, unordered_set<string> > build_graph(
448+
const unordered_set<string>& dict) {
449+
unordered_map<string, unordered_set<string> > adjacency_list;
450+
451+
for (const auto& word : dict) {
452+
for (size_t i = 0; i < word.size(); ++i) {
453+
string new_word(word);
454+
for (char c = 'a'; c <= 'z'; c++) {
455+
// 防止同字母替换
456+
if (c == new_word[i]) continue;
457+
458+
swap(c, new_word[i]);
459+
460+
if ((dict.find(new_word) != dict.end())) {
461+
auto iter = adjacency_list.find(word);
462+
if (iter != adjacency_list.end()) {
463+
iter->second.insert(new_word);
464+
}
465+
else {
466+
adjacency_list.insert(pair<string,
467+
unordered_set<string>>(word, unordered_set<string>()));
468+
adjacency_list[word].insert(new_word);
469+
}
470+
}
471+
swap(c, new_word[i]); // 恢复该单词
472+
}
473+
}
474+
}
475+
return adjacency_list;
476+
}
477+
};
478+
\end{Code}
479+
480+
330481
\subsubsection{相关题目}
331482

332483
\begindot
@@ -575,7 +726,18 @@ \subsubsection{如何表示状态}
575726
vector<vector<state_t> > &result) {
576727
path.push_back(state);
577728
if (state == start) {
578-
result.push_back(path);
729+
if (!result.empty()) {
730+
if (path.size() < result[0].size()) {
731+
result.clear();
732+
result.push_back(path);
733+
} else if(path.size() == result[0].size()) {
734+
result.push_back(path);
735+
} else {
736+
// throw exception
737+
}
738+
} else {
739+
result.push_back(path);
740+
}
579741
reverse(result.back().begin(), result.back().end());
580742
} else {
581743
for (const auto& f : father[state]) {
@@ -753,6 +915,7 @@ \subsubsection{求所有路径}
753915
unordered_set<state_t> visited; // 判重
754916
unordered_map<state_t, vector<state_t> > father; // DAG
755917

918+
int level = 0; // 层次
756919

757920
// 判断状态是否合法
758921
auto state_is_valid = [&](const state_t &s) { /*...*/ };
@@ -776,6 +939,11 @@ \subsubsection{求所有路径}
776939
vector<vector<string> > result;
777940
current.insert(start);
778941
while (!current.empty()) {
942+
++ level;
943+
// 如果当前路径长度已经超过当前最短路径长度,可以中止对该路径的
944+
// 处理,因为我们要找的是最短路径
945+
if (!result.empty() && level > result[0].size()) break;
946+
779947
// 1. 延迟加入visited, 这样才能允许两个父节点指向同一个子节点
780948
// 2. 一股脑current 全部加入visited, 是防止本层前一个节点扩展
781949
// 节点时,指向了本层后面尚未处理的节点,这条路径必然不是最短的
@@ -802,3 +970,4 @@ \subsubsection{求所有路径}
802970
return result;
803971
}
804972
\end{Codex}
973+

C++/leetcode-cpp.pdf

4.63 KB
Binary file not shown.

0 commit comments

Comments
 (0)