提高组(计数)
求长度为 \(n\) 的排列中满足最长下降子序列长度不超过 2 ,且符合\(p_x=y\) 的排列数。\(n \le 10^7,T \le 10^6\)
题意转化:不存在三个点,使得左边的点比中间大,右边的点比中间小。
我们要知道一个 trick : 从大到小/从小到大枚举数,尝试将其插入当前排列,并使之合法。这样的话我们在插入的时候只需要保证如果前面有比它大的的话,以后比它小的就不能放后边了。由于我们从大到小放,当前序列上的数都比他大,之后的数都比他小,于是我们要么放最前面,要么放到某个位置,并且要求以后放的数都要在它之前。
我们发现这可能会有一个“只能放在前 \(j\) 个数之后”的限制。于是设置DP状态:\(f_{i,j}\) 表示已经放了 \(i\) 个数,要求以后只能放在前 \(j\) 个数之后(或者最前面)的方案数,那么转移就比较显然了:
放最前面:
放第 \(k\) 个的后面:
总的来说:
如果我们在一张网格图上看,这像是网格图上的路径;经过一些转化,我们发现转移其实就是只能向右上走,且不能穿过 \(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转化为“卡特兰数”网格图的方法:
这种转化方法将“放在最前面”映射成了横着走
必经的那个点:
根据 \(y=x+1\) 翻折:
路径:
注意,我们不仅要求经过一点,还要求经过前一步不能横着走过来,经过的后一步要先向右走一步。
关键代码:
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);
}