【動態規劃】上升子序列
阿新 • • 發佈:2020-08-17
題意
給一個長度為\(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}\)
但連邊的複雜度為\(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; } } }