【问题标题】:Longest subarray whose elements form a continuous sequence元素构成连续序列的最长子数组
【发布时间】:2013-04-12 08:03:41
【问题描述】:

给定一个未排序的正整数数组,求排序后元素连续的最长子数组的长度。你能想到一个 O(n) 的解决方案吗?

例子:

{10, 5, 3, 1, 4, 2, 8, 7},答案是 5。

{4, 5, 1, 5, 7, 6, 8, 4, 1},答案是 5。

对于第一个例子,子数组 {5, 3, 1, 4, 2} 排序后可以形成一个最长的连续序列 1,2,3,4,5。

对于第二个示例,子数组 {5, 7, 6, 8, 4} 是结果子数组。

我可以想到一种方法,对于每个子数组,检查 (maximum - minimum + 1) 是否等于该子数组的长度,如果为真,则它是一个连续子数组。取最长的。但它是O(n^2),不能处理重复。

谁能给出更好的方法?

【问题讨论】:

  • 你可以修改数组吗?还有多少可用空间?
  • 数组中整数的取值有什么限制?如果没有,我敢打赌:在复杂性低于O(n*log(n)) 的情况下是不可能做到的
  • 允许对重复进行哪些假设?假设每个整数最多出现一次是否安全?
  • 你能定义“子数组”吗?它必须在原始数组中是连续的吗?
  • @Shedal 我认为它必须是连续的,正如示例中所建议的那样,带有重复项

标签: algorithm


【解决方案1】:

在 O(n) 中解决原始问题的算法没有重复。也许,它可以帮助某人开发处理重复项的 O(n) 解决方案。

输入:[a1, a2, a3, ...]

将原始数组映射为对,其中第一个元素是一个值,第二个是数组的索引。

数组:[[a1, i1], [a2, i2], [a3, i3], ...]

使用一些 O(n) 算法(例如计数排序)对这个对数组进行排序,以进行整数排序按值。 我们得到另一个数组:

数组:[[a3, i3], [a2, i2], [a1, i1], ...]

其中 a3, a2, a1, ... 按排序顺序排列。

通过对的排序数组运行循环

在线性时间内,我们可以检测到连续的数字组 a3、a2、a1。连续组定义为下一个值 = 上一个值 + 1。 在该扫描期间,保持当前组大小 (n)、索引的最小值 (min) 和当前的索引总和 (actualSum)。

在连续组内的每个步骤上,我们可以估计索引的总和,因为它们创建了等差数列,其中第一个元素 min、步骤 1 和到目前为止看到的组大小n。 这个总和估计可以使用算术级数公式在 O(1) 时间内完成:

估计总和 = (a1 + an) * n / 2;

估计总和 = (min + min + (n - 1)) * n / 2;

估计总和 = min * n + n * (n - 1) / 2;

如果在连续组内的某个循环步骤上估计总和等于实际总和,则到目前为止看到的连续组满足条件。将 n 保存为当前最大值结果,或在当前最大值和 n 之间选择最大值。

如果在值元素上我们不再看到连续组,则重置所有值并执行相同操作。

代码示例:https://gist.github.com/mishadoff/5371821

【讨论】:

  • 你不需要总和,你可以使用max - min + 1 == n instead。您的代码使用O(n*log(n)) 排序。如果整数没有上限,则不清楚如何在O(n) 中对整数进行排序,例如,如果n**sqrt(n) 整数中有sqrt(n),则输入大小仍为O(n),但计数排序为O(n + maxdiff) = O(n + n**sqrt(n)) = O(n**sqrt(n)) 或基数排序是O(n*ndigits) = O(n * sqrt(n))。我在这里使用计算模型,假设n 可以存储在O(1) 机器字中。
  • 鉴于我们只对“连续序列”感兴趣,可以将输入拆分为O(n) 中的bin,这样min(bin[j]) - max(bin[i]) > nmax(bin[i]) - min(bin[i]) 就是O(n)。并在单个箱内搜索“连续序列”。这可能会导致O(n) 算法输入不重复。
  • 这不适用于 100,80,17,12,10,15,14,16,19,30,13,70 。该算法将对数组进行排序并从 12 开始,一直持续到 19,而不是从给出答案的 14 开始。这个例子的正确答案是 14,15,16
  • @mishadoff 这不适用于[4,100,3,2,1000,1] 这样的数组。您的算法将检查 [1->5,2->3,3->2,4->0] 并确定这是不可能的,并给出 1 作为最终答案。但是[2->3,3->2] 是有效的,答案应该是2
【解决方案2】:

在它的数学集合定义中查看数组 S

S = Uj=0k (Ij)

Ij 是不相交的整数段。您可以设计一个特定的区间树(基于您喜欢的红黑树或自平衡树:))以将数组存储在此数学定义中。节点和树结构应如下所示:

struct node {
    int d, u;
    int count;
    struct node *n_left, *n_right;
}

