Längste K sequenziell zunehmende Subsequenzen

8

Warum ich einen doppelten Thread erstellt habe

Ich habe diesen Thread nach dem Lesen der längsten ansteigenden Teilsequenz mit K zulässigen Ausnahmen erstellt . Mir wurde klar, dass die Person, die die Frage stellte, das Problem nicht wirklich verstanden hatte, weil sie sich auf einen Link bezog, der das Problem "Am längsten ansteigendes Subarray mit einer erlaubten Änderung" löst. Die Antworten, die er erhielt, waren für das LIS-Problem eigentlich irrelevant.

Beschreibung des Problems

Angenommen, ein Array A ist mit der Länge N gegeben . Finden Sie die am längsten ansteigende Teilsequenz mit K zulässigen Ausnahmen.

Beispiel
1) N = 9, K = 1

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

Antwort: 7

Erläuterung:

Die am längsten zunehmende Teilsequenz ist: 3,4,5,8 (oder 6), 1 (Ausnahme), 3,7 -> gesamt = 7

2) N = 11, K = 2

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

Antwort: 8

Was ich bisher gemacht habe ...

Wenn K = 1 ist, ist nur eine Ausnahme zulässig. Wenn der bekannte Algorithmus zur Berechnung der längsten ansteigenden Folge in O (NlogN) verwendet wird ( klicken Sie hier, um diesen Algorithmus anzuzeigen) , können wir den LIS für jedes Element des Arrays von A [0] bis A [N-1] berechnen A. Wir speichern die Ergebnisse in einem neuen Array L mit der Größe N . In Beispiel n.1 wäre das L-Array: L = [1,2,2,3,4,4,4,4,5].

Unter Verwendung der umgekehrten Logik berechnen wir das Array R , von dem jedes Element die aktuelle längste abnehmende Sequenz von N-1 bis 0 enthält.

Das LIS ist mit einer Ausnahme nur sol = max (sol, L [i] + R [i + 1]), wobei sol als sol = L [N-1] initialisiert wird . Also berechnen wir LIS von 0 bis zu einem Index i (Ausnahme), stoppen dann und starten ein neues LIS bis 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

-> Schritt für Schritt Erklärung:

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

Komplexität: O (NlogN + NlogN + N) = O (NlogN)

weil die Arrays R, L NlogN-Zeit zum Berechnen benötigen und wir auch Θ (N) benötigen, um Sol zu finden .

Code für k = 1 Problem

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

Verallgemeinerung auf K Ausnahmen

Ich habe einen Algorithmus für K = 1 bereitgestellt. Ich habe keine Ahnung, wie ich den obigen Algorithmus so ändern kann, dass er für K Ausnahmen funktioniert. Ich würde mich freuen, wenn mir jemand helfen könnte.

( PS. Bei Bedarf kann ich Code für den K = 1-Algorithmus in C ++ bereitstellen.)

Ermolai
quelle
Dieser Trick kann helfen, ihn in n log n zu lösen . Sie verwenden nämlich den binären Indexbaumalgorithmus, um die Größe LIS-x k ' zu berechnen , wobei * k' die Ausnahmen sind, die Sie haben, und x ein beliebiger Wert ist; Sie suchen also binär nach jedem x und erhalten ein k ', bis Sie k' * = * k erhalten. In einigen Fällen, in denen die binäre Suche nie das gewünschte k erhält, sondern von einem kleineren zu einem größeren springt, ist besondere Aufmerksamkeit erforderlich .
ilias_pap
@ilias_pap Bitte detaillieren und erarbeiten Sie den vollständigen Algorithmus. Aus Ihrem Kommentar ist völlig unklar, wie jede Iteration der von Ihnen erwähnten binären Suche in O (n) durchgeführt werden könnte.
ברקן ברקן
Eine ähnliche Frage finden Sie auch unter cs.stackexchange.com/q/118858/755 .
DW

Antworten:

7

Diese Antwort wurde von meiner Antwort auf eine ähnliche Frage bei Computer Science Stackexchange geändert.

Das LIS-Problem mit höchstens k Ausnahmen lässt einen O (n log² n) -Algorithmus unter Verwendung der Lagrange-Relaxation zu. Wenn k größer als log n ist, verbessert sich dies asymptotisch auf dem O (nk log n) DP, was wir auch kurz erklären werden.

DP [a] [b] bezeichne die Länge der am längsten ansteigenden Teilsequenz mit höchstens b Ausnahmen (Positionen, an denen die vorherige Ganzzahl größer als die nächste ist), die am Element b a enden . Dieser DP ist nicht am Algorithmus beteiligt, aber seine Definition erleichtert das Testen des Algorithmus.

