web-dev-qa-db-fra.com

Séquences croissantes séquentielles K les plus longues

Pourquoi j'ai créé un fil de discussion en double

J'ai créé ce fil après avoir lu La sous-séquence augmentant le plus longtemps avec K exceptions autorisées . J'ai réalisé que la personne qui posait la question n'avait pas vraiment compris le problème, car il faisait référence à un link qui résout le problème "Sous-tableau croissant le plus long avec un changement autorisé". Ainsi, les réponses qu'il a obtenues n'étaient en fait pas pertinentes pour le problème LIS.

Description du problème

Supposons qu'un tableau [~ # ~] a [~ # ~] soit donné avec une longueur [~ # ~] n [~ # ~]. Trouvez la sous-séquence croissante la plus longue avec [~ # ~] k [~ # ~] exceptions autorisées.

Exemple
1) N = 9, K = 1

A ,9,4,5,8,6,1,3,7]

Réponse: 7

Explication:

La sous-séquence croissante la plus longue est: 3,4,5,8 (ou 6), 1 (exception), 3,7 -> total = 7

2) N = 11, K = 2

A = [5,6,4,7,3,9,2,5,1,8,7]

réponse: 8

Ce que j'ai fait jusqu'à présent ...

Si K = 1, alors une seule exception est autorisée. Si l'algorithme connu pour calculer la suite croissante la plus longue dans == [- O (NlogN) est utilisé ( cliquez ici pour voir cet algorithme ), alors nous pouvons calculer le LIS en A [0] à A [N-1] pour chaque élément du tableau A. Nous enregistrons les résultats dans un nouveau tableau [~ # ~] l [~ # ~] avec la taille [~ # ~] n [~ # ~]. Dans l'exemple n.1, le tableau L serait: L = [1,2,2,3,4,4,4,4,5].