这里,d 是整数段的下界,u 是上界。添加count 是为了处理数组中可能出现的重复:当尝试在树中插入一个已经存在的元素时,我们不会什么都不做,而是增加它所在节点的count 值。

struct root {
    struct node *root;
}        

树只会存储 不相交 节点,因此,插入比经典的红黑树插入要复杂一些。插入间隔时,您必须扫描现有间隔的潜在溢出。在您的情况下,由于您只会插入单例,因此不应增加太多开销。

给定三个节点 P、L 和 R,L 是 P 的左孩子,R 是 P 的右孩子。然后,您必须强制 L.u

在插入整数段[x,y]时,必须找到“重叠”的段,即满足下列不等式之一的区间[u,d]:

y >= d - 1

x

如果插入的区间是单例x,则最多只能找到2个重叠区间节点N1和N2,即N1.d == x + 1N2.u == x - 1。然后你必须合并这两个间隔并更新计数,这样你就剩下 N3 了,这样N3.d = N2.dN3.u = N1.uN3.count = N1.count + N2.count + 1。由于N1.dN2.u 之间的增量是两个分段不相交的最小增量,因此您必须具有以下条件之一:

  • N1 是 N2 的右孩子
  • N2 是 N1 的左孩子

所以在最坏的情况下插入仍然会在O(log(n))中。

从这里开始,我无法弄清楚如何处理初始序列中的顺序,但这里的结果可能很有趣:如果输入数组定义了一个 完美 整数段,那么树只有一个节点。

