【动态规划】上升子序列
题意
给一个长度为\(n\)的数组\(a\)。试将其划分为两个严格上升子序列,并使其长度差最小。
\(n\leq 10^5\),\(a_i \in [0,2^{30}]\)
合法的划分方案数不超过\(10^{18}\),记为\(k\)
思路
对于\(i<j\),如果\(a_i\geq a_j\),给它们连边,代表属于不同的子序列。得到了一个连通块,跑二分图匹配,即可判断是否有解。
显然这个连通块中只有1种子序列的排列,所以我们每次遇到一个数,判断它属于哪个子序列,可以求出这个连通块的答案。
每个块都有两个不同的子序列,所以块与块之间的子序列可以随便匹配,用dp求解。
设\(f_{i,j}\)为前\(i\)块相差\(j\)的情况是否存在,\(f_{i,j}=f_{i-1,j+z_i}|f_{i-1,j-z_i}\),其中\(z_i\)代表当前块的差值,意为不同的子序列拼接方法。
但连边的复杂度为\(O(n^2)\),考虑优化。发现连通块肯定是连续的一段,因为如果\(i,j\)有连边,那么\(i<k<j\),\(k\)也会和\(i,j\)的其中一个连边。
于是找出块的分界点\(i\),即\(max(1\sim i)<min(i+1\sim n)\)。
块的个数为\(log\ k\)个,时间复杂度为\(O(nlogk)\)。
代码
#include <cstdio>
#include <cstring>
#include <algorithm>
int t, n, cnt, flag;
int a[100001], h[100001], z[100001], mx[100001], mn[100001], f[2][100001];
int main() {
scanf("%d", &t);
for (; t; t--) {
flag = 0;
cnt = 0;
scanf("%d", &n);
for (int i = 1; i <= n; i++)
scanf("%d", &a[i]), mx[i] = std::max(mx[i - 1], a[i]);
mn[n] = a[n];
for (int i = n - 1; i >= 1; i--)
mn[i] = std::min(mn[i + 1], a[i]);
h[++cnt] = 0;
for (int i = 1; i <= n; i++)
if (mx[i] < mn[i + 1]) h[++cnt] = i;
h[++cnt] = n;
for (int i = 1; i < cnt; i++) {
int aa = -1, bb = -1, xa = 0, xb = 0;
for (int j = h[i] + 1; j <= h[i + 1]; j++)
if (a[j] > aa) aa = a[j], xa++;
else if (a[j] > bb) bb = a[j], xb++;
else flag = 1;
z[i] = abs(xa - xb);
}
if (flag) {
printf("-1\n");
continue;
}
memset(f, 0, sizeof(f));
f[0][0] = 1;
for (int i = 1; i < cnt; i++)
for (int j = 0; j <= n - 2; j++)
f[i & 1][j] = f[(i - 1) & 1][abs(j + z[i])] | f[(i - 1) & 1][abs(j - z[i])];
for (int i = 0; i <= n - 2; i++)
if (f[(cnt - 1) & 1][i]) {
printf("%d\n", i);
break;
}
}
}

浙公网安备 33010602011771号