1. 程式人生 > 實用技巧 >【動態規劃】上升子序列

【動態規劃】上升子序列

題意

給一個長度為\(n\)的陣列\(a\)。試將其劃分為兩個嚴格上升子序列,並使其長度差最小。
\(n\leq 10^5\),\(a_i \in [0,2^{30}]\)

思路

對於\(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)\)

程式碼

#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;
			}
	}
}