【讨论】:

    【解决方案3】:

    UPD2: 以下解决方案是针对不需要子数组连续的问题。我误解了问题陈述。不要删除它,因为有人可能会根据我的想法提出一个可以解决实际问题的想法。


    这是我想出的:

    创建一个字典的实例(它被实现为哈希表,在正常情况下给出 O(1))。键是整数,值是整数的哈希集(也是 O(1))——var D = new Dictionary<int, HashSet<int>>

    遍历数组A,并为每个整数n索引i做:

    1. 检查n-1n+1是否包含在D中。
      • 如果两个键都不存在,则执行D.Add(n, new HashSet<int>)
      • 如果只存在一个键,例如n-1,做D.Add(n, D[n-1])
      • 如果两个键都存在,请执行D[n-1].UnionWith(D[n+1]); D[n+1] = D[n] = D[n-1];
    2. D[n].Add(n)

    现在遍历D 中的每个键,找到长度最大的哈希集(找到长度为O(1))。最大的长度将是答案。

    据我了解,最坏情况的复杂度将是 O(n*log(n)),这仅仅是因为 UnionWith 操作。我不知道如何计算平均复杂度,但它应该接近 O(n)。如果我错了,请纠正我。

    UPD:说代码,这是一个 C# 中的测试实现,它在 OP 的两个示例中都给出了正确的结果:

    var A = new int[] {4, 5, 1, 5, 7, 6, 8, 4, 1};
    var D = new Dictionary<int, HashSet<int>>();
    
    foreach(int n in A)
    {
        if(D.ContainsKey(n-1) && D.ContainsKey(n+1))
        {
            D[n-1].UnionWith(D[n+1]);
            D[n+1] = D[n] = D[n-1];
        }
        else if(D.ContainsKey(n-1))
        {
            D[n] = D[n-1];
        }
        else if(D.ContainsKey(n+1))
        {
            D[n] = D[n+1];
        }
        else if(!D.ContainsKey(n))
        {
            D.Add(n, new HashSet<int>());
        }
    
        D[n].Add(n);
    }
    
    int result = int.MinValue;
    foreach(HashSet<int> H in D.Values)
    {
        if(H.Count > result)
        {
            result = H.Count;
        }
    }
    
    Console.WriteLine(result);
    

    【讨论】:

    • 数组[10, 5, 3, 1, 4, 2, 8, 7, 0]的解应该是5,最大子数组是[5, 3, 1, 4, 2]。最后附加的0 并没有改变这一点!但是,您的代码返回结果 6,因为它假定元素 0 是(非连续!)最大子数组的一部分。
    • @blubb OP 从未声明子数组必须是连续的。只有排序后的元素必须是连续的。但现在我看到问题陈述是模棱两可的。 OP 可能具有隐含的连续性。
    • 问题中暗示:如果允许不连续,[4, 5, 1, 5, 7, 6, 8, 4, 1] 的解决方案将是 7 而不是 5。此外,可能的子集数量将是 2^n 而不是 n^2
    • 即使子数组在原始数组中必须是连续的,我的解决方案也可以稍作修改以尊重这一点。复杂性不会改变。真正的问题是:我的解决方案有多复杂?
    • @blubb 我认为不应该计算重复项。这可能是一个错误的假设。
    【解决方案4】:

    这将需要对数据进行两次传递。首先创建一个哈希映射,将整数映射到布尔值。我更新了我的算法以不使用来自 STL 的地图,我很肯定它在内部使用排序。该算法使用散列,可以轻松更新任何最大或最小组合,甚至可能是整数可以获得的所有可能值。

    #include <iostream>
    
    using namespace std;
    const int MINIMUM = 0;
    const int MAXIMUM = 100;
    const unsigned int ARRAY_SIZE = MAXIMUM - MINIMUM;
    
    int main() {
    
    bool* hashOfIntegers = new bool[ARRAY_SIZE];
    //const int someArrayOfIntegers[] = {10, 9, 8, 6, 5, 3, 1, 4, 2, 8, 7};
    //const int someArrayOfIntegers[] = {10, 6, 5, 3, 1, 4, 2, 8, 7};
    const int someArrayOfIntegers[] = {-2, -3, 8, 6, 12, 14,  4, 0, 16, 18, 20};
    const int SIZE_OF_ARRAY = 11;
    
    //Initialize hashOfIntegers values to false, probably unnecessary but good practice.
    for(unsigned int i = 0; i < ARRAY_SIZE; i++) {
        hashOfIntegers[i] = false;
    }
    
    //Chage appropriate values to true.
    for(int i = 0; i < SIZE_OF_ARRAY; i++) {
        //We subtract the MINIMUM value to normalize the MINIMUM value to a zero index for negative numbers.
        hashOfIntegers[someArrayOfIntegers[i] - MINIMUM] = true;
    }
    
    int sequence = 0;
    int maxSequence = 0;
    //Find the maximum sequence in the values
    for(unsigned int i = 0; i < ARRAY_SIZE; i++) {
    
        if(hashOfIntegers[i]) sequence++;
        else sequence = 0;
    
        if(sequence > maxSequence) maxSequence = sequence;
    }
    
    cout << "MAX SEQUENCE: " << maxSequence << endl;
    return 0;
    }
    

    基本思想是将哈希映射用作桶排序,这样您只需对数据进行两次传递。这个算法是O(2n),又是O(n)

    【讨论】:

      【解决方案5】:

      别抱太大希望,这只是部分答案。

      我非常有信心在O(n) 中无法解决该问题。不幸的是,我无法证明这一点。

      如果有办法在小于O(n^2) 的时间内解决它,我怀疑该解决方案基于以下策略:

      1. O(n)(或者可能是O(n log n))中确定是否存在存在一个连续子数组,正如您所描述的那样,它至少包含i 元素。让我们将此谓词称为E(i)
      2. 使用二分法求出i 的最大值,而E(i) 的值最大。

      那么这个算法的总运行时间将是O(n log n)(或O(n log^2 n))。

      这是我能想出的将问题简化为另一个问题的唯一方法,该问题至少有可能比原始公式更简单。但是,我找不到比O(n^2) 计算E(i) 的方法,所以我可能完全没用了...

      【讨论】:

        【解决方案6】:

        这是思考问题的另一种方式:假设您有一个仅由 1 和 0 组成的数组,您想找到最长连续运行的 1。这可以通过对 1 进行游程编码(忽略 0)在线性时间内完成。为了将您的原始问题转换为这个新的运行长度编码问题,您需要计算一个新数组 b[i] = (a[i]

        【讨论】:

          【解决方案7】:

          这里有 3 种可接受的解决方案:

          第一个是时间上的O(nlog(n))和空间上的O(n),第二个是时间上的O(n)和空间上的O(n),第三个是时间上的O(n)和空间上的O(1)

          1. 构建一个binary search tree,然后遍历它in order
            保留 2 个指针,一个用于最大子集的开始,一个用于结束。 在迭代树时保持max_size 值。 这是一个O(n*log(n)) 时间和空间复杂度。

          2. 您始终可以在线性时间内对使用counting sort 设置的数字进行排序 并遍历数组,表示O(n)时空 复杂性。

          3. 假设没有溢出或大整数数据类型。假设数组是一个数学集(没有重复值)。你可以在内存的O(1) 里做:

            • 计算数组的和与数组的乘积
            • 假设您拥有原始集合的最小值和最大值,请找出其中包含的数字。完全是O(n) 时间复杂度。

          【讨论】:

          • 能否详细说明如何构造二叉树?我假设您的意思是一棵平衡树,但是您能否为 OP 给出的示例展示树的外观?
          • 我还想出了一个红黑自定义间隔树,但无法弄清楚如何以良好的方式保持序列顺序。这是关键。构建二叉树对数组进行排序并不难。识别在初始数组中连续的整数段要复杂得多!你能坚持那一步吗?
          • @Rerito,如果您了解如何在有序数组中找到最大子集,那是一回事。
          • @0x90 不,不是。你必须找到一个连续且未排序的子数组,它略有不同。
          • @Rerito 一旦你在二叉树中获得了值并遍历它,它就相当于一个排序数组。
          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 2018-05-22
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多