web-dev-qa-db-fra.com

sous-séquence croissante la plus longue (O (nlogn))

LIS: wikipedia

Il y a une chose que je ne peux pas comprendre:

pourquoi X [M [i]] est-elle une séquence non décroissante?

32
outsiders

Regardons d'abord l'algorithme 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;
         }
      }
   }
}

Maintenant, l'amélioration se produit à la deuxième boucle, en gros, vous pouvez améliorer la vitesse en utilisant la recherche binaire. Outre le tableau dp [], ayons un autre tableau c [], c est assez spécial, c [i] signifie: la valeur minimale du dernier élément de la séquence croissante la plus longue dont la longueur est 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

Ceci est la solution O (n * lg (n)) de The Hitchhiker’s Guide to the Programming Contests (note: cette implémentation suppose qu’il n’y a pas de doublons dans la liste):

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;

Pour tenir compte des doublons, on pourrait vérifier, par exemple, si le numéro est déjà dans l'ensemble. Si c'est le cas, ignorez le nombre, sinon continuez en utilisant la même méthode que précédemment. Alternativement, on pourrait inverser l'ordre des opérations: d'abord supprimer, puis insérer. Le code ci-dessous implémente ce comportement:

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;

Le deuxième algorithme pourrait être étendu pour trouver la sous-séquence croissante la plus longue (LIS) elle-même en conservant un tableau parent qui contient la position de l'élément précédent du LIS dans le tableau d'origine.

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

Nous devons maintenir des listes de séquences croissantes.

En général, nous avons mis en place des listes actives de longueur variable. Nous ajoutons un élément A [i] à ces listes. Nous parcourons les listes (pour les éléments finaux) par ordre décroissant de leur longueur. Nous vérifierons les éléments de fin de toutes les listes pour trouver une liste dont l'élément de fin est plus petit que A [i] (valeur plancher).

Notre stratégie déterminée par les conditions suivantes,
1. Si A [i] est le plus petit parmi tous les candidats finaux des listes actives, nous commencerons une nouvelle liste active de longueur 1.
2. Si A [i] est le plus grand parmi tous les candidats finaux des listes actives, nous clonerons la plus grande liste active et la prolongerons par A [i].
3. Si A [i] est entre les deux, nous trouverons une liste avec le plus grand élément d'extrémité qui est plus petit que A [i]. Clonez et étendez cette liste par A [i]. Nous éliminerons toutes les autres listes de même longueur que celle de cette liste modifiée.

Notez qu'à tout moment pendant notre construction de listes actives, la condition suivante est maintenue.

"l'élément de fin d'une liste plus petite est plus petit que les éléments de fin de listes plus grandes".

Ce sera clair avec un exemple, prenons l'exemple du wiki:
{0, 8, 4, 12, 2, 10, 6, 14, 1, 9, 5, 13, 3, 11, 7, 15}.

A [0] = 0. Cas 1. Il n'y a pas de listes actives, créez-en une.
0.
------------------------------------ -----------------------------------------
A [1] = 8. Cas 2. Cloner et étendre.
0.
0, 8.
------------------------------------ -----------------------------------------
A [2] = 4. Cas 3. Clonez, étendez et jetez.
0.
0, 4.
0, 8. Jeté
------------------------------------ -----------------------------------------
A [3] = 12. Cas 2. Clonez et étendez.
0.
0, 4.
0, 4, 12.
------------------------------------ -----------------------------------------
A [4] = 2. Cas 3. Cloner, étendre et supprimer.
0.
0, 2.
0, 4. Jeté.
0, 4, 12.
------------------------------------ -----------------------------------------
A [5] = 10. Cas 3. Clonez, étendez et jetez.
0.
0, 2.
0, 2, 10.
0, 4, 12. Rejeté.
------------------------------------ -----------------------------------------
A [6] = 6. Cas 3. Clonez, étendez et jetez.
0.
0, 2.
0, 2, 6.
0, 2, 10. Rejeté.
------------------------------------ -----------------------------------------
A [7] = 14. Cas 2. Cloner et étendre.
0.
0, 2.
0, 2, 6.
0, 2, 6, 14.
------------------------------------ -----------------------------------------
A [8] = 1. Cas 3. Clonez, étendez et jetez.
0.
0, 1.
0, 2. Jeté.
0, 2, 6.
0, 2, 6, 14.
------------------------------------ -----------------------------------------
A [9] = 9. Cas 3. Cloner, étendre et supprimer.
0.
0, 1.
0, 2, 6.
0, 2, 6, 9.
0, 2, 6, 14. Rejeté.
------------------------------------ -----------------------------------------
A [10] = 5. Cas 3. Clonez, étendez et jetez.
0.
0, 1.
0, 1, 5.
0, 2, 6. Rejeté.
0, 2, 6, 9.
------------------------------------ -----------------------------------------
A [11] = 13. Cas 2. Clonez et étendez.
0.
0, 1.
0, 1, 5.
0, 2, 6, 9.
0, 2, 6, 9, 13.
------------------------------------ -----------------------------------------
A [12] = 3. Cas 3. Clonez, étendez et jetez.
0.
0, 1.
0, 1, 3.
0, 1, 5. Jeté.
0, 2, 6, 9.
0, 2, 6, 9, 13.
------------------------------------ -----------------------------------------
A [13] = 11. Cas 3. Clonez, étendez et jetez.
0.
0, 1.
0, 1, 3.
0, 2, 6, 9.
0, 2, 6, 9, 11.
0, 2, 6, 9, 13. Rejeté.
------------------------------------ -----------------------------------------
A [14] = 7. Cas 3. Clonez, étendez et jetez.
0.
0, 1.
0, 1, 3.
0, 1, 3, 7. 0, 2, 6, 9. Rejeté.
0, 2, 6, 9, 11.
------------------------------------ ----------------------------------------
A [15] = 15. Cas 2. Cloner et étendre.
0.
0, 1.
0, 1, 3.
0, 1, 3, 7.
0, 2, 6, 9, 11.
0, 2, 6, 9, 11, 15. <- Liste LIS

