若是要用一个词来形容上午的测试,那真是体无完肤。
成绩:node
题目 | 成绩 | 评价 |
---|---|---|
T1 | 50 | 通常 |
T2 | 10 | 大失所望 |
T3 | 0 | 差 |
:指经过观察、概括,发现较大规模问题和较小规模问题之间的关系,用一些数学公式表达出来,在一些教材中,也称为计数DP。递推的模型最主要有:斐波那契数列,卡特兰数,bell数,错排等等。c++
:所谓递归,是指函数“本身调用本身”的一种编程方法,在解决一个问题时,若是发现问题能拆解为一个或多个相同规模的子问题时,就能够考虑使用这种方法求解。最经典的题型有汉诺塔等。算法
:分治解决问题的通常步骤:
分,将问题划分为若干个规模较小、形式相同的子问题
治,递归求解,当问题规模足够小时直接求答案
合,将规模更小的问题获得的答案合并起来,获得原问题的答案。
主要模型应用有归并排序、快速幂等。
是否是很难懂?呐看一个例题
分治经典例题:给定 2^n * 2^n 的棋盘,其中有一个坏点,须要用若干个 L 型方块覆盖棋盘:不能覆盖到坏点,其余点要被覆盖刚好一次。给出方案。
解:这道题目有个基本单元必定反复存在:
这个只须要放一个L形状的方块去覆盖就解决了问题。咱们把它拓展一下,把每一个格子想成多个格子的集合,若是咱们能肯定这个坏点在其中一个集合里面,那么咱们能够把整个问题分红四个部分:
而后发现左下角的格子就是一个基本模型,而后中间的四块连在一块儿也是一个模型,而后咱们能够在中间搞事情,放上一个L形状的木块,因为每一个格子只能刚好被覆盖一次,因此咱们能够把全部被覆盖的地方都标记为坏点。而后另外三个大格子也变成了三个模型
是否是就作出来了。
二分:二分实际上是分治的一种。二分是竞赛中一个很是实用的算法思想经过二分,把最优化问题转化为断定性问题,把优化目标转化为断定的标准,就能发现问题的一些“贪心性质”,从而设计高效算法进行断定。
求“ XXX 的最大值 ” 最小多是多少(或者反过来)均可以考虑二分
有些时候,二分的模型比较隐蔽,须要更深刻的思考和发现性质
二分答案的时间复杂度是O(logn)编程
:倍增字面上意思是:成倍地增长。当咱们模拟一个过程时,一步一步进行太慢,考虑把模拟的步数二进制分解;通过一些预处理,每次能够模拟 2^i 步,从而达到优化复杂度的目的。主要模型有RMQ,LCA等。数组
:哈哈哈哈哈哈贪心是个好东西啊。由贪心获得局部最优解,从而获得全局最优解,通常容易实现、时间复杂度优秀,可是……难于证实,贪心的脑洞通常都很大。数据结构
特特特特特特就是个数论。ide
模算术能够这样简单理解:就是不停地模啊模。就能够了。好比a+b=c(mod p)就表明(a+b) mod p=c mod p;模算术只有在模意义下才有固定的意义。函数
类比倒数,若是a*b=1,那么ab互为倒数;相似地,咱们定义若是在 mod p意义下,a *b=1,那么a和b互为mod p意义下的乘法逆元。工具
方程 ax + by = c 有解,当且仅当 gcd(a, b) | c。性能
gcd(a, b) = gcd(a - b, b)
事实上,定理直接给出了一种计算 gcd 的方式,直接根据上述公式计算,复杂度 O(\log min(a, b))
事实上,咱们能够直接在欧几里德算法求解 gcd(a, b) 的过程当中,构造一组 ax + by = gcd(a, b) 的解
这个方法依赖于递归的思想
边界:b = 0 时, a * 1 + b * 0 = gcd(a, b)
设咱们找到了一组 bx + (a % b)y = gcd(a, b) 的解,那么:
bx + (a - [a / b] * b]) y = gcd(a, b) ==>
ay + b * (x - [a / b] * y) = gcd(a, b)
令 x’ = y, y’ = (x - [a / b] * y),能够获得:
ax’ + by’ = gcd(a, b)
令 d = gcd(a, b)
当咱们求出 ax + by = c 的一组解 x0、y0 以后
方程的通解具备如下形式:
x = x0 + k * (b / d)
y = y0 - k * (a / d)
直观理解一下,这是一个设法让正负互相抵消的过程
定义:大于 1 的、只被 1 和它自己整除的正整数称为素数
惟一分解定理,
又称算术基本定理:
对于正整数 n,咱们必定能够将其写为若干个质数的幂的乘积形式,即
n = ∏ (pi ^ ai),其中 pi 为质数
而且,这种分解是惟一的
若是咱们要筛出 [1, n] 内的全部素数,使用 [1, √n] 内的素数去筛就能够了
设数组 mark[],mark[i] 表示 i 是否被某个素数筛过
从 2 开始枚举每一个数 i:
若 mark[i] = 0,表示 i 没有更小的素因子,从而知道 i 是素数。枚举 i 的全部倍数 j,令 mark[j] = 1
若 mark[i] = 1,知道 i 是一个合数
复杂度 O(n\lg\lg n)
来一段程序吧:
埃氏筛法的应用:在进行筛法的同时,咱们能够对 [1, n] 内的合数进行素数分解,从而完成一些其余工做,,例如,对于一个素数 i,枚举它的倍数 j 时,把 j 中的全部因子 i 除干净,就知道 j 的质因子分解中 i 的幂了。有一类问题,须要你预处理区间 [L, R] 内的素数分布,其中 R - L <= 10^6, R <= 10^10。此时,区间 [L, R] 的长度比较小;另外一方面,√R <= 10^5。咱们就能够考虑使用埃氏筛的思想来求解问题。用一个数组 mark2[] 维护区间 [L, R] 被筛的结果,用素数 p 去筛 [L, R] 的计算。为 (R - L) / p,总计算复杂度大概为 H_{R-L},即 O((R - L) \log R)
p 是质数的充要条件为
(p - 1)! ≡ -1 (mod p)
充分性:
若 p 不是质数,则 gcd( (p-1)!, p ) > 1,与 Bezout 定理相悖
必要性:
考虑 [1, p) 中的某个数 x 和它的乘法逆元 y,若是 x != y, 那么 xy ≡ 1,能够令它们互相抵消,因而只须要考虑 x 是本身的逆元的状况,解 x^2 ≡ 1 ==> p | (x - 1) * (x + 1) ,由于 p 是质数,只有多是 x ≡ ±1,得证
欧拉函数 φ(n),表示不大于 n 的、与 n 互质的正整数个数,令 n = ∏ (pi^ai),则 φ(n) = n * ∏ (1 - 1/pi)
计算方法:
对 n 进行质因子分解的过程当中维护,O(√n)
埃氏筛的时候顺便维护,复杂度同埃氏筛
欧拉函数的两个公式:
∑ { φ(d) | d | n } = n
证实:在 [1, n] 中,刚好有 φ(d) 个数和 n 的 gcd 为 n / d
n > 1 时, ∑ { d | gcd(d, n) = 1 } = n * φ(n) / 2
证实:当 gcd(x, n) = 1 时,可得 gcd(n - x, n) = 1,能够将与 n 互质的数两两配对
对于 n <= 2 的状况,特殊讨论
剩余类:
给定 n,整数按照模 n 取值的不一样,能够分为 n 个子集,称为剩余类
剩余系:
给定 n,从模 n 的 n 个剩余类中各取一个数构成的集合,称为模 n 的一个剩余系,剩余系通常指彻底剩余系
简化剩余系:
也称既约剩余系,是模 n 的彻底剩余系的一个子集,其中每一个元素与n 互素,容易验证,简化剩余系中刚好有 φ(n) 个元素
考虑模 n 的一个简化剩余系 S
由于 S 中的每一个元素和 n 互质,它们在模 n 意义下存在乘法逆元
任取一个 a,使得 (a, n) = 1
考虑集合 T = { ax | x ∈ S }
容易验证:|T| = |S|,且 |T| 中的数模 n 互不一样余,|T| 中的数均和 n 互质
由此,可得 T 也是模 n 的一个简化剩余系。由于 S 和 T 均为模 n 的简系,可得:
∏ {x | x ∈ S} ≡ ∏ {y | y ∈ T} ==>
∏ {x | x ∈ S} ≡ ∏ {ax | x ∈ S} ==>
∏ {x | x ∈ S} ≡ a ^ {|S|} * ∏ {x | x ∈ S} ==>
a^{|S|} ≡ 1
这就是欧拉定理:
若 (a, n) = 1,则 a ^ φ(n)=1(模意义下)
由欧拉定理能够推导出一些颇有用的结论
费马小定理:
若 p 是质数且 (a, p) = 1,则 a ^ (p-1) ≡ 1 (mod p)
一个应用:a ^ (p - 2) ≡ 1 / a (mod p),能够经过快速幂求逆元
欧拉定理 EXT:
若 (a, p) = 1,则 a ^ x ≡ a ^ (x % φ(p)) (mod p)
任何状况下,若 x > φ(p),则 a ^ x ≡ a ^ (x % φ(p) + φ(p)) (mod p)
能够用来求解这样的同余方程组:给定长度为 k 的数组 a[] 和数组 m[],求解,x ≡ a[i] (mod m[i]),保证 m[i] 两两互质。
推导:定理:令 M = ∏ (m[i]),方程在 [0, M) 中有惟一解 x0,而且通解具备 kM + x0 的形式
这里证实惟一性:
设 x1 != x2 是方程的两个解,咱们有
x1 - x2 ≡ 0 (mod m[i])
x1 - x2 是每一个 m[i] 的倍数,它们必然也是 M 的倍数,即 M | x1 - x2
因而,x1 和 x2 最多只有一个落在区间 [0, M) 中
解的存在性,咱们经过一种构造算法来证实
。
构造解
令 Mi = M / m[i],由于 (M[i], m[i]) = 1,咱们能够找到 Mi 模 m[i] 的乘法逆元 t[i]
考察 Mi * t[i]:
j = i 时,M[i] * t[i] ≡ 1 (mod m[j])
j != i 时,M[i] * t[i] ≡ 0 (mod m[j])
令 x = ∑ (a[i] * M[i] * t[i])
不难验证,x 是知足要求的
模数不互质
模数不互质的时候,怎么办?
将方程两两合并,考虑方程组:
x ≡ a1 (mod m1)
x ≡ a2 (mod m2)
令 x = k1 * m1 + a1 = k2 * m2 + a2
则有 k1 * m1 - k2 * m2 = a2 - a1
由 Bezout 定理判断解的存在性,用 extend_gcd 解方程便可
事实上,能够获得一个推论:
若是存在解,则 x 在 [0, lcm(m1, m2) ) 中有惟一解
应用
能够直接解决一类问题,有些数学计算题,给出的模数 m 并非一个素数,而不少计算在模素数(或素数的幂)下会方便不少,因而能够将 m 分解质数,求答案模每一个素数(的幂)的值,最后使用中国剩余定理合并
C(n, m) ≡ C(n / p, m / p) * C(n % p, m % p) (mod p)
证实:
核心思想:用两种方法展开 (x + 1) ^ n,考虑 x^m 一项的系数
重要公式:(x + 1) ^ p ≡ x^p + 1 (mod p)
一种优秀的筛法
线性复杂度的来源:每一个数只会被它的最小的质因子筛掉
简单证实: 设有一个数 m = p * q * s
其中 p 是最小的质因子,q 是另一个质因子,若是 m 会被 q 筛掉,那必定是在 i = p * s 时筛掉的,可是咱们发现 i = p * s 时,j 枚举到 p 这个质数时就会 break,把每一个数被筛的过程画成一张图,发现相同的质因子是在连续的几步被筛掉的。实际上,筛的过程至关于按照质因子降序分解了每一个数。
如有函数 f(n) 的定义域为正整数,值域为复数,称为数论函数
进一步地,若数论函数 f(n) 知足:对于互质的 p、q,有 f(p * q) = f(p) * f(q),称为积性函数,或者说函数知足积性。更进一步地,若数论函数 f(n) 知足:对于任意 p、q,有 f(p * q) = f(p) * f(q),称为彻底积性函数
常见的积性函数
除数函数 σk(n)=∑(d|n) d ^ k,表示n的约数的k次幂和
约数个数函数 τ(n)=σ0(n)=∑(d|n) 1,表示n的约数个数,通常也写为d(n)。
约数和函数 σ(n)=σ1(n)=∑(d|n) d,表示n的约数之和
欧拉函数 φ(n)
莫比乌斯函数μ(n):
n 有平方因子时值为 0
不然值为 (-1) ^ (质因子个数)
元函数 e(n) = [n = 1],彻底积性
恒等函数 I(n) = 1,彻底积性
单位函数 id(n) = n,彻底积性
对两个数论函数进行的运算
设咱们有两个数论函数 f(n) 和 g(n)
它们的狄利克雷卷积是一个新的函数 (f * g) (n)
设这个函数为 h
咱们有 h(n) = ∑(k|n) f(k) * g(n / k)
Dirichlet卷积的性质
积性函数的狄利克雷卷积仍然知足积性
证实:对互质的 p、q,有
h(p) * h(q)
= ∑ { f(a) * g(b) | ab = p }* ∑ { f(c) * g(d) | cd = q }
= ∑(ab = p, cd = q) f(ac) * g(bd)
=∑(xy = pq) f(x) * g(y)
=h(p * q)
注意:彻底积性函数的狄利克雷卷积不必定知足彻底积性。
狄利克雷卷积知足结合律,即对于三个数论函数 f、g、h,有 (f * g) * h = f * (g * h)
证实:
(f * g) * h (n)
= ∑(ab = n) h(b) * (∑(xy = a) f(x) * g(y))
= ∑(xyb = n) f(x) * g(y) * h(b)
= ∑(xy = n) f(x) * (∑(ab = y) g(a) * h(b))
= f * (g * h) (n)
Dirichlet 卷积同时也具备交换律、分配律
Dirichlet 卷积运算存在单位元(元函数 e):f * e = e * f = f。
常见的公式
id = �� * 1
d = 1 * 1
σ = id * 1
e = 1 * μ (反演式) *
�� = id * μ *
积性函数的预处理
借助线性筛,能够在 O(n) 时间内预处理出某种数论函数在 [1, n] 的取值
咱们以约数个数函数为例说明
莫比乌斯函数、欧拉函数、约数和函数的预处理都应该熟练掌握
约数个数函数
n = ∏ (pi^ai)
d(n) = ∏ (ai + 1)
咱们维护一个辅助数组 num[x] ,表示 x 的最小的质因子的幂次
线性筛每次会筛掉一个数最小的质因子,这让咱们能够很方便地维护 num[],从而计算 d[]
咱们设置一个步长 m
则任意一个可行解能够写成 x = am + b (0 <= b < m)
A ^ (am + b) = B mod P => A ^ b = B * A ^ (-am) mod P
把 0 <= b < m 的 A ^ b 预先算出来,扔到哈希表中
查询的时候暴力枚举 a,算出等式右边,在哈希表中查询
复杂度 O(P / m) + O(m)
m = √P 时,取得最优复杂度 O(√P)
若是用 map 实现哈希表,还要乘一个 log
若是 P 不是质数,怎么办?
若 (A, P) = 1,算法仍是能够跑的,求逆元改为 extend_gcd 就好了
(A, P) != 1时,问题出在哪儿?
A 不存在关于 P 的逆元
令 d = gcd(A, P)
A/d * A ^ (x - 1) = B/d (mod (P/d) )
递归求解
最多 log(P) 层,注意最后算出来的答案 x 要加上层数
所以,须要特判掉 x <= log(P) 的状况
黑色星期一??蒟蒻被虐的第一天。数论越讲到后面越听不懂,上面列出的全部知识点都要本身好好去学下。
题目 | 成绩 | 评价 |
---|---|---|
T1 | 10 | 觉得暴力能拿60分,结果只有十分 |
T2 | 0 | 翻车 |
T3 | 4 | 骗到4分 |
翻车翻到底了
搜索算法是利用计算机的高性能来有目的的穷举一个问题解空间的部分或全部的可能状况,从而求出问题的解的一种方法。现阶段通常有枚举算法、深度优先搜索、广度优先搜索、A* 算法、回溯算法、蒙特卡洛树搜索、散列函数等算法。在大规模实验环境中,一般经过在搜索前,根据条件下降搜索规模;根据问题的约束条件进行剪枝;利用搜索过程当中的中间解,避免重复计算这几种方法进行优化。
dfs深度优先搜索和bfs广(宽)度优先搜索是搜索的两种基本模型。
例题1——斗地主
题目地址
luogu斗地主
luogu斗地主加强版
解:咱们须要考虑预处理dp数组: dp[x][y][z][w] 表示分别有 x、y、z、w 种出现 四、三、二、1 次的牌,不出顺子最少几步打完。DFS 搜索打顺子的状况,结合 dp 计算答案。加上最优性剪枝就能够A掉。
中途相遇法指的是,从起点和答案两边同时开始搜索,在中间相遇并求解的方法。这种方法有什么好呢?咱们用给一张图片来解决。
空白部分在中途相遇法中就能够忽略掉了。
例题2
已知 N 元高次方程:sum_{i<=N} k[i] * x[i] ^ p[i] = 0,设未知数均为不大于 M 的正整数,求解的个数。N <= 6, M <= 150。
解:将前半部分的搜索结果存入 Hash 表,再搜后半部分。这就是一个中途相遇法。
给定素数 M 和整数 A、B,解方程 A^x = B (mod M),给出任意一组解,或者说明无解。A, B <= M <= 10^9。
解:这道题目须要使用离散对数。什么是离散对数呢?
咱们能够联系对数的概念,若A^x=B那么,x是B以A为底对数。一样的,在模意义下,A^x=B(mod m),x就是B以A为底,模m意义下的对数。
启发式搜索又称为有信息搜索,它是利用问题拥有的启发信息来引导搜索,达到减小搜索范围、下降问题复杂度的目的,这种利用启发信息的搜索过程称为启发式搜索。启发式搜索中应用最为普遍的搜索技巧当属 A* 和 IDA* 了,前者是 BFS 的启发式版本,后者是前者的迭代加深版本。
原理
A* 和 IDA* 算法中,对每个状态 x 引入了一个估价函数 f (x) = g(x) + h(x), 其中 g(x) 是目前状态的实际代价,而 h(x) 是 “目前状态到目标状态的估计代价”。在 A* 算法中,估价函数的做用是调整搜索顺序让最优的解最 先搜索到。而在 IDA* 中,估价函数做为最优性剪枝出现。 为了保证正确性,通常要求 h(x) <= 从 x 到目标状态的实际代价。特别地,当 h(x) = 0 时,A* 和 IDA* 分别退化为通常的BFS和迭代加深算法。
算法原理:
A*:
BFS 的改进,用优先队列维护全部能够扩展的状态
每次把指望里目标更近的状态拿出来扩展
IDA*:
从小到大“试答案”并进行断定
ans = 0; while (!check(ans)) ans++;
断定通常使用深搜(当 深度 > ans 就没必要搜下去了)
优劣比较:
A*:容易理解,实现容易;空间需求巨大
IDA*:不太容易实现,空间需求小,可是有重复扩展问题
PS:由于通常搜索树第 x 层大小远大于 x-1 层,通常认为 IDA* 的在找到答案以前的重复扩展都“微不足道。
例题3——骑士精神
题目地址
luogu骑士精神
在一个 5 * 5 的棋盘上有 12 个白骑士和12 个黑骑士,每次能够选择一个骑士合法地走到空地,给定初始和目标状态,问最少走多少步能到达目标状态,(同种颜色的骑士是不互相区分的)。
小总结
从名字就能够看出,启发式搜索是一个比较玄学的东西,具体效果和估价函数有很大的关系,若是能判断一道题是玄学搜索,或者没有其它好办法的时候,不妨试一试。
一种强大的搜索算法,可是咱们并不讲,有兴趣的同窗自行查阅相关资料。
工具和方法
基础计数原理——加法、乘法原理(ex:减法、除法原理)
排列组合数及其公式、容斥大法
ex:线性递推数列、母函数方法
经典计数数列
Fibonacci、Catalan、Bell 数
错位排列数
知足的状态转移方程是:f(n
)=f(n-1)+f(n-2),边界条件是f(1)=1,f(2)=1;
例题4——数楼梯
题目地址:luogu数楼梯
解:咱们能够考虑最后一级(第n级)们怎么越过去呢,很显然,咱们要么同时越两级上去,要么只越一级上去。若是越两级,那么去掉这两级后,剩下n-2级楼梯就是一个规模更小的子问题了;若是越一级,那么去掉这一级后,剩下n-1级楼梯就是一个规模跟小的子问题了,根据加法原理,咱们最后n级楼梯的解就是两个规模分别为n-1和n-2的子问题的答案之和。f(n)=f(n-1)+f(n-2);就是一个斐波那契数列。
卡特兰数应当知足下面的关系:
卡特兰数知足递推式:
h(n)= h(0)* h(n-1)+h(1)*h(n-2) + … + h(n-1) *h(0) (n>=2)
或者知足
h(n)=h(n-1)*(4 *n-2)/(n+1);
并且知足
h(n)=C(2n,n)/(n+1) (n=0,1,2,…)
也就是知足
h(n)=c(2n,n)-c(2n,n-1)n=0,1,2,…)
例题5——栈
题目地址
luogu栈
解:咱们这道题目须要枚举的是每一元素出栈的顺序,对于第一个点,若是它是第一个出栈的,那么它以前就会有0个元素出栈,这个是一个规模为0的子问题,记做f(0),而后,它以后会有n-1个元素出栈,这是一个规模为n-1的子问题,记做f(n-1),根据乘法原理,当第一个点是第一个出栈的时候,方案数为f(n-1)*f(0);一样的,咱们继续枚举,若是第一个点是第二个出栈的,那么它以前的点有1个,出栈的方案数是f(1),在它以后有n-2个点,出栈的方案数是f(n-2),根据乘法原理,f(n-2) *f(1)…………咱们发现这是一个卡特兰数。
知足:s(n + 1, k) = s(n, k - 1) + n * s(n, k)
S(n,0) = 0,
S(1,1) = 1.
边界条件: S(0 , 0) = 1 S(p , 0) = 0 p>=1 S(p , p) =1 p>=0
一些性质: S(p ,1) = 1 p>=1 S(p, 2) = 2^(p-1)– 1 p>=2
经典模型:包含n个元素的集合分做k个环排列的方法数目
知足:s(n + 1, k) = s(n, k - 1) + k * s(n, k)
S(n,k)=0; (n
Bn是基数为n的集合划分数目。集合S的一个划分是定义为S的两两不相交的非空子集的族,它们的并是S。
bell的递推公式:
并且适合Dobinski
也是适合Touchard同余:
若p是任意质数,则会有
每一个bell数都是第二类斯特林数的和
今天主要讲了搜索入门和简单计数。搜索入门中A*和IDA *要从新去学习一下,简单计数中的一些公式要再记忆一下。
···字符串 Hash:一种从字符串到整数的映射
···经过这样的映射,把比较两字符串是否相同转化为两整数是否相同
····若比较发现两字符串hash值相等,咱们认为两字符串很大多是相同的
····另外一方面,若 hash 值不等,则两字符串必定不一样
····比较字符串 O(L),比较整数 O(1)
··竞赛中经常使用的 hash 策略
··把字符串视为一个 base 进制的大整数,对某个质数 P 取模获得 hash 值
··sum_i = (sum_{i-1} *base + str_i) mod P
··base 能够取 3一、13一、13131 等,须要知足 base > |字符集|
··P 取 long long 范围内一个质数,注意溢出问题
··使用 unsigned long long 天然溢出能够视为对 2^64 取模
(电脑自动取最低的64位 即为自动取模)
··可是可能被卡(对任意base)
··惧怕 Hash 被卡的同窗,也能够选择双 hash(常数翻倍
····给定字符串 S,预处理出它的前缀 Hash 函数;同时计算好 mod P 意义下 base 的幂次表
····sum[i] = (sum[i - 1] * base + str[i]) % P
····pw[i] = (pw[i - 1] * base) % P
····基础应用:
··提取一段子串的 hash 值
··合并两个串的 hash 值
··O(\log n) 计算两个子串的 lcp 和字典序大小
想提取【l,r】的哈希值:
【l,r】=s(r)-s(i-1)*10^(l+r-1)
eg:
企鹅QQ
给定 N 个长度均为 L 的串
问有多少对字符串知足:刚好有一位对应不一样
N <= 30000, L <= 200
解:枚举删掉每个位置,用 Hash 来进行答案统计
Trie 树
又称字母树,能够用来维护字符串集合
优化思想是,利用字符串的公共前缀来减小查询时间,最大限度地减小无心义的比较
结构:有根树,每条边上存有一个字符
从根到每一个叶子的路径上通过的字符写下来,对应了一个字符串
无敌伪代码:
支持的操做:插入、查找、删除
例 1
给定 2 * N 个字符串,你须要将它们配对起来
两个字符串 x、y 配对的得分是它们的 lcp 长度
最大化得分
N <= 10^5,字符串总长 <= 2 * 10^6
解:建出 trie 树,两个串的 LCP 即为它们的 LCA 的深度
使用贪心算法,按照树的 DFS 序列配对
(树的DFS序列如图)
例 2
给定一棵有根树,每条边有权值 w_i
求树上的一条简单路径,使得路径通过的边权异或和最大
N <= 2 * 10^5, w_i <= 10^9
解:
记录 dis[a] 表示 a 到根的链的异或和
考虑 x、y 之间的链的异或和,设 LCA 为 z
= (dis[x] ^ dis[z]) ^ (dis[y] ^ dis[z]) = dis[x] ^ dis[y]
不难发现与 z 无关!
因而问题转化为,给定 N 个数,选出两个使得异或和最大
解 cont’d:
考虑枚举两个数之一 x,咱们想在其它数中找到一个与 x 的异或和最大
从高到低考虑每一位,尽量让更高位为 1
不难发现可使用 Trie 树!
复杂度 O(N * 32)
维护 N 个集合,初始时第 i 个集合为 { i }
支持两个操做:
把两个集合合并起来
查询两个元素是否在同一集合
N <= 10^6
对每一个集合,创建一个有根树的结构
令树的根为整个集合的“表明”
想知道两个元素是否在同一集合,只需比较它们的表明
合并时,将一棵树接到另外一棵下边便可
优化策略
路径压缩
按秩合并(把小的并到大的里面去)
能够证实,使用这两种优化的并查集复杂度为 O(α(n))
绝大多数状况这个值不大于 5,能够认为是线性的
应用
最小生成树的 Kruskal 算法
Tarjan’s off-line LCA Algorithm
带权:
在一些应用中,能够在每一个点上额外维护一些信息,表示“它与父亲”之间的关系
进而尝试推算集合中任意两个元素之间的关系
例:
某市有两个帮派,有 N 我的,每一个人属于两个帮派之一。
给定 M 个事件:
1 x y,表示告诉你 x 和 y 属于同一帮派
2 x y,表示告诉你 x 和 y 不属于同一帮派
3 x y,表示请你推理 x 和 y 之间的关系
N <= 5 * 10^5, M <= 10^6
解:
给每一个人额外维护一个标记 rel[x] 表示 x 和 x 的父亲的关系
由 rel[x] 和 rel[fa[x]] 能够推算 x 和 fa[fa[x]] 的关系。。。以此类推能够推算 x 和 Root[x] 的关系
因而任意两我的只要在同一连通块,就能推算他们的关系
问题:这个并查集如何使用路径压缩优化呢?
只按秩合并
只按秩合并的并查集,能够在合并的时候必定程度上保留元素合并在一块儿的 “过程”
看一个经典例题
给定 N 个点,支持 M 个操做:
1 x y,在 x 和 y 之间连边
2 x y,询问 x 和 y 是否连通,若是是,那它们最先在哪一次操做以后连通的
N <= 2 * 10^5, M <= 5 * 10^5
解:
@货车运输
离线的时候能够建树倍增 blabla。。。
强制在线呢?
只按秩合并,link(x, y, tim) 时,咱们在 Root[x] 和 Root[y] 之间连一条边权为 tim 的边
询问 (x, y) 时,找到 x 和 y 之间边权最大的边便可
这种算法的复杂度是容易证实 O(\log N) 的正确性?
支持这样几种操做的数据结构:
插入一个优先级为 key 的元素
询问优先级最高的元素
删除优先级最高的 / 任意一个元素
升高一个元素的优先级值
优先队列通常使用堆来实现
最经典的堆即为大名鼎鼎的二叉堆
二叉堆是一个彻底二叉树结构,而且它具备堆性质:
每一个点的优先级高于它的两个孩子(若是有)
能够用一个数字来存储二叉堆,避免指针:
1 是根结点
对于 x,它的左右孩子分别是 2x 和 2x+1
容易验证 N 个点的二叉堆,它用到的数组即为 1 ~ N
给定一个大小为 N 的数组,咱们能够 O(N) 的建堆(How?
随着操做的进行,二叉堆的“堆性质”可能会遭到破坏,为此咱们定义两种调整操做,来维护二叉堆的堆性质保持不变
向上调整:
当一个点的优先级升高时,咱们须要向上调整
比较它和它的父亲的优先级,它的优先级高就与父亲交换位置并递归进行
向下调整:
当一个点的优先级下降时,咱们须要向下调整
比较它和它左右儿子中优先级较高的那个,它的优先级低就与儿子交换并递归下去
容易验证两种操做的复杂度均为 O(\log N)
插入:插入一个叶子,而后向上调整
询问:返回 a[1]
删除根:令 a[1] = a[N],而后向下调整
也是一种优秀的堆
而且是支持合并的(可并堆)
比较简单,容易实现
可是咱们不讲
Dijkstra 算法和 Prim 算法的优化
哈夫曼编码
一些奇怪的应用
给定数轴上 N 个点,你须要选出 2 * K 个,把它们配对起来
把两个点配对起来的花费是它们坐标之差的绝对值
最小化花费
N <= 3 * 10^5
解:
必定是取相邻两点配对
问题能够转化为,选出 K 个相邻点对
进一步转化为,有 N - 1 个线段,选出 K 个,且相邻的不一样时选
用堆进行贪心
给贪心一个“修正”的余地:
选了 p[i],把 p[i - 1] + p[i + 1] - p[i] 入堆
炒股,一共有 2 * N 天,天天有一个买入和卖出价
刚开始时你有 0 支股和充分多的钱
天天要选择买或者卖一支股票(股票数时刻不为负
最后一天结束你要清仓
虽然你有充分多的钱,你仍是想知道本身最多能在这些天赚到多少钱
N <= 2 * 10^5
解:转化成括号序列问题?
线段树是一种二叉搜索树,通常能够用来维护序列的子区间
对一个长度为 n 的序列建线段树,根结点即表示 [1, n]
对于一个表示 [l, r] 的节点:
若 l = r,则它是叶子
不然,令 m = (l + r) / 2,它有左右两个孩子,分别记为:[l, m] 和 [m + 1, r]
不难验证,这样一个线段树中有 2N - 1 个节点,而且树的深度是 O(\log N) 级别
线段树的优化思想:
根据问题的要求,用每一个节点维护它对应的子区间中、能够高效合并的相关信息
在动态的序列问题中,对于修改操做没有动过的部分。咱们能够考虑把这些地方的求解的结果保存并复用,从而达到优化程序效率、下降复杂度的目的。
给定一个长度为 N 的序列,支持:
修改一个位置的值
查询一个子区间的元素和
解:
线段树每一个节点维护对应子区间的和
区间覆盖:
对于一个区间 [l, r],咱们能够将其分解为线段树上 O(\log N) 个节点的并;
这里的分解是指,咱们选取的区间并起来刚好为 [l, r]。且选择的区间不会相互重叠
修改操做中,为了维护线段树性质,须要修改总共 O(\log N) 个节点
查询操做,将区间拆为 O(\log N) 个区间的并,从而优化查询的复杂度
总时间复杂度 O(\log N)
一种支持单点修改和查询前缀和的数据结构
复杂度为 O(\log N),可是常数很小
定义 lowbit(x),表示将 x 写成二进制后,只保留二进制下最低一个 1 对应的整数
例:lowbit(1001100) = 100, lowbit(1000) = 1000
十进制:lowbit(76)=4, lowbit(8) = 8
对一个数组 a[],咱们构造数组 c[],其中
c[i] = sum (. a[i - lowbit(i) + 1 … i] )
巧妙的事情来了:
咱们查询 a[] 的前缀和只须要访问 c 中 log N 个节点
修改 a[] 中任意一个元素的值,只须要同时修改 c 中的 log N 个节点
因而能够在 O(\log N) 的时间内支持单点修改、前缀和查询
树状数组 如图:
动态规划是noip最重要的知识点之一 刚开始学的时候理解起来有些困难 入门理解请见:
转送们
三要素:阶段、状态、决策
三前提:子问题重叠性、无后效性、最优子结构性质
动态规划是对问题空间进行的分阶段、有顺序、无重复、决策性的遍历求解类比有向无环图及其拓扑序
还有一些例题就不一一列举了 ,你们能够上各类oj上去切
背包问题(Knapsack problem)是一种组合优化的NP彻底问题。问题能够描述为:给定一组物品,每种物品都有本身的重量和价格,在限定的总重量内,咱们如何选择,才能使得物品的总价格最高。(来自百度百科)
简单来讲 背包就是DP中比较重要的一个分支
eg:
eg:
伪代码:
经典例题:
伪代码(不保证能过):
优化“阶段” ——倍增优化
优化“状态” ——状态压缩动态规划
优化“转移” ——矩阵乘法加速、数据结构优化、单调队列优化、斜率优化
伪代码:
DP就像物理中的力学 又重要又难