【问题标题】:Finding the shortest word ladder between two given words and a dictionary找到两个给定单词和字典之间的最短单词阶梯
【发布时间】:2017-04-16 09:11:36
【问题描述】:

我试图从字典中找到两个给定单词之间的最短阶梯。包括给定单词和字典中的所有单词都具有相同数量的字符。在一次通过中,只能更改一个字符,并且需要最短路径。例如:给定:“hit”和“cil” Dic:[“hil”,“hol”,“hot”,“lot”,“lit”,“lil”] 所以,答案应该是“hit”-> “希尔”->“希尔”

我尝试使用 BFS 解决这个问题;通过在字典中查找下一个单词并检查它是否与队列中弹出的项目相邻。这种方法不会给我最短的路径:

如果,我尝试用 26 个字母替换每个字母,并且如果结果单词出现在字典中,请接受:仍然这种方法不会给我最短路径。例如:在这里,它应该给我:hit->lit->lot->hot->hol->lil->cil

可能更好的方法是先构造一棵树,然后在树中找到从起始词到结束词的最短路径。

我知道,这个论坛上有这个问题的解决方案,但没有一个解释算法。我是 BFS 的新手,所以不太熟悉。 我有兴趣知道如何找到最短路径之一,如果有几条则所有最短路径。

【问题讨论】:

  • 听起来很像旅行推销员问题,我无法想象除了蛮力之外还有其他解决方案,如果你找到了一个不错的算法,请回答你自己的问题
  • 你读过Levenshtein距离吗?
  • 字典有多大?字典里有多少个词?
  • “可能更好的方法是先构造一棵树,然后在树中找到从起始词到结束词的最短路径。”... 像 trie 这样的东西是更好的数据结构对于字典,但不适用于这个问题,因为 trie 非常依赖于字母序列。第 0 个字符不同的单词会比第 n 个字符不同的单词更远。
  • BFS 或 A-Star(以 Levenshtein 距离作为启发式)应该可以工作。您的实现中确实存在错误(一旦发现就不要停止,没有将节点标记为正确访问,...)。

标签: c++ algorithm


【解决方案1】:

我的建议是在字典中的单词上构建一个图,其中一个节点代表一个单词,如果 b 可以通过仅更改 a (当然,反之亦然)。此过程将花费 O(n*n) 时间,其中 n 为否。字典中的单词。
如何做到这一点如下:
对于每个单词构建频率字符数组,将其称为 farr,长度为 26,farr[i] 告诉字符 i 在单词中按字母顺序出现的次数,然后在嵌套循环中运行 n*n 次只需要比较单词的频率表的条目,它们必须仅相差一个字符才能从单词 a 到 b。
另请注意,此图中的边是无向的(在两个方向上)。
在字典的单词上构建完整的图形后,将问题词也添加到图形中。然后继续 BFS 从初始词的节点搜索目标词,其中所需的转换是初始词 -> 目标词。 现在假设您在“i”级找到目标词,同时从初始词进行探索,那么最短路径是“i”个单位长。

【讨论】:

  • 有向图应该这样做,因为我只需要朝 1 个方向移动。该过程应该是 O(n*Constant);其中 n 是字典中的单词数,m 是单词的大小。
  • 我觉得在最坏的情况下你可能必须在所有的单词之间画一条边,最坏的情况下单词的大小会像 no 一样长。字典中的单词,所以最差的时间复杂度是O(n*n),一如既往地忽略常数因素。
  • 我不知道你将如何绘制有向边,但我觉得有向图不会解决问题,因为虽然你会朝一个方向移动,但一旦图形建立在单词之上对于字典,您不知道源词将附加到图中的所有节点,以及您可能需要遵循哪些路径才能到达结束词。拥有双向边缘更安全。
  • 我用过一棵树。我不需要所有节点之间的边缘,而是彼此相邻的单词之间的边缘,这也不是全部。如果 s 和 a 相邻且 s 和 b 相邻,则 a 和 b 将相邻,但我不需要它们之间的边。我已经使用树提供了一个解决方案,用代码回答
  • 你已经修复了根,这意味着你正在为一对源和目标建模数据结构,在这里我犯了错误。我以为你只想为字典中的单词构建数据结构,然后你会得到一些源和目标对,可能很多,并且对于每个你必须尽快报告答案。但是因为情况并非如此,所以树结构不是问题,因为无论如何在构建图之后你都会做一个 BFS,这将产生一个 BFS 树。