Der Einfachheit halber nehmen wir an, dass alle Elemente unterschiedlich sind und dass das letzte Element im Array das Maximum ist. Beachten Sie, dass dies uns nicht einschränkt, da wir einfach m / 2n zum m-ten Erscheinungsbild jeder Zahl hinzufügen und dem Array unendlich hinzufügen und eins von der Antwort subtrahieren können. Sei V die Permutation, für die 1 <= V [i] <= n der Wert des i-ten Elements ist.

Um das Problem in O (nk log n) zu lösen, behalten wir die Invariante bei, dass DP [a] [b] für b <j berechnet wurde. Schleife j von 0 nach k bei der j-ten Iteration, in der DP [a] [j] für alle a berechnet wird. Schleifen Sie dazu i von 1 nach n. Wir behalten das Maximum von DP [x] [j-1] über x <i und eine maximale Präfixdatenstruktur bei, die am Index i DP [x] [j] an Position V [x] für x <i und 0 hat an jeder anderen Position.

Wir haben DP [i] [j] = 1 + max (DP [i '] [j], DP [x] [j-1]), wobei wir über i', x <i, V [i '] <gehen V [i]. Das Präfixmaximum von DP [x] [j-1] gibt uns das Maximum der Terme des zweiten Typs, und das Abfragen der maximalen Präfixdatenstruktur für das Präfix [0, V [i]] gibt uns das Maximum der Terme des ersten Art. Aktualisieren Sie dann die Datenstruktur für das maximale Präfix und das maximale Präfix.

Hier ist eine C ++ - Implementierung des Algorithmus. Beachten Sie, dass diese Implementierung nicht davon ausgeht, dass das letzte Element des Arrays maximal ist oder dass das Array keine Duplikate enthält.


#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';
}

Nun kehren wir zum O (n log² n) -Algorithmus zurück. Wählen Sie eine Ganzzahl 0 <= r <= n. Definieren Sie DP '[a] [r] = max (DP [a] [b] - rb), wobei das Maximum über b, MAXB [a] [r] als das Maximum b übernommen wird, so dass DP' [a] [ r] = DP [a] [b] - rb und MINB [a] [r] ähnlich wie das Minimum wie b. Wir werden zeigen, dass DP [a] [k] = DP '[a] [r] + rk genau dann ist, wenn MINB [a] [r] <= k <= MAXB [a] [r]. Weiterhin werden wir zeigen, dass für jedes k ein r existiert, für das diese Ungleichung gilt.

Man beachte, dass MINB [a] [r]> = MINB [a] [r '] und MAXB [a] [r]> = MAXB [a] [r'], wenn r <r ', also wenn wir die beiden beanspruchten annehmen Als Ergebnis können wir eine binäre Suche nach dem r durchführen und O (log n) -Werte versuchen. Daher erreichen wir die Komplexität O (n log² n), wenn wir DP ', MINB und MAXB in O (n log n) berechnen können.

Dazu benötigen wir einen Segmentbaum, der die Tupel P [i] = (v_i, low_i, high_i) speichert und die folgenden Operationen unterstützt:

  1. Bestimmen Sie bei gegebenem Bereich [a, b] den Maximalwert in diesem Bereich (Maximum v_i, a <= i <= b) und das Minimum niedrig und Maximum hoch gepaart mit diesem Wert im Bereich.

  2. Stellen Sie den Wert des Tupels P [i] ein.

Dies ist einfach zu implementieren, da die Komplexität O (log n) pro Operation vorausgesetzt wird, dass Sie mit den Segmentbäumen vertraut sind. Einzelheiten finden Sie in der folgenden Implementierung des Algorithmus.

Wir werden nun zeigen, wie DP ', MINB und MAXB in O (n log n) berechnet werden. Fix r. Erstellen Sie den Segmentbaum, der anfänglich n + 1 Nullwerte enthält (-INF, INF, -INF). Wir behaupten, dass P [V [j]] = (DP '[j], MINB [j], MAXB [j]) für j kleiner als die aktuelle Position i ist. Setze DP '[0] = 0, MINB [0] = 0 und MAXB [0] auf 0, wenn r> 0, andernfalls auf INF und P [0] = (DP' [0], MINB [0], MAXB [ 0]).