En utilisant la logique inverse, nous calculons le tableau [~ # ~] r [~ # ~], dont chaque élément contient la séquence décroissante la plus longue actuelle de N-1 à 0.

Le LIS à une exception près est juste sol = max (sol, L [i] + R [i + 1]), sol est initialisé comme sol = L [N-1]. Nous calculons donc LIS de 0 jusqu'à un index i (exception), puis arrêtons et démarrons un nouveau LIS jusqu'à N-1.

A=[3,9,4,5,8,6,1,3,7]

L=[1,2,2,3,4,4,4,4,5]

R=[5,4,4,3,3,3,3,2,1]

Sol = 7

-> explication étape par étape:

init: sol = L[N]= 5

i=0 : sol = max(sol,1+4) = 5 
i=1 : sol = max(sol,2+4) = 6
i=2 : sol = max(sol,2+3) = 6
i=3 : sol = max(sol,3+3) = 6
i=4 : sol = max(sol,4+3) = 7
i=4 : sol = max(sol,4+3) = 7
i=4 : sol = max(sol,4+2) = 7
i=5 : sol = max(sol,4+1) = 7

Complexité: O (NlogN + NlogN + N) = O (NlogN)

car les tableaux R, L ont besoin de temps NlogN pour calculer et nous avons également besoin de Θ (N) pour trouver sol.

Code pour le problème k = 1

#include <stdio.h>
#include <vector>

std::vector<int> ends;

int index_search(int value, int asc) {
    int l = -1;
    int r = ends.size() - 1;
    while (r - l > 1) { 
        int m = (r + l) / 2; 
        if (asc && ends[m] >= value) 
            r = m; 
        else if (asc && ends[m] < value)
            l = m;
        else if (!asc && ends[m] <= value)
            r = m;
        else
            l = m;
    } 
    return r;
}

int main(void) {
    int n, *S, *A, *B, i, length, idx, max;

    scanf("%d",&n);
    S = new int[n];
    L = new int[n];
    R = new int[n];
    for (i=0; i<n; i++) {
        scanf("%d",&S[i]);
    }

    ends.Push_back(S[0]);
    length = 1;
    L[0] = length;
    for (i=1; i<n; i++) {
        if (S[i] < ends[0]) {
            ends[0] = S[i];
        }
        else if (S[i] > ends[length-1]) {
            length++;
            ends.Push_back(S[i]);
        }
        else {
            idx = index_search(S[i],1);
            ends[idx] = S[i];
        }
        L[i] = length;
    }

    ends.clear();
    ends.Push_back(S[n-1]);
    length = 1;
    R[n-1] = length;
    for (i=n-2; i>=0; i--) {
        if (S[i] > ends[0]) {
            ends[0] = S[i];
        }
        else if (S[i] < ends[length-1]) {
            length++;
            ends.Push_back(S[i]);
        }
        else {
            idx = index_search(S[i],0);
            ends[idx] = S[i];
        }
        R[i] = length;
    }

    max = A[n-1];
    for (i=0; i<n-1; i++) {
        max = std::max(max,(L[i]+R[i+1]));
    }

    printf("%d\n",max);
    return 0;
}

Généralisation à K exceptions

J'ai fourni un algorithme pour K = 1. Je n'ai aucune idée de comment modifier l'algorithme ci-dessus pour fonctionner pour les exceptions K. Je serais heureux si quelqu'un pouvait m'aider.

( PS. Si nécessaire, je peux fournir du code pour l'algorithme K = 1 en C++.)

8
Ermolai

Cette réponse est modifiée de ma réponse à une question similaire à Computer Science Stackexchange.

Le problème LIS avec au plus k exceptions admet un algorithme O (n log² n) utilisant la relaxation lagrangienne. Lorsque k est plus grand que log n, cela s'améliore asymptotiquement sur le O (nk log n) DP, que nous expliquerons également brièvement.

Soit DP [a] [b] la longueur de la sous-séquence croissante la plus longue avec au plus b exceptions (positions où l'entier précédent est plus grand que le suivant) se terminant à l'élément b une. Ce DP n'est pas impliqué dans l'algorithme, mais sa définition facilite la démonstration de l'algorithme.

Pour plus de commodité, nous supposerons que tous les éléments sont distincts et que le dernier élément du tableau est son maximum. Notez que cela ne nous limite pas, car nous pouvons simplement ajouter m/2n à la mième apparence de chaque nombre, et ajouter l'infini au tableau et soustraire un de la réponse. Soit V la permutation pour laquelle 1 <= V [i] <= n est la valeur du ième élément.

Pour résoudre le problème dans O (nk log n), nous maintenons l'invariant selon lequel DP [a] [b] a été calculé pour b <j. Boucle j de 0 à k, à la jième itération calculant DP [a] [j] pour tout a. Pour ce faire, bouclez i de 1 à n. Nous maintenons le maximum de DP [x] [j-1] sur x <i et une structure de données maximale de préfixe qui à l'indice i aura DP [x] [j] à la position V [x] pour x <i et 0 à tous les autres postes.

Nous avons DP [i] [j] = 1 + max (DP [i '] [j], DP [x] [j-1]) où nous allons sur i', x <i, V [i '] < V [i]. Le maximum de préfixe DP [x] [j-1] nous donne le maximum de termes du deuxième type, et l'interrogation de la structure de données maximale de préfixe pour le préfixe [0, V [i]] nous donne le maximum de termes du premier type. Mettez ensuite à jour la structure de données prefix maximum et prefix maximum.

Voici une implémentation C++ de l'algorithme. Notez que cette implémentation ne suppose pas que le dernier élément du tableau est son maximum ou que le tableau ne contient aucun doublon.


#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

// Fenwick tree for prefix maximum queries
class Fenwick {
    private:
        vector<int> val;
    public:
        Fenwick(int n) : val(n+1, 0) {}

        // Sets value at position i to maximum of its current value and 
        void inc(int i, int v) {
            for (++i; i < val.size(); i += i & -i) val[i] = max(val[i], v);
        }

        // Calculates prefix maximum up to index i
        int get(int i) {
            int res = 0;
            for (++i; i > 0; i -= i & -i) res = max(res, val[i]);
            return res;
        }
};

// Binary searches index of v from sorted vector
int bins(const vector<int>& vec, int v) {
    int low = 0;
    int high = (int)vec.size() - 1;
    while(low != high) {
        int mid = (low + high) / 2;
        if (vec[mid] < v) low = mid + 1;
        else high = mid;
    }
    return low;
}

// Compresses the range of values to [0, m), and returns m
int compress(vector<int>& vec) {
    vector<int> ord = vec;
    sort(ord.begin(), ord.end());
    ord.erase(unique(ord.begin(), ord.end()), ord.end());
    for (int& v : vec) v = bins(ord, v);
    return ord.size();
}

// Returns length of longest strictly increasing subsequence with at most k exceptions
int lisExc(int k, vector<int> vec) {
    int n = vec.size();
    int m = compress(vec);
    vector<int> dp(n, 0);
    for (int j = 0;; ++j) {
        Fenwick fenw(m+1); // longest subsequence with at most j exceptions ending at this value
        int max_exc = 0; // longest subsequence with at most j-1 exceptions ending before this
        for (int i = 0; i < n; ++i) {
            int off = 1 + max(max_exc, fenw.get(vec[i]));
            max_exc = max(max_exc, dp[i]);

            dp[i] = off;
            fenw.inc(vec[i]+1, off);
        }
        if (j == k) return fenw.get(m);
    }
}

int main() {
    int n, k;
    cin >> n >> k;

    vector<int> vec(n);
    for (int i = 0; i < n; ++i) cin >> vec[i];

    int res = lisExc(k, vec);
    cout << res << '\n';
}

Revenons maintenant à l'algorithme O (n log² n). Sélectionnez un entier 0 <= r <= n. Définissez DP '[a] [r] = max (DP [a] [b] - rb), où le maximum est repris sur b, MAXB [a] [r] comme le maximum b tel que DP' [a] [ r] = DP [a] [b] - rb et MINB [a] [r] de la même manière que le minimum tel b. Nous montrerons que DP [a] [k] = DP '[a] [r] + rk si et seulement si MINB [a] [r] <= k <= MAXB [a] [r]. De plus, nous montrerons que pour tout k existe un r pour lequel cette inégalité est vraie.

Notez que MINB [a] [r]> = MINB [a] [r '] et MAXB [a] [r]> = MAXB [a] [r'] si r <r ', donc si nous supposons les deux revendiqués résultats, nous pouvons faire une recherche binaire pour les r, en essayant les valeurs O (log n). On obtient donc la complexité O (n log² n) si l'on peut calculer DP ', MINB et MAXB en O (n log n) temps.

Pour ce faire, nous aurons besoin d'une arborescence de segments qui stocke les tuples P [i] = (v_i, low_i, high_i) et prend en charge les opérations suivantes:

  1. Étant donné une plage [a, b], trouvez la valeur maximale dans cette plage (maximum v_i, a <= i <= b), et les valeurs minimale minimale et maximale maximale associées à cette valeur dans la plage.

  2. Définissez la valeur du Tuple P [i]

Ceci est facile à implémenter avec une complexité O (log n) de temps par opération en supposant une certaine familiarité avec les arbres de segments. Vous pouvez vous référer à l'implémentation de l'algorithme ci-dessous pour plus de détails.

Nous allons maintenant montrer comment calculer DP ', MINB et MAXB dans O (n log n). Fixer r. Construisez l'arborescence des segments contenant initialement n + 1 valeurs nulles (-INF, INF, -INF). Nous maintenons que P [V [j]] = (DP '[j], MINB [j], MAXB [j]) pour j inférieur à la position actuelle i. Réglez DP '[0] = 0, MINB [0] = 0 et MAXB [0] sur 0 si r> 0, sinon sur INF et P [0] = (DP' [0], MINB [0 ], MAXB [0]).

Boucle i de 1 à n. Il existe deux types de sous-séquences se terminant par i: celles où l'élément précédent est supérieur à V [i] et celles où il est inférieur à V [i]. Pour tenir compte du deuxième type, interrogez l'arborescence des segments dans la plage [0, V [i]]. Que le résultat soit (v_1, low_1, high_1). Déclencher1 = (v_1 + 1, low_1, high_1). Pour le premier type, interrogez l'arborescence des segments dans la plage [V [i], n]. Soit le résultat (v_2, low_2, high_2). Set off2 = (v_2 + 1 - r, low_2 + 1, high_2 + 1), où nous encourons la pénalité de r pour avoir créé une exception.

Ensuite, nous combinons off1 et off2 en off. Si off1.v> off2.v, off = off1, et si off2.v> off1.v, off = off2. Sinon, désactivez = (off1.v, min (off1.low, off2.low), max (off1.high, off2.high)). Réglez ensuite DP '[i] = off.v, MINB [i] = off.low, MAXB [i] = off.high et P [i] = off.

Puisque nous effectuons deux requêtes d'arborescence de segments à chaque i, cela prend au total O (n log n) temps. Il est facile de prouver par induction que nous calculons les valeurs correctes DP ', MINB et MAXB.

Donc, en bref, l'algorithme est:

  1. Prétraitez, en modifiant les valeurs afin qu'elles forment une permutation, et la dernière valeur est la plus grande valeur.

  2. Recherche binaire du r correct, avec bornes initiales 0 <= r <= n

  3. Initialisez l'arborescence des segments avec des valeurs nulles, définissez DP '[0], MINB [0] et MAXB [0].

  4. Boucle de i = 1 à n, à l'étape i

    • Interrogation des plages [0, V [i]] et [V [i], n] de l'arborescence des segments,
    • calculer DP '[i], MINB [i] et MAXB [i] sur la base de ces requêtes, et
    • définir la valeur à la position V [i] dans l'arborescence des segments sur le tuple (DP '[i], MINB [i], MAXB [i]).
  5. Si MINB [n] [r] <= k <= MAXB [n] [r], renvoyez DP '[n] [r] + kr - 1.

  6. Sinon, si MAXB [n] [r] <k, le r correct est inférieur au r actuel. Si MINB [n] [r]> k, le r correct est supérieur au r actuel. Mettez à jour les limites de r et revenez à l'étape 1.

Voici une implémentation C++ pour cet algorithme. Il trouve également la sous-séquence optimale.

    #include <iostream>
    #include <vector>
    #include <algorithm>
    using namespace std;
    using ll = long long;
    const int INF = 2 * (int)1e9;

    pair<ll, pair<int, int>> combine(pair<ll, pair<int, int>> le, pair<ll, pair<int, int>> ri) {
        if (le.first < ri.first) swap(le, ri);
        if (ri.first == le.first) {
            le.second.first = min(le.second.first, ri.second.first);
            le.second.second = max(le.second.second, ri.second.second);
        }
        return le;
    }

    // Specialised range maximum segment tree
    class SegTree {
        private:
            vector<pair<ll, pair<int, int>>> seg;
            int h = 1;

            pair<ll, pair<int, int>> recGet(int a, int b, int i, int le, int ri) const {
                if (ri <= a || b <= le) return {-INF, {INF, -INF}};
                else if (a <= le && ri <= b) return seg[i];
                else return combine(recGet(a, b, 2*i, le, (le+ri)/2), recGet(a, b, 2*i+1, (le+ri)/2, ri));
            }
        public:
            SegTree(int n) {
                while(h < n) h *= 2;
                seg.resize(2*h, {-INF, {INF, -INF}});
            }
            void set(int i, pair<ll, pair<int, int>> off) {
                seg[i+h] = combine(seg[i+h], off);
                for (i += h; i > 1; i /= 2) seg[i/2] = combine(seg[i], seg[i^1]);
            }
            pair<ll, pair<int, int>> get(int a, int b) const {
                return recGet(a, b+1, 1, 0, h);
            }
    };

    // Binary searches index of v from sorted vector
    int bins(const vector<int>& vec, int v) {
        int low = 0;
        int high = (int)vec.size() - 1;
        while(low != high) {
            int mid = (low + high) / 2;
            if (vec[mid] < v) low = mid + 1;
            else high = mid;
        }
        return low;
    }

    // Finds longest strictly increasing subsequence with at most k exceptions in O(n log^2 n)
    vector<int> lisExc(int k, vector<int> vec) {
        // Compress values
        vector<int> ord = vec;
        sort(ord.begin(), ord.end());
        ord.erase(unique(ord.begin(), ord.end()), ord.end());
        for (auto& v : vec) v = bins(ord, v) + 1;

        // Binary search lambda
        int n = vec.size();
        int m = ord.size() + 1;
        int lambda_0 = 0;
        int lambda_1 = n;
        while(true) {
            int lambda = (lambda_0 + lambda_1) / 2;
            SegTree seg(m);
            if (lambda > 0) seg.set(0, {0, {0, 0}});
            else seg.set(0, {0, {0, INF}});

            // Calculate DP
            vector<pair<ll, pair<int, int>>> dp(n);
            for (int i = 0; i < n; ++i) {
                auto off0 = seg.get(0, vec[i]-1); // previous < this
                off0.first += 1;

                auto off1 = seg.get(vec[i], m-1); // previous >= this
                off1.first += 1 - lambda;
                off1.second.first += 1;
                off1.second.second += 1;

                dp[i] = combine(off0, off1);
                seg.set(vec[i], dp[i]);
            }

            // Is min_b <= k <= max_b?
            auto off = seg.get(0, m-1);
            if (off.second.second < k) {
                lambda_1 = lambda - 1;
            } else if (off.second.first > k) {
                lambda_0 = lambda + 1;
            } else {
                // Construct solution
                ll r = off.first + 1;
                int v = m;
                int b = k;
                vector<int> res;
                for (int i = n-1; i >= 0; --i) {
                    if (vec[i] < v) {
                        if (r == dp[i].first + 1 && dp[i].second.first <= b && b <= dp[i].second.second) {
                            res.Push_back(i);
                            r -= 1;
                            v = vec[i];
                        }
                    } else {
                        if (r == dp[i].first + 1 - lambda && dp[i].second.first <= b-1 && b-1 <= dp[i].second.second) {
                            res.Push_back(i);
                            r -= 1 - lambda;
                            v = vec[i];
                            --b;
                        }
                    }
                }
                reverse(res.begin(), res.end());
                return res;
            }
        }
    }

    int main() {
        int n, k;
        cin >> n >> k;

        vector<int> vec(n);
        for (int i = 0; i < n; ++i) cin >> vec[i];

        vector<int> ans = lisExc(k, vec);
        for (auto i : ans) cout << i+1 << ' ';
        cout << '\n';
    }

Nous allons maintenant prouver les deux affirmations. Nous voulons prouver que

  1. DP '[a] [r] = DP [a] [b] - rb si et seulement si MINB [a] [r] <= b <= MAXB [a] [r]

  2. Pour tout a, k il existe un entier r, 0 <= r <= n, tel que MINB [a] [r] <= k <= MAXB [a] [r]

Ces deux éléments découlent de la concavité du problème. La concavité signifie que DP [a] [k + 2] - DP [a] [k + 1] <= DP [a] [k + 1] - DP [a] [k] pour tout a, k. C'est intuitif: plus nous sommes autorisés à faire d'exceptions, moins nous en autorisons plus.

Fixez a et r. Définissez f(b) = DP [a] [b] - rb et d(b) = f (b + 1) - f (b). Nous avons d (k + 1) <= d(k) de la concavité du problème. Supposons que x <y et f(x) = f(y)> = f(i) pour tout i. D'où d(x) <= 0, donc d(i) <= 0 pour i dans [x, y). Mais f(y) = f(x) + d(x) + d (x + 1) + ... + d (y - 1), donc d(i) = 0 pour i dans [x, y). D'où f(y) = f(x) = f(i) pour i dans [x, y]. Cela prouve la première affirmation.

Pour prouver la seconde, définissez r = DP [a] [k + 1] - DP [a] [k] et définissez f, d comme précédemment. Alors d(k) = 0, donc d(i)> = 0 pour i <k et d(i) <= 0 pour i> k, donc f(k) est maximal comme souhaité.

Il est plus difficile de prouver la concavité. Pour une preuve, voir ma réponse sur cs.stackexchange.

7
Antti Röyskö