web-dev-qa-db-ja.com

最長増加サブシーケンス(O(nlogn))

LIS:wikipedia

理解できないことが1つあります。

x [M [i]]が非減少シーケンスなのはなぜですか?

32
outsiders

最初に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;
         }
      }
   }
}

改善は2番目のループで行われます。基本的に、バイナリ検索を使用して速度を改善できます。配列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;
   }
}
73
hiddenboy

これは、 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;

2番目のアルゴリズムを拡張して、元の配列内の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;
}
23

増加するシーケンスのリストを維持する必要があります。

一般に、さまざまな長さのアクティブリストのセットがあります。これらのリストに要素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、14。
------------------------------------ -----------------------------------------
A [8] = 1.ケース3.クローン、拡張、および破棄。
0。
0、1。
0、2。破棄。
0、2、6。
0、2、6、14。
------------------------------------ -----------------------------------------
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、13。
------------------------------------ -----------------------------------------
A [12] = 3.ケース3.クローン、拡張、および破棄。
0。
0、1。
0、1、3。
0、1、5。破棄。
0、2、6、9。
0、2、6、9、13。
------------------------------------ -----------------------------------------
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<-LISリスト

また、「小さいリストの終了要素が大きいリストの終了要素よりも小さい」という条件を維持していることを確認してください。
このアルゴリズムは、忍耐ソートと呼ばれます。
http://en.wikipedia.org/wiki/Patience_sorting

だから、カードのデッキからスーツを選んでください。シャッフルされたスーツから最も長く増加するサブシーケンスのカードを見つけます。アプローチを決して忘れません。

複雑さ:O(NlogN)

ソース: http://www.geeksforgeeks.org/longest-monotonically-increasing-subsequence-size-n-log-n/

9
Shekhar Kumar

これを思いついた

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();
0
Bill

ウィキペディアのコードが間違っているため、理解できません(そう信じています)。それは間違っているだけでなく、変数の名前が間違っています。しかし、それがどのように機能するかを理解するために時間を費やすことができました:D。

今、忍耐ソートを読んだ後。アルゴリズムを書き直しました。修正されたバイナリ検索も作成しました。

忍耐ソートは挿入ソートのようなものです

挿入ソートと同様に、patience-sortはバイナリ検索を実行して次のアイテムの適切な場所を見つけます。バイナリ検索は、ソートされた順序で構築されたカードパイルで実行されます。カードパイルに変数を割り当ててみましょう(忍耐は単純化されたカードゲームであるため、私はカードのプレイについて話します)。

_//! 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のカードパイルの一番上のカードが含まれます。忍耐ソートは、それよりも小さい(または反対の)一番上の一番上のカードの上にカードを配置します。ソートの詳細については、忍耐ソートについてのウィキペディアのページを参照してください。

_             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]_(私の修正)を返します。また、dpの_top_card_list[]_が更新されていることに注意してください。このコードは、境界の場合についてテストされています。役に立てば幸いです。

0
shuva

ここに証拠があります https://strncat.github.io/jekyll/update/2019/06/25/longest-increasing-subsequence.html

基本的に、厳密に増加するサブシーケンスにならないことは不可能です。証明は矛盾によるものです。そうでない場合、2つのケースがあるとします。ケース1)長さjとj + some numberの2つのサブシーケンスを終了する要素M [j]があります。これは不可能です(リンクで証明)

ケース2)ケース1とは少し異なりますが、ほぼ同じ理由です。どうすれば、2つの異なる長さの2つのサブシーケンスを最小の数で終了できますか?できません。

0
user1781626

アルゴリズムの背後にある基本的な考え方は、可能な限り小さい要素で終わる、指定された長さのLISのリストを保持することです。そのようなシーケンスの構築

  1. 既知の最後の要素のシーケンスで直近の先行要素を見つけます(長さkと言います)
  2. 現在の要素をこのシーケンスに追加し、k+1長さ

最初のステップで、X [i]より小さい値を検索するため、新しいソリューション(k+1)最後の要素は短いシーケンスよりも大きくなります。

私はそれが役立つことを願っています。

0
jethro