提高组(计数)

题目链接

类题:冒泡排序

求长度为 \(n\) 的排列中满足最长下降子序列长度不超过 2 ,且符合\(p_x=y\) 的排列数。\(n \le 10^7,T \le 10^6\)

题意转化:不存在三个点,使得左边的点比中间大,右边的点比中间小。

我们要知道一个 trick : 从大到小/从小到大枚举数,尝试将其插入当前排列,并使之合法。这样的话我们在插入的时候只需要保证如果前面有比它大的的话,以后比它小的就不能放后边了。由于我们从大到小放,当前序列上的数都比他大,之后的数都比他小,于是我们要么放最前面,要么放到某个位置,并且要求以后放的数都要在它之前。

我们发现这可能会有一个“只能放在前 \(j\) 个数之后”的限制。于是设置DP状态:\(f_{i,j}\) 表示已经放了 \(i\) 个数,要求以后只能放在前 \(j\) 个数之后(或者最前面)的方案数,那么转移就比较显然了:

放最前面:

\[f_{i,j} \to f_{i+1,j+1} \]

放第 \(k\) 个的后面:

\[f_{i,j} \to f_{i+1,k},k\le j \]

总的来说:

\[f_{i,j} \to f_{i+1,k},k\le j+1 \]

如果我们在一张网格图上看,这像是网格图上的路径;经过一些转化,我们发现转移其实就是只能向右上走,且不能穿过 \(y=x\) 的一条条路径,一次转移相当于向右走一步,再向上走若干步。

然后题目还要求 \(p_x=y\)。如果 \(x=y\),那么第 \(x\) 个之前一定都是比 \(y\) 小的,第 \(x\) 个之后一定都是比 \(y\) 大的。直接单独用卡特兰数算后乘起来即可。

如果 \(x > y\),那么我们在放第 \(y\) 小的数的时候就不能让它放到最前面了,否则仅剩的 \(y-1\) 个数不足以支持其成为第 \(x\) 个数。那么他只能放到某一个数的后面,这样的话以后比他小的数就都会放到他的前面,那么这个数到底插到第几个数的后面就知道了,即能算出一个数 \(k\),使得所有第 \(x-1\) 列的状态都要转移到 \(f_{x,k}\)。然后问题转化为了一个网格图上必经一点的“卡特兰数”,亦用翻折法可解(根据 \(y=x+1\) 对称)。

如果 \(x < y\),看起来它是可以放到最前面的,并且对以后的影响也不太好处理。不过这种问题实际上可以转化为 \(x \gets n - x + 1, y \gets n - y + 1\) 的问题(全部取反后成为反向问题)

下面给出一些关键参数:

由DP转化为“卡特兰数”网格图的方法:

\[(x,y) \to (x-1, x-y) \]

这种转化方法将“放在最前面”映射成了横着走

必经的那个点:

\[(n-y,n-x+1) \]

根据 \(y=x+1\) 翻折:

\[(x,y) \to (y-1,x+1) \]

路径:

\[(0,0) \to (n-y,n-x+1) \to (n,n) \]

注意,我们不仅要求经过一点,还要求经过前一步不能横着走过来,经过的后一步要先向右走一步。

关键代码:

inline ll calc(int n, int m) {
	ll res = (get_c(n + m, n) - get_c(n + m, n + 1) + P) % P;
	return res;
}

inline void work() {
	int n, x, y; read(n), read(x), read(y);
	if (x == y) {
		ll ans = calc(x - 1, x - 1) * calc(n - x, n - x) % P;
		printf("%lld\n", (ans % P + P) % P);
		return ;
	}
	if (x < y)	x = n - x + 1, y = n - y + 1;
	ll ans = (calc(n - y, n - x + 1) - (x == y + 1 ? 0 : calc(n - y - 1, n - x + 1))) * (get_c(y-1+x-1,y-1) - get_c(y-2+x,x)) % P;
	//Attention!!! 
	printf("%lld\n", (ans % P + P) % P);
}
posted @ 2020-09-14 15:07  JiaZP  阅读(138)  评论(0编辑  收藏  举报