【解决方案2】:

这种方法有点蛮力,但可能是一个很好的起点。

如果目标词等于起始词,或者具有Levenshtein distance 1,则结果为[start, target],您就完成了。

否则,您必须找到字典中与起始词的 Levenshtein 距离为 1 的所有成员。如果其中一个与目标词的 Levenshtein 距离为 1,则结果为 [start, word, target],您就完成了。否则,您将以所选列表中的每个单词作为开始,将目标作为目标进行递归,并将 start 前置到最短的结果中。

伪代码——有点像python:

myDict = {"hil", "hol", "hot", "lot", "lit", "lil"}

used_wordlist = {}

shortestWordLadder(start, target):
    if start == target or levenshtein(start, target) = 1:
        return [start, target]

    current_wordlist = [x for x in myDict
                              if x not in used_wordlist and
                              levenshtein(ladder[-1], x) = 1]

    if current_wordlist.size = 0:
        return null

    for word in current_wordlist:
        if levenshtein(word, target) == 1:
            return [start, word, target]

     used_wordlist.insert_all(current_wordlist)
     min_ladder_size = MAX_INT
     min_ladder = null

     for word in currrent_wordlist:
         ladder = shortestWordLadder(word, target)

         if ladder is not null and ladder.size < min_ladder_size:
             min_ladder_size = ladder.size
             min_ladder = ladder.prepend(start)

     return min_ladder

可能的优化:

我考虑重用矩阵,levenshtein(start, target) 将在内部创建,但我无法获得足够的信心,它可以在所有情况下工作。这个想法是从矩阵的右下角开始并选择最小的邻居,这将从字典中创建一个单词。然后继续那个位置。如果当前单元格的邻居没有从字典中创建一个单词,我们将不得不回溯,直到找到一个到值为 0 的字段的方法。如果回溯将我们带回到右下角的单元格,则没有解决方案。

我现在不确定,可能没有解决方案,你可能会忽略这种方式。如果它找到了解决方案,我很有信心,它是最短的解决方案之一。

目前我没有时间仔细考虑。如果这被证明不是一个完整的解决方案,您可以将其用作优化步骤,而不是 shortestWOrdLadder() 第一行中的 levenshtein(start, target) 调用,因为该算法为您提供 Levenshtein 距离,如果可能的话,最短路径.

【讨论】:

  • 感谢您的详细解释
【解决方案3】:

我通过采用以下方法制定了解决方案: 1.)我首先从字典中构建了一棵树,假设起点是给定的单词;并找到与该单词相邻的所有单词,依此类推 2.) 接下来我尝试使用这棵树构造从给定单词到结束单词的所有可能路径。

复杂度:O(n*70 + 2^n-1 * lg(n)) = O(2^n-1*lg(n)) 这里n是字典的单词数,出来70作为 65 到 122(A 到 a)的 ASCII 值范围,我在这里取了一个整数。正如预期的那样,复杂性是指数级的。 即使经过某些优化,最坏情况的复杂性也不会改变。

这是我编写的代码(经过我的测试并且有效。任何错误或建议都将受到高度赞赏。):

#include <iostream>
#include <vector>
#include <cstring>
#include <deque>
#include <stack>
#include <algorithm>

using namespace std;

struct node {
    string str;
    vector<node *> children;
    node(string s) {
        str = s;
        children.clear();
    }
};

