📜  最长递增子序列大小(N log N)

📅  最后修改于: 2021-04-24 22:25:38             🧑  作者: Mango

给定一个随机数数组。在数组中找到最长的递增子序列(LIS)。我知道你们中的许多人可能已经阅读了递归和动态编程(DP)解决方案。在论坛帖子中很少有O(N log N)算法的请求。

暂时,无需考虑递归和DP解决方案。让我们抽取少量样本并将解决方案扩展到大型实例。尽管乍一看可能很复杂,但是一旦我们理解了逻辑,编码就很简单。

考虑输入数组A = {2,5,3}。我将在解释过程中扩展数组。

通过观察,我们知道LIS是{2,3}或{2,5}。请注意,我只考虑严格增加顺序

让我们再添加两个元素,例如7、11。这些元素将扩展现有序列。现在,输入数组{2,5,3,7,11}的递增序列为{2,3,7,11}和{2,5,7,11}。

此外,我们在数组中添加了另一个元素,例如8,即输入数组变为{2,5,5,3,7,11,8}。请注意,最新元素8大于任何活动序列的最小元素(将在短期内讨论活动序列)。如何将现有序列扩展为8?首先,8可以成为LIS的一部分吗?如果是,怎么办?如果我们要添加8,它应该在7之后(通过替换11)。

由于该方法是脱机的(我们所说的是脱机的?) ,因此我们不确定是否添加8将扩展该系列。假设输入数组中有9个,例如{2,5,3,7,11,11,8,7,9…}。我们可以将8替换为11,因为可能存在最佳候选者(9),可以扩展新系列{2,3,7,8}或{2,5,7,8}。

我们的观察是,假设最大序列的末尾元素为E。如果存在元素A [j](j> i),使得E A [i]

对于原始数组{2,5,3},请注意,当我们将3添加到递增序列{2,5}时,我们会遇到相同的情况。我只是创建了两个递增的序列,以使说明变得简单。 3可以代替序列{2,5}中的5,而不是两个序列。

我知道这会令人困惑,我会尽快清除它!

问题是,什么时候可以安全地添加或替换现有序列中的元素?

让我们考虑另一个样本A = {2,5,3}。假设下一个元素是1。如何扩展当前序列{2,3}或{2,5}。显然,它也不能扩展。但是,新的最小元素有可能成为LIS的开始。为了清楚起见,请考虑数组为{2,5,3,1,2,3,4,5,6}。将1设为新序列将创建最大的新序列。

观察结果是,当我们在数组中遇到新的最小元素时,它可能是开始新序列的潜在候选者。

从观察中,我们需要维护递增序列的列表。

通常,我们有一组长度可变的活动列表。我们将元素A [i]添加到这些列表中。我们以长度减少的顺序扫描列表(用于结束元素)。我们将核实所有列表的末尾元素查找其结束元素比[I](本底值)小的列表。

我们的策略由以下条件决定,

1. If A[i] is smallest among all end 
   candidates of active lists, we will start 
   new active list of length 1.
2. If A[i] is largest among all end candidates of 
  active lists, we will clone the largest active 
  list, and extend it by A[i].
3. If A[i] is in between, we will find a list with 
  largest end element that is smaller than A[i]. 
  Clone and extend this list by A[i]. We will discard all
  other lists of same length as that of this modified list.

请注意,在我们构造活动列表的任何时候,都会保持以下条件。

“较小列表的结尾元素小于较大列表的结尾元素”

通过一个示例将很清楚,让我们以Wiki {0,8,4,12,12,2,10,6,14,1,9,5,13,3,11,7,15}为例。

