我最近被问到以下面试问题:
你有一个用外星语写的字典页面。假使,假设 语言类似于英语,从左到右读/写 对。此外,单词按字典顺序排列。对于 例如,页面可以是:ADG,ADH,BCD,BCF,FM,FN
你必须给出角色的所有词典排序 设置在页面中。
我的方法如下: A优先于B,G优先于H. 因此,我们有关于某些字符的排序信息:
A->B, B->F, G->H, D->F, M->N
可能的订单可以是ABDFGNHMC,ACBDFGNHMC,...... 我的方法是使用数组作为位置持有者并生成所有排列以识别所有有效排序。最糟糕的情况是时间复杂度为N!其中N是字符集的大小。 我们能比蛮力方法做得更好。
提前致谢。
答案 0 :(得分:3)
如果有N,那么没有算法可以比O(N!)更好!答案。但我认为有更好的方法来理解这个问题:
你可以用这种方式建立有向图:如果A出现在B之前,那么从A到B就有一条边。构建图之后,你只需要找到所有可能的拓扑排序结果。仍然是O(N!),但比你的方法更容易编码和更好(不必生成无效的排序)。
答案 1 :(得分:3)
解决这个问题的一种自然方法是让x 1 成为一个 元素没有前辈,然后擦除所有的关系 来自x 1 < j并让x 2 成为元素≠ x 1 ,系统中没有前置任务,因为它现在存在, 然后擦除from x 2 <的所有关系。 j等等 不难确认这种方法总是会成功,除非 输入中有一个导向循环。而且,从某种意义上说,它是 仅方式继续,因为x 1 必须是一个元素 没有前辈,x 2 必须没有前辈 当所有关系x 1 < j被删除等等 观察结果自然会导致找到所有的算法 拓扑排序问题的解决方案;这是一个典型的例子 一个“回溯”程序,在每个阶段我们考虑一个 来自“找到完成给定部分的所有方法”的子问题 置换x 1 x 2 ... x k 到a 拓扑排序x 1 x 2 ... x n 。“ 一般方法是分支所有可能的选择 x k + 1 。
回溯应用中的一个核心问题是 找到一种合适的方式来安排数据,这样很容易 序列通过x k + 1 的可能选择;在这 我们需要一种有效的方法来发现所有元素的集合≠ {x 1 ,...,x k }没有前辈其他 比x 1 ,...,x k ,并保持这种知识 我们从一个子问题转移到另一个子问题时有效。
该论文包括一个有效算法的伪代码。每个输出的时间复杂度为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 :(得分:0)
我会这样解决:
括号中的内容是您从集合中获取的所有信息(所有可能的排序)。忽略只有一个字母的括号,因为它们不代表排序。然后在括号中进行翻译并进行拓扑排序。
答案 3 :(得分:0)
首先,这是约束库的明显候选者。如果你在实践中这样做(比如,这是工作中的一些任务)那么你会得到一个约束求解器,给它你有各种成对顺序,然后要求列出所有结果。
第二,这通常是作为搜索实现的。如果你有N个字符,考虑一个树的根节点有N个孩子(选择第一个字符);下一个节点有N-1个孩子(选择第二个字符);等等,显然这是N!完全探索的最坏情况。
即使进行“愚蠢”搜索,您也可以看到通常可以通过在任何一点检查您的订单来修剪搜索。
但是既然您知道存在总排序,即使您(可能)只有部分信息,您也可以提高搜索效率。例如,你知道第一个字符不得出现在&lt;的右边“。对于任何一对(如果我们假设每个字符都给出一个数值,第一个字符最低)。同样地,向下移动树,以获得适当减少的数据。
简而言之,您可以通过探索树来枚举可能的解决方案,使用不完整的排序信息来约束每个节点的可能选择。
希望有所帮助。