增长最长的子序列(O(nlogn))

时间:2011-05-25 19:20:50

标签: algorithm lis

LIS:wikipedia

有一件事我无法理解:

为什么X [M [i]]是非递减序列?

8 个答案:

答案 0 :(得分:73)

让我们先来看看n ^ 2算法:

dp[0] = 1;
for( int i = 1; i < len; i++ ) {
   dp[i] = 1;
   for( int j = 0; j < i; j++ ) {
      if( array[i] > array[j] ) {
         if( dp[i] < dp[j]+1 ) {
            dp[i] = dp[j]+1;
         }
      }
   }
}

现在改进发生在第二个循环,基本上,你可以通过使用二进制搜索来提高速度。除了数组dp []之外,让我们有另一个数组c [],c非常特殊,c [i]表示:长度为i的最长增长序列的最后一个元素的最小值。

sz = 1;
c[1] = array[0]; /*at this point, the minimum value of the last element of the size 1 increasing sequence must be array[0]*/
dp[0] = 1;
for( int i = 1; i < len; i++ ) {
   if( array[i] < c[1] ) {
      c[1] = array[i]; /*you have to update the minimum value right now*/
      dp[i] = 1;
   }
   else if( array[i] > c[sz] ) {
      c[sz+1] = array[i];
      dp[i] = sz+1;
      sz++;
   }
   else {
      int k = binary_search( c, sz, array[i] ); /*you want to find k so that c[k-1]<array[i]<c[k]*/
      c[k] = array[i];
      dp[i] = k;
   }
}

答案 1 :(得分:23)

这是来自The Hitchhiker’s Guide to the Programming Contests的O(n * lg(n))解决方案(注意:此实现假设列表中没有重复项):

set<int> st;
set<int>::iterator it;
st.clear();
for(i=0; i<n; i++) {
  st.insert(array[i]);
  it=st.find(array[i]);
  it++;
  if(it!=st.end()) st.erase(it);
}
cout<<st.size()<<endl;

要考虑重复项,可以检查,例如,数字是否已在集合中。如果是,则忽略该号码,否则使用与以前相同的方法进行。或者,可以颠倒操作的顺序:首先删除,然后插入。下面的代码实现了这种行为:

set<int> st;
set<int>::iterator it;
st.clear();
for(int i=0; i<n; i++) {
    it = st.lower_bound(a[i]);
    if (it != st.end()) st.erase(it);
    st.insert(a[i]);
}
cout<<st.size()<<endl;

第二种算法可以通过维护包含原始数组中LIS的前一个元素位置的父数组来扩展,以找到最长的增加子序列(LIS)本身。

typedef pair<int, int> IndexValue;

struct IndexValueCompare{
    inline bool operator() (const IndexValue &one, const IndexValue &another){
        return one.second < another.second;
    }
};

vector<int> LIS(const vector<int> &sequence){
    vector<int> parent(sequence.size());
    set<IndexValue, IndexValueCompare> s;
    for(int i = 0; i < sequence.size(); ++i){
        IndexValue iv(i, sequence[i]);
        if(i == 0){
            s.insert(iv);
            continue;
        }
        auto index = s.lower_bound(iv);
        if(index != s.end()){
            if(sequence[i] < sequence[index->first]){
                if(index != s.begin()) {
                    parent[i] = (--index)->first;
                    index++;
                }
                s.erase(index);
            }
        } else{
            parent[i] = s.rbegin()->first;
        }
        s.insert(iv);
    }
    vector<int> result(s.size());
    int index = s.rbegin()->first;
    for(auto iter = s.rbegin(); iter != s.rend(); index = parent[index], ++iter){
        result[distance(iter, s.rend()) - 1] = sequence[index];
    }
    return result;
}

答案 2 :(得分:9)

我们需要维护增加序列的列表。

通常,我们有一组不同长度的活动列表。我们在这些列表中添加了元素A [i]。我们按照长度的降序扫描列表(对于最终元素)。我们将验证所有列表的结束元素,以查找结尾元素小于A [i](底值)的列表。

我们的策略由以下条件决定,
1.如果A [i]在活动列表的所有最终候选者中最小,我们将启动长度为1的新活动列表 2.如果A [i]在活动列表的所有最终候选者中最大,我们将克隆最大的活动列表,并通过A [i]扩展它。
3.如果A [i]介于两者之间,我们将找到一个最大结束元素小于A [i]的列表。通过A [i]克隆并扩展此列表。我们将丢弃与此修改列表长度相同的所有其他列表。

请注意,在构建活动列表的任何实例中,都会保持以下条件。

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

