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

Link

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

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

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

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

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

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

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

  • 时间复杂度被降到了 O ( n 2 m ) O(n^2m) O(n2m)级别的。事实上,此时 m m m完全达不到(因为预处理了!),加上一些剪枝就可以过了。例如,我们枚举的是 f k , l → f i , j f_{k,l}\rightarrow f_{i,j} fk,lfi,j,可如果 max ⁡ f k &lt; f i , j \max{f_k}\lt f_{i,j} maxfk<fi,j那么显然就没有必要去更新 l 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} fp,q+1fi,j.

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

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

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} fi,j={fi1,jmaxk<j,bk<bj{fi1,k}+1,if ai̸=bjif ai=bj

  • 一下子就把时间复杂度降到了 O ( n 2 m ) O(n^2m) O(n2m)。注意到后面那个转移 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 maxk<j,bk<bj{fi1,k}+1=maxk<j,bk<ai{fi1,k}+1,所以可以预处理一下!时间复杂度 O ( n m ) O(nm) O(nm)
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值