【问题标题】:Rank the suffixes of a list对列表的后缀进行排名
【发布时间】:2014-01-19 17:32:01
【问题描述】:

排序数组/列表中的元素x 只是找出数组/列表中有多少元素严格小于x。

所以对列表进行排名只是获取列表中所有元素的排名。

例如rank [51, 38, 29, 51, 63, 38] = [3, 1, 0, 3, 5, 1],即有3个小于51的元素等

可以在 O(NlogN) 中对列表进行排名。基本上,我们可以在记住每个元素的原始索引的同时对列表进行排序,然后查看每个元素之前有多少个。


这里的问题是如何在O(NlogN)中对列表的后缀进行排名

对列表的后缀进行排名意味着:

对于列表 [3; 1个; 2],排名[[3;1;2]; [1;2]; [2]]

请注意,元素可能没有区别。


编辑

我们不需要打印出所有后缀的所有元素。您可以想象我们只需要打印出一个列表/数组,其中每个元素都是一个后缀的等级。

例如rank suffix_of_[3;1;2] = rank [[3;1;2]; [1;2]; [2]] = [2;0;1] 你只需打印出 [2;0;1]。


编辑 2

让我在这里更清楚地解释什么是所有后缀以及对所有后缀进行排序/排名的含义。

假设我们有一个数组/列表 [e1;e2;e3;e4;e5]。

那么[e1;e2;e3;e4;e5]的所有后缀都是:

[e1;e2;e3;e4;e5]
[e2;e3;e4;e5]
[e3;e4;e5]
[e4;e5]
[e5]

例如 [4;2;3;1;0] 的所有后缀都是

[4;2;3;1;0]
[2;3;1;0]
[3;1;0]
[1;0]
[0]

对 5 个后缀进行排序意味着字典排序。排序所有后缀,你得到

[0]
[1;0]
[2;3;1;0]
[3;1;0]
[4;2;3;1;0]

顺便说一句,如果您无法想象 5 个列表/数组是如何在其中排序的,请考虑按字典顺序对字符串进行排序。

“0”


似乎对所有后缀进行排序实际上是对原始数组的所有元素进行排序。

但是,请注意所有元素可能不是不同的,例如

对于 [4;2;2;1;0],所有后缀都是:

[4;2;2;1;0]
[2;2;1;0]
[2;1;0]
[1;0]
[0]

那么顺序是

[0]
[1;0]
[2;1;0]
[2;2;1;0]
[4;2;2;1;0]

【问题讨论】:

  • 你确定可以吗?
  • 如果您已经对列表进行了排序,您是否也对其后缀进行了排序?
  • 后缀的等级大约有 n^2 个不同的数字。气味不太可能在 O(nlogn) 中做这样的事情。
  • @DmitriChubarov 不,没有人说列表中的元素是不同的
  • @ZiyaoWei 是的,我相信这是可能的。 itu.dk/courses/SGDS/F2012/fastsufsort.pdf。我只是无法完全理解它

标签: algorithm time-complexity


【解决方案1】:

正如 MBo 正确指出的那样,您的问题是构建输入列表的 suffix array。实现这一点的快速而复杂的算法实际上是线性时间,但由于您只针对O(n log n),我将尝试提出一个更容易实现的更简单的版本。

基本思想和初始O(n log² n) 实现

我们以序列[4, 2, 2, 1] 为例。它的后缀是

0: 4 2 2 1
1: 2 2 1
2: 2 1
3: 1

我用原始序列中的起始索引对后缀进行编号。最终,我们希望按字典顺序快速对这组后缀进行排序。我们知道我们可以使用其在恒定空间中的起始索引来表示每个后缀,并且我们可以使用合并排序、堆排序或类似算法对O(n log n) 比较进行排序。那么问题来了,我们怎样才能快速比较两个后缀呢?

假设我们要比较后缀 [2, 2, 1][2, 1]。我们可以填充具有负无穷大值的那些来改变比较结果:[2, 2, 1, -∞][2, 1, -∞, -∞]

现在这里的关键思想是以下分而治之的观察:我们可以将两个列表分成两半并按字典顺序比较两半,而不是逐个字符地比较序列,直到找到两者不同的位置:

     [a, b, c, d]     < [e, f, g, h] 
 <=> ([a, b], [c, d]) < ([e, f], [g, h])
 <=> [a, b] < [e, f] or ([a, b,] = [e, f] and [c, d] < [g, h])

基本上我们已经将比较序列的问题分解为比较较小序列的两个问题。这导致了以下算法:

步骤 1:对长度为 1 的子字符串(连续子序列)进行排序。在我们的示例中,长度为 1 的子字符串是[4], [2], [2], [1]。每个子字符串都可以由原始列表中的起始位置表示。我们通过简单的比较排序对它们进行排序并得到[1], [2], [2], [4]。我们通过将结果分配给它在列表排序列表中的排名来存储结果:

position   substring   rank
0          [4]         2
1          [2]         1
2          [2]         1
3          [1]         0

重要的是我们为相等的子字符串分配相同的等级!

第 2 步: 现在我们要对长度为 2 的子字符串进行排序。实际上只有 3 个这样的子字符串,但如果需要,我们通过使用负无穷大填充来为每个位置分配一个。这里的诀窍是,我们可以使用上面的分而治之的想法和在步骤 1 中分配的排名来进行快速比较(这还不是必需的,但以后会变得很重要)。