A[0] = 0. Case 1. There are no active lists, create one.
0.
-----------------------------------------------------------------------------
A[1] = 8. Case 2. Clone and extend.
0.
0, 8.
-----------------------------------------------------------------------------
A[2] = 4. Case 3. Clone, extend and discard.
0.
0, 4.
0, 8. Discarded
-----------------------------------------------------------------------------
A[3] = 12. Case 2. Clone and extend.
0.
0, 4.
0, 4, 12.
-----------------------------------------------------------------------------
A[4] = 2. Case 3. Clone, extend and discard.
0.
0, 2.
0, 4. Discarded.
0, 4, 12.
-----------------------------------------------------------------------------
A[5] = 10. Case 3. Clone, extend and discard.
0.
0, 2.
0, 2, 10.
0, 4, 12. Discarded.
-----------------------------------------------------------------------------
A[6] = 6. Case 3. Clone, extend and discard.
0.
0, 2.
0, 2, 6.
0, 2, 10. Discarded.
-----------------------------------------------------------------------------
A[7] = 14. Case 2. Clone and extend.
0.
0, 2.
0, 2, 6.
0, 2, 6, 14.
-----------------------------------------------------------------------------
A[8] = 1. Case 3. Clone, extend and discard.
0.
0, 1.
0, 2. Discarded.
0, 2, 6.
0, 2, 6, 14.
-----------------------------------------------------------------------------
A[9] = 9. Case 3. Clone, extend and discard.
0.
0, 1.
0, 2, 6.
0, 2, 6, 9.
0, 2, 6, 14. Discarded.
-----------------------------------------------------------------------------
A[10] = 5. Case 3. Clone, extend and discard.
0.
0, 1.
0, 1, 5.
0, 2, 6. Discarded.
0, 2, 6, 9.
-----------------------------------------------------------------------------
A[11] = 13. Case 2. Clone and extend.
0.
0, 1.
0, 1, 5.
0, 2, 6, 9.
0, 2, 6, 9, 13.
-----------------------------------------------------------------------------
A[12] = 3. Case 3. Clone, extend and discard.
0.
0, 1.
0, 1, 3.
0, 1, 5. Discarded.
0, 2, 6, 9.
0, 2, 6, 9, 13.
-----------------------------------------------------------------------------
A[13] = 11. Case 3. Clone, extend and discard.
0.
0, 1.
0, 1, 3.
0, 2, 6, 9.
0, 2, 6, 9, 11.
0, 2, 6, 9, 13. Discarded.
-----------------------------------------------------------------------------
A[14] = 7. Case 3. Clone, extend and discard.
0.
0, 1.
0, 1, 3.
0, 1, 3, 7.
0, 2, 6, 9. Discarded.
0, 2, 6, 9, 11.
----------------------------------------------------------------------------
A[15] = 15. Case 2. Clone and extend.
0.
0, 1.
0, 1, 3.
0, 1, 3, 7.
0, 2, 6, 9, 11.
0, 2, 6, 9, 11, 15. <-- LIS List
----------------------------------------------------------------------------

设计算法需要了解以上策略。另外,确保我们保持以下条件:“较小列表的结束元素小于较大列表的结束元素”。在进一步阅读之前,请尝试其他一些示例。重要的是要了解结束元素发生了什么。

算法:

查询最长的长度是很容易的。请注意,我们仅处理末端元素。我们不需要维护所有列表。我们可以将结束元素存储在数组中。丢弃操作可以通过替换进行模拟,并且扩展列表类似于向数组中添加更多元素。

我们将使用一个辅助数组来保留结束元素。该数组的最大长度是输入的长度。在最坏的情况下,数组会分成N个大小为1的列表(请注意,这不会导致最坏的情况下的复杂性)。要丢弃一个元素,我们将在辅助数组中跟踪A [i]的ceil值(再次观察粗略工作中的末端元素),并用A [i]替换ceil值。我们通过将元素添加到辅助数组来扩展列表。我们还维护一个计数器来跟踪辅助数组的长度。

奖励:您已经部分学习了耐心排序技术🙂

这是一个谚语,“告诉我,我会忘记的。给我看,我会记得。让我参与进来,我会明白的。”因此,从一副纸牌中挑选一套西装。从经过改组的套装中找出牌中最长的递增子序列。您将永远不会忘记这种方法。 🙂

更新– 2016年7月17日:读者的回响颇为深刻,很少有网站引用此帖子,作为我为他人提供的辛勤工作感到很高兴。看来读者在发表评论之前没有做任何功课。阅读本文后,要求阅读一些示例,并请在纸上做您的工作(不要使用编辑器/编译器)。要求是帮助自己。 “知道”的专业不同于真正的理解(不尊重)。以下是我的个人经历。

最初的内容准备工作大约花了我6个小时。但是,这是一个很好的教训。我在一个小时内完成了初始代码。当我开始写内容向读者解释时,我意识到我不理解这些案例。拿了我的笔记本(我习惯于装订绑定的笔记本以跟踪我的粗略工作),几个小时后,我填写了将近15页的粗略工作。无论您在灰色示例中看到的内容是来自这些页面的。由“ Udi Manber的算法简介”一书中的注释触发的解决方案的所有思考过程,我强烈建议您实践该书。

我怀疑,许多读者可能无法理解CeilIndex(二进制搜索)背后的逻辑。我把它作为练习让读者理解它是如何工作的。在纸上浏览几个示例。我意识到我已经在另一篇文章中介绍了该算法。

更新– 2016年8月5日

完成工作后,以下链接值得参考。我是通过最近创建的Disqus个人资料了解链接的。该链接对Wiki中提到的方法进行了解释。

http://stackoverflow.com/questions/2631726/how-to-determine-the-longest-increasing-subsequence-using-dynamic-programming

