nssl 1488.上升子序列

D e s c r i p t i o n Description Description

给定一个长度为 n n n的序列,将其划分为两个严格单调递增的子序列,求这两个序列长度的最小差值,若无解,输出-1(保证合法的方案数不超过 1 0 18 10^{18} 1018

数据范围: n ≤ 1 0 5 n\leq 10^5 n105


S o l u t i o n Solution Solution

假设只有一种合法方案,设两个序列分别为 q 1 , q 2 q_1,q_2 q1,q2,能接到 q 1 q_1 q1就接 q 1 q_1 q1,否则就接 q 2 q_2 q2,接不了则无解,复杂度 O ( n ) O(n) O(n)

而我们知道,一个单调递增的序列,必然可以拆分成若干个连续的单调递增的子串(后面会给出证明),如果这些子串满足它们的方案唯一,那么我们就可以通过拼凑这些子串来得到最终答案

假设我们已经拆分出了 c n t cnt cnt个这样的子串,每个子串要么去到 q 1 q_1 q1,要么去到 q 2 q_2 q2,所以是一个0/1序列,方案最多为 2 c n t 2^{cnt} 2cnt种,题目已经给出答案最多有 1 0 18 10^{18} 1018种,所以 c n t cnt cnt最大也就是六十几,我们完全可以做 d p dp dp解决

问题是如何找到这些子串(如何划分)呢?
首先我们知道,如果存在 ∀ i < j \forall ii<j如果 a i ≥ a j a_i\geq a_j aiaj,那么 a i a_i ai a j a_j aj必定不属于同一个单调递增的子序列,所以我们可以利用二分图的思想,将它们连边,表示这两个不能染成同一种颜色,做完二分图匹配后,我们就可以划分出 c n t cnt cnt来,但这样的复杂度是 O ( n 2 ) O(n^2) O(n2)

所以我们就要继续思考,继续发现对于 ∀ i < k < j \forall ii<k<j,如果 i , j i,j i,j有连边,那么 k k k必然和 i , j i,j i,j中的任何一个有连边(也有可能两个都连边)【因为如果 a k > a i a_k>a_i ak>ai k k k连向 j j j a j < a k < a i a_jaj<ak<ai,两个都连, a k < a j a_kak<aj,连 i i i
所以这样的划分块一定是连续的(也就是我们刚刚说的,它一定会是若干个子串)

那么,我们就要找出这样块的分界点,满足它前面的不可能和后面的连边,转换成几何语言就是找到所有的 i i i,满足 m a x ( a 1 ∼ i ) ≤ m i n ( a i + 1 ∼ n ) max(a_{1\sim i})\leq min(a_{i+1\sim n}) max(a1i)min(ai+1n),这样的 i i i必然是这些块的分界点,这个东西是可以 O ( n ) O(n) O(n)维护的

而我们又知道,每个这样的块中,它的答案是唯一的,我们只需要开两个队列,按照刚开始说的那种方法,求出每个块队列长度的差值 z [ i ] = q 1 . s i z e ( ) − q 2 . s i z e ( ) z[i]=q_1.size()-q_2.size() z[i]=q1.size()q2.size()【这样子复杂度显然是 O ( n ) O(n) O(n)的】,然后做 d p dp dp

f [ i ] [ j ] f[i][j] f[i][j]表示前 i i i个块,两个单调递增子序列长度的差值为 j j j的情况是否存在,则对于每一个块,我们可以选择放入 q 1 q_1 q1,也可以选择放入 q 2 q_2 q2,即
f [ i ] [ j ] = f [ i − 1 ] [ ∣ j + z [ i ] ∣ ] ∣ f [ i − 1 ] [ ∣ j − z [ i ] ∣ ] f[i][j]=f[i-1][|j+z[i]|]|f[i-1][|j-z[i]|] f[i][j]=f[i1][j+z[i]]f[i1][jz[i]]

时间复杂度: O ( c n t n ) O(cntn) O(cntn),其中 c n t cnt cnt是一个上限为64的常数


C o d e Code Code

#include
#include 
#include
#include
#define N 100010
#define LL long long
using namespace std;int t,n,a[N],maxn[N],minn[N],st[N],ed[N],cnt,z[70];
queue<int>q1,q2;
bool flag,f[70][N];
inline LL read()
{
	char c;LL d=1,f=0;
	while(c=getchar(),!isdigit(c)) if(c=='-') d=-1;f=(f<<3)+(f<<1)+c-48;
	while(c=getchar(),isdigit(c)) f=(f<<3)+(f<<1)+c-48;
	return d*f;
}
signed main()
{
	t=read();
	while(t--)
	{
		n=read();maxn[0]=-1e9;minn[n+1]=1e9;
		for(register int i=1;i<=n;i++) a[i]=read(),maxn[i]=max(maxn[i-1],a[i]);
		for(register int i=n;i>=1;i--) minn[i]=min(minn[i+1],a[i]);
		cnt=0;
		for(register int i=1;i<=n;i++) 
		if(maxn[i]<=minn[i+1]||i==n)
		{
			st[++cnt]=ed[cnt-1]+1;
			ed[cnt]=i;
		}
		if(cnt>67) {puts("-1");continue;}
		flag=false;
		for(register int i=1;i<=cnt;i++)
		{
			while(q1.size()) q1.pop();
			while(q2.size()) q2.pop();
			for(register int j=st[i];j<=ed[i];j++)
			{
				if(q1.empty()||a[j]>q1.back()) q1.push(a[j]);else
				if(q2.empty()||a[j]>q2.back()) q2.push(a[j]);else
				{flag=true;break;}
			}
			if(flag) break;
			z[i]=q1.size()-q2.size();
		}
		if(flag) {puts("-1");continue;}
		f[0][0]=1;
		for(register int i=1;i<=cnt;i++)
		 for(register int j=0;j<=n;j++)
		  f[i][j]=f[i-1][abs(j+z[i])]|f[i-1][abs(j-z[i])];
		for(register int j=0;j<=n;j++)
		 if(f[cnt][j]) {printf("%d\n",j);break;}
	}
}

你可能感兴趣的:(贪心,dp,nssl,1488,上升子序列)