この回答は、コンピューターサイエンス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)を格納し、次の操作をサポートするセグメントツリーが必要です。
範囲[a、b]を指定して、その範囲の最大値(最大v_i、a <= i <= b)、および範囲内のその値と対になっている最小の下限と上限の上限を見つけます。
タプル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を計算することは簡単に証明できます。
つまり、アルゴリズムは次のとおりです。
前処理、値を変更して順列を形成し、最後の値が最大値になるようにします。
初期境界0 <= r <= nでの正しいrの二分探索
null値でセグメントツリーを初期化し、DP '[0]、MINB [0]、およびMAXB [0]を設定します。
ステップiでi = 1からnにループ
- セグメントツリーの範囲[0、V [i]]および[V [i]、n]のクエリ、
- これらのクエリに基づいてDP '[i]、MINB [i]およびMAXB [i]を計算し、
- セグメントツリーの位置V [i]の値をタプルに設定します(DP '[i]、MINB [i]、MAXB [i])。
MINB [n] [r] <= k <= MAXB [n] [r]の場合、DP '[n] [r] + kr-1を返します。
それ以外の場合、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つの主張を証明します。それを証明したい
DP '[a] [r] = DP [a] [b]-RB(MINB [a] [r] <= b <= MAXB [a] [r]の場合のみ
すべての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で私の回答を参照してください。