下面给出的是查找LIS长度的代码(已更新为C++ 11代码,没有C样式的数组),

C++
#include 
#include 
  
// Binary search (note boundaries in the caller)
int CeilIndex(std::vector& v, int l, int r, int key)
{
    while (r - l > 1) {
        int m = l + (r - l) / 2;
        if (v[m] >= key)
            r = m;
        else
            l = m;
    }
  
    return r;
}
  
int LongestIncreasingSubsequenceLength(std::vector& v)
{
    if (v.size() == 0)
        return 0;
  
    std::vector tail(v.size(), 0);
    int length = 1; // always points empty slot in tail
  
    tail[0] = v[0];
    for (size_t i = 1; i < v.size(); i++) {
  
        // new smallest value
        if (v[i] < tail[0])
            tail[0] = v[i];
  
        // v[i] extends largest subsequence
        else if (v[i] > tail[length - 1])
            tail[length++] = v[i];
  
        // v[i] will become end candidate of an existing
        // subsequence or Throw away larger elements in all
        // LIS, to make room for upcoming grater elements
        // than v[i] (and also, v[i] would have already
        // appeared in one of LIS, identify the location
        // and replace it)
        else
            tail[CeilIndex(tail, -1, length - 1, v[i])] = v[i];
    }
  
    return length;
}
  
int main()
{
    std::vector v{ 2, 5, 3, 7, 11, 8, 10, 13, 6 };
    std::cout << "Length of Longest Increasing Subsequence is "
              << LongestIncreasingSubsequenceLength(v) << '\n';
    return 0;
}


Java
// Java program to find length of longest increasing subsequence
// in O(n Log n) time
import java.io.*;
import java.util.*;
import java.lang.Math;
  
class LIS {
    // Binary search (note boundaries in the caller)
    // A[] is ceilIndex in the caller
    static int CeilIndex(int A[], int l, int r, int key)
    {
        while (r - l > 1) {
            int m = l + (r - l) / 2;
            if (A[m] >= key)
                r = m;
            else
                l = m;
        }
  
        return r;
    }
  
    static int LongestIncreasingSubsequenceLength(int A[], int size)
    {
        // Add boundary case, when array size is one
  
        int[] tailTable = new int[size];
        int len; // always points empty slot
  
        tailTable[0] = A[0];
        len = 1;
        for (int i = 1; i < size; i++) {
            if (A[i] < tailTable[0])
                // new smallest value
                tailTable[0] = A[i];
  
            else if (A[i] > tailTable[len - 1])
                // A[i] wants to extend largest subsequence
                tailTable[len++] = A[i];
  
            else
                // A[i] wants to be current end candidate of an existing
                // subsequence. It will replace ceil value in tailTable
                tailTable[CeilIndex(tailTable, -1, len - 1, A[i])] = A[i];
        }
  
        return len;
    }
  
    // Driver program to test above function
    public static void main(String[] args)
    {
        int A[] = { 2, 5, 3, 7, 11, 8, 10, 13, 6 };
        int n = A.length;
        System.out.println("Length of Longest Increasing Subsequence is " + LongestIncreasingSubsequenceLength(A, n));
    }
}
/* This code is contributed by Devesh Agrawal*/


Python3
# Python program to find
# length of longest
# increasing subsequence
# in O(n Log n) time
  
# Binary search (note
# boundaries in the caller)
# A[] is ceilIndex
# in the caller
def CeilIndex(A, l, r, key):
  
    while (r - l > 1):
      
        m = l + (r - l)//2
        if (A[m] >= key):
            r = m
        else:
            l = m
    return r
   
def LongestIncreasingSubsequenceLength(A, size):
  
    # Add boundary case,
    # when array size is one
   
    tailTable = [0 for i in range(size + 1)]
    len = 0 # always points empty slot
   
    tailTable[0] = A[0]
    len = 1
    for i in range(1, size):
      
        if (A[i] < tailTable[0]):
  
            # new smallest value
            tailTable[0] = A[i]
   
        elif (A[i] > tailTable[len-1]):
  
            # A[i] wants to extend
            # largest subsequence
            tailTable[len] = A[i]
            len+= 1
   
        else:
            # A[i] wants to be current
            # end candidate of an existing
            # subsequence. It will replace
            # ceil value in tailTable
            tailTable[CeilIndex(tailTable, -1, len-1, A[i])] = A[i]
          
   
    return len
  
   
# Driver program to
# test above function
  
A = [ 2, 5, 3, 7, 11, 8, 10, 13, 6 ]
n = len(A)
  
print("Length of Longest Increasing Subsequence is ",
       LongestIncreasingSubsequenceLength(A, n))
  
# This code is contributed
# by Anant Agarwal.


