P4071 - SDOI2016 - D2T1 - 排列计数#
题目信息#
难度:提高+/省选-
算法:数学递推,逆元
题目描述#
求有多少种 1 1 1 到 n n n 的排列 a a a ,满足序列恰好有 m m m 个位置 i i i ,使得 a i = i a_i=i a i = i .
答案对 1 0 9 + 7 10^9+7 1 0 9 + 7 取模,多组询问,询问次数为 T T T .
测试点编号 T = T= T = n , m ≤ n, m\leq n , m ≤ 测试点编号 T = T= T = n , m ≤ n, m\leq n , m ≤ 1 ∼ 3 1 \sim 3 1 ∼ 3 1 0 3 10^3 1 0 3 8 8 8 10 ∼ 12 10 \sim 12 10 ∼ 12 1 0 3 10^3 1 0 3 1 0 3 10^3 1 0 3 4 ∼ 6 4 \sim 6 4 ∼ 6 1 0 3 10^3 1 0 3 12 12 12 13 ∼ 14 13 \sim 14 13 ∼ 14 5 × 1 0 5 5\times 10^5 5 × 1 0 5 1 0 3 10^3 1 0 3 7 ∼ 9 7 \sim 9 7 ∼ 9 1 0 3 10^3 1 0 3 100 100 100 15 ∼ 20 15 \sim 20 15 ∼ 20 5 × 1 0 5 5\times 10^5 5 × 1 0 5 1 0 6 10^6 1 0 6
对于 100 % 100\% 100% 的数据,0 ≤ m ≤ 1 0 6 0\leq m\leq 10^6 0 ≤ m ≤ 1 0 6 .
S u b t a s k \rm{Subtask} Subtask 1 \rm{1} 1 #
T = 1 0 3 T=10^3 T = 1 0 3 ,n , m ≤ 8 n, m\leq 8 n , m ≤ 8 .
对于每次询问,暴力枚举所有排列,时间复杂度 O ( T × n ! ) \mathcal{O}(T\times n!) O ( T × n !) ,期望得分 15 p t s \rm{15pts} 15pts .
S u b t a s k \rm{Subtask} Subtask 2 \rm{2} 2 #
想到错排 之后,正解非常自然。
根据小学排列组合知识,我们从数列中任意取 n n n 个数,让它们满足 a i = i a_i=i a i = i ,情况数为 C n m C_{n}^{m} C n m .
又因为 n n n 个数的排列中,有 m m m 个位置满足 a i = i a_i=i a i = i ,对于剩下的 n − m n-m n − m 个位置需要满足 a i ≠ i a_i\neq i a i = i .
这样的情况数是 n − m n-m n − m 的错排数,也就是 D n − m D_{n-m} D n − m .
由于乘法原理,最终的答案就是 C n m × D n − m C_{n}^m\times D_{n-m} C n m × D n − m .
对于多组询问,我们需要 O ( n ) \mathcal{O}(n) O ( n ) 预处理组合数 和错排数。
对于一个 n n n 个元素的排列,若一个排列中所有的元素都不在自己的位置上,这样的一个排列称为一个错排。
一般将 n n n 个元素的错排数量记作 D n D_n D n .
通过递推,可以在 O ( n ) \mathcal{O}(n) O ( n ) 的复杂度内处理出 D 1 , D 2 ⋯ , D n D_1,D_2\cdots, D_n D 1 , D 2 ⋯ , D n .
递推式#
推导过程不唯一。
第一步,在 [ 1 , n − 1 ] [1, n-1] [ 1 , n − 1 ] 中任取一个元素 i i i ,将 n n n 连向 i i i ,方法数量为 ( n − 1 ) (n-1) ( n − 1 ) .
第二步,确定 i i i 连向哪个元素,分类讨论:
i i i 连向 n n n .
此时剩下的 n − 2 n-2 n − 2 个元素要满足错排,对答案贡献为 ( n − 1 ) × D n − 2 (n-1)\times D_{n-2} ( n − 1 ) × D n − 2 .
i i i 不连向 n n n .
此时,相当于有 n − 1 n-1 n − 1 个元素需要满足错排,对答案贡献为 ( n − 1 ) × D n − 1 (n-1)\times D_{n-1} ( n − 1 ) × D n − 1 .
根据加法原理,得到递推式:
D n = ( n − 1 ) × ( D n − 1 + D n − 2 ) \begin{align}
D_{n}=(n-1)\times(D_{n-1}+D_{n-2})\tag{n>2}
\end{align} D n = ( n − 1 ) × ( D n − 1 + D n − 2 ) ( n>2 )
特殊地,D 1 = 0 D_1=0 D 1 = 0 ,D 2 = 1 D_2=1 D 2 = 1 .
求逆元 & 组合数#
扩展欧几里得#
扩展欧几里得算法是用来在已知 a , b a,b a , b 的情况下求解一组 x , y x,y x , y ,使满足 a x + b y = g c d ( a , b ) = d ax+by=gcd(a,\,b)=d a x + b y = g c d ( a , b ) = d .
gcd ( a , b ) \gcd(a, b) g cd( a , b ) 为 a , b a, b a , b 的最大公因数,方程一定有整数解 。
要计算 gcd ( a , b ) \gcd(a,\,b) g cd( a , b ) ,并求出满足条件的 x , y x,y x , y .
将下一个状态 b b b 和 ( a % b ) (a\%b) ( a % b ) 表示为:b ⋅ x 1 + ( a % b ) ⋅ y 1 = d b\cdot x_1+(a\%b)\cdot y_1=d b ⋅ x 1 + ( a % b ) ⋅ y 1 = d
则:
∵ b ⋅ x 1 + ( a % b ) ⋅ y 1 = d , 又 ∵ a % b = a − ⌊ a b ⌋ × b \begin{align}
\because b\cdot x_1+(a\%b)\cdot y_1=d,\\
\text{又}\because a\%b=a-\left\lfloor\frac{a}{b}\right\rfloor\times b\\
\end{align} ∵ b ⋅ x 1 + ( a % b ) ⋅ y 1 = d , 又 ∵ a % b = a − ⌊ b a ⌋ × b
∴ g c d ( a , b ) = d = b ⋅ x 1 + ( a − ⌊ a b ⌋ × b ) ⋅ y 1 = b ⋅ x 1 + a ⋅ y 1 − ⌊ a b ⌋ × b × y 1 = a × y 1 + b × ( x 1 − ⌊ a b ⌋ × y 1 ) \begin{aligned}\therefore gcd(a,\,b)=d&=b\cdot x_1+(a-\left\lfloor\frac{a}{b}\right\rfloor\times b)\cdot y_1\\
&=b\cdot x_1+a\cdot y_1-\left\lfloor\frac{a}{b}\right\rfloor\times b\times y_1\\
&=a\times y1+b\times(x_1-\left\lfloor\frac{a}{b}\right\rfloor\times y_1)
\end{aligned} ∴ g c d ( a , b ) = d = b ⋅ x 1 + ( a − ⌊ b a ⌋ × b ) ⋅ y 1 = b ⋅ x 1 + a ⋅ y 1 − ⌊ b a ⌋ × b × y 1 = a × y 1 + b × ( x 1 − ⌊ b a ⌋ × y 1 )
所以满足条件的 x = y 1 , y = x 1 − ⌊ a b ⌋ × y 1 x=y_1,\,y=x_1-\left\lfloor\frac{a}{b}\right\rfloor\times y_1 x = y 1 , y = x 1 − ⌊ b a ⌋ × y 1 .
int exgcd ( int a , int b , int & x , int & y ) { //函数返回值为gcd(a,b)
if (b == 0 ) {
x = 1 ; //gcd(a,b) = a = 1a+0b
y = 0 ;
return a;
}
int r = exgcd (b, a % b, x, y); //先递归
int t = x; //再计算x,y
x = y;
y = t - a / b * y;
return r;
}
cpp
时间复杂度为:O ( min ( log a , log b ) ) \mathcal{O}(\min(\log a, \log b)) O ( min ( log a , log b )) ,即 O ( log n ) \mathcal{O}(\log n) O ( log n ) 级别。
假设特解为 ( x 0 , y 0 ) (x_0,y_0) ( x 0 , y 0 ) ,满足 a x 0 + b y 0 = g c d ( a , b ) = d ax_0+by_0=gcd(a,\,b)=d a x 0 + b y 0 = g c d ( a , b ) = d ,
则有:
a ( x 0 + k b d ) + b ( y 0 − k a d ) = d (k为整数) a(x_0+k\frac{b}{d})+b(y_0-k\frac{a}{d})=d\tag{k为整数} a ( x 0 + k d b ) + b ( y 0 − k d a ) = d ( k 为整数 )
所以方程的通解为:
x = x 0 + k b d y = y 0 − k a d x=x_0+k\frac{b}{d}\\y=y_0-k\frac{a}{d} x = x 0 + k d b y = y 0 − k d a
对于其它二元一次不定方程a x + b y = c ax+by=c a x + b y = c (c c c 为任意正整数)只有当 d ∣ c d\mid c d ∣ c 时才有整数解 ,通解形式:
x = c d x 0 + k b d y = c d y 0 − k a d x=\frac{c}{d}x_0+k\frac{b}{d}\\y=\frac{c}{d}y_0-k\frac{a}{d} x = d c x 0 + k d b y = d c y 0 − k d a
同余方程(模方程)#
指形如 a x ≡ b ( m o d c ) ax\equiv b\pmod c a x ≡ b ( mod c ) 的一种方程。其中 a , b , c a,b,c a , b , c 是参数,a , c a,c a , c 是正整数,b b b 是小于 c c c 的非负整数,x x x 是未知数。
解同余方程#
根据同余定义,a x ≡ b ( m o d c ) ax\equiv b\pmod c a x ≡ b ( mod c ) 可化为 x a + k c = b xa+kc=b x a + k c = b . 其中 a , b , c a,b,c a , b , c 已知,x , k x,k x , k 未知,所以能用扩展欧几里得解同余方程 。
如果 b ∤ g c d ( a , c ) b\nmid gcd(a,c) b ∤ g c d ( a , c ) ,方程无整数解.
否则同扩展欧几里得酸法,求得通解:
x = b d x 0 + k c d k = b d p 0 − k a d x=\frac{b}{d}x_0+k\frac{c}{d}\\k=\frac{b}{d}p_0-k\frac{a}{d} x = d b x 0 + k d c k = d b p 0 − k d a
其中 k ∈ Z k\in \Bbb{Z} k ∈ Z .
最小非负整数解#
因为 x = b d x 0 + k c d x=\frac{b}{d}x_0+k\frac{c}{d} x = d b x 0 + k d c ,其中 x 0 x_0 x 0 是方程 x a + k c = g c d ( a , c ) xa+kc=gcd(a,\,c) x a + k c = g c d ( a , c ) 的特解, b d x 0 \frac{b}{d}x_0 d b x 0 是原同余方程的特解,
若 x > 0 x>0 x > 0 ,则有 x ≡ b d x 0 ( m o d c d ) x\equiv \frac{b}{d}x_0 \pmod{ \frac{c}{d}} x ≡ d b x 0 ( mod d c ) 。最小非负整数解为 b d x 0 % c d \frac{b}{d}x_0\,\%\,\frac{c}{d} d b x 0 % d c .
若 x < 0 x<0 x < 0 ,同理,最小非负整数解为 ( b d x 0 m o d c d + c d ) m o d c d (\frac{b}{d}x_0\bmod \frac{c}{d}+\frac{c}{d} )\bmod\frac{c}{d} ( d b x 0 mod d c + d c ) mod d c .
综上,最小非负整数解为:
x = ( b d x 0 m o d c d + c d ) m o d c d x=(\frac{b}{d}x_0\bmod\frac{c}{d}+\frac{c}{d})\bmod \frac{c}{d} x = ( d b x 0 mod d c + d c ) mod d c
乘法逆元#
给定两个整数 a a a 和 p p p ,假设存在 x x x 使得 a x ≡ 1 ( m o d p ) ) ax\equiv 1\pmod p) a x ≡ 1 ( mod p )) ,称 x x x 为 a a a 关于 p p p 的乘法逆元 。
a a a 关于 p p p 的逆元存在的充分必要条件 是 gcd ( a , p ) = 1 \gcd(a,p)=1 g cd( a , p ) = 1 .
在模意义下,乘一个数 x x x 的逆元,可以理解为除以 x x x .
求一个数的逆元#
将上面式子变形,变成 a x + k p = 1 ax+kp=1 a x + k p = 1 ,用扩展欧几里得能够求出一个 x x x .
如果 p p p 是质数,有另一种方法求 a a a 关于 p p p 的逆元(费马小定理)
a p − 1 m o d p = ( a p − 2 × a ) m o d p = ( x × a ) m o d p = 1 a^{p-1}\bmod p=(a^{p-2}\times a)\bmod p=(x\times a)\bmod p=1 a p − 1 mod p = ( a p − 2 × a ) mod p = ( x × a ) mod p = 1
所以 a a a 关于 p p p 的逆元是 a b − 2 m o d b a^{b-2}\bmod b a b − 2 mod b 的值,用快速幂优化。
时间复杂度:均为 O ( log n ) \mathcal{O}(\log n) O ( log n ) 级别。
线性预处理逆元#
在 O ( n ) \mathcal{O}(n) O ( n ) 的时间复杂度内,求出任意 n n n 个数的逆元 ,一般用于处理 1 ∼ n 1\sim n 1 ∼ n 的逆元。
设我们要处理出 a 1 , a 2 ⋯ , a n a_1, a_2\cdots, a_n a 1 , a 2 ⋯ , a n 的逆元。
第一步,预处理出前缀积数组 pre i = ∏ j = 1 i a i \operatorname{pre}_i=\prod_{j=1}^ia_i pre i = ∏ j = 1 i a i .
第二步,求出 pre n \operatorname{pre}_n pre n 的逆元 pre_inv n \operatorname{pre\_inv}_n pre_inv n ,复杂度 O ( log n ) \mathcal{O}(\log n) O ( log n ) .
第三步,通过 pre_inv i \operatorname{pre\_inv}_i pre_inv i ,求出 pre_inv i − 1 \operatorname{pre\_inv}_{i-1} pre_inv i − 1 .
在模意义下,pre_inv i = 1 a 1 × a 2 × ⋯ × a i \operatorname{pre\_inv}_i=\frac{1}{a_1\times a_{2}\times \cdots \times a_{i}} pre_inv i = a 1 × a 2 × ⋯ × a i 1 ,pre_inv i − 1 = 1 a 1 × a 2 × ⋯ × a i − 1 = pre_inv i × a i \operatorname{pre\_inv}_{i-1}=\frac{1}{a_1\times a_2\times\cdots\times a_{i-1}}=\operatorname{pre\_inv}_{i}\times a_i pre_inv i − 1 = a 1 × a 2 × ⋯ × a i − 1 1 = pre_inv i × a i .
第四步,通过 pre_inv i \operatorname{pre\_inv}_i pre_inv i ,求出 inv i \operatorname{inv}_i inv i .
在模意义下,inv i = pre i − 1 × pre_inv i \operatorname{inv}_i=\operatorname{pre}_{i-1}\times \operatorname{pre\_inv}_{i} inv i = pre i − 1 × pre_inv i .
在上面的步骤中,pre_inv i \operatorname{pre\_inv}_i pre_inv i 即为 i i i 的阶乘的逆元,在处理组合数时常用。
有另一种方法也可以处理逆元,时间复杂度相同,但不好记,而且证明麻烦,不需要掌握。
inv[ 1 ] = 1 ;
for ( int i = 2 ; i <= n; ++ i) {
inv[i] = (p - p / i) * inv[p % i] % p;
}
cpp
预处理组合数#
预处理后,O ( 1 ) \mathcal{O}(1) O ( 1 ) 地求解 [ 1 , n ] [1, n] [ 1 , n ] 范围内的组合数,对大质数取模的结果。
记 fac n = n ! \operatorname{fac}_n=n! fac n = n ! ,根据组合数的定义式:
C n m = n ! m ! ( n − m ) ! = fac n fac m × fac n − m C_n^m=\frac{n!}{m!(n-m)!}=\frac{\operatorname{fac}_n}{\operatorname{fac}_m\times \operatorname{fac}_{n-m}} C n m = m ! ( n − m )! n ! = fac m × fac n − m fac n
可以得到:
C n m m o d p = fac n ( fac m ) − 1 ( fac n − m ) − 1 C_n^m \bmod p = \operatorname{fac}_n (\operatorname{fac}_m)^{-1}(\operatorname{fac}_{n-m})^{-1} C n m mod p = fac n ( fac m ) − 1 ( fac n − m ) − 1
所以预处理出 [ 1 , n ] [1, n] [ 1 , n ] 的阶乘,以及阶乘的逆元,每次即可 O ( 1 ) \mathcal{O}(1) O ( 1 ) 求组合数。
int fac[], inv_fac[];
fac[ 0 ] = 1 ;
for ( int i = 1 ; i <= n; i ++ )
fac[i] = fac[i - 1 ] * i % p;
inv_fac[n] = power (fac[n], p - 1 );
for ( int i = n - 1 ; i >= 0 ; i -- )
inv_fac[i] = inv_fac[i + 1 ] * (i + 1 ) % p;
cpp
P4343 - SHOI2015 - 自动刷题机#
题目信息#
难度:提高+/省选-
算法:二分答案
题目描述#
自动刷题机刷题的方式非常简单:首先会瞬间得出题目的正确做法,然后开始写程序。每秒,自动刷题机的代码生成模块会有两种可能的结果:
写了 x x x 行代码
心情不好,删掉了之前写的 y y y 行代码。(如果 y y y 大于当前代码长度则相当于全部删除。)
对于一个 OJ,存在某个固定的正整数长度 n n n ,一旦自动刷题机在某秒结束时积累了大于等于 n n n 行的代码,它就会自动提交并 AC 此题,然后新建一个文件(即弃置之前的所有代码)并开始写下一题。SHTSC 在某个 OJ 上跑了一天的自动刷题机,得到了很多条关于写代码的日志信息。他突然发现自己没有记录这个 OJ 的 n n n 究竟是多少。所幸他通过自己在 OJ 上的 Rank 知道了自动刷题机一共切了 k k k 道题,希望你计算 n n n 可能的最小值和最大值。
题目给定 l l l 条刷题日志,每条有一个整数 x i x_i x i 表示添加的代码行数。
1 ≤ l ≤ 1 0 5 , − 1 0 9 ≤ x i ≤ 1 0 9 1\leq l\leq 10^5, -10^9\leq x_i\leq 10^9 1 ≤ l ≤ 1 0 5 , − 1 0 9 ≤ x i ≤ 1 0 9 .