【动态规划】上升子序列

题意

给一个长度为\(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;
			}
	}
}
posted @ 2020-08-17 21:57  nymph181  阅读(324)  评论(0)    收藏  举报