C#
// C# program to find length of longest
// increasing subsequence in O(n Log n)
// time
using System;
  
class GFG {
  
    // Binary search (note boundaries
    // in the caller) A[] is ceilIndex
    // in the caller
    static int CeilIndex(int[] A, int l,
                         int r, int key)
    {
        while (r - l > 1) {
            int m = l + (r - l) / 2;
  
            if (A[m] >= key)
                r = m;
            else
                l = m;
        }
  
        return r;
    }
  
    static int LongestIncreasingSubsequenceLength(
        int[] A, int size)
    {
  
        // Add boundary case, when array size
        // is one
  
        int[] tailTable = new int[size];
        int len; // always points empty slot
  
        tailTable[0] = A[0];
        len = 1;
        for (int i = 1; i < size; i++) {
            if (A[i] < tailTable[0])
                // new smallest value
                tailTable[0] = A[i];
  
            else if (A[i] > tailTable[len - 1])
  
                // A[i] wants to extend largest
                // subsequence
                tailTable[len++] = A[i];
  
            else
  
                // A[i] wants to be current end
                // candidate of an existing
                // subsequence. It will replace
                // ceil value in tailTable
                tailTable[CeilIndex(tailTable, -1,
                                    len - 1, A[i])]
                    = A[i];
        }
  
        return len;
    }
  
    // Driver program to test above function
    public static void Main()
    {
        int[] A = { 2, 5, 3, 7, 11, 8, 10, 13, 6 };
        int n = A.Length;
        Console.Write("Length of Longest "
                      + "Increasing Subsequence is " + LongestIncreasingSubsequenceLength(A, n));
    }
}
  
// This code is contributed by nitin mittal.


PHP
 1)
    {
        $m = (int)($l + ($r - $l)/2);
        if ($A[$m] >= $key)
            $r = $m;
        else
            $l = $m;
    }
    return $r;
}
  
function LongestIncreasingSubsequenceLength($A, $size)
{
    // Add boundary case,
    // when array size is one
  
    $tailTable = array_fill(0, ($size + 1), 0);
    $len = 0; // always points empty slot
  
    $tailTable[0] = $A[0];
    $len = 1;
    for($i = 1; $i < $size; $i++)
    {
      
        if ($A[$i] < $tailTable[0])
            // new smallest value
            $tailTable[0] = $A[$i];
  
        else if ($A[$i] > $tailTable[$len-1])
        {
            // A[i] wants to extend
            // largest subsequence
            $tailTable[$len] = $A[$i];
            $len++;
        }
        else
            // A[i] wants to be current
            // end candidate of an existing
            // subsequence. It will replace
            // ceil value in tailTable
            $tailTable[CeilIndex($tailTable, -1, $len-1, $A[$i])] = $A[$i];
          
    }
    return $len;
}
  
// Driver program to
// test above function
$A = array( 2, 5, 3, 7, 11, 8, 10, 13, 6 );
$n = count($A);
  
print("Length of Longest Increasing Subsequence is ".
        LongestIncreasingSubsequenceLength($A, $n));
  
// This code is contributed by chandan_jnu
?>


输出:

Length of Longest Increasing Subsequence is 6

复杂:

循环运行N个元素。在最坏的情况下(什么是最坏情况的输入?),我们可能最终会使用二进制搜索(log i )查询许多A [i]的ceil值。

因此,T(n)

练习:

1.设计一种算法以构造最长的递增列表。另外,使用DAG对解决方案进行建模。

2.设计一种算法,以构造所有等长最长的递增列表。

3.以上算法是在线算法吗?

4.设计一种算法来构造最长的递减列表。


在C++中使用lower_bound()的替代实现:

#include
using namespace std; 
  
int LongestIncreasingSubsequenceLength(std::vector& v) 
{ 
    if (v.size() == 0) 
        return 0; 
  
    std::vector tail(v.size(), 0); 
    int length = 1; // always points empty slot in tail 
  
    tail[0] = v[0]; 
      
    for (int i = 1; i < v.size(); i++) { 
  
            // Do binary search for the element in 
            // the range from begin to begin + length
        auto b = tail.begin(), e = tail.begin() + length;
        auto it = lower_bound(b, e, v[i]); 
              
        // If not present change the tail element to v[i] 
        if (it == tail.begin() + length)
        tail[length++] = v[i]; 
        else   
        *it = v[i]; 
    } 
  
    return length; 
} 
  
int main() 
{ 
    std::vector v{ 2, 5, 3, 7, 11, 8, 10, 13, 6 }; 
    std::cout << "Length of Longest Increasing Subsequence is "
            << LongestIncreasingSubsequenceLength(v); 
    return 0; 
} 

输出:

Length of Longest Increasing Subsequence is 6