(我还不能发布图片,所以我不能为阵列提供LaTex)
我在阅读this 问题后创建了该线程(单击以阅读该问题),其标题完全相同。我意识到,提出问题的人并没有真正理解问题,因为他引用了link,解决了“允许一个更改的最长增长子数组”问题。因此,他得到的答案与LIS问题无关。
假设给定数组 A 的长度为 N 。找到允许最长的递增子序列,并允许 K 。
示例
1)
N = 9,K = 1
A = [3,9,4,5,8,6,1,3,7]
answer:7
说明:
最长递增子序列是:3,4,5,8(或6),1(例外),3,7-> total = 7
2)N = 11,K = 2
A = [5,6,4,7,3,9,2,5,1,8,7]
answer:8
如果K = 1,则只允许一个例外。如果使用已知的算法来计算 O(NlongN)中最长的递增子序列(click me to see this algorithm),那么我们可以计算对于数组A的每个元素,LIS从A [0]到A [N-1]。我们将结果保存到大小为 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
复杂度: O(NlongN + NlongN + N)= O(NlongN)
,因为数组R,L需要NlongN的时间来计算,并且我们还需要Θ(N)才能找到 sol 。
我提供了K = 1的算法。我不知道如何更改上述算法以适用于K个例外,如果有人可以帮助我,我将非常高兴。 谢谢
( ps 如果需要,我可以为C ++中的K = 1算法提供代码)
答案 0 :(得分:7)
此答案从my answer修改为Computer Science Stackexchange上的类似问题。
最多有k个例外的LIS问题允许使用拉格朗日松弛法进行O(nlog²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)中的问题,我们保持对b 我们有DP [i] [j] = 1 + max(DP [i'] [j],DP [x] [j-1]),我们经过i',x
这是该算法的C ++实现。请注意,此实现不假定数组的最后一个元素是它的最大值,或者该数组不包含重复项。 现在,我们将返回O(nlog²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。我们将证明,当且仅当MINB [a] [r] <= k <= MAXB [a] [r]时,DP [a] [k] = DP'[a] [r] + rk。此外,我们将证明对于任何k,存在一个不等式成立的r。 请注意,如果r 为此,我们将需要一个存储元组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])。 将i从1循环到n。有两种以i结尾的子序列:上一个元素大于V [i]的子序列和小于V [i]的子序列。要说明第二种,请查询范围为[0,V [i]]的段树。令结果为(v_1,low_1,high_1)。设置为1 =(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] 这是此算法的C ++实现。它还找到最佳子序列。 我们现在将证明这两个主张。我们希望证明这一点 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 为证明第二个,设置r = DP [a] [k + 1]-DP [a] [k]并如前定义f,d。然后d(k)= 0,因此对于i 证明凹度更加困难。有关证明,请参见c.stackexchange上的my answer。
#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';
}
#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';
}
答案 1 :(得分:0)
幼稚的动态程序可以考虑为第max_j[i][l]
个输入元素存储最大索引i
的概念表j ≤ i
,这样长度l
的递增子序列可以在A[j]
和A[i]
之间形成。我们有
max_j[i][l] = max(max_j[v][l-1])
for min(A[0..i-1]) ≤ v < A[i]
我们的函数/表f[i][k]
可以表示为
f[i][k] = max(
// Skip this element
f[i - 1][k],
// Contribute to a sequence
l + f[j - 1][k - 1]
)
forall 1 < l ≤ longest increasing
subsequence ending at A[i]
(ideally, although there are cheap heuristics)
where j = max_j[i][l]
我声称对l
的遍历将是单峰的,这意味着我们可以对其进行二进制搜索。随着我们增加l
,我们将j
(区间的左索引)移到更左侧,并且由于f
是单调的(总是在上一个值或更大的值之间进行选择),会到达起点,或者l
的成本过高,因此必须设置较大的间隔,以使f
超出最佳点。
答案 2 :(得分:0)
您可以修改标准算法以获取O(k * n * logn),尽管我不确定,我认为二进制搜索仅对n执行一次,而不对k次执行。因为您不需要知道实际的序列就不需要跟踪列表,所以知道最后一个数字就足够了,这使它稍微容易一些。这是带有注释的代码(适用于您的示例,但我不确定边缘情况100%):
private static int lis(int[] numbers, int k) {
if (numbers.length <= k + 1)
return numbers.length;
int[][] active = new int[k + 1][numbers.length + 1]; // exceptions, length, last number
int[] start = new int[k + 1]; // at which length the lists with x exceptions start
int[] end = new int[k + 1]; // at which length the lists with x exceptions end
// add first number
active[0][1] = numbers[0];
start[0] = 1;
end[0] = 1;
// go through the rest of the numbers using the standard algorithm
for (int i = 1; i < numbers.length; i++) {
for (int ex = k; ex >= 0; ex--) {
// can we just extend?
if (end[ex] > 0 && active[ex][end[ex]] < numbers[i]) {
active[ex][++end[ex]] = numbers[i];
// can we create a better list using the largest with one less exception?
} else if (ex > 0 && end[ex - 1] > 0 && active[ex - 1][end[ex - 1]] > numbers[i]) {
if (end[ex] == 0) { // no entries yet for this number of exceptions
end[ex] = end[ex - 1] + 1;
start[ex] = end[ex];
active[ex][end[ex]] = numbers[i];
} else if (active[ex][end[ex - 1] + 1] > numbers[i]) {
active[ex][end[ex - 1] + 1] = numbers[i];
}
// did we reach a new minimum?
} else if (ex == 0 && active[0][1] > numbers[i]) {
active[0][1] = numbers[i];
// update value using binary search like the standard algorithm
} else if (start[ex] > 0 && numbers[i] > active[ex][start[ex]]) {
int left = start[ex];
int right = end[ex];
while (left + 1 != right) {
int mid = left + right / 2;
if (active[ex][mid] < numbers[i]) {
left = mid;
} else {
right = mid;
}
}
if (active[ex][right] > numbers[i]) {
active[ex][right] = numbers[i];
}
}
}
}
return Arrays.stream(end).max().getAsInt();
}