bool isAdjacent(string s1, string s2) {
    int table1[70], table2 [70];
    int ct = 0;

    for (int i = 0; i < 70; i++) {
        table1[i] = 0;
        table2[i] = 0;
    }
    for (int i = 0; i < s1.length(); i++) {
        table1[((int)s1[i])- 65] += 1;
        table2[((int)s2[i])- 65] += 1;
     }

     for (int i = 0; i < 70; i++) {
        if (table1[i] != table2[i])
            ct++;
        if (ct > 2)
            return false;
     }
     if (ct == 2)
        return true;
     else
        return false;
}

void construct_tree(node *root, vector<string> dict) {
    deque<node *> q;
    q.push_back(root);
    while (!q.empty()) {
        node *curr = q.front();
        q.pop_front();
        if (dict.size() == 0)
            return;
        for (int i = 0; i < dict.size(); i++) {
            if (isAdjacent(dict[i], curr->str)) {
                string n = dict[i];
                dict.erase(dict.begin()+i);
                i--;
                node *nnode = new node(n);
                q.push_back(nnode);
                curr->children.push_back(nnode);
            }
        }
    }
}

void construct_ladders(stack<node *> st, string e, vector<vector <string> > &ladders) {
    node *top = st.top();
    if (isAdjacent(top->str,e)) {
        stack<node *> t = st;
        vector<string> n;
        while (!t.empty()) {
            n.push_back(t.top()->str);
            t.pop();
        }
        ladders.push_back(n);
    }
    for (int i = 0; i < top->children.size(); i++) {
        st.push(top->children[i]);
        construct_ladders(st,e,ladders);
        st.pop();
    }
}

void print(string s, string e, vector<vector<string> > ladders) {
    for (int i = 0; i < ladders.size(); i++) {
        for (int j = ladders[i].size()-1; j >= 0; j--) {
            cout<<ladders[i][j]<<" ";
        }
        cout<<e<<endl;
    }
}

int main() {
    vector<string> dict;
    string s = "hit";
    string e = "cog";

    dict.push_back("hot");
    dict.push_back("dot");
    dict.push_back("dog");
    dict.push_back("lot");
    dict.push_back("log");

    node *root = new node(s);
    stack<node *> st;
    st.push(root);

    construct_tree(root, dict);

    vector<vector<string> > ladders;
    construct_ladders(st, e, ladders);

    print(s,e,ladders);

    return 0;
}

【讨论】:

  • 关键是如果一个人只能用一条最短路径来解决,有没有更好的解决方案来找到它?无论如何,所有路径都是指数级的。
  • 问题1 复杂度分析错误。 O(n*70 + 2^n-1 * lg(n)) = O(lgn) ,是错误的,实际复杂度相当于 O(n+2^n*lg(n)) 和任何形式O(2^n * lg(n)) 是灾难性的。您的代码实际上并没有那么复杂。 问题 2. 探索树的两个节点之间的路径永远不会呈指数级增长,如果您对自己的代码确实构建了树很有信心,那么任何节点之间只存在一条路径树中有两个节点,因此您不会看到多个路径,复杂性也将是多项式的。
  • 为了获得更好的性能,建议您不要使用字符串向量,而是使用字符串队列,或者不要从向量中删除字符串,而是使用单独的 bool 名为 exists 的向量并设置 exists[i] = true,仅当原始向量中存在第 i 个字符串时。删除步骤是昂贵的。使用字符数组是比字符串更好的优化方法,因为字符串会带来很多开销。
  • @prem ktiw。我感觉合理。我纠正了这个值。我想我弄乱了construct_ladders的代码,它基本上遍历了所有可能的路径。这个construct_ladders 函数的复杂性对我来说是指数级的。然而,研究从根到叶和中间路径的每条路径在 n 中可能是线性的。那么,我的算法的复杂度应该是 O(n*70 + n*C) = O(n),这对我来说看起来很奇怪。怎么会这么低? n*70 用于construct_tree 函数。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2016-12-06
  • 1970-01-01
  • 1970-01-01
  • 2014-05-04
  • 2015-12-26
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多