web-dev-qa-db-ja.com

最長K順次増加サブシーケンス

重複したスレッドを作成した理由

私はこのスレッドを作成した後に作成しました K例外を許可した最長のサブシーケンス 。質問をしている人は、問題を理解していないことに気付きました。なぜなら、「1つの変更で許可される最長のサブアレイ」問題を解決する link を参照していたからです。したがって、彼が得た答えは実際にはLIS問題とは無関係でした。

問題の説明

配列[〜#〜] a [〜#〜]が長さ[〜#〜] n [〜#〜][〜#〜] k [〜#〜]例外を許可して、最も長く増加するサブシーケンスを見つけます。


1)N = 9、K = 1

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

正解:7

説明:

最も長いサブシーケンスの増加は、3、4、5、8(または6)、1(例外)、3、7->合計= 7です。

2)N = 11、K = 2

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

答え:8

これまでに行ったこと...

K = 1の場合、許可される例外は1つだけです。 O(NlogN)の最長増加サブシーケンスを計算するための既知のアルゴリズムが使用されている場合( このアルゴリズムを表示するにはここをクリック )、配列Aの各要素のA [0]からA [N-1]で始まるLIS。結果を新しい配列に保存します[〜#〜] l [〜#〜]サイズ付き[〜#〜] n [〜#〜]。例n.1を見ると、L配列はL = [1,2,2,3,4,4,4,4,5]。になります。

逆のロジックを使用して、配列[〜#〜] r [〜#〜]を計算します。その各要素には、N-1から0までの現在の最長減少シーケンスが含まれています。

例外が1つあるLISは、sol = max(sol、L [i] + R [i + 1])、solsol = L [N-1]として初期化されます。したがって、LISを0からインデックスi(例外)まで計算し、N-1まで新しいLISを停止して開始します。

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

->ステップバイステップの説明:

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

複雑さ:O(NlogN + NlogN + N)= O(NlogN)

配列R、Lは計算にNlogN時間を必要とし、を見つけるにはΘ(N)も必要なので)ソル

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

K例外への一般化

K = 1のアルゴリズムを提供しました。上記のアルゴリズムをK例外で機能するように変更する方法はわかりません。誰かが私を助けてくれたらうれしいです。

PS。必要に応じて、C++でK = 1アルゴリズムのコードを提供できます。)

8
Ermolai

この回答は my answer からComputer Science Stackexchangeでの同様の質問に変更されています。

最大でk個の例外があるLIS問題は、ラグランジュ緩和を使用したO(nlog²n)アルゴリズムを許可します。 kがlog nより大きい場合、これはO(nk log n)DPで漸近的に向上します。これについても簡単に説明します。

要素で終了する最大bの例外(前の整数が次の整数よりも大きい位置)で、最も長く増加するサブシーケンスの長さをDP [a] [b]で表すとする b a。このDPはアルゴリズムに含まれていませんが、それを定義するとアルゴリズムの証明が容易になります。

便宜上、すべての要素が異なり、配列の最後の要素が最大であると想定します。すべての数値のm番目の外観にm/2nを追加し、配列に無限大を追加して、答えから1を引くことができるため、これは私たちを制限しないことに注意してください。 1 <= V [i] <= nがi番目の要素の値である順列をVとします。

O(nk log n)の問題を解決するために、DP [a] [b]がb <jに対して計算されたという不変条件を維持します。すべてのaに対してDP [a] [j]を計算するj番目の反復で、jを0からkにループします。これを行うには、iを1からnにループします。 x <i上のDP [x] [j-1]の最大値と、インデックスiでx <iの位置V [x]にDP [x] [j]を持つプレフィックス最大データ構造を維持します。他のすべての位置で。

