JZOJ4762(DP、最长公共上升子序列)

版权声明:蒟蒻写的文章,能看就行了,同时欢迎大佬们指点错误 https://blog.csdn.net/Algor_pro_king_John/article/details/86770595
Link

https://jzoj.net/senior/#main/show/4762

Problem
  • 给定 { a n } , { b m } \{a_n\},\{b_m\} 两个数组

  • 求其最长公共严格上升子序列,并输出这个子序列。

Data constraint
  • n , m 5000 , a i , b i 2 30 n,m\le 5000, a_i,b_i\le 2^{30}
Solution
  • 这道题实质上有两种方法,一种是我自己在考场的思路,实质上是个暴力,但复杂度玄学,加一些剪枝竟然被我卡过了。
My method
  • 实质上就是设 f i , j f_{i,j} 表示 A A 序列到 i i 位置, B B 序列到 j j 位置的最长公共子序列,且 i , j i,j 必选,即必须有 A i = B j A_i=B_j 的最长公共子序列。

  • 转移很显然是枚举一个 k k ,然后由 f k , l f_{k,l} 去转移,这样的时间复杂度是接近 O ( n 2 m 2 ) O(n^2m^2) 的,虽然实质上会小很多。

  • 我们考虑优化,显然,当确定了一个 k k 后,因为要保证 A k = B l A_k=B_l ,所以 l l 的个数显然不会很多。事实上,我们可以先预处理每个 A i A_i 所对应的所有相等的 B j ( j i ) B_j|(j\ge i) .

  • 然而这样的时间复杂度依然在 O ( n 2 m 2 ) O(n^2m^2) 级别!我们可以考虑优化枚举顺序。我们先枚举一个 i i ,再枚举 k k ,此时枚举 j j ,那么随着 j j 的递增,最优的 f k , l f_{k,l} 一定是在 k k 固定时,并且满足单调的前提下,让 l l 尽量小,所以此时显然 l l 是可以单调的。

  • 时间复杂度被降到了 O ( n 2 m ) O(n^2m) 级别的。事实上,此时 m m 完全达不到(因为预处理了!),加上一些剪枝就可以过了。例如,我们枚举的是 f k , l f i , j f_{k,l}\rightarrow f_{i,j} ,可如果 max f k < f i , j \max{f_k}\lt f_{i,j} 那么显然就没有必要去更新 l l 了!

#include <bits/stdc++.h>
#define F(i, a, b) for (I i = a; i <= b; i ++)
#define G(i, a, b) for (I i = a; i >= b; i --)
#define max(a, b) ((a) > (b) ? (a) : (b))
#define I register int
using namespace std;
const int N = 5e3 + 10;

int n, a[N], m, L, cnt, b[N], r[N], h[N], mx[N], Len[N];
int f[N][N], c[N][N], Ans, k1, k2;

struct node { int num, x; } d[N * 2];
bool cmp(node x, node y) { return x.x < y.x; }
struct Go { int x, y; } g[N][N];
void Doit() {
	F(i, 1, n) {
		I x = a[i], Sx = Len[x];
		if (Sx)	G(p, i - 1, 0) {
			I y = a[p], q = 0, Sy = Len[y];
			if ((x > y) && Sy)
				F(j, 0, Sx - 1)
					if (mx[p] + 1 > f[i][j]) {
						while (q < Sy - 1 && c[y][q + 1] < c[x][j])
							q ++;
						if (c[y][q] < c[x][j] && f[p][q] + 1 > f[i][j]) {
							f[i][j] = f[p][q] + 1;
							g[i][j] = {p, q};
							mx[i] = max(mx[i], f[i][j]);
							if (f[i][j] == cnt)
								return;
						}
					}
		}
	}
}

int main() {
	scanf("%d", &n);
	F(i, 1, n) scanf("%d", &a[i]), d[++ L] = {i, a[i]}, h[i] = a[i];
	scanf("%d", &m);
	F(i, 1, m) scanf("%d", &b[i]), d[++ L] = {n + i, b[i]};

	sort(d + 1, d + L + 1, cmp), d[0].x = d[1].x - 1;
	F(i, 1, L) {
		cnt += d[i].x != d[i - 1].x;
		d[i].num > n ? b[d[i].num - n] = cnt : a[d[i].num] = cnt;
	}
	F(i, 1, m)
		c[b[i]][Len[b[i]] ++ ] = i;
	F(i, 1, N - 1)
		sort(c[i], c[i] + Len[i]);
	F(i, 1, n)
		if (Len[a[i]])
			F(j, 0, Len[a[i]] - 1)
				f[i][j] = mx[i] = Ans = 1, k1 = i, k2 = 0;

//	I st = clock();
	Doit();
//	printf("%lf\n", (double) (clock() - st) / CLOCKS_PER_SEC);

	F(i, 1, n)
		if (Len[a[i]])
			F(j, 0, Len[a[i]] - 1)
				if (f[i][j] > Ans) Ans = f[i][j], k1 = i, k2 = j;
	printf("%d\n", Ans), L = 0;

	while (k1 || k2) {
		r[++ L] = h[k1]; I x = k1;
		k1 = g[x][k2].x, k2 = g[x][k2].y;
	}

	G(i, L, 1)
		printf("%d ", r[i]);
	puts("");
}
Official Solution
  • 我们观察到上面的方法实质上是 f p , q + 1 f i , j f_{p,q}+1\Rightarrow f_{i,j} .

  • 显然,这样转移太笨了,虽然当我们去除了很多冗余状态后可以拿到一个不错的分数,但实质上我们还有更简单的方法。

  • 我们可以设 f [ i ] [ j ] f[i][j] 表示以 A A 序列的前 i i 个数中某个和 B j B_j 结尾的最长长度。然后我们可以得出一个这样的转移:

f i , j = { f i 1 , j if  a i b j max k &lt; j , b k &lt; b j { f i 1 , k } + 1 , if  a i = b j f_{i,j} = \begin{cases} f_{i-1,j} &amp; \text{if $a_i\neq b_j$} \\ \max_{k\lt j,b_k\lt b_j}\{f_{i-1,k}\}+1, &amp; \text{if $a_i=b_j$} \end{cases}

  • 一下子就把时间复杂度降到了 O ( n 2 m ) O(n^2m) 。注意到后面那个转移 max k &lt; j , b k &lt; b j { f i 1 , k } + 1 = max k &lt; j , b k &lt; a i { f i 1 , k } + 1 \max_{k\lt j,b_k\lt b_j}\{f_{i-1,k}\}+1=\max_{k\lt j,b_k\lt a_i}\{f_{i-1,k}\}+1 ,所以可以预处理一下!时间复杂度 O ( n m ) O(nm)

猜你喜欢

转载自blog.csdn.net/Algor_pro_king_John/article/details/86770595