Erm*_*lai 7 arrays algorithm dynamic-programming greedy lis
我在阅读了Longest increase subsequence with K exceptions allowed之后创建了这个线程。我意识到,谁问这个问题的人并没有真正理解这个问题,因为他指的是一个链接解决了“最长递增子阵列与一个变化允许”的问题。所以他得到的答案实际上与 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(exception),3,7 -> total=7
2) N=11 , K=2
A=[5,6,4,7,3,9,2,5,1,8,7]
答案:8
如果 K=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 的最长递减序列。
除了一个例外,LIS 只是sol=max(sol,L[i]+R[i+1]), 其中sol被初始化为sol=L[N-1]。所以我们从 0 开始计算 LIS 直到索引i(异常),然后停止并开始一个新的 LIS 直到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
Run Code Online (Sandbox Code Playgroud)
->分步说明:
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
Run Code Online (Sandbox Code Playgroud)
复杂度: O(NlogN + NlogN + N) = O(NlogN)
因为数组R, L需要 NlogN 时间来计算,我们还需要 ?(N) 才能找到sol。
k=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
Run Code Online (Sandbox Code Playgroud)
我已经提供了 K=1 的算法。我不知道如何更改上述算法以适用于 K 异常。如果有人可以帮助我,我会很高兴。
(PS。如果需要,我可以提供 C++ 中 K=1 算法的代码。)
这个答案修改自 我在 Computer Science Stackexchange 上对类似问题的。
最多有 k 个例外的 LIS 问题承认使用拉格朗日松弛的 O(n log² n) 算法。当 k 大于 log n 时,这会在 O(nk log n) DP 上渐进改进,我们还将对此进行简要说明。
让 DP[a][b] 表示最长递增子序列的长度,最多有 b 个例外(前一个整数大于下一个整数的位置)在元素b a处结束。这个 DP 不涉及算法,但定义它可以更容易地证明算法。
为方便起见,我们假设所有元素都是不同的,并且数组中的最后一个元素是其最大值。请注意,这并不限制我们,因为我们可以将 m / 2n 添加到每个数字的第 m 个出现处,并将无穷大附加到数组并从答案中减去 1。令 V 为 1 <= V[i] <= n 是第 i 个元素的值的排列。
为了解决 O(nk log n) 中的问题,我们保持 DP[a][b] 已经为 b < j 计算的不变量。从 0 到 k 循环 j,在第 j 次迭代计算所有 a 的 DP[a][j]。为此,将 i 从 1 循环到 n。我们保持 DP[x][j-1] 在 x < i 上的最大值和一个前缀最大数据结构,在索引 i 将有 DP[x][j] 在位置 V[x] for x < i, 和 0在所有其他位置。
我们有 DP[i][j] = 1 + max(DP[i'][j], DP[x][j-1]) 在这里我们经过 i', x < i, V[i'] < V[i]。DP[x][j-1] 的前缀最大值给出了第二类词条的最大值,查询前缀 [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';
}
Run Code Online (Sandbox Code Playgroud)
现在我们将回到 O(n log² n) 算法。选择一些整数 0 <= r <= n。定义DP'[a][r] = max(DP[a][b] - rb),其中取最大值超过b,MAXB[a][r]为最大值b,使得DP'[a][ r] = DP[a][b] - rb,而 MINB[a][r] 与 b 的最小值类似。我们将证明 DP[a][k] = DP'[a][r] + rk 当且仅当 MINB[a][r] <= k <= MAXB[a][r]。此外,我们将证明对于任何 k 都存在一个 r,该不等式成立。
请注意,如果 r < 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(n log² 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 个空值(-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])。
从 1 到 n 循环 i。有两种以 i 结尾的子序列:前一个元素大于 V[i] 的子序列和小于 V[i] 的子序列。为了解决第二种情况,请查询 [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 处进行两次段树查询,因此总共需要 O(n log n) 时间。通过归纳很容易证明我们计算了正确的值 DP'、MINB 和 MAXB。
简而言之,算法是:
预处理,修改值,使它们形成排列,最后一个值是最大值。
二进制搜索正确的 r,初始边界 0 <= r <= n
用空值初始化段树,设置 DP'[0]、MINB[0] 和 MAXB[0]。
从 i = 1 循环到 n,在步骤 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';
}
Run Code Online (Sandbox Code Playgroud)
现在我们将证明这两个主张。我们希望证明
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]
这两者都是从问题的凹性推导出来的。凹度意味着 DP[a][k+2] - DP[a][k+1] <= DP[a][k+1] - DP[a][k] 对于所有 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。因此 f(y) = f(x) = f(i) for i in [x, y]。这证明了第一个主张。
为了证明第二个,设置 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 上的回答。