通过一个例子很清楚,让我们从wiki中举例:
{0,8,4,12,2,10,6,14,1,9,5,13,​​3,11,7,15}。

A [0] = 0.案例1.没有活动列表,创建一个 0
----------------------------------------------- ------------------------------
A [1] = 8.案例2.克隆和扩展。
0
0,8。
----------------------------------------------- ------------------------------
A [2] = 4.案例3.克隆,延长和丢弃 0
0,4。
0,8。丢弃的 的 ----------------------------------------------- ------------------------------
A [3] = 12.案例2.克隆并延长。
0
0,4。
0,4,12。
----------------------------------------------- ------------------------------
A [4] = 2.案例3.克隆,延长和丢弃。
0
0,2。
0,4。丢弃。
0,4,12。
----------------------------------------------- ------------------------------
A [5] = 10.案例3.克隆,延长和丢弃。
0
0,2。
0,2,10。
0,4,12。丢弃。
----------------------------------------------- ------------------------------
A [6] = 6.案例3.克隆,延长和丢弃。
0
0,2。
0,2,6。
0,2,10。丢弃。
----------------------------------------------- ------------------------------
A [7] = 14.案例2.克隆并延长 0
0,2。
0,2,6。
0,2,6,1。
----------------------------------------------- ------------------------------
A [8] = 1.案例3.克隆,延长和丢弃。
0
0,1。
0,2。丢弃。
0,2,6。
0,2,6,1。
----------------------------------------------- ------------------------------
A [9] = 9.案例3.克隆,延长和丢弃 0
0,1。
0,2,6。
0,2,6,9。
0,2,6,14。丢弃。
----------------------------------------------- ------------------------------
A [10] = 5.案例3.克隆,延长和丢弃 0
0,1。
0,1,5。
0,2,6。丢弃。
0,2,6,9。
----------------------------------------------- ------------------------------
A [11] = 13.案例2.克隆和延伸。
0
0,1。
0,1,5。
0,2,6,9。
0,2,6,9,1。
----------------------------------------------- ------------------------------
A [12] = 3.案例3.克隆,延长和丢弃 0
0,1。
0,1,3。
0,1,5。丢弃。
0,2,6,9。
0,2,6,9,1。
----------------------------------------------- ------------------------------
A [13] = 11.案例3.克隆,延长和丢弃 0
0,1。
0,1,3。
0,2,6,9。
0,2,6,9,11 丢弃。
。0,2,6,9,13 的 ----------------------------------------------- ------------------------------
A [14] = 7.案例3.克隆,延长和丢弃 0
0,1。
0,1,3。
0,1,3,7。 丢弃。
。0,2,6,9 0,2,6,9,11 的 ----------------------------------------------- -----------------------------
A [15] = 15.案例2.克隆并延长。
0
0,1。
0,1,3。
0,1,3,7。
0,2,6,9,11 0,2,6,9,11,15。&lt; - LIS List

另外,确保我们保持条件,“较小列表的结尾元素小于较大列表的结尾元素”。
该算法称为耐心排序 http://en.wikipedia.org/wiki/Patience_sorting

所以,从一副牌中挑选一套西装。从洗牌的西装中找出增长最快的子牌序列。你永远不会忘记这种方法。

复杂性:O(NlogN)

来源:http://www.geeksforgeeks.org/longest-monotonically-increasing-subsequence-size-n-log-n/

答案 3 :(得分:0)