Schleife i von 1 bis n. Es gibt zwei Arten von Teilsequenzen, die mit i enden: diejenigen, bei denen das vorherige Element größer als V [i] ist, und diejenigen, bei denen es kleiner als V [i] ist. Um die zweite Art zu berücksichtigen, fragen Sie den Segmentbaum im Bereich [0, V [i]] ab. Das Ergebnis sei (v_1, low_1, high_1). Setze off1 = (v_1 + 1, low_1, high_1). Fragen Sie für die erste Art den Segmentbaum im Bereich [V [i], n] ab. Das Ergebnis sei (v_2, low_2, high_2). Setzen Sie off2 = (v_2 + 1 - r, low_2 + 1, high_2 + 1), wobei wir die Strafe von r für das Erstellen einer Ausnahme erhalten.

Dann kombinieren wir off1 und off2 zu off. Wenn off1.v> off2.v set off = off1 und wenn off2.v> off1.v set off = off2. Andernfalls setzen Sie = (off1.v, min (off1.low, off2.low), max (off1.high, off2.high)). Dann setze DP '[i] = off.v, MINB [i] = off.low, MAXB [i] = off.high und P [i] = off.

Da wir bei jedem i zwei Segmentbaumabfragen durchführen, dauert dies insgesamt O (n log n). Es ist leicht durch Induktion zu beweisen, dass wir die korrekten Werte DP ', MINB und MAXB berechnen.

Kurz gesagt lautet der Algorithmus:

  1. Vorverarbeiten, Werte so ändern, dass sie eine Permutation bilden, und der letzte Wert ist der größte Wert.

  2. Binäre Suche nach dem richtigen r mit Anfangsgrenzen 0 <= r <= n

  3. Initialisieren Sie den Segmentbaum mit Nullwerten, setzen Sie DP '[0], MINB [0] und MAXB [0].

  4. Schleife von i = 1 nach n in Schritt i

    • Abfragebereiche [0, V [i]] und [V [i], n] des Segmentbaums,
    • Berechnen von DP '[i], MINB [i] und MAXB [i] basierend auf diesen Abfragen und
    • Setzen des Wertes an Position V [i] im Segmentbaum auf das Tupel (DP '[i], MINB [i], MAXB [i]).
  5. Wenn MINB [n] [r] <= k <= MAXB [n] [r], geben Sie DP '[n] [r] + kr - 1 zurück.

  6. Andernfalls ist, wenn MAXB [n] [r] <k ist, das korrekte r kleiner als das aktuelle r. Wenn MINB [n] [r]> k ist, ist das korrekte r größer als das aktuelle r. Aktualisieren Sie die Grenzen für r und kehren Sie zu Schritt 1 zurück.

Hier ist eine C ++ - Implementierung für diesen Algorithmus. Es findet auch die optimale Teilsequenz.

    #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';
    }

Wir werden nun die beiden Behauptungen beweisen. Das wollen wir beweisen

  1. DP '[a] [r] = DP [a] [b] - rb genau dann, wenn MINB [a] [r] <= b <= MAXB [a] [r]

  2. Für alle a, k existiert eine ganze Zahl r, 0 <= r <= n, so dass MINB [a] [r] <= k <= MAXB [a] [r]

Beides ergibt sich aus der Konkavität des Problems. Konkavität bedeutet, dass DP [a] [k + 2] - DP [a] [k + 1] <= DP [a] [k + 1] - DP [a] [k] für alle a, k. Dies ist intuitiv: Je mehr Ausnahmen wir machen dürfen, desto weniger hilft es uns, eine weitere zuzulassen.

Fix a und r. Setze f (b) = DP [a] [b] - rb und d (b) = f (b + 1) - f (b). Wir haben d (k + 1) <= d (k) aus der Konkavität des Problems. Angenommen, x <y und f (x) = f (y)> = f (i) für alle i. Daher ist d (x) <= 0, also d (i) <= 0 für i in [x, y). Aber f (y) = f (x) + d (x) + d (x + 1) + ... + d (y - 1), daher ist d (i) = 0 für i in [x, y). Daher ist f (y) = f (x) = f (i) für i in [x, y]. Dies beweist den ersten Anspruch.

Um das zweite zu beweisen, setze r = DP [a] [k + 1] - DP [a] [k] und definiere f, d wie zuvor. Dann ist d (k) = 0, daher ist d (i)> = 0 für i <k und d (i) <= 0 für i> k, daher ist f (k) wie gewünscht maximal.

Konkavität zu beweisen ist schwieriger. Einen Beweis finden Sie in meiner Antwort unter cs.stackexchange.

Antti Röyskö
quelle