【问题标题】:Given a dictionary, find all possible letter orderings给定一本字典,找出所有可能的字母顺序
【发布时间】:2012-04-01 03:28:26
【问题描述】:

我最近被问到以下面试问题:

你有一个用外语写的字典页面。假使,假设 语言类似于英语,从左到右读/写 正确的。此外,单词按字典顺序排列。为了 例如页面可以是:ADG、ADH、BCD、BCF、FM、FN
您必须给出字符的所有可能的字典顺序 设置在页面中。

我的做法如下: A 的优先级高于 B,G 的优先级高于 H。 因此我们有一些字符的排序信息:

A->B, B->F, G->H, D->F, M->N

可能的排序可以是 ABDFGNHMC, ACBDFGNHMC, ... 我的方法是使用数组作为位置持有者并生成所有排列以识别所有有效排序。最坏情况的时间复杂度是 N!其中 N 是字符集的大小。 我们能比蛮力方法做得更好吗?

提前致谢。

【问题讨论】:

  • 您可以构造一个由您所拥有的单词所暗示的偏序的(非必要连接的)DAG,然后使用该规则详尽地找到从每个节点(字母)到所有其他节点的所有路由你可以去任何节点,除了你的“上游”节点或你已经访问过的任何节点。因此,您可以立即修剪任何您将踏入具有“上游”节点的节点的路径,该节点是您尚未使用的节点,击败蛮力。不过,可能还有更聪明的方法。
  • 我认为你不能比 n! 做得更好,但是识别模式是否有效会增加一些复杂性,所以我不相信你的建议是 @ 987654322@,或者。听起来像m*n^2 * n!,其中m 是规则的数量,尽管您并没有具体说明如何验证每条规则。
  • @jswolf19:你不能比n!做得更好,原因很简单,如果输入是单个单词ABCDE,那么所有5!排序都是可能的,所以你必须输出商场。但是您可以希望通过某些取决于施加什么约束的术语比n!做得“更好”,接受某些输入的术语是O(bupkis)。在最好的情况下,输入是 5 个单字母单词,A B C D E,那么我们应该能够很快回答。
  • 你应该明确表示你想在平均情况下做得更好,因为很明显你不能在最坏的情况下做得更好。
  • 我不清楚你在问什么。您谈论最坏的情况,但听起来您的实现也是最坏的情况,因为您正在生成所有排列然后过滤。那么您是否要求更好地解决一般问题?因为在大多数情况下,你当然可以比最坏的情况做得更好。

标签: algorithm language-agnostic


【解决方案1】:

