以下为力扣官方思路,以及本人代码
给你 n 笔订单,每笔订单都需要快递服务。
请你统计所有有效的 收件/配送 序列的数目,确保第 i 个物品的配送服务 delivery(i) 总是在其收件服务 pickup(i) 之后。
由于答案可能很大,请返回答案对 10^9 + 7 取余的结果。
输入:n = 1
输出:1
解释:只有一种序列 (P1, D1),物品 1 的配送服务(D1)在物品 1 的收件服务(P1)后。
输入:n = 2
输出:6
解释:所有可能的序列包括:
(P1,P2,D1,D2),(P1,P2,D2,D1),(P1,D1,P2,D2),(P2,P1,D1,D2),(P2,P1,D2,D1) 和 (P2,D2,P1,D1)。
(P1,D2,P2,D1) 是一个无效的序列,因为物品 2 的收件服务(P2)不应在物品 2 的配送服务(D2)之后。
输入:n = 3
输出:90
设 f[i] 表示订单数量为 i 时的序列数目,由于 f[i] 包含了 i 份订单,我们可以将其拆分为前 i-1 份订单(编号为 1,2,……,i-1)与 1 份额外的订单(编号为 i)。
对于 f[i-1],它的长度为 (i-1)*2,我们只需要在这个序列中加上第 i 份订单,就可以得到一条订单数量为 i 的序列。
由于第 i 份订单包含的 Pi 在序列中必须出现在 Di 之前,因此有两种将第 i 份订单加入 f[i-1] 中,具体如下:
将这两类的方法数量相加,就可以得到:对于一个固定的包含前 i-1 份订单的固定序列,用 (2i−1)*i 种方法可以加入第 i 份订单。易得递推公式 f[i] = (2i-1)*i *f[i-1]。
class Solution {
public int countOrders(int n) {
if(n==1)
return 1;
else
return (int)(((long)(countOrders(n-1)) *(2*n-1)*n) % 1000000007); // countOrders(n-1) 不可以是 int 类型,不然 n-1>=8 时,该值会因为大数值被强制转换成 int 而出错
}
}
除了递推法之外,我们也可以从整体进行考虑,直接得到 f[i] 的通项公式。
假设现在没有 Di 必须在 Pi 之后的要求,那么 f[i]=(2i)!。这是因为我们可以将 P1,D1,P2,D2,⋯ ,Pi,Di 的任意一个排列作为答案,排列的数目为 (2i)!,我们称这些排列为初始排列。
在加上了「Di 必须在 Pi 之后」的要求后,有一些初始排列就不能作为答案了。但是我们可以对它们进行调整,使它们变成满足要求的排列。具体地,对于任意一个初始排列,如果第 j 个物品的 Dj 出现在 Pj 之前,那么我们就把 Dj 和 Pj 交换顺序。在对 j=1, 2, …, i 全部判断一遍之后,我们就可以得到一个满足要求的排列了。
然而这样会导致重复计数,这是因为不同的初始排列在交换顺序之后会得到相同的排列,例如当 i=2 时,初始排列(D1, D2, P1, P2)和(D1, P2, P1, D2)在交换后都会得到相同的排列(P1, P2, D1, D2)。
如何去除重复计数呢?我们可以进行逆向思考,反过来计算一个排列可以从多少个初始排列得来。显然,对于排列中的 i 对 Pj 和 Dj,我们将其中任意数量的对交换顺序,都可以得到一个不同的初始排列。交换顺序的选择有 2i 种(每一对 Pj 和 Dj 交换或不交换),因此一个排列对应着 2i 个初始排列,即排列的数目为:f[i] = (2i)! / 2i 。
这样就得到了 f[i] 的通项公式。由于通项公式中包含除法,在取模的意义下不好直接计算,我们可以将分母 2i 拆分成 i 个 2,与分子阶乘中的 i 个偶数相除,得到 f[i] = i!(2i-1)!!,其中 (2i-1)!! = 1 · 3 · … ·(2i-1),其与思路一中的递推式是一致的,可以直接使用思路一的代码。