DP [i] [j] = 1 + max(DP [i '] [j]、DP [x] [j-1])があり、i'、x <i、V [i '] < V [i]。接頭辞の最大値DP [x] [j-1]は2番目のタイプの項の最大値を示し、接頭辞[0、V [i]]の接頭辞最大データ構造をクエリすると、最初の項の最大値が得られます。タイプ。次に、最大プレフィックスおよび最大プレフィックスのデータ構造を更新します。

これは、アルゴリズムのC++実装です。この実装では、配列の最後の要素がその最大値であること、または配列に重複が含まれていないことを想定していないことに注意してください。


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

次に、O(nlog²n)アルゴリズムに戻ります。 0 <= r <= nの整数を選択します。 DP '[a] [r] = max(DP [a] [b]-rb)を定義します。ここで、最大値はbに取られ、MAXB [a] [r]は、DP' [a] [ r] = DP [a] [b]-rb、およびMINB [a] [r]は、そのような最小のbと同様に。 MINB [a] [r] <= k <= MAXB [a] [r]の場合に限り、DP [a] [k] = DP '[a] [r] + rkであることを示します。さらに、任意のkに対して、この不等式が成り立つrが存在することを示します。

MINB [a] [r]> = MINB [a] [r ']およびMAXB [a] [r]> = MAXB [a] [r']であることに注意してください。結果、rのバイナリ検索を実行して、O(log n)値を試すことができます。したがって、O(n log n)時間でDP '、MINB、およびMAXBを計算できれば、複雑度O(nlog²n)を達成できます。

これを行うには、タプルP [i] =(v_i、low_i、high_i)を格納し、次の操作をサポートするセグメントツリーが必要です。

  1. 範囲[a、b]を指定して、その範囲の最大値(最大v_i、a <= i <= b)、および範囲内のその値と対になっている最小の下限と上限の上限を見つけます。

  2. タプルP [i]の値を設定します。

これは、セグメントツリーにある程度の知識があることを前提として、操作ごとに複雑なO(log n)時間で実装するのは簡単です。詳細については、以下のアルゴリズムの実装を参照してください。

O(n log n)でDP '、MINB、MAXBを計算する方法を示します。 rを修正します。最初にn + 1個のnull値(-INF、INF、-INF)を含むセグメントツリーを構築します。現在の位置iより小さいjについて、P [V [j]] =(DP '[j]、MINB [j]、MAXB [j])を維持します。 r> 0の場合はDP '[0] = 0、MINB [0] = 0およびMAXB [0]を0に設定し、それ以外の場合はINFおよびP [0] =(DP' [0]、MINB [0 ]、MAXB [0])。

Iを1からnにループします。 iで終わるサブシーケンスには2つのタイプがあります。前の要素がV [i]より大きいものと、V [i]より小さいものです。 2番目の種類を説明するには、[0、V [i]]の範囲でセグメントツリーをクエリします。結果を(v_1、low_1、high_1)にします。 off1 =(v_1 + 1、low_1、high_1)に設定します。最初の種類については、[V [i]、n]の範囲のセグメントツリーをクエリします。結果を(v_2、low_2、high_2)とします。 off2 =(v_2 + 1-r、low_2 + 1、high_2 + 1)に設定します。ここで、例外を作成するとrのペナルティが発生します。

次に、off1とoff2を組み合わせてoffにします。 off1.v> off2.vの場合、off = off1に設定し、off2.v> off1.vの場合、off = off2に設定します。それ以外の場合は、off =(off1.v、min(off1.low、off2.low)、max(off1.high、off2.high))を設定します。次に、DP '[i] = off.v、MINB [i] = off.low、MAXB [i] = off.high、P [i] = offに設定します。

すべてのiで2つのセグメントツリークエリを作成するため、合計でO(n log n)時間かかります。誘導によって、正しい値DP '、MINB、MAXBを計算することは簡単に証明できます。

つまり、アルゴリズムは次のとおりです。

  1. 前処理、値を変更して順列を形成し、最後の値が最大値になるようにします。

  2. 初期境界0 <= r <= nでの正しいrの二分探索

  3. Null値でセグメントツリーを初期化し、DP '[0]、MINB [0]、およびMAXB [0]を設定します。

  4. ステップiでi = 1からnにループ

    • セグメントツリーの範囲[0、V [i]]および[V [i]、n]のクエリ、
    • これらのクエリに基づいてDP '[i]、MINB [i]およびMAXB [i]を計算し、
    • セグメントツリーの位置V [i]の値をタプルに設定します(DP '[i]、MINB [i]、MAXB [i])。
  5. MINB [n] [r] <= k <= MAXB [n] [r]の場合、DP '[n] [r] + kr-1を返します。

  6. それ以外の場合、MAXB [n] [r] <kの場合、正しいrは現在のrよりも小さくなります。 MINB [n] [r]> kの場合、正しいrは現在のrよりも大きくなります。 rの境界を更新し、手順1に戻ります。

このアルゴリズムのC++実装は次のとおりです。また、最適なサブシーケンスを見つけます。

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

ここで、2つの主張を証明します。それを証明したい

  1. DP '[a] [r] = DP [a] [b]-RB(MINB [a] [r] <= b <= MAXB [a] [r]の場合のみ

  2. すべてのa、kについて、整数r、0 <= r <= nが存在し、MINB [a] [r] <= k <= MAXB [a] [r]

これらは両方とも、問題の凹面から生じます。凹面とは、すべてのa、kに対してDP [a] [k + 2]-DP [a] [k + 1] <= DP [a] [k + 1]-DP [a] [k]であることを意味します。これは直感的です。許可する例外が多いほど、許可する許可が少なくなればなるほど役立ちます。

Aとrを修正します。 f(b) = DP [a] [b]-rb、およびd(b) = f(b + 1)-f(b)を設定します。問題の凹面からd(k + 1)<= d(k)があります。すべてのiについて、x <yおよびf(x) = f(y)> = f(i)と仮定します。したがって、d(x) <= 0、つまり、d(i) <= 0は、[x、y)のiに対して0になります。しかし、f(y) = f(x) + d(x) + d(x + 1)+ ... + d(y-1)、したがってd(i) = [x、y)のiに対して0。したがって、f(y) = f(x) = f(i)は、[x、y]のiに対してです。これは最初の主張を証明します。

2番目を証明するには、r = DP [a] [k + 1]-DP [a] [k]を設定し、以前のようにf、dを定義します。次に、d(k) = 0、したがってd(i)> = 0 for i <kおよびd(i) <= 0 for i> k、したがってf(k)は必要に応じて最大にします。

凹面を証明することはより困難です。証明については、cs.stackexchangeの my answer を参照してください。

7
Antti Röyskö