算法背后的基本思想是保持给定长度的LIS列表以最小可能元素结束。构建这样的序列

  1. 在已知的最后一个元素序列中查找前一个前导(让我们说它的长度为k
  2. 尝试将当前元素附加到此序列,并为k+1 length
  3. 构建新的更好的解决方案

    因为在第一步中搜索较小的值然后X [i]新解决方案(对于k+1)将使最后一个元素大于较短的序列。

    我希望它会有所帮助。

答案 4 :(得分:0)

我想出了这个

set<int> my_set;
set<int>::iterator it;
vector <int> out;
out.clear();
my_set.clear();
for(int i = 1; i <= n; i++) {
    my_set.insert(a[i]);
    it = my_set.find(a[i]);
    it++;
    if(it != my_set.end()) 
        st.erase(it);
    else
        out.push_back(*it);
}
cout<< out.size();

答案 5 :(得分:0)

您无法理解,因为维基百科中的代码是错误的(我坚信是这样)。这不仅是错误的,而且变量的命名也很差。但这让我花时间了解它的工作原理:D。

现在,我阅读耐心排序之后。我重写了算法。我还写了更正后的二进制搜索。

耐心排序就像插入排序

像插入排序一样,耐心排序通过二进制搜索找到下一个项目的适当位置。二进制搜索是对按排序顺序构建的卡堆进行的。让我为纸牌堆分配一个变量。(我说的是纸牌,因为耐心是一种简化的纸牌游戏。)

//! card piles contain pile of cards, nth pile contains n cards.
int top_card_list[n+1];
for(int i = 0; i <= n; i++) {
    top_card_list[i] = -1;
}

现在top_card_list包含高度为n的牌堆的顶牌。耐心排序会将牌手放在小于它的最高顶牌上(或相反)。有关进一步的分类说明,请参阅Wikipedia页面上的耐心分类。

             3
  *   7      2                   
-------------------------------------------------------------
  Pile of cards above (top card is larger than lower cards)
 (note that pile of card represents longest increasing subsequence too !)

在一堆纸牌上进行二进制搜索

现在,当我们为最长增长的子序列进行动态编程时要找到一个数字,我们运行一个O(n)的内部循环。

for(int i = 1; i < n; i++) { // outer loop
    for(int j = 0; j < i; j++) { // inner loop
        if(arr[i] > arr[j]) {
            if(memo_len[i] < (memo_len[j]+1)) {
                // relaxation
                memo_len[i] = memo_len[j]+1;
                result = std::max(result,memo_len[i]);
                pred[i] = j;
            }
        }
    }
 }

然后就可以在内部循环中找到比我们手头上的卡片还要小的最高卡片。

但是我们知道我们可以通过二进制搜索来做到! (练习:证明正确性)这样,我们可以在O(log (number of piles))时间内做到这一点。现在O(number of piles) = O(number of cards)(但是卡号是52,应该是O(1)!,只是在开玩笑!)。因此,整个应用程序将在O(n log n)时间内运行。

这是经过修订的带有二进制搜索的DP。

for(int i = 1; i < n; i++) {
    pile_height[i] = 1;
    const int j = pile_search(top_card_list, arr, pile_len, arr[i]);
    if(arr[i] > arr[j]) {
        if(pile_height[i] < (pile_height[j]+1)) {
            // relaxation
            pile_height[i] = pile_height[j]+1;
            result = std::max(result,pile_height[i]);
            pile_len = std::max(pile_len,pile_height[i]);
        }
    }
    if(-1 == top_card_list[pile_height[i]] || arr[top_card_list[pile_height[i]]] > arr[i]) {
        top_card_list[pile_height[i]] = i; // top card on the pile is now i
    }
}

这是下面的正确桩搜索。这只是一个二进制搜索,但它会找到顶卡的索引,该索引比手中的卡要小。

inline static int pile_search(const int*top_card_list, const vector<int>& arr, int pile_len, int strict_upper_limit) {
    int start = 1,bound=pile_len;
    while(start < bound) {
        if(arr[top_card_list[bound]] < strict_upper_limit) {
            return top_card_list[bound];
        }
        int mid = (start+bound)/2 + ((start+bound)&1);
        if(arr[top_card_list[mid]] >= strict_upper_limit) {
            // go lower
            bound = mid-1;
        } else {
            start = mid;
        }
    }
    return top_card_list[bound];
}

请注意,与维基百科不同,它返回top_card_list[bound](我的解决方法)。还要注意top_card_list[]在dp中的更新位置。此代码已针对边界情况进行了测试。希望对您有所帮助。

答案 6 :(得分:0)

https://strncat.github.io/jekyll/update/2019/06/25/longest-increasing-subsequence.html

这里有证据

从根本上讲,不可能不是严格增加的子序列。 证明是矛盾的:假设不是,那么我们有两种情况: 情况1)有一个元素M [j]终止于长度为j和j +某数的两个子序列。这是不可能的(在链接中证明)

案例2)与案例1略有不同,但推理基本相同。您如何才能在两个不同长度的两个子序列的末尾得到一个最小的数字?不可能。

答案 7 :(得分:0)

您当然可以查看此视频以获取说明:

https://www.youtube.com/watch?v=nf3YG4CnTbg&feature=youtu.be

我的登录名代码为:

int n;
cin>>n;//LENGTH OF ARRAY
vector<int>v(n);
for(int i=0;i<n;i++){
    cin>>v[i];
}
vector<int>d(n+1,INT_MAX);//AUXILLARY ARRAY
for(int i=0;i<=n;i++){
    *lower_bound(d.begin(),d.end(),v[i])=v[i];
}
for(int i=0;i<n;i++){
    if(d[i]==INT_MAX){
        cout<<i;//LENGTH OF LIS
        exit(0);
    }
}