说在前面
这篇博客给了我思路与代码,里面有三道题,而且讲述的逻辑非常骚气。
所以我把这道题拿了出来,单独讲,逻辑清楚些。
题目
题目描述
数据范围与约定
对于
30
%
30\%
30% 的数据,
n
k
≤
12
nk\le 12
nk≤12 。
对于另外 20 % 20\% 20% 的数据, m = 2 , k = 1 m=2,k=1 m=2,k=1 。
对于另外另外 20 % 20\% 20% 的数据, m = 3 , k = 2 m=3,k=2 m=3,k=2 。
对于 100 % 100\% 100% 的数据, 1 ≤ n k ≤ 1 0 7 , k < m ≤ 1 0 7 1\le nk\le 10^7,k<m\le 10^7 1≤nk≤107,k<m≤107 。
思路
等价转换
看看数据中的一个奇怪的限制:
k
<
m
k<m
k<m(而且还跟
m
≤
1
0
7
m\le 10^7
m≤107 隐藏在一起),出题人好坏。
众所周知, ∑ i = 0 x − 1 m i = m x − 1 m − 1 \sum_{i=0}^{x-1}m^i=\frac{m^x-1}{m-1} ∑i=0x−1mi=m−1mx−1;结合 k ≤ m − 1 k\le m-1 k≤m−1 这一条,得到了非常骇人的结论,那就是 k ∑ i = 0 x − 1 m i ≤ m x − 1 k\sum_{i=0}^{x-1}m^i\le m^x-1 ki=0∑x−1mi≤mx−1
这就是在说,对于一个砝码 m x m^x mx ,比它轻的 k x kx kx 个砝码加在一起,也没有 m x m^x mx 重。
所以,看似限制条件 R x − L x < max R_x-L_x<\max Rx−Lx<max 很奇怪,其实本质就是这句话:
在任意时刻,对于已经放过的最重的砝码,左边的数量不小于右边的数量。
简化情况
试着去处理 n = 1 n=1 n=1 的情况。不妨用 g ( x , y ) g(x,y) g(x,y) 表示左边有 x x x 个、右边有 y y y 个,方案数是多少。
也许你会试着去递推。但是方案数太多,不可能通过 k < 1 0 7 k<10^7 k<107 的数据。考虑用数学方法计算。
在 平面直角坐标系 中找到 g g g 的意义。在左边放一个,相当于向右走一个单位;在右边放一个,相当于向上走一个单位。将起点规定为 ( 0 , 0 ) (0,0) (0,0) ,那么终点就应该是 ( x , y ) (x,y) (x,y) 。这个限制,就相当于不能碰到 l : y = x + 1 l:y=x+1 l:y=x+1 这条直线。
在坐标系内行走……不能碰到某条直线……有 卡特兰数 内味了。
仿照卡特兰数的求解方法(什么?你是像作者一样直接背的结论?)即可——碰到警戒线就将其翻折。
如图,绿色(深绿
a
n
d
and
and 浅绿)的线是一条非法路径。将其第一次触碰到警戒线以后的部分(浅绿部分)翻折,得到了紫色的线。不难发现,这种情况就是走到上面的紫色点的路径数量(可以证明,到达上面紫色点的路径与非法路径一一对应)。
你别告诉我没限制的情况的方案数 ( x + y x ) {x+y\choose x} (xx+y) 你都不知道!因为一共要走 x + y x+y x+y 步,其中 x x x 步是向右走。
那么,对称之后的点是什么呢?是 ( y − 1 , x + 1 ) (y-1,x+1) (y−1,x+1) 。这个是初中数学的范畴,我就不推了。
于是我们可以直接求出 g ( x , y ) = ( x + y x ) − ( x + y y − 1 ) g(x,y)={x+y\choose x}-{x+y\choose y-1} g(x,y)=(xx+y)−(y−1x+y)
所以,我们现在要求的就是 ∑ i = 0 ⌊ n 2 ⌋ g ( n − i , i ) \sum_{i=0}^{\lfloor\frac{n}{2}\rfloor}g(n-i,i) i=0∑⌊2n⌋g(n−i,i)
根据刚才得出的结论, g ( n − i , i ) = ( n i ) − ( n i − 1 ) g(n-i,i)={n\choose i}-{n\choose{i-1}} g(n−i,i)=(in)−(i−1n) ;将其代入,就有了骇人的结论:
a n s = ∑ i = 0 ⌊ n 2 ⌋ [ ( n i ) − ( n i − 1 ) ] = ( n ⌊ n 2 ⌋ ) ans =\sum_{i=0}^{\lfloor\frac{n}{2}\rfloor}\left[{n\choose i}-{n\choose{i-1}}\right] ={n\choose\lfloor\frac{n}{2}\rfloor} ans=i=0∑⌊2n⌋[(in)−(i−1n)]=(⌊2n⌋n)
好的!现在我们就 得到了0分的高分 解决了
n
=
1
n=1
n=1 的情况。
所有情况
然后奇技淫巧出炉了:从重到轻的考虑每一种砝码。
这是题解。我连题解都看不懂了,宣告提前退役
其实就是这样的,我们对最轻的砝码进行考虑,并转移。以下都是讨论最轻的那一种砝码的放置。
放在最前面的(数量可以枚举一下),由于只有它这一种,其方案数就应该是先前所求出来的 ( i ⌊ i / 2 ⌋ ) {i\choose\lfloor i/2\rfloor} (⌊i/2⌋i) (毕竟它就是当前最重,必须满足条件)。
后面就可以放飞自我了。想放哪里放哪里——它一定不是最重的砝码了。然后就是经典的隔板法了;最后乘上一个 2 2 2 的幂,表示放左边或右边。这里简单提一下隔板法;会的就可以直接跳过了。
隔板法简单介绍。
已经放了 k ( n − 1 ) k(n-1) k(n−1) 个砝码,就有了 n k − k nk-k nk−k 个间隔(不能放在最前方,一定是放在某个砝码之后)。我现在要将 k − i k-i k−i 个砝码塞到这些间隔中;这将会把原有的 n k − k nk-k nk−k 个砝码分割成 k − i + 1 k-i+1 k−i+1 段。不妨设它们各自的数量为 x 1 , x 2 , x 3 , … , x k − i + 1 x_1,x_2,x_3,\dots,x_{k-i+1} x1,x2,x3,…,xk−i+1 ,相当于求这个方程组的解: { ∑ j = 1 k − i + 1 x j = n k − k x 1 ≥ 1 , x y ( y ≠ 1 ) ≥ 0 \begin{cases}\sum_{j=1}^{k-i+1}x_j=nk-k\\x_1\ge 1,x_y(y\ne 1)\ge 0\end{cases} {∑j=1k−i+1xj=nk−kx1≥1,xy(y=1)≥0
为什么 x 1 ≥ 1 x_1\ge 1 x1≥1 呢?因为第一段不可能没有砝码(不会放在最前方)。其它的就无所谓了,非负即可。
于是我们将 x y ( y ≠ 1 ) x_y(y\ne 1) xy(y=1) 共同增大 1 1 1 ,现在要求这个方程的正整数解 x 1 + x 2 + x 3 + ⋯ + x k − i + 1 = n k − k + ( k − i ) = n k − i x_1+x_2+x_3+\cdots+x_{k-i+1}=nk-k+(k-i)=nk-i x1+x2+x3+⋯+xk−i+1=nk−k+(k−i)=nk−i
这就是经典的隔板法——将 k n − i kn-i kn−i 个小球划分成 k − i + 1 k-i+1 k−i+1 组,相当于在 k n − i − 1 kn-i-1 kn−i−1 个空隙中选 k − i k-i k−i 个作为分割线。所以方案数就是 ( n k − i − 1 k − 1 ) {nk-i-1\choose{k-1}} (k−1nk−i−1) 。最后枚举一下,这 k − i k-i k−i 个砝码究竟是放左边还是放右边,乘 2 k − i 2^{k-i} 2k−i 。
隔板法很简单吧?
写出结论: a n s n = a n s n − 1 ∑ i = 0 k ( i ⌊ i 2 ⌋ ) ( n k − i − 1 k − i ) 2 k − i ans_{n}=ans_{n-1}\sum_{i=0}^{k}{i\choose\lfloor \frac{i}{2}\rfloor}{nk-i-1\choose{k-i}}2^{k-i} ansn=ansn−1i=0∑k(⌊2i⌋i)(k−ink−i−1)2k−i
递推解决战斗。时间复杂度
O
(
n
k
)
\mathcal O(nk)
O(nk) 。为什么我的组合数学之魂凌乱不堪
代码实现
这里有一个非常有意思的优化,众所周知,
( n m ) = n ! m ! ( n − m ) ! {n\choose m}=\frac{n!}{m!(n-m)!} (mn)=m!(n−m)!n!
n − ⌊ n 2 ⌋ = ⌈ n 2 ⌉ n-\left\lfloor\frac{n}{2}\right\rfloor=\left\lceil\frac{n}{2}\right\rceil n−⌊2n⌋=⌈2n⌉
所以就得到了这个结论, ( n ⌊ n 2 ⌋ ) = n ! ⌊ n 2 ⌋ ! ⌈ n 2 ⌉ ! {n\choose\left\lfloor\frac{n}{2}\right\rfloor}=\frac{n!}{\lfloor\frac{n}{2}\rfloor!\lceil\frac{n}{2}\rceil!} (⌊2n⌋n)=⌊2n⌋!⌈2n⌉!n!
将其记为 A A A 。考虑一下它与 ( n − 1 ) ! ⌊ n − 1 2 ⌋ ! ⌈ n − 1 2 ⌉ ! \frac{(n-1)!}{\lfloor\frac{n-1}{2}\rfloor!\lceil\frac{n-1}{2}\rceil!} ⌊2n−1⌋!⌈2n−1⌉!(n−1)!
的关系。将其记为 B B B 。分类讨论一下。
- 如果
2
∣
n
2|n
2∣n ,那么就有
⌊
n
−
1
2
⌋
+
1
=
⌊
n
2
⌋
,
⌈
n
−
1
2
⌉
=
⌈
n
2
⌉
\lfloor\frac{n-1}{2}\rfloor+1=\lfloor\frac{n}{2}\rfloor,\lceil\frac{n-1}{2}\rceil=\lceil\frac{n}{2}\rceil
⌊2n−1⌋+1=⌊2n⌋,⌈2n−1⌉=⌈2n⌉ ,所以推出:
A B = n ⌊ n 2 ⌋ = n ⌊ n + 1 2 ⌋ \frac{A}{B}=\frac{n}{\lfloor\frac{n}{2}\rfloor}=\frac{n}{\lfloor\frac{n+1}{2}\rfloor} BA=⌊2n⌋n=⌊2n+1⌋n - 如果
2
∣
(
n
+
1
)
2|(n+1)
2∣(n+1) ,那么就有
⌊
n
−
1
2
⌋
=
⌊
n
2
⌋
,
⌈
n
−
1
2
⌉
+
1
=
⌈
n
2
⌉
\lfloor\frac{n-1}{2}\rfloor=\lfloor\frac{n}{2}\rfloor,\lceil\frac{n-1}{2}\rceil+1=\lceil\frac{n}{2}\rceil
⌊2n−1⌋=⌊2n⌋,⌈2n−1⌉+1=⌈2n⌉ ,所以推出:
A B = n ⌈ n 2 ⌉ = n ⌊ n + 1 2 ⌋ \frac{A}{B}=\frac{n}{\lceil\frac{n}{2}\rceil}=\frac{n}{\lfloor\frac{n+1}{2}\rfloor} BA=⌈2n⌉n=⌊2n+1⌋n
然后就可以递推一发了。形式化地,若 f ( x ) = ( x ⌊ x 2 ⌋ ) f(x)={x\choose \lfloor\frac{x}{2}\rfloor} f(x)=(⌊2x⌋x) ,则
f ( x ) = x ⋅ f ( x − 1 ) ⌊ x + 1 2 ⌋ f(x)=\frac{x\cdot f(x-1)}{\lfloor\frac{x+1}{2}\rfloor} f(x)=⌊2x+1⌋x⋅f(x−1)
当然,我们要计算 2 k − i 2^{k-i} 2k−i 次方,所以不妨将 i i i 从大到小进行枚举,就可以直接嘿嘿嘿了。
哦,对了,还有一个组合数没有处理……
∵ ( n k − i − 1 k − i ) = ( n k − i − 1 ) ! ( k − i ) ! ( n k − k − 1 ) ! (1) \because {nk-i-1\choose k-i}=\frac{(nk-i-1)!}{(k-i)!(nk-k-1)!}\tag{1} ∵(k−ink−i−1)=(k−i)!(nk−k−1)!(nk−i−1)!(1)
a n d ∵ ( n k − ( i + 1 ) − 1 k − ( i + 1 ) ) = [ n k − ( i + 1 ) − 1 ] ! [ k − ( i + 1 ) ] ! ( n k − k − 1 ) ! (2) and\space\because {nk-(i+1)-1\choose k-(i+1)}=\frac{\left[nk-(i+1)-1\right]!}{[k-(i+1)]!(nk-k-1)!}\tag{2} and ∵(k−(i+1)nk−(i+1)−1)=[k−(i+1)]!(nk−k−1)![nk−(i+1)−1]!(2)
∴ ( 1 ) ( 2 ) = n k − i − 1 k − i \therefore \frac{(1)}{(2)}=\frac{nk-i-1}{k-i} ∴(2)(1)=k−ink−i−1
所以跟 2 k − i 2^{k-i} 2k−i 一起递推即可(共同存在一个临时变量里)。
#include<cstdio>
const int MAXN = 10002030, mod = 998244353;
int n, m, k, inv[MAXN], f[MAXN]; // f存储n=1的情况的答案
int main(){
inv[0] = inv[1] = f[0] = 1;
scanf("%d %d %d",&n,&m,&k);
for(int i=2; i<=k; i++) // 线性筛逆元
inv[i] = 1ll*(mod-mod/i)*inv[mod%i]%mod;
for(int i=1; i<=k; i++) // 递推求组合数
f[i]=1ll*f[i-1]*i%mod*inv[(i+1)>>1]%mod;
long long ans = f[k]; // ans_1的值
for(int i=2; i<=n; i++){
long long t = 0, c = 1; // c是临时变量;t是求和结果
for(int j=k; j>=0; --j){
// 枚举前面放了几个
if(j != k) // 递推组合数2
c = (c<<1)*(i*k-j-1)%mod*inv[k-j]%mod;
t = (t+1ll*c*f[j]%mod)%mod;
}
ans = ans*t%mod; // 乘上ans_{i-1}
}
printf("%d\n",int(ans));
}