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


8

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

Kの例外が許可された最長の増加するサブシーケンスを読んだ後、このスレッドを作成しました。質問をしている人は、「1つの変更を許可して最長のサブアレイを増やす」問題を解決するリンクを参照していたため、問題を本当に理解していないことに気付きました。したがって、彼が得た答えは実際には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 [0]からA [N-1]までのLISを計算できます。 A.結果をサイズNの新しい配列Lに保存します。例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時間を必要とし、solを見つけるためにΘ(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アルゴリズムのコードを提供できます。)


このトリックは、n log nでそれを解決するのに役立ちます。つまり、バイナリインデックスツリーアルゴリズムを使用して、数量LIS-x k 'を計算します。ここで、* k'は例外であり、xは任意の値です。それぞれの検索バイナリそんなにX、およびget 'kは、あなたが得るまで* = * K K'を。バイナリ検索で必要なkが得られず、小さい方から大きい方にジャンプする場合には、特別な注意が必要です。
ilias_pap

@ilias_papは、アルゴリズム全体を詳しく説明し、詳しく説明してください。あなたが言及しているバイナリ検索の各反復がO(n)でどのように実行されるかはコメントから完全に不明確です。
גלעדברקן

同様の質問については、cs.stackexchange.com / q / 118858/755も参照してください。
DW

回答:


7

この回答は、コンピューターサイエンスStackexchangeでの同様の質問に対する私の回答から変更されています

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

DP [a] [b]が最大で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、したがって、[x、y)のiに対してd(i)<= 0になります。しかし、f(y)= f(x)+ d(x)+ d(x + 1)+ ... + d(y-1)なので、[x、y)のiに対してd(i)= 0になります。したがって、[x、y]のiに対してf(y)= f(x)= f(i)です。これは最初の主張を証明します。

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

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

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.