求长度为 (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
", (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
", (ans % P + P) % P);
}