第一天 基础算法&&数学
day1难度测试
如果要用一个词来形容上午的测试,那真是体无完肤。
成绩:
题目 | 成绩 | 评价 |
---|---|---|
T1 | 50 | 一般 |
T2 | 10 | 大失所望 |
T3 | 0 | 差 |
基础算法
递推
:指通过观察、归纳,发现较大规模问题和较小规模问题之间的关系,用一些数学公式表达出来,在一些教材中,也称为计数DP。递推的模型最主要有:斐波那契数列,卡特兰数,bell数,错排等等。
递归
:所谓递归,是指函数“自己调用自己”的一种编程方法,在解决一个问题时,如果发现问题能拆解为一个或多个相同规模的子问题时,就可以考虑使用这种方法求解。最经典的题型有汉诺塔等。
分治
:分治解决问题的一般步骤:
分,将问题划分为若干个规模较小、形式相同的子问题
治,递归求解,当问题规模足够小时直接求答案
合,将规模更小的问题得到的答案合并起来,得到原问题的答案。
主要模型应用有归并排序、快速幂等。
是不是很难懂?呐看一个例题
分治经典例题:给定 2^n * 2^n 的棋盘,其中有一个坏点,需要用若干个 L 型方块覆盖棋盘:不能覆盖到坏点,其他点要被覆盖恰好一次。给出方案。
解:这道题目有个基本单元一定反复存在:
这个只需要放一个L形状的方块去覆盖就解决了问题。我们把它拓展一下,把每个格子想成多个格子的集合,如果我们能确定这个坏点在其中一个集合里面,那么我们可以把整个问题分成四个部分:
然后发现左下角的格子就是一个基本模型,然后中间的四块连在一起也是一个模型,然后我们可以在中间搞事情,放上一个L形状的木块,由于每个格子只能恰好被覆盖一次,所以我们可以把所有被覆盖的地方都标记为坏点。然后另外三个大格子也变成了三个模型
是不是就做出来了。
二分:二分其实是分治的一种。二分是竞赛中一个非常实用的算法思想通过二分,把最优化问题转化为判定性问题,把优化目标转化为判定的标准,就能发现问题的一些“贪心性质”,从而设计高效算法进行判定。
求“ XXX 的最大值 ” 最小可能是多少(或者反过来)都可以考虑二分
有些时候,二分的模型比较隐蔽,需要更深入的思考和发现性质
二分答案的时间复杂度是O(logn)
倍增
:倍增字面上意思是:成倍地增加。当我们模拟一个过程时,一步一步进行太慢,考虑把模拟的步数二进制分解;经过一些预处理,每次可以模拟 2^i 步,从而达到优化复杂度的目的。主要模型有RMQ,LCA等。
贪心
:哈哈哈哈哈哈贪心是个好东西啊。由贪心得到局部最优解,从而得到全局最优解,一般容易实现、时间复杂度优秀,但是……难于证明,贪心的脑洞一般都很大。
数论
特特特特特特就是个数论。
模算术
模算术可以这样简单理解:就是不停地模啊模。就可以了。比如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意义下的乘法逆元。
bezout 定理
方程 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)
来一段程序吧:
#include<bits/stdc++.h>
using namespace std;
inline int read()
{
char c;
c=getchar();
for(;c>'9'||c<'0';c=getchar());
int sum=0;
for(;c<='9'&&c>='0';c=getchar())sum=sum*10+c-48;
return sum;
}//快读
int n,m;
bool f[10010000];//用来标记当前数是否是素数
int main()
{
n=read(),m=read();
memset(f,true,sizeof(f));
f[0]=f[1]=false;
for(int i=2;i<=n;i++)
if(f[i])//如果没有被标记是合数
for(int j=2;i*j<=n;f[i*j]=false,j++);//用它去更新所有的倍数。枚举I,所有的j*i就是它的倍数。
for(int i=1;i<=m;i++)
{
int x;
x=read();
if(f[x])printf("Yes\n");
else printf("No\n");
}//这里是判断
return 0;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
埃氏筛法的应用:在进行筛法的同时,我们可以对 [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(模意义下)
欧拉定理EXT
由欧拉定理可以推导出一些很有用的结论
费马小定理:
若 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 分解质数,求答案模每个素数(的幂)的值,最后使用中国剩余定理合并
LUCAS定理
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,完全积性
Dirichlet卷积
对两个数论函数进行的运算
设我们有两个数论函数 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[]
大步小步算法(bSGS)
我们设置一个步长 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) 的情况
总结
黑色星期一??蒟蒻被虐的第一天。数论越讲到后面越听不懂,上面列出的所有知识点都要自己好好去学下。
第二天 搜索
day2难度测试
题目 | 成绩 | 评价 |
---|---|---|
T1 | 10 | 以为暴力能拿60分,结果只有十分 |
T2 | 0 | 翻车 |
T3 | 4 | 骗到4分 |
翻车翻到底了
搜索
搜索算法是利用计算机的高性能来有目的的穷举一个问题解空间的部分或所有的可能情况,从而求出问题的解的一种方法。现阶段一般有枚举算法、深度优先搜索、广度优先搜索、A* 算法、回溯算法、蒙特卡洛树搜索、散列函数等算法。在大规模实验环境中,通常通过在搜索前,根据条件降低搜索规模;根据问题的约束条件进行剪枝;利用搜索过程中的中间解,避免重复计算这几种方法进行优化。
dfs深度优先搜索和bfs广(宽)度优先搜索是搜索的两种基本模型。
例题1——斗地主
题目地址
luogu斗地主
luogu斗地主增强版
解:我们需要考虑预处理dp数组: dp[x][y][z][w] 表示分别有 x、y、z、w 种出现 4、3、2、1 次的牌,不出顺子最少几步打完。DFS 搜索打顺子的情况,结合 dp 计算答案。加上最优性剪枝就可以A掉。
中途相遇法 Meet in middle
中途相遇法指的是,从起点和答案两边同时开始搜索,在中间相遇并求解的方法。这种方法有什么好呢?我们用给一张图片来解决。
空白部分在中途相遇法中就可以忽略掉了。
例题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* 和IDA*
算法原理:
A*:
BFS 的改进,用优先队列维护所有可以扩展的状态
每次把期望里目标更近的状态拿出来扩展
IDA*:
从小到大“试答案”并进行判定
ans = 0; while (!check(ans)) ans++;
判定一般使用深搜(当 深度 > ans 就不必搜下去了)
优劣比较:
A*:容易理解,实现容易;空间需求巨大
IDA*:不太容易实现,空间需求小,但是有重复扩展问题
PS:因为一般搜索树第 x 层大小远大于 x-1 层,一般认为 IDA* 的在找到答案之前的重复扩展都“微不足道。
例题3——骑士精神
题目地址
luogu骑士精神
在一个 5 * 5 的棋盘上有 12 个白骑士和12 个黑骑士,每次可以选择一个骑士合法地走到空地,给定初始和目标状态,问最少走多少步能到达目标状态,(同种颜色的骑士是不互相区分的)。
小总结
从名字就可以看出,启发式搜索是一个比较玄学的东西,具体效果和估价函数有很大的关系,如果能判断一道题是玄学搜索,或者没有其它好办法的时候,不妨试一试。
舞蹈链搜索Dancing Links
一种强大的搜索算法,但是我们并不讲,有兴趣的同学自行查阅相关资料。
简单计数
前置技能
工具和方法
基础计数原理——加法、乘法原理(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值相等,我们认为两字符串很大可能是相同的
····另一方面,若 hash 值不等,则两字符串一定不同
····比较字符串 O(L),比较整数 O(1)
BKDR-Hash
··竞赛中常用的 hash 策略
··把字符串视为一个 base 进制的大整数,对某个质数 P 取模得到 hash 值
··sum_i = (sum_{i-1} *base + str_i) mod P
··base 可以取 31、131、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 树
又称字母树,可以用来维护字符串集合
优化思想是,利用字符串的公共前缀来减少查询时间,最大限度地减少无意义的比较
结构:有根树,每条边上存有一个字符
从根到每个叶子的路径上经过的字符写下来,对应了一个字符串
无敌伪代码:
plain copy
- namespace Trie
- {
- struct node
- {
- int ch[26];
- //other info
- }t[Max_M];
- int Root,pookCur;
- void inti()
- {
- Root=1;
- poolCur=2;
- }
- inline int newnode()
- {
- int o=poolCur++;
- memset(&t[o],0,sizeof(node));
- return poolCur++;
- }
- int insert(char s[],int n)//s[0..n-1]
- {
- int o=Root;
- for(int )
- }
- }
支持的操作:插入、查找、删除
例 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 算法的优化
哈夫曼编码
一些奇怪的应用
例 1
给定数轴上 N 个点,你需要选出 2 * K 个,把它们配对起来
把两个点配对起来的花费是它们坐标之差的绝对值
最小化花费
N <= 3 * 10^5
解:
一定是取相邻两点配对
问题可以转化为,选出 K 个相邻点对
进一步转化为,有 N - 1 个线段,选出 K 个,且相邻的不同时选
用堆进行贪心
给贪心一个“修正”的余地:
选了 p[i],把 p[i - 1] + p[i + 1] - p[i] 入堆
例 2
炒股,一共有 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) 级别
原理
线段树的优化思想:
根据问题的要求,用每个节点维护它对应的子区间中、可以高效合并的相关信息
在动态的序列问题中,对于修改操作没有动过的部分。我们可以考虑把这些地方的求解的结果保存并复用,从而达到优化程序效率、降低复杂度的目的。
例 1
给定一个长度为 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) 的时间内支持单点修改、前缀和查询
树状数组 如图:
Day4、5、6
动态规划(DP)
动态规划是noip最重要的知识点之一 刚开始学的时候理解起来有些困难 入门理解请见:
转送们
线性DP复习
动态规划思想
三要素:阶段、状态、决策
三前提:子问题重叠性、无后效性、最优子结构性质
动态规划是对问题空间进行的分阶段、有顺序、无重复、决策性的遍历求解类比有向无环图及其拓扑序
例题:
解析:
还有一些例题就不一一列举了 ,大家可以上各种oj上去切
背包问题
背包问题(Knapsack problem)是一种组合优化的NP完全问题。问题可以描述为:给定一组物品,每种物品都有自己的重量和价格,在限定的总重量内,我们如何选择,才能使得物品的总价格最高。(来自百度百科)
简单来说 背包就是DP中比较重要的一个分支
01背包
完全背包
eg:
多重背包
直接拆分法
二进制拆分法
单调队列
eg:
分组背包
伪代码:
memset(f,0xcf,sizeof(f));
f[0]=0;
for(int I= 1;i<=n;++I)
for(int j=m;j>=0;--j)
for(int k=1;k<=c[i];++k)
if(j>=v[I][k])
f[j]=max(f[j],f[j-v[I][k]]+w[i][k]);
- 1
- 2
- 3
- 4
- 5
- 6
- 7
区间DP
经典例题:
计数类DP
无向连通图计数
树形DP
背包类树形DP
伪代码(不保证能过):
动态规划的优化
优化“阶段” ——倍增优化
优化“状态” ——状态压缩动态规划
优化“转移” ——矩阵乘法加速、数据结构优化、单调队列优化、斜率优化
伪代码:
DP就像物理中的力学 又重要又难