题目:UOJ #200. 【CTSC2016】NOIP十合一
题意:
这是一道提交答案题,共有十个测试点,对应的输入数据将提前下发给你,你需要在至多五小时内计算出相应的输出数据,不需要提交你的解题程序。各测试点对应的问题一致,均为如下题目。
输入给定整数 P P P,一个 n n n 个点 m m m 条边的带边权有向图以及 q q q 组询问。这里点从 1 1 1 到 n n n 编号,边从 1 1 1 到 m m m 编号,每条边形如 ( u , v , w ) (u, v, w) (u,v,w) 表示 u u u 到 v v v 有一条边权为 w w w 的有向边。
对于每组询问 ( s , t , k ) (s, t, k) (s,t,k),你需要求出从点 s s s 到点 t t t 有多少条路径满足路径上的边权之和恰好是 k k k,输出路径数量对整数 P P P 取模的值。这里路径可以看作边的序列,两条路径不同当且仅当它们依次经过的边的编号不能一一对应。
数据范围需要你自己观察输入数据得出。
为了便于核查正确性,每个测试点第一组询问的正确答案会提前下发给你。此外,每个测试点答对一定数量的询问,你可以获得该测试点分数的一部分,而记录这些数量与分数的关系表格也会提前下发给你。
题解:
(笔者写这篇文章的目的不在于讨论如何获得部分分,而是尝试从如何在限定时间内快速解题入手进行分析,并且有时会根据对编写程序和运行程序耗时的估计,对可行做法进行选择)
首先,我们需要对数据的整体规模有一定的认识,便于思考出如何快速处理有特征的数据,同时也要尽量使用相同的方式处理尽量多的数据,以便减少编写程序的耗时。为了实现这一点,你可以写一个检测程序,批量地分析测试点的特性,并列出相应的表格,以便寻找不同测试点的共性。
对于此题,我们除了需要观察点数 n n n、询问数 q q q、模数 P P P 的特征外,还需要观察连边的情况与询问的特点。这里,笔者将各测试点中比较明显的特征罗列如下:(注意,有些特征不一定有用)
测试点 | n n n | m m m | q q q | P P P | v − u v - u v−u | w w w | s and t s \text{ and } t s and t | k k k | 其他 |
---|---|---|---|---|---|---|---|---|---|
#1 | 1 0 5 10^5 105 | n + ⌊ n − 1 2 ⌋ n + \left \lfloor \frac{n - 1}{2} \right \rfloor n+⌊2n−1⌋ | 1 0 5 10^5 105 | P 1 P_1 P1 | [ 1 , 2 ] [1, 2] [1,2] | [ 0 , 1 ] [0, 1] [0,1] | s ≤ t s \leq t s≤t | [ 0 , 50000 ) [0, 50000) [0,50000) | 无重边,边形如 ( 2 x − 1 , 2 x , 1 ) (2 x - 1, 2 x, 1) (2x−1,2x,1), ( 2 x − 1 , 2 x + 1 , 0 ) (2 x - 1, 2 x + 1, 0) (2x−1,2x+1,0) 和 ( 2 x , 2 x + 1 , 0 ) (2 x, 2 x + 1, 0) (2x,2x+1,0) |
#2 | 1 0 2 10^2 102 | 2 n − 1 2 n - 1 2n−1 | 1 0 5 10^5 105 | P 2 P_2 P2 | [ 0 , 1 ] [0, 1] [0,1] | [ 0 , 200 ] [0, 200] [0,200] | s = 1 s = 1 s=1 | [ 1 , 50000 ] [1, 50000] [1,50000] | 无重边,边形如 ( x , x , + ) (x, x, +) (x,x,+) 和 ( x , x + 1 , 0 ) (x, x + 1, 0) (x,x+1,0) |
#3 | 1 0 4 10^4 104 | 2 n − 1 2 n - 1 2n−1 | 1 0 5 10^5 105 | P 3 P_3 P3 | [ 0 , 1 ] [0, 1] [0,1] | [ 0 , 300 ] [0, 300] [0,300] | s ≤ t s \leq t s≤t | [ 1 , 1000 ] [1, 1000] [1,1000] | 无重边,边形如 ( x , x , + ) (x, x, +) (x,x,+) 和 ( x , x + 1 , 0 ) (x, x + 1, 0) (x,x+1,0) |
#4 | 5 5 5 | 1 0 5 10^5 105 | 1 0 5 10^5 105 | P 3 P_3 P3 | [ 1 , 50000 ] [1, 50000] [1,50000] | [ 1 , 50000 ] [1, 50000] [1,50000] | |||
#5 | 50000 50000 50000 | 2 n − 20 2 n - 20 2n−20 | 1 0 5 10^5 105 | P 3 P_3 P3 | [ 1 , 200 ] [1, 200] [1,200] | [ 1 , 8000 ] [1, 8000] [1,8000] | 编号大于 10 10 10 的点入度、出度为 1 1 1 | ||
#6 | 1 0 4 10^4 104 | 1 0 5 10^5 105 | 1 0 5 10^5 105 | P 3 P_3 P3 | = 1 = 1 =1 | [ 1 , 1 0 9 ) [1, 10^9) [1,109) | |||
#7 | 1 0 4 10^4 104 | 1 0 5 10^5 105 | 1 0 5 10^5 105 | P 3 P_3 P3 | [ 1 , 2 ] [1, 2] [1,2] | [ 1 , 1 0 9 ) [1, 10^9) [1,109) | 只有 100 100 100 条 w = 2 w = 2 w=2 的边 | ||
#8 | 1 0 3 10^3 103 | 1 0 5 10^5 105 | 1 0 4 10^4 104 | P 3 P_3 P3 | = 1 = 1 =1 | s = 1 s = 1 s=1 | [ 1 , 1 0 9 ) [1, 10^9) [1,109) | ||
#9 | 1 0 4 10^4 104 | 1 0 5 10^5 105 | 1 0 5 10^5 105 | P 4 P_4 P4 | = 1 = 1 =1 | [ 1 , 1 0 8 ) [1, 10^8) [1,108) | v − u ≡ 1 , t − s ≡ k ( m o d 1 0 3 ) v - u \equiv 1, t - s \equiv k \pmod{10^3} v−u≡1,t−s≡k(mod103) | ||
#10 | 3000 3000 3000 | 1 0 5 10^5 105 | 1 0 4 10^4 104 | P 3 P_3 P3 | [ − 5 , 5 ] [-5, 5] [−5,5] | = 1 = 1 =1 | s = 1 s = 1 s=1 | [ 1 , 1 0 9 ) [1, 10^9) [1,109) |
这里 { P 1 = 26873856 = 2 12 × 3 8 P 2 = 19960928 = 2 5 × 1 3 2 × 3691 P 3 = 1998585857 = 2 21 × 953 + 1 P 4 = 19920826 = 2 × 151 × 65963 \begin{cases} P_1 = 26873856 = 2^{12} \times 3^8 \\ P_2 = 19960928 = 2^5 \times 13^2 \times 3691 \\ P_3 = 1998585857 = 2^{21} \times 953 + 1 \\ P_4 = 19920826 = 2 \times 151 \times 65963 \end{cases} ⎩⎪⎪⎪⎨⎪⎪⎪⎧P1=26873856=212×38P2=19960928=25×132×3691P3=1998585857=221×953+1P4=19920826=2×151×65963
其中除了 P 3 P_3 P3 外都是合数,而 P 3 P_3 P3 是一个适合快速数论变换(NTT, Numer-Theoretic Transform)的质数。需要注意的是 2 30 < P 3 < 2 31 2^{30} < P_3 < 2^{31} 230<P3<231,两个 [ 0 , P 3 ) [0, P_3) [0,P3) 中的整数相加的结果可能会超过 2 31 − 1 2^{31} - 1 231−1。
根据以上信息,有经验的做题者应该分析出如下内容:
- 问题模型
- 组合数取模:#1(缩去偶数编号的点)
- 完全背包:#2(前缀物品),#3(区间物品)
- 单位边权问题(矩阵乘法加速递推):#6,#7(拆边或增加状态),#8,#9,#10
- 一般问题:#4,#5(缩去编号大于 10 10 10 的点)
- 优化模型
- #3:物品加入背包的操作可逆,因此区间物品可以规约到前缀物品
- #6 和 #7:点数和询问数规模严重不对等,可以尝试用分块技巧,在增加预处理的基础上,降低询问的运算量
- #9:图可以分为 100 100 100 个大小为 10 10 10 的块,加快递推过程,同时可以沿用预处理技巧
- #4:动态规划的转移类似卷积,可以用分治和 NTT 加速求解
- #8 和 #10:起点固定,可以优化矩阵乘法,并用特征多项式加速递推,同时可以沿用分块技巧
具体来说,各测试点的可选做法分析如下:(太长不看可以跳过,后面有选定的做法)
- #1:去掉必须要选的边,将
s
s
s 和
t
t
t 移动到奇数编号的点上,所求形如
(
x
k
)
=
x
!
(
x
−
k
)
!
k
!
\binom{x}{k} = \frac{x!}{(x - k)! k!}
(kx)=(x−k)!k!x!,
x
=
t
−
s
2
x = \frac{t - s}{2}
x=2t−s
- 这个点有很多种做法,均可在文章组合数求模中找到
- 最暴力的做法可以是 O ( n 2 ) \mathcal{O}(n^2) O(n2) 递推求解杨辉三角的每一行,由于数组大小的限制,这里需要滚动数组,并且将询问离线排序处理(大约需要跑一分钟)
- 另一种暴力做法可以是对于每个询问用线性筛将组合数的质因数分解求出,这样是 O ( n q ) \mathcal{O}(n q) O(nq) 的,不需要离线询问(大约需要跑两分钟)
- 比较快的做法是预处理阶乘 x ! x! x! 中与 P P P 互质部分的乘积,以及不互质部分里各个质因子的幂次,求组合数时需要求互质部分的逆元,以及用快速幂求出不互质部分的值,这样是 O ( n ω ( P ) + q ( ω ( P ) log n + log P ) ) \mathcal{O}(n \omega(P) + q (\omega(P) \log n + \log P)) O(nω(P)+q(ω(P)logn+logP)) 的(一秒出解)
- 最快的做法是在模 P P P 的每个质因子幂次意义下,预处理阶乘,求出每个询问的值,最后再用中国剩余定理(CRT, Chinese Remainder Theorem)合并起来,这样是 O ( n ω ( P ) + log P + q ( ω ( P ) log log P + log P ) ) \mathcal{O}(n \omega(P) + \log P + q (\omega(P) \log \log P + \log P)) O(nω(P)+logP+q(ω(P)loglogP+logP)) 的(一秒出解)
- #2:因为起点是
1
1
1,所以每个询问只需要考虑
[
1
,
t
]
[1, t]
[1,t] 的每个点的自环走了多少次即可,这些自环可以看成互相独立的几种物品,现在要用大小
k
k
k 的背包去装这些物品,每种物品可以选多次
- 令 f ( i , j ) f(i, j) f(i,j) 表示 [ 1 , i ] [1, i] [1,i] 的自环里走了长度 j j j 的路径数,则有 f ( 0 , 0 ) = 1 f(0, 0) = 1 f(0,0)=1, f ( i , j ) = ∑ k ≥ 0 f ( i − 1 , j − k l i ) f(i, j) = \sum_{k \geq 0} {f(i - 1, j - k l_i)} f(i,j)=∑k≥0f(i−1,j−kli),这里 l i l_i li 表示点 i i i 的自环长度,时间复杂度 O ( n max 2 { k } + q ) \mathcal{O}(n \max^2\{k\} + q) O(nmax2{k}+q)(大约需要跑半小时)
- 注意到有 f ( i , j ) − f ( i , j − l i ) = f ( i − 1 , j ) f(i, j) - f(i, j - l_i) = f(i - 1, j) f(i,j)−f(i,j−li)=f(i−1,j) 即可优化到 O ( n max { k } + q ) \mathcal{O}(n \max\{k\} + q) O(nmax{k}+q)(一秒出解)
- 另一方面,从生成函数的角度看,令 F i ( x ) = ∑ j ≥ 0 f ( i , j ) x j F_i(x) = \sum_{j \geq 0}{f(i, j) x^j} Fi(x)=∑j≥0f(i,j)xj,则有 F 0 ( x ) = 1 F_0(x) = 1 F0(x)=1, F i ( x ) = F i − 1 ( x ) ( 1 + x l i + x 2 l i + ⋯ ) = F i − 1 ( x ) 1 − x l i F_i(x) = F_{i - 1}(x) (1 + x^{l_i} + x^{2 l_i} + \cdots) = \frac{F_{i - 1}(x)}{1 - x^{l_i}} Fi(x)=Fi−1(x)(1+xli+x2li+⋯)=1−xliFi−1(x),也即 F i ( x ) = F i − 1 ( x ) + x l i F i ( x ) F_i(x) = F_{i - 1}(x) + x^{l_i} F_i(x) Fi(x)=Fi−1(x)+xliFi(x),从而得到同上的结果
- #3:同 #2,但是询问的区间不再是前缀区间
- 如果不考虑生成函数,只能做到 O ( q n max { k } ) \mathcal{O}(q n \max\{k\}) O(qnmax{k}),运气不好的话可能要跑三个小时
- 考虑模 x max { k } + 1 x^{\max\{k\} + 1} xmax{k}+1 意义下的生成函数,所求即 F t ( x ) F s − 1 ( x ) \frac{F_t(x)}{F_{s - 1}(x)} Fs−1(x)Ft(x) 的 x k x^k xk 项系数,最暴力又好写的做法就是预处理 F i ( x ) F_i(x) Fi(x),在询问时暴力计算除法,这样是 O ( n max { k } + q max 2 { k } ) \mathcal{O}(n \max\{k\} + q \max^2\{k\}) O(nmax{k}+qmax2{k}),可能要跑个十五分钟
- 实际上我们也可以处理 G i ( x ) = 1 F i ( x ) = G i − 1 ( x ) ( 1 − x l i ) G_i(x) = \frac{1}{F_i(x)} = G_{i - 1}(x) (1 - x^{l_i}) Gi(x)=Fi(x)1=Gi−1(x)(1−xli),这样询问时只需要进行求一个系数所需的乘积,时间复杂度 O ( ( n + q ) max { k } ) \mathcal{O}((n + q) \max\{k\}) O((n+q)max{k}),几秒内出解
- #4:点数很少,长度也很小
- 令 f ( i , j , d ) f(i, j, d) f(i,j,d) 表示从点 i i i 到点 j j j 走了长度 d d d 的路径数量,那么有 f ( i , j , d ) = ∑ ( k , j , w ) ∈ E f ( i , k , d − w ) f(i, j, d) = \sum_{(k, j, w) \in E}{f(i, k, d - w)} f(i,j,d)=∑(k,j,w)∈Ef(i,k,d−w),最暴力的做法是按照 d d d 升序求解,时间复杂度 O ( n m max { k } + q ) \mathcal{O}(n m \max\{k\} + q) O(nmmax{k}+q),大约需要跑五分钟
- 令 F i , j ( x ) = ∑ d ≥ 0 f ( i , j , d ) x d F_{i, j}(x) = \sum_{d \geq 0}{f(i, j, d) x^d} Fi,j(x)=∑d≥0f(i,j,d)xd, G i , j ( x ) = ∑ ( i , j , w ) ∈ E x w G_{i, j}(x) = \sum_{(i, j, w) \in E}{x^w} Gi,j(x)=∑(i,j,w)∈Exw,那么有 F i , j ( x ) = [ i = j ] + ∑ k F i , k ( x ) G k , j ( x ) F_{i, j}(x) = [i = j] + \sum_{k}{F_{i, k}(x) G_{k, j}(x)} Fi,j(x)=[i=j]+∑kFi,k(x)Gk,j(x),这里 [ i = j ] [i = j] [i=j] 在 i i i 和 j j j 相等时为 1 1 1,否则为 0 0 0,而这个卷积方程可以用分治的方法加速到 O ( n 3 max { k } log 2 max { k } + q ) \mathcal{O}(n^3 \max\{k\} \log^2 \max\{k\} + q) O(n3max{k}log2max{k}+q),但如果不对 NTT 进行常数优化,仍然需要跑一分钟
- #5:编号大于
10
10
10 的点恰好有一条入边、一条出边,可以将这些点与相应的两条边合成一条,从而变成 #4 的情况
- 同 #4,由于长度变得更小,可以将许多相同的边合并起来考虑,使 #4 的暴力方法做到 O ( N 3 max { w } max { k } + q ) \mathcal{O}(N^3 \max\{w\} \max\{k\} + q) O(N3max{w}max{k}+q),这里 N = 10 N = 10 N=10,大概需要跑一分钟
- 相应地,如果用 #4 的分治方法加速,则依然会跑到一分钟左右,有些得不偿失
- #6:单位边权的图,点数远小于询问数
- 令 f d ( i , j ) f_d(i, j) fd(i,j) 表示从点 i i i 到点 j j j 走了长度 d d d 的路径数量, E ( i , j ) E(i, j) E(i,j) 表示点 i i i 到点 j j j 的边数,则有 f 0 ( i , j ) = [ i = j ] f_0(i, j) = [i = j] f0(i,j)=[i=j], f d ( i , j ) = ∑ k f d − 1 ( i , k ) E ( k , j ) f_d(i, j) = \sum_{k}{f_{d - 1}(i, k) E(k, j)} fd(i,j)=∑kfd−1(i,k)E(k,j),若将它们视为 n × n n \times n n×n 的矩阵,则有 f 0 = I f_0 = I f0=I, f d = f d − 1 E = f 0 E d = E d f_d = f_{d - 1} E = f_0 E^d = E^d fd=fd−1E=f0Ed=Ed,于是我们可以用快速幂处理询问,这样的复杂度是 O ( q n 3 log max { k } ) \mathcal{O}(q n^3 \log \max\{k\}) O(qn3logmax{k})(大概五小时也跑不出来)
- 注意到对于每个询问,所求只是 E k E^k Ek 的一个点的信息,如果我们在计算 E k E^k Ek 之前先乘上一个 1 × n 1 \times n 1×n 的行向量 R s R_s Rs,则快速幂时其中一部分的矩阵乘法可以做到单次 O ( n 2 ) \mathcal{O}(n^2) O(n2),而剩下的矩阵乘法都是在计算形如 E 2 e + 1 = E 2 e ⋅ E 2 e E^{2^{e + 1}} = E^{2^e} \cdot E^{2^e} E2e+1=E2e⋅E2e,我们可以预处理这些 E 2 e E^{2^e} E2e,或者每计算出一个 E 2 e E^{2^e} E2e 就对相应的询问产生贡献,从而做到 O ( ( n 3 + q n 2 ) log max { k } ) \mathcal{O}((n^3 + q n^2) \log \max\{k\}) O((n3+qn2)logmax{k})(然后跑个二十分钟)
- 注意到快速幂的两部分运算量差距还是很大,不妨尝试用分块进行改进,设定阈值 T T T 表示预先计算是 E c ⋅ T e E^{c \cdot T^e} Ec⋅Te ( c = 1 , 2 , … , T − 1 ) (c = 1, 2, \ldots, T - 1) (c=1,2,…,T−1),则复杂度会变为 O ( ( n 3 T + q n 2 ) log T max { k } ) \mathcal{O}((n^3 T + q n^2) \log_{T}{\max\{k\}}) O((n3T+qn2)logTmax{k}),取 T = q n T = \frac{q}{n} T=nq 可以做到最优复杂度(需要跑个五分钟)
- #7:有
100
100
100 条边的边权是
2
2
2,其他边权都是
1
1
1,点数远小于询问数
- 一种想法是在这 100 100 100 条边的中点加上新点,然后这 ( n + 100 ) (n +100) (n+100) 个点套用 #6 的做法
- 另一个想法是构造 [ f d f d + 1 ] = [ f d − 1 f d ] ⋅ [ O E 2 I E 1 ] \begin{bmatrix} f_d & f_{d + 1} \end{bmatrix} = \begin{bmatrix} f_{d - 1} & f_d \end{bmatrix} \cdot \begin{bmatrix} \mathbf{O} & E_2 \\ I & E_1 \end{bmatrix} [fdfd+1]=[fd−1fd]⋅[OIE2E1],其中 E w E_w Ew 表示边权是 w w w 的边形成的矩阵,然后还是套用 #6 的做法与分析
- #9:单位边权的图,将所有点的编号放到模
1
0
3
10^3
103 意义下后只有形如
(
u
,
(
u
+
1
)
m
o
d
1
0
3
,
1
)
(u, (u + 1) \bmod 10^3, 1)
(u,(u+1)mod103,1) 的边,同时询问满足
t
−
s
≡
k
(
m
o
d
1
0
3
)
t - s \equiv k \pmod{10^3}
t−s≡k(mod103)
- 如果不分析图的特征,直接套用 #6 最快的做法大概要跑个九天九夜
- 记 B = 1 0 3 B = 10^3 B=103,把点按照模 B B B 的余值划分剩余类,设 E i E_i Ei 表示余数是 i i i 的点到余数是 ( i + 1 ) m o d B (i + 1) \bmod B (i+1)modB 的点的边形成的矩阵,那么从 s s s 到 t t t 的路径中连续经过 E 0 ⋅ E 2 ⋯ E B − 1 E_0 \cdot E_2 \cdots E_{B - 1} E0⋅E2⋯EB−1 的部分都可以合并成一个整体考虑,剩下的部分长度不会超过 2 ( B − 1 ) 2 (B - 1) 2(B−1),如果直接暴力计算,我们可以做到 O ( ( n B ) 3 ( B + max { k } B ) + q B ( n B ) 2 ) \mathcal{O}\left(\left(\frac{n}{B}\right)^3 \left(B + \frac{\max\{k\}}{B}\right) + q B \left(\frac{n}{B}\right)^2\right) O((Bn)3(B+Bmax{k})+qB(Bn)2),大约需要跑五分钟
- 注意到询问的路径要么是一个区间的矩阵乘积,要么可以写成一个后缀区间、许多个整体、一个前缀区间,如果预处理这些区间,则可以做到 O ( ( n B ) 3 ( B 2 + max { k } B ) + q ( n B ) 2 ) \mathcal{O}\left(\left(\frac{n}{B}\right)^3 \left(B^2 + \frac{\max\{k\}}{B}\right) + q \left(\frac{n}{B}\right)^2\right) O((Bn)3(B2+Bmax{k})+q(Bn)2),可以在几秒内出解,但是由于空间限制,这些区间可能需要用上滚动数组的技巧
- 这里虽然也可以对整体的幂次计算部分进行 #6 中的分块优化,但由于其优化的部分不是运算量的主要因素,改进后的效果会和暴力方法持平,故这里略去
- #8 和 #10:单位边权的图,询问起点唯一,其中 #10 只在编号之差不超过
5
5
5 的点之间连边
- 虽然 #10 比 #8 多了些特性,但笔者也只能发现边数上的优化,其他部分还是得同 #8 中的做法,所以这里将两个测试点合起来分析
- 沿用 #6 的分析,#8 每组询问单独处理需要跑一个月,用上预处理和行向量需要跑一个小时,加上分块优化需要半个小时,相应地,#10 需要两年半、十个小时和九个小时,因此必须继续优化
- 注意这两个测试点和 #6 唯一的不同就是询问起点唯一,这意味着我们在预处理时可以尝试就提前乘上一个行向量 R s R_s Rs,这能使得我们在 O ( n min { n 2 , m } ) = O ( n m ) \mathcal{O}(n \min\{n^2, m\}) = \mathcal{O}(n m) O(nmin{n2,m})=O(nm) 的时间内求出 R s E i R_s E^i RsEi ( i = 0 , 1 , … , n − 1 ) (i = 0, 1, \ldots, n - 1) (i=0,1,…,n−1),在此基础上可以利用特征多项式将 R s E k R_s E^k RsEk 表示成预处理信息的线性组合,即 R s E k = ∑ i = 0 n − 1 c k , i R s E i R_s E^k = \sum_{i = 0}^{n - 1}{c_{k, i} R_s E^i} RsEk=∑i=0n−1ck,iRsEi,那么在计算出特征多项式的基础上,复杂度可以做到 O ( n m + D ( n ) + q M ( n ) log max { k } ) \mathcal{O}(n m + D(n) + q M(n) \log \max\{k\}) O(nm+D(n)+qM(n)logmax{k}),这里 D ( n ) D(n) D(n) 为计算特征多项式的复杂度, M ( n ) M(n) M(n) 为长度 n n n 的多项式进行乘、取模的复杂度
- 为了达到比 #6 快的效果,这里 M ( n ) M(n) M(n) 需要做到 O ( n log n ) \mathcal{O}(n \log n) O(nlogn),那么必须要用 NTT 加速乘法,而多项式取模中所需的求逆部分是对同一个多项式求逆,这可以暴力预处理出来
- 同样地, D ( n ) D(n) D(n) 要做到 O ( n 3 ) \mathcal{O}(n^3) O(n3) 或更快,不然还是无法在五小时内跑完 #10,为了做到这个复杂度,我们可以利用相似变换,从而在不改变特征多项式的情况下,将矩阵化为 Hessenberg 型(主对角线的一边除了与它相邻的对角线外的所有元素为 0),然后依次求出每个顺序主子式的特征多项式
- 经过上述优化后,#8 和 #10 分别能在三分钟和二十分钟内得到结果,当然,你也可以沿用 #6 的分块优化(甚至由于初始值计算方便,这里的生成函数更适合进行这样的优化),做到三十秒和十分钟出解
综合考虑各测试点的分析与编写和运行的取舍,并预估可能存在的调试难度,对在训练时的做法,笔者建议按如下顺序进行编写:
- #1:写杨辉三角的做法,跑一分钟
- #2 和 #3:都写区间完全背包的做法,只改动空间相关的参数,跑几秒
- #4 和 #5:都写暴力动态规划的做法,#5 讨论一下缩点的问题,跑五分钟
- #9:写小区间暴力的做法,跑五分钟
- #6 和 #7:#7 加点转化为 #6,都写带有行向量和分块预处理优化的做法,跑十分钟
- #8 和 #10:都写特征多项式的做法,带分块预处理优化,但是先将多项式乘法和取模写成暴力,跑着程序的时候再用 NTT 优化卷积、用乘法优化除法(即取模),如果能写完,则看哪个先跑出结果
注意,这里在实现时最好充分利用下发文件里第一组询问的答案来做验证,多数情况下能帮助调试,不过还是得小心一些可能存在的细节问题,除了实现上的笔误,也要注意是否有忘记讨论的情况,例如 #1 和 #5 都存在缩点后 k < 0 k < 0 k<0 的情况,漏掉对其的判断可能会出现数组越界的问题,但程序不一定会崩溃,而是给出错误的答案。
在这一万字文章的最后,我们来总结一下可以使用的技巧:
- 初等数论
- 整数模运算(加减乘、逆元、快速幂):所有测试点都需要
- 整数分解:人工分析 P 1 P_1 P1, P 2 P_2 P2, P 3 P_3 P3, P 4 P_4 P4 并求解模 P 4 P_4 P4 意义下的原根时需要,这里求原根是使用 NTT 加速卷积的必要过程
- 生成函数与多项式(涉及组合数学)
- 暴力乘、除(求逆):#8 和 #10 可选,形式上 #2, #3, #4 和 #5 也涉及到
- 卷积(NTT 加速):#8 和 #10 需要,#4 和 #5 可选
- 矩阵(涉及线性代数、矩阵论)
- 矩阵乘法:#6, #7 和 #9 需要,形式上 #4 和 #5 也涉及到
- 高斯消元(化矩阵为相似 Hessenberg 形):#8 和 #10 需要
- 算法技巧
- 询问离线降低空间复杂度:#4 不需要,其他测试点均可选
- 分块或预处理平摊时间复杂度:#6, #7, #8 和 #10 需要,#9 可选
最后祝你身体健康!