算法总结
第三讲 搜索与图论
DFS
1.使用枚举+递归的方法进行操作
BFS
- 队列+枚举的方法进行操作
- 发散式一层一层的寻找能够走的点,加入队列中
- 并且使用一个另外的数组存储走过的步数
- 返回即可
树与图的深度优先遍历
1.适用场景:边无权值图,递归处理子结点(需要获取到子结点的某些状态时)
2. 方法:
①每次查找子树的结点个数
②再反过来 父结点连接的结点数 = 总结点数 - 子树总结点数
③返回以当前结点为根节点的树 的总结点数
树与图的广度优先遍历
1.适用场景:边无权值图,求最短路(可以上下左右行走的情况,不往回走即可)
2. 方法:分层的更新
拓扑排序
适用于:无负环的有向图
时间复杂度:O(n)
方法:
① 所有的结点均有入度
② 入度为0时,加入队列中
③枚举所有结点获取入度为0的结点,加入队列中(初始化)
④ 队列不为空时
1.读取队头元素
2.用当前的队头元素更新与当前元素相连且未被处理过的结点的入度
(相当于此时图内删除队头结点)
3. 如果更新后的结点入度为零,则加入队列中
Dijkstra求最短路
适用于:无负权边且有权重的图\存在自环和重边
朴素做法:
① 枚举n次 (长度为n的路径)
② 找到每次的最短边结点 ,将其记录为已处理O(1)
③更新与最短结点相连的结点的距离 O(m)
时间复杂度:O(nm)
堆优化做法
① 枚举n次 (长度为n的路径)
② 找到每次的最短边结点 ,将其记录为已处理(使用小根堆查询当前最小值)
③更新与最短结点相连的结点的距离 (使用邻接表处理)
时间复杂度:O(nlogn)
使用数据类型:优先队列 priority_queue<数据类型,数据容器,排列方式>
bellman-ford求最短路 (单源)
有边数限制的最短路
适用于:图中可能存在重边和自环, 边权可能为负数
做法:
- 循环n次(路径长度为n)
- 备份(每次更新的时候是在上一次更新的路径基础上更新)
- 枚举每一条边(a,b,w) ,用a的距离更新b的距离
时间复杂度 O(n*m)
spfa求最短路(单源)
bellman的优化
邻接表+BFS队列
适用于:图中可能存在重边和自环, 边权可能为负数。
想法: 只有被更新过的点,其后续的点到起点的距离才会发生改变
做法:
1.用队列存储更新过的点
2. 当队列不为空时
①使用被更新过的点 t 去更新相连的其他结点
②将后续结点 m 加入队列、并且设置 m 状态为正在更新
③ t 对其他结点的更新完成,将其设置为未在更新
时间复杂度为O(n*m)
spfa判断负环(单源)
邻接表+BFS队列
想法:
如果要是不存在负环的话,
每次 d [ j ] = d [ t ]+w [ t ][ j ] 更新后,该路径上的结点就增加1
所以一条经过图上所有结点的路径的结点最大为 n
if 路径上到达某个结点的结点数 > n,就意味着这条路径上出现负环
思路:
1.用队列存储更新过的点
2. 当队列不为空时
①使用被更新过的点 t 去更新相连的其他结点
②如果后续结点需要更新,就将到达m路径的结点数+1;
(如果大于n,直接返回存在负环)
③将后续结点 m 加入队列、并且设置 m 状态为正在更新
④ t 对其他结点的更新完成,将其设置为未在更新
⑤返回不存在负环
时间复杂度为O(n*m)
Floyd算法(多源)
适用于:图中可能存在重边和自环,边权可能为负数。 不存在负权回路
想法:
循环中间结点 k
循环开始结点 i
循环终点结点 j
d[i,j] = min(d[i,k]+d[k,j])
解读 :对于每个i,j而言,相当于从i,j经历了k条边;而且每次循环并更新一次k,就给 i --> j 的路径内加了一个结点,所以循环k,相当于给所有的结点找个长度为n到达某个点的路径。
Prim 算法
适用于:图中可能存在重边和自环,边权可能为负数而且边数比较少时。求最小生成树
朴素想法:
每次找到距离集合最短的点,用它更新剩下所有点到集合的距离
做法:
- 循环n次,保证能够将所有结点收到集合内,一次操作一个结点
- 循环所有的结点,找到没在集合内,且距离集合最小的点
- 用该点更新剩下不在集合内的所有点
- 重复2 、3步,直到所有点均在集合内
Kruskal 算法
适用于:图中可能存在重边和自环,边权可能为负数,,而且边数比较多时。求最小生成树
朴素想法:
枚举边,每次都是添加的最小的边。并查集
做法:
- 将所有边按照权重从小到达排序
- 枚举每条边 a,b,c
if(a,b 不连通)
将这条边加入到集合内,就是说将两个结点放到同一个集合内
染色法判定二分图
适用于:图中可能存在重边和自环,图中不含有奇数环
过程:
- 枚举每个结点i,如果没有染色就将他染为1(此时为同一层的结点,第一层) dfs(i,1)
- 先将此结点染成目标颜色
- 循环子结点相连接的后续结点,
①.如果没有染色,就将其染成另外一种颜色(此时可能子结点染色失败)
②如果已经染色了,且与染的颜色发生冲突,就失败 - 处理结束时,都没有失败,则染色成功
匈牙利算法 二分图最大匹配
想法:
强势找女朋友
先给每个男生找,再对应的女生的已经好上的对象找
做法:
1.如果右侧女生没有对象,就直接匹配成功
2.如果右侧女生有对象,且她的对象能够找到下家,匹配成功
3.如果右侧女生有对象,且她的对象无法找到下架,匹配失败
第四讲 数学
快速幂
想法:
1.将数字 x^k 中的 k 转为二进制存储
扩展欧几里得
想法:
//题解成立的前提下
ax + by = gcd(a,b)
by+(a%b)y=gcd(b,a%b)
因为 gcd(a,b) = gcd(b,a%b)
所以 ax +by = bx2+(a%b)y2
= bx2 +(a- ⌊a/b⌋)y2
= ay2 + b ( x2 - a/b * y2 )
所以算得 x=y2 ; y = ( x2 - a/b * y2 )
又因为 ① exgcd(a,b,x,y) 和 ② exgcd(b , a%b , y , x)
所以当取得②时,y = x2 ; x = y2
由因为等式替换后 ① 的 y = ( x2 - a/b * y2 )
所以此时 y = y - a/b * x = ( x2 - a/b * y2 )
x不需要进行操作 已经等于y2
中国剩余定理
组合数Ⅰ(取模)
适用于: 数字较小的情况,p一定
时间复杂度: O(n)
原因: 如果数字较大的话,需要开的C数组也会很大,超出范围
公式:C(n,m)=C(n,n-m)=C(n-1,m-1)+C(n-1,m)
方法:递推+数组预处理
组合数Ⅱ(取模)
适用于: 数字较大,同时查询多(此时Ⅰ利用数组的方式已经不再适用)
公式:
C(a,b) = fact[a] * infact[b] * infact[a-b]
方法:使用快速幂求逆元,预处理阶乘
- infact:存储逆元的阶乘 O(logn)
infact[i] = infact[i-1] * qmi(i,mod-2,mod)%mod; - fact:存储阶乘 O(1)
- 所以从前往后枚举,计算阶乘 O(n)
时间复杂度 : 1*3 --> O(nlogn)
注意:如果这个算式中出现除法,我们就需要逆元了,将除法运算转换为乘法运算。
参考文章:
https://www.cnblogs.com/czc1999/p/11682068.html
组合数 III(取模)
适用于 p为素数且可变时,数字大,查询少
方法: 将公式化简后结果。
公式:
C(a,b) = C( a%p , b%p ) * C( a/p , b/p ) %p
做法:
- 化简约分公式
- 计算C(a , b ) O(nlogn)
仍然使用快速幂求倒数
使用lucas算法可以减少 能被p 取模的数字的计算量 - 查询 O(n)
时间复杂度: O(n2logn)
组合数 IV
组合数不取模,结果会很大
想法:
a! = a*(a-1)(a-2)…*1
目标:取得a!内 p的指数
p的指数 = 1-a内所有数的质因子p的指数相加,所以需要获取所有数的质因子p的指数。
每一次a/p 算的是 a内质因子内包含 p 的个数,也就是a!中p的指数
相当于取出所有数内pk 中一个 p1
a/p :表示1-a内有多少数含有p因子
每次循环将a!阶乘结果取出一个p
a!
1~a 内的数= k1 * p2 , k2* p2 , k3 * p3 , k4 * p3 * … kn*pk ①
a/p处理以后
1~a 内的数= k1*p , k2*p , k3 * p2 , k4 * p2 * … kn*pk-1 ②
k=a/p就是将①内1-a所有的数抽出一个p,p的个数k res=res+k;
k=a/p2就是将①内1-a所有的数抽出一个p2,p2的个数k res=res+k;
每一层操作永远只在1-a内取 p1 直到取到k次为止,将1-a 的 pk取完。
容斥原理
适用于:需要在集合内选出一些数,计算可以选的方案数时
计算p1 ^ p2 ^ p3 的个数 = C(3,1) - C(3,2) +C(3,3)
总结:
在某个集合中并集 = C(k,1)-C(k,2)+C(k,3)-C(k,4)+…C(k,k)
与、异或运算
1.与运算(&)
运算规则:0&0=0; 0&1=0; 1&0=0; 1&1=1;
即:两位同时为“1”,结果才为“1”,否则为0
2.或运算(|)
运算规则:0|0=0; 0|1=1; 1|0=1; 1|1=1;
即 :参加运算的两个对象只要有一个为1,其值为1。
3.异或运算(^)
运算规则:0 ^ 0=0; 0 ^ 1=1; 1 ^ 0=1; 1^1=0;
即:参加运算的两个对象,如果两个相应位为“异”(值不同),则该位结果为1,否则为0。
4. 因为二进制除了最后一位,其他位都是2的幂次方,必然是为偶数的,那么我们可以通过最后一位为0或者1来判断
x^1 =1 奇数
x^1 =0 偶数
博弈论
Nim 游戏 (线性)
拿取一定数目的物品,判断是否必胜
先手必胜状态:先手操作完,可以走到某一个必败状态
先手必败状态:先手操作完,走不到任何一个必败状态
先手必败状态:a1 ^ a2 ^ a3 ^ … ^an = 0
先手必胜状态:a1 ^ a2 ^ a3 ^ … ^an ≠ 0
证明:
a1 ^ a2 ^ a3 ^ ai… ^an = x ≠ 0
拿走 ai - (ai ^ x) 后转化为 :
a1 ^ a2 ^ a3 ^ ai ^ x … ^an
= a1 ^ a2 ^ a3 ^ ai … ^an ^ x
=x ^ x
=0
所以:如果一开始是 !0 的状态一定能够走到 0 的状态
且最后一步为0状态的人走,所以当异或为0极为必败状态
集合Nim
定义:
SG(终点) = 0
SG(x) =mex(SG(y1),SG(y2),…SG(yk))
SG(x) : 不能到达的最小自然数
必胜与必败状态
SG(x) = 0 必败
SG(x) ≠ 0 必胜
集合:只能抽取集合内数目的数字进行操作
sg函数
- 如果已经计算过的状态就直接返回 fx //记忆话搜索
- 每次枚举能够走到的状态(枚举可以操作的集合)
- for循环找到无法到达的整数最小值fx (从0开始)
- 如果找到就返回当前的状态fx
拆分Nim
适用于:将一个状态分为两个或者多个状态,求必胜与必败结果
sg函数二维使用
sg(a,b) = sg(a) ^ sg(b)
想法:
一堆的sg(a)转化为两堆的sg(b,c)
做法:
- 如果计算过状态,则直接返回结果 fx;
- 枚举所有的分法,得到多个方案,计算每个方案的结果,为sg(a)可以到达的状态(sg(i,j) 为了避免重复计算,i<j )
- for循环找到不能到达的最小自然整数 fx;
- 返回状态的结果fx;
- 总的:
所有堆开头的sg()异或结果 ≠ 0 即必胜
否则 = 0 必败
第五讲 动态规划
01背包问题
核心:选与不选
二维数组+动规
状态转移方程:
定义f[i][j]:前i个物品,背包容量j下的最优解
1)当前背包容量不够(j < w[i]),为前i-1个物品最优解:f[i][j] = f[i-1][j]
2)当前背包容量够,判断选与不选第i个物品
选:f[i][j] = f[i-1][j-w[i]] + v[i]
不选:f[i][j] = f[i-1][j]
完全背包问题
核心:每个物品可以无限选择,选与选k个
定义: f[i][j]:前i个物品,背包容量j下的最优解
状态转换公式: 可以错位相消,减少纬度
f[i][j] = max(f[i-1][j] , f[i-1][ j-v[i] ]+w[i],f[i-1][j-2 * v[i] ]+2w[i],…,f[i-1][j-k * v[i]]+k * w[i]
f[i][j-v[i]] = max(f[i-1][j]-v[i] , f[i-1][ j-2v[i] ]+w[i],f[i-1][j-3 * v[i] ]+2*w[i],…,f[i-1][j-(k+1) * v[i]]+k * w[i]
f[i][j] = max(f[i-1][j] ,f[i][j-v[i]]+w[i])
多重背包问题
核心:可以不选或者选择k个物品,但是有上限Si
朴素想法:
可以不选或者选择k个物品,三重for循环解决
二维优化:
想法:
由于公式
f[i][j]=max(f[i][j],f[i-1][j-kv[i]]+kw[i]);
需要使用到上一层 i-1 的数据,所以体积从大到小处理,再更新需要使用未更新的体积较小的数据,即i-1层数据
二进制优化:
想法:
将Si物品用二进制表示,分别当作新的物品,
2k *v[i]的体积,2k *w[i] 价值
分组背包
核心:每个分组只能选一个物品
朴素想法:
- for三重循环,分组,体积,每个分组内的物品
- i分组内,不选或者选择第k个物品
公式:
f[i][j]=max(f[i-1][j],f[i-1][j-v[i][k]]+w[i][k]);
二维优化:
体积由大到小处理
f[j]=max(f[j],f[j-v[i][k]]+w[i][k]);
线性DP
数字三角形
闫氏分析
f[i,j]:从起点到达 ( i , j ) 数字和的所有方案
属性: max
状态转移:
1.从左上角到达 (i,j) 的方案 f[i-1][j-1]
2.从右上角到达 (i,j) 的方案 f[i-1][j]
转移公式: f[i][j] =max(f[i-1][j-1],f[i-1][j])
最长上升子序列
闫氏分析
f[i]:前i个数字中以 a[i]结尾 的上升子序列所有方案
属性: max
状态转移:
- 1 ~ j 内找到比a[i]小的方案+1,找到最大值
- 默认当前以 a[i]结尾的上升子序列的长度为1
转移公式: f[i] =max(f[i],f[ j ]+1)
优化:
适用于:数据较多时
时间复杂度: O(nlogn)
做法:
- 循环数组内每个数字 O(n)
- 使用二分法获取到最右的<x的数 O(logn)
最长公共子序列
闫氏分析法:
f[i,j] : 字符串 A 的 1-i 与 字符串 B的 1-j 匹配的公共子序列
属性: max
状态转换:
- 最长公共子序列 包含 Ai , Bi ,此时Ai=Bi 11
f(i-1,j-1)+1 - 最长公共子序列包含 Ai , 不包含 Bi 10
f(i,j-1) - 最长公共子序列不包含 Ai , 包含 Bi 01
f(i-1,j) - 最长公共子序列不包含 Ai , 不包含 Bi 00
f(i-1,j-1) 此项被包含于 2、3 项处理完毕,不需要再冗余的处理
区间DP
将区间划分为子区间,然后枚举划分的方式,找到最值
属性:min/max
计数类DP
子结点所有的可能性相加 为 父结点的 结果
属性:count
f[i,j] 前i个整数拼成j 的方案数
属性: count
状态转移
不选i / 选k个i 的情况 —>完全背包问题
f[i][j] = f[i - 1][j] + f[i - 1][j - i] + f[i - 1][j - 2 * i] + …;
f[i][j - i] = f[i - 1][j - i] + f[i - 1][j - 2 * i] + …;
错位相消后:
f[i][j]=f[i−1][j]+f[i][j−i];
注意:
如果总和为0, 不选也算是一种方案
数位统计DP
想法:
- 可以将所有的数字抽象为 abcdefg
- abcdefg
- 1~ abcdefg 查看某个数字出现的位置
可以表达为 xxxdxxx - count(n,x) 1-n中x出现的次数
- 某个区间内x出现的次数 = count ( R , x ) - count ( L -1 , x )
- 枚举x在 每一位中出现的次数
过程:
-
1~n, x=1;
-
n = abcdefg
-
求 x 在每一位出现的次数
-
eg:
求 x 在第四位出现的次数(求1-n中出现的次数,所以需要保证xxx1yyy < = abcdefg )xxx1yyy
①如果 xxx选择0~abc-1
xxx<abc ,则后面的yyy = 0~999 ,所以方案数:abc*1000
② 如果 xxx = abc
②.1 如果d=1
yyy = 0~yyy-1
此时方案数为:yyy
②.2 d>1
yyy= 0~999
方案数为: 1000
②.3 d<1 则没有方案数
特殊情况:
- 如果查找的x = 0 时,
情况①,因为前导0的问题,只能选择 1 ~ abc-1 - 如果x放于第一位a时,不用考虑情况①
- 如果x放于最后一位g时,不同考虑情况②,情况①已经包含了所有方案数。
状态压缩DP === 图案连通性
- 将前一列的放置作为一种状态
- 用二进制表示放置的状态
- 如果前一列中出现连续的空位不是偶数个,即可认为失败
- 当前i列的状态需要与前一列j的放置状态一起占据当前列的位置,剩余的空格不为偶数,也认为失败
做法:
f[i,j] 第i列,放置的状态为j的所有情况
- 预处理所有状态中,k中所有连续的空位均为偶数的状态。 st[x] = true;
- for 每一列
- for 此一列的状态j
- for 前一列的状态k
①如果k状态与j状态一起占据的i列的位置后,剩余的空位出现不是为偶数的连续空位,则失败 st[j|k] == true;
② i-1层伸出来k的与当前i列需要放置j的位置不能冲突 j&k == 0
状态压缩DP === 集合式
思想:由前一个状态走到当前状态,且距离最短
用二进制表示所有点的选择情况
树状DP
想法:
每个位置的状态有哪些+不同状态之间如何互相转换
树状DP的体现: DFS
闫氏分析
状态表示:
f[i,0]: 第i个物品不选择
f[i,1]:第i个物品选择
属性:最大值
状态转化:
f[i,0]:此时子结点可以选择 选/不选 max(f[j,0],f[j,1]) 相加
f[i,1]:此时子结点选 f[j,01]相加 + h[i]
记忆化搜索
想法:
对于不会出现改变的结果,可以采用记忆化搜索的方式存储
有效减少时间消耗
对于每一个点而言,往下走的路径是一定的,不会受到外界影响,所以用记忆化搜索
区间合并
左右取法:
1.如果是需要尽量多的重合,则取右排序
区间选点
将每个区间按右端点从小到大排序
从前往后枚举每个区间
- 如果当前区间中包含点,则直接pass
- 否则,选额当前区间的右端点
区间最大不相交区间数量
理解:将整个区间 选 几个集合(最多) 互不相交
想法: (想法可能会错误)
- 如果选择左排序,可能会导致,每次都是用了最长的区间占据了整个区间
- 如果使用右排序,因为每次都是尽量多的产生并集,所以相对的每个区间会短一点
做法:
如果互相有交集的区间,就分到一个部分去
- 将所有区间按照右端点从小到大排序
- 从前往后枚举每个区间
①如果当前区间已经包含点,则直接pass
②否则,选择当前区间的右端点。
区间分组
想法:
- 选择左排序,需要不相交的结果最大,所以用左排序
- 选择右排序,相交的结果最大,所以不适用
判断存不存在组的条件:
- 一组区间,如果下一个区间左端<当前组的最大右端点,则表示有交集,不能合并,需要新的分组
- 否则,就合并在一起,并且更新最右侧的点
- 用优先队列存储每个分组的右端点,则每次都比较最小右端点。
①如果某个区间左端点<=最小的右端点,说明该区间与所有区间有交点,需要新开分组
② 如果> 最小右端点,表示至少和当前的最小右端点的分组不冲突,可以合并,更新当前最小右端点。
做法:
- 将所有区间按照左端点从小到大排序
- 从前往后枚举区间
- 判断是否将其放入当前现有的组内
①如果不存在这样的组,则开新组,让后将其放进去
②如果存在这样的组,就将其放进去,并且更新当前组的Max_r
区间覆盖
想法:
- 将所有区间按照右端点从小往大排序
- 枚举每个区间,
①每次选择 左端点<=当前区间的右端点,且右端点最大的区间
②更新当前区间的右端点
Huffman树
想法:
Huffman树:
每次合并所有选择中最小的两个。
总和为最小
排序不等式
第一个人等待:
0
第二个人等待:
t1
第三个人等待:
t1+t2
.
.
.
第n个人等待:
t1+t2+…+tn-1
总的等待时间:
t1*(n-1)+t2(n-2)+…tn-1
所以应该让等待时间从小到达排列
绝对值不等式
如果x取在ab外,则x到a和b的距离为:
ax+bx
显然,如果x取在ab内,到a和b的距离和为:
ab
所以x取在ab内为最小值 = ab的中点
推公式
耍杂技的牛
假设:已经排好奶牛的顺序
颠倒一下 i 奶牛 和 i-1 奶牛
- 第 i-1 奶牛的风险值:
allW[i-2] - Si-1 - 第 i 奶牛的风险值:
allW[i-2] + Wi-1 - Si
颠倒后:
- 第 i-1 奶牛的风险值:
allW[i-2] +Wi - Si-1 - 第 i 奶牛的风险值:
allW[i-2] - Si
化简:
- 第 i-1 奶牛的风险值:
- Si-1
- 第 i 奶牛的风险值:
Wi-1 - Si
颠倒后的化简:
- 第 i-1 奶牛的风险值:
Wi - Si-1 - 第 i 奶牛的风险值:
- Si
因为:
Wi-1 - Si > - Si
Wi - Si-1 > - Si-1
所以i奶牛和 i-1 奶牛的最大值为:
- 交换前最大值:Wi-1 - Si
- 交换后最大值:Wi - Si-1
如果让交换过的结果中每个的风险值 < 原排序
Wi-1 - Si > Wi - Si-1
Wi-1 +Si-1 > Wi + Si
所以所有大小从小到大排序,最大值最小