position  substring    halves        ranks from step 1   final rank
0         [4,  2]      ([4], [2])    (2,  1)             3               
1         [2,  2]      ([2], [2])    (1,  1)             2
2         [2,  1]      ([2], [2])    (1,  0)             1
3         [1, -∞]      ([1], [-∞])   (0, -∞)             0

第 3 步:您猜对了,现在我们对长度为 4 (!) 的子字符串进行排序。这些正是列表的后缀!这次我们可以使用分治法和第 2 步的结果:

position  substring         halves              ranks from step 2   final rank
0         [4,  2,  2,  1]   ([4, 2], [2,  1])   (3,  1)             3
1         [2,  2,  1, -∞]   ([2, 2], [1, -∞])   (2,  0)             2
2         [2,  1, -∞, -∞]   ([2, 1], [-∞,-∞])   (1, -∞)             1
3         [1, -∞, -∞, -∞]   ([1,-∞], [-∞,-∞])   (0, -∞)             0

我们完成了!如果我们的初始序列大小为2^k,我们将需要k 步骤。或者反过来说,我们需要log_2 n 步骤来处理大小为n 的序列。如果它的长度不是 2 的幂,我们只需填充负无穷大。

对于实际的实现,我们只需要记住算法每一步的“最终排名”序列。

C++ 中的实现可能如下所示(使用 -std=c++11 编译):

#include <algorithm>
#include <iostream>
using namespace std;

int seq[] = {8, 3, 2, 4, 2, 2, 1};
const int n = 7;
const int log2n = 3;       // log2n = ceil(log_2(n))
int Rank[log2n + 1][n];    // Rank[i] will save the final Ranks of step i
tuple<int, int, int> L[n]; // L is a list of tuples. in step i,
                           // this will hold pairs of Ranks from step i - 1
                           // along with the substring index
const int neginf = -1;     // should be smaller than all the numbers in seq
int main() {
  for (int i = 0; i < n; ++i)
    Rank[1][i] = seq[i]; // step 1 is actually simple if you think about it
  for (int step = 2; step <= log2n; ++step) {
    int length = 1 << (step - 1); // length is 2^(step - 1)
    for (int i = 0; i < n; ++i)
      L[i] = make_tuple(
                Rank[step - 1][i],
                (i + length / 2 < n) ? Rank[step - 1][i + length / 2] : neginf,
                i); // we need to know where the tuple came from later
    sort(L, L + n); // lexicographical sort
    for (int i = 0; i < n; ++i) {
      // we save the rank of the index, but we need to be careful to
      // assign equal ranks to equal pairs
      Rank[step][get<2>(L[i])] = (i > 0 && get<0>(L[i]) == get<0>(L[i - 1])
                                        && get<1>(L[i]) == get<1>(L[i - 1]))
                                    ? Rank[step][get<2>(L[i - 1])] 
                                    : i;
    }
  }
  // the suffix array is in L after the last step
  for (int i = 0; i < n; ++i) {
    int start = get<2>(L[i]);
    cout << start << ":";
    for (int j = start; j < n; ++j)
      cout << " " << seq[j];
    cout << endl;
  }
}

输出:

6: 1
5: 2 1
4: 2 2 1
2: 2 4 2 2 1
1: 3 2 4 2 2 1
3: 4 2 2 1
0: 8 3 2 4 2 2 1

复杂度是O(log n * (n + sort)),在这个实现中是O(n log² n),因为我们使用了一种比较复杂度O(n log n)

一个简单的O(n log n) 算法

如果我们设法在O(n) 中按步骤进行排序,我们将得到O(n log n) 绑定。所以基本上我们必须对(x, y) 的对序列进行排序,其中0 &lt;= x, y &lt; n。我们知道我们可以使用counting sortO(n) 时间内对给定范围内的整数序列进行排序。我们可以将我们的对 (x, y) 解释为以 n 为底的数字 z = n * x + y。我们现在可以看到如何使用LSD radix sort 对对进行排序。 在实践中,这意味着我们使用计数排序通过增加y 对对进行排序,然后使用计数排序再次通过增加x 进行排序。由于计数排序是稳定的,这为我们提供了 2 * O(n) = O(n) 中配对的字典顺序。因此最终的复杂度是O(n log n)

如果您有兴趣,可以找到方法at my Github repoO(n log² n) 实现。该实现有 27 行代码。整洁,不是吗?

【讨论】:

  • 很好,这就是我所追求的。 Instead of comparing the sequences character by character until we find a position where the two differ, we can instead split both lists in half and compare the halves lexicographically。其实我可以在Pearls of Functional Algorithm Design一书中找到这个问题的答案,但我就是看不懂。现在通过你的回答,我明白了,那本书中的算法只是描述它的另一种方式。如果你有时间,你能描述一下 O(n) 算法吗?无论如何,我会将您的答案标记为正确。
  • @JacksonTale:嗯,我的意思是对列表进行排序是问题的下限,所以除非对列表的数字有一些特殊限制(字符串术语中的“字母”),否则整数排序是下限:) 假设字母大小不变,我可以尝试解释 Sanders 等人的构造算法,这是最简单的恕我直言。我稍后会尝试写一些东西。
  • 能否使用一轮计数排序对(x,y)进行排序,即排序z = n * x +y?而不是两轮计数排序(LSD基数排序)
  • @JacksonTale 是的,但我们会得到 n^2 运行时,因为 z 可以变得像 n*(n - 1) 一样大
【解决方案2】:

这正是suffix array 构造问题,wiki 页面包含指向线性复杂度算法的链接(可能取决于字母表)

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2015-05-02
    • 1970-01-01
    • 1970-01-01
    • 2021-08-20
    • 2021-07-15
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多