De plus, assurez-vous que nous avons maintenu la condition, "l'élément de fin d'une liste plus petite est plus petit que les éléments de fin de listes plus grandes".
Cet algorithme est appelé Tri par patience.
http://en.wikipedia.org/wiki/Patience_sorting

Alors, choisissez une couleur dans le jeu de cartes. Trouvez la sous-séquence de cartes qui augmente le plus longtemps dans la couleur mélangée. Vous n'oublierez jamais l'approche.

Complexité: O (NlogN)

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

9
Shekhar Kumar

Vous ne pouvez pas comprendre, car le code dans wikipedia est incorrect (je le crois fortement). Ce n'est pas seulement faux, mais les variables sont mal nommées. Mais cela m'a permis de passer du temps à comprendre comment cela fonctionne: D.

Maintenant, après avoir lu le genre patience. J'ai réécrit l'algorithme. J'ai également écrit la recherche binaire corrigée.

Le tri de patience est comme le tri d'insertion

Comme le tri par insertion, le tri par patience trouve la place appropriée pour l'élément suivant en effectuant une recherche binaire. La recherche binaire se fait sur les piles de cartes construites dans l'ordre trié. Permettez-moi d'attribuer une variable à la pile de cartes (je parle de jouer aux cartes parce que la patience est un jeu de cartes simplifié).

//! 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;
}

Maintenant, top_card_list Contient la carte du dessus de la pile de cartes de hauteur n. Le tri par patience place la carte en main sur la carte supérieure la plus haute qui est plus petite qu'elle (ou l'inverse). Pour plus d'informations sur le tri, veuillez vous référer à la page wikipedia pour le tri de la patience.

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

Recherche binaire sur un tas de cartes

Maintenant, pour trouver un nombre pendant que nous faisons de la programmation dynamique pour la sous-séquence qui augmente le plus longtemps, nous exécutons une boucle interne qui est 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;
            }
        }
    }
 }

Et la boucle intérieure est là pour trouver la carte la plus haute qui est plus petite que notre carte en main.

Mais nous savons que nous pouvons le faire par recherche binaire! (exercice: prouver l'exactitude) De cette façon, nous pouvons le faire en O(log (number of piles)) temps. Maintenant O(number of piles) = O(number of cards) (mais le nombre de cartes est de 52, ce devrait être O (1) !, plaisante!). Ainsi, l'application totale s'exécute en O(n log n) temps.

Voici le DP révisé avec recherche binaire.

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
    }
}

Voici la recherche de pile correcte ci-dessous. Il s'agit simplement d'une recherche binaire, mais il trouve l'index de la carte du dessus qui est plus petit que la carte en main.

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];
}

Notez que contrairement à wikipedia, il renvoie top_card_list[bound] (Ma correction). Notez également où le top_card_list[] Est mis à jour dans le dp. Ce code est testé pour les cas limites. J'espère que ça aide.

0
shuva

Il y a une preuve ici https://strncat.github.io/jekyll/update/2019/06/25/longest-increasing-subsequence.html

fondamentalement, il est impossible de ne pas être une sous-séquence strictement croissante. La preuve est par contradiction: Supposons que ce ne soit pas alors nous avons deux cas: Cas 1) Il y a un élément M [j] qui termine deux sous-séquences de longueur j et j + un certain nombre. C'est impossible (preuve en lien)

Cas 2) Légèrement différent du cas 1 mais à peu près le même raisonnement. Comment pouvez-vous avoir un plus petit nombre fin deux sous-séquences de deux longueurs différentes? ça ne peut pas être.

0
user1781626

L'idée de base de l'algorithme est de conserver la liste des LIS d'une longueur donnée se terminant par le plus petit élément possible. Construire une telle séquence

  1. Trouver un prédécesseur immédiat dans la dernière séquence d'éléments déjà connue (disons sa longueur k)
  2. Essayez d'ajouter l'élément actuel à cette séquence et de créer une nouvelle meilleure solution pour k+1 longueur

Parce qu'à la première étape, vous recherchez une valeur plus petite, puis X [i] la nouvelle solution (pour k+1) aura le dernier élément plus grand que la séquence plus courte.

J'espère que cela aidera.

0
jethro