Donald Knuth 撰写了这篇论文 A Structured Program to Generate all Topological Sorting Arrangements。这篇论文最初发表于 1974 年。论文中的以下引用使我对这个问题有了更好的理解(在文本中,关系 i

解决这个问题的一个自然方法是让 x1 成为 没有前辈的元素,然后删除所有关系 从 x12 成为一个元素 ≠ x1 在系统中没有前任,因为它现在存在, 然后删除 x2only 方法,因为 x1 必须是一个元素 没有前任,并且 x2 必须没有前任 当所有关系 x1所有的算法 拓扑排序问题的解决方案;这是一个典型的例子 “回溯”程序,在每个阶段,我们都考虑一个 来自“找到所有方法来完成给定部分 排列 x1x2...xk 到 a 拓扑排序 x1x2...xn 。” 一般方法是对所有可能的选择进行分支 xk+1.
回溯应用程序的一个核心问题是 找到一种合适的方式来排列数据,以便于 对 xk+1 的可能选择进行排序;在这 如果我们需要一种有效的方法来发现所有元素的集合≠ {x1,...,xk} 没有其他前任 比 x1,...,xk,并保持这个知识 当我们从一个子问题转移到另一个子问题时,效率很高。

本文包含一个高效算法的伪代码。每个输出的时间复杂度为 O(m+n),其中 m 是输入关系的数量,n 是字母的数量。我编写了一个 C++ 程序,它实现了论文中描述的算法——维护变量和函数名称——它将你问题中的字母和关系作为输入。我希望没有人抱怨给程序提供这个答案——因为与语言无关的标签。

#include <iostream>
#include <deque>
#include <vector>
#include <iterator>
#include <map>

// Define Input
static const char input[] =
    { 'A', 'D', 'G', 'H', 'B', 'C', 'F', 'M', 'N' };
static const char crel[][2] =
    {{'A', 'B'}, {'B', 'F'}, {'G', 'H'}, {'D', 'F'}, {'M', 'N'}};

static const int n = sizeof(input) / sizeof(char);
static const int m = sizeof(crel) / sizeof(*crel);

std::map<char, int> count;
std::map<char, int> top;
std::map<int, char> suc;
std::map<int, int> next;
std::deque<char> D;
std::vector<char> buffer;

void alltopsorts(int k)
{
    if (D.empty())
        return;
    char base = D.back();

    do
    {
        char q = D.back();
        D.pop_back();

        buffer[k] = q;
        if (k == (n - 1))
        {
            for (std::vector<char>::const_iterator cit = buffer.begin();
                 cit != buffer.end(); ++cit)
                 std::cout << (*cit);
            std::cout << std::endl;
        }

        // erase relations beginning with q:
        int p = top[q];
        while (p >= 0)
        {
            char j = suc[p];
            count[j]--;
            if (!count[j])
                D.push_back(j);
            p = next[p];
        }

        alltopsorts(k + 1);

        // retrieve relations beginning with q:
        p = top[q];
        while (p >= 0)
        {
            char j = suc[p];
            if (!count[j])
                D.pop_back();
            count[j]++;
            p = next[p];
        }

        D.push_front(q);
    }
    while (D.back() != base);
}

int main()
{
    // Prepare
    std::fill_n(std::back_inserter(buffer), n, 0);
    for (int i = 0; i < n; i++) {
        count[input[i]] = 0;
        top[input[i]] = -1;
    }

    for (int i = 0; i < m; i++) {
        suc[i] = crel[i][1]; next[i] = top[crel[i][0]];
        top[crel[i][0]] = i; count[crel[i][1]]++;
    }

    for (std::map<char, int>::const_iterator cit = count.begin();
         cit != count.end(); ++cit)
        if (!(*cit).second)
            D.push_back((*cit).first);

    alltopsorts(0);
}

【讨论】:

    【解决方案2】:

    如果有 N!,没有算法可以比 O(N!) 做得更好!答案。但我认为有更好的方法来理解这个问题:

    你可以这样构建一个有向图:如果A出现在B之前,那么从A到B有一条边。构建图之后,你只需要找到所有可能的拓扑排序结果。仍然是 O(N!),但比您的方法更容易编码和更好(不必生成无效排序)。

    【讨论】:

    • 这是真的。但在这个例子中,最坏情况的复杂性并不是关键所在。如果您有大量数据,那么您可以或多或少有效地修剪无效的解决方案。
    • @Rok 完全同意。我不知道为什么这是一个面试问题,因为没有任何有效的解决方案。
    • 虽然我很欣赏最坏情况的复杂性。有时你必须现实一点。例如,在编码比赛中,您可能必须考虑最坏情况的复杂性。但在现实生活中,您必须意识到效率并不总是等同于良好的最坏情况复杂性。
    • @Mu:我也不知道。也许是因为有很多可行的解决方案,他们会了解你的思维方式和你的创新/创造力能力。
    【解决方案3】:

    我会这样解决:

    1. 看第一个字母:(A -> B -> F)
    2. 查看第二个字母,但只考虑第一个字母相同的那些:(D)(C)(M -> N)
    3. 查看第三个字母,但只考虑具有相同 1. 和 2. 字母的那些:(G -> H), (D -> F)
    4. 以此类推,虽然还剩下一些……(看第 N 个字母,按前面的字母分组)

    括号中的内容是您从集合中获得的所有信息(所有可能的排序)。忽略只有一个字母的括号,因为它们不代表排序。然后将括号中的所有内容进行拓扑排序。

    【讨论】:

    • 拓扑排序只会给出一个排序。
    • @user434345: 是的,尽管如果您打开拓扑排序实现的盖子,您可能会发现您可以轻松识别它做出任意决定的点,并将其更改为处理所有可能性选择一个。
    • 特别是按顺序枚举顶点的拓扑排序算法,而不是基于深度优先搜索的算法。
    【解决方案4】:

    好的,我马上承认我没有估计一般情况下的时间复杂度,但也许以下两个观察结果会有所帮助。

    首先,这是一个明显的约束库候选者。如果您在实践中这样做(例如,这是一项工作中的任务),那么您将获得一个约束求解器,给它您拥有的各种成对排序,然后询问所有结果的列表。

    其次,这通常作为搜索来实现。如果您有 N 个字符,请考虑其根节点有 N 个子节点的树(选择第一个字符);下一个节点有 N-1 个子节点(选择第二个字符);等等。显然这是N!全面探索的最坏情况。

    即使使用“愚蠢”的搜索,您也可以看到,您可以随时根据您拥有的配对检查您的订单来修剪搜索。

    但是由于您知道存在总排序,即使您(可能)只有部分信息,您也可以使搜索更有效率。例如,您知道任何对的第一个字符都不能出现在

    简而言之,您可以通过探索一棵树来枚举可能的解决方案,使用不完整的排序信息来限制每个节点的可能选择。

    希望对一些人有所帮助。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2017-02-17
      • 2014-10-07
      • 2013-05-12
      • 1970-01-01
      • 2019-12-24
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多