只有把基础的算法模板熟练掌握,才有可能解决考场上变化的题目,本篇文章最适合了解这些算法的同学进行全盘复习,不断背诵使用。当然如果是小白,大部分算法模板我都放了视频链接,相信你们一定能看懂的,蓝桥杯冲冲冲!
应用场景:设计到区间求和
前缀和:就是原数组中前i个数之和,就是高中数列的**前n项和,**只不过不是等差数列、等比数列。
前缀和优点:可以将for循环以O(n)的时间复杂度求前缀和了可以降到O(1)复杂度。简直是超级加速器。
如何得出前缀和:只需要知道第 i 项和前 i - 1 项和的关系。S[i] = S[i - 1] + a[i]
如何利用前缀和求区间和:很简单的,就一个公式,区间l,r的前缀和为s[r] - s[l - 1]。
前缀和模板:
for i in range(1,n+1):
s[i]=s[i-1]+a[i]
print(s[r]-s[l-1])
'''
注意:给前缀和数组赋值时,需要从1开始,而不是0。为什么呢?当 i 等于0时,S[0]=S[-1] + a[0]。这样就会越界访问了,所以要从1开始,并且令S[0] = 0
说明:
n个数,区间左端点l,区间右端点r,
权值列表a,前缀和列表s
测试样例:
n 1 2 3
a= [0, 8, 5, 6]
s=[0, 7, 13, 19]
'''
应用场景:给定一个区间[l,r],把A数组里面这些区间全部加上一个C,只需要在B数组修改两个数就可以了这两个数是**B[ l ] +C,B[ r + 1 ] - C
原理:B[ l ] + C会导致,从前缀和数组A[ l ]开始,每一个前缀和数组都加上C(因为al及以后的数组元素求和时会加上bl,而bl又加上了C)。
差分:有一个原始数列a1 、a2、a3 …an,构造一个新数列b1、b2…bn,使得ai = b1 + b2 +… + bi,使得ai是数列b的前缀和,b是a的差分,是前缀和的逆运算 。
差分模板:
#1.构建差分数组
for i in range(len(B)-1,0,-1):
B[i]-=B[i-1]
#2.转换加减(区间加减→端点加减)
B[l-1]+=v
B[r]-=v
#3.前缀相加(前缀和公式)
for i in range(1,n):
B[i]+=B[i-1]
二分:就是一分为二。就是在有序序列里面,通过不断地二分,进而缩小解的范围直到1个元素,从而更快地寻找满足条件的解。
二分条件:如果可以找到某种性质,使得整个区间一分为二,其中一半区间满足条件,另一半区间不满足条件;二分就可以寻找性质的边界。
整数二分主要有两种情况:第一种情况是把区间分为[l,mid] 和[mid + 1,r];第二种是把区间分为**[l,mid - 1] 和[mid,r]**;这两种去分取决于mid属于左区间还是右区间。
上来默认使用第一种,等写出代码来的时候看看mid属于左边还是右边。注意都是闭区间。
二分模板:
#check函数是核心
def check(x):
#假设x为答案
#题目一般有有个约束条件
#如果通过某种手段使得在x的条件下存在符合约束条件的解
#那么就是可行解
#区间[l, r]被划分成[l, mid]和[mid + 1, r]时使用,把中间点放在左半边,不管三七二十一先用这个,做题过程中再修改
def bsearch_1(l, r):
while (l < r):
mid = l + r >> 1 #取中间值
if (check(mid)) : #判断是否满足某种性质
r = mid; #更新区间
else :
l = mid +1
return l
#区间[l, r]被划分成[l, mid - 1]和[mid, r]时使用,把中间点放在左半边(比如左边是合法的,右边是不合法的,此时mid属于合法的那一边)
def bsearch_2(l, r):
while (l < r):
mid = l + r + 1 >> 1 #取中间值
if (check(mid)) : #判断是否满足某种性质
l = mid; #更新区间
else :
r = mid - 1
return l
以前我以为只有升序无重复元素的序列才可以二分,其实不是,具体看y总的讲解吧:二分
双指针:**双指针法(快慢指针法)在数组和链表的操作中是非常常见的,很多考察数组、链表、字符串等操作,都使用双指针法。 双指针算法通过设置两个指针不断进行单向移动**来解决问题的方法。
**形式:**两个指针分别指向不同的序列;两个指针指向同一个序列。比如:快速排序的划分过程。
实现:本来是双层for循环,O(n²)的时间复杂度。通过双指针算法可以优化到O(n)的时间复杂度。那是如何优化的时间复杂度呢?其实是当一个指针满足条件后才会单向移动,没有指针的回溯,而每次都会有指针的移动。简单说就是有个快指针可以不停的移动,有个慢指针需要符合条件后才能移动
双指针模板:
#模板:i快指针,j慢指针
for i in range(n):
j=0
while j < i and check(j): #当j指针满足一一定条件后才会移动
j++;
# 每道题的具体逻辑
介绍:这个操作是为了求 整数 x (十进制)的 第 k 位数是0 / 1(二进制)
原理:1、先把第k位移到最后一位,x >> k,用位移运算
2、看个位是几,x & 1(位运算&,只有两个1&时,才会返回1;否则返回0)
求n的第k位数字: n >> k & 1
介绍:lowbit(x)函数,返回x的最后一位1(二进制下的)
原理:x = 1010(二进制),lowbit(x) = 10(二进制) = 2(十进制)
返回n的最后一位1:lowbit(n) = n & -n
两者关系:若a,b>0 那么a*b=gcd(a,b)*lcm(a,b)
gcd模板:
def gcd(a,b):
while b:
a,b=b,a%b
return a
lmc模板:
def lcm(a,b):
s=a*b
while b:
a,b=b,a%b
return s//a
#因为到最后a,b的值都不是原来的了,需要一开始用s存a*b
这两个板子我相信大家都记得滚瓜烂熟吧。
案例:给定整数n,请问n以内有多少个素数?n<=10^6
你可能会轻蔑的笑了,感觉能自己分分钟秒杀。但是请别急,下面介绍的埃氏算法,能让你更快解决。
思想:首先,将2~n范围内的所有数都写下来,存入一张线性表里(用一维数组实现)。其中最小的数字2是素数。将表中2的倍数都划去,当然也包括其本身。之后表中剩余的最小数字是3。它显然也是素数。那么继续将所有3的倍数划去。同理,依次类推,每次表中剩下的最小数字m都是素数,然后将表中所有的m的倍数划去。如此反复操作,便能筛出n以内的所有素数。
判断质数模板(这个大家一定都会):
#由于一个数n的因子是成对出现的 故只需要枚举到int(n**0.5)
def judge(x):
for i in range(2,int(x**0.5)+1):
if x%i==0:
return False
return True
埃氏筛法模板:
maxn=10000#这个范围自己依据要查找数据范围内的质数设定
is_prime=[True for i in range(maxn+1)]
prime=[] #记录质数
for i in range(2,maxn):
if is_prime[i]:
prime.append(i)
j=i
while j<=maxn:
is_prime[j]=False
j+=i
唯一分解定理:又称为正整数的唯一分解定理,即:每个大于1的自然数均可写为质数的积,而且这些素因子按大小排列之后,写法仅有一种方式。如:n>1,n=(2a)*(3b)*(5c)…*(xy),**其中y>=0,x为质数,简单来说就是一个数一定可以*分解*为多个质数的连乘积。
推论:n的约数个数=(a+1)(b+1)…(y+1),(求出数n的因子个数)
质因数分解模板
#该模板函数把分解出来的质数存到列表里并输出
#怎么加工利用看自己需要
def f(x):
i=2
l=[]
while i<=x:
if x%i==0:
l.append(i)
x//=i
else:
i+=1
return l
#例如:12=3⋅2⋅2,这样就完全分解为质数的乘积了
介绍:并查集是一种树型的数据结构,常见操作有两种
1、 将两个集合合并
2、 询问两个元素是否在一个集合当中
并查集进行这两种操作的时间复杂度近乎O(1),而其他数据结构可能完成不了
并查集模板:
#并查集模板
def find(x): #返回x的祖宗节点
if p[x]!=x:
p[x]=find(p[x])
return p[x]
def union(x,y): #合并x,y为同一家族
if root(x)!=root(y):
p[root(x)]=root(y)
# p[N]存储每个点的祖宗节点,索引为当前节点,当前节点的值代表当前节点的祖宗
p=[i for i in range(1,N+1)] #初始化,假定节点编号是1~n
不懂?不懂咱就上视频:并查集
介绍:大部分都是用贪心的策略去解决,不是按照左端点排序,就是按照右端点排序,或者按照左右端点双关键字排序。这里我们按照左端点进行排序的,本质其实还是判断重叠区间问题。
实现:先排序,让所有的相邻区间尽可能的重叠在一起,按左边界进行判断。
区间合并模板:
n=int(input()) #多少个初始区间
intervals=[list(map(int,input().split())) for _ in range(n)] #区间集合,intervals = [[1,3],[2,6],[8,10],[15,18]]
def merge(intervals):
if len(intervals) == 0: return intervals
intervals.sort(key=lambda x: x[0]) #按左区间排序
result = [] #记录合并后的区间,操作直接在result中进行
result.append(intervals[0]) #初始化,把第一个区间放入result数组
for i in range(1, len(intervals)):
last = result[-1] #取出result最后一个元素
if last[1] >= intervals[i][0]: #重叠
result[-1] = [last[0], max(last[1], intervals[i][1])] #注意取最大值,因为无法确定两个右边界谁大。
else:
result.append(intervals[i]) #不重叠直接加入result数组
return result
重温一下我卡哥:合并区间视频
介绍:从图中一个未访问的顶点 V 开始,沿着一条路一直走到底,然后从这条路尽头的节点回退到上一个节点,再从另一条路开始走到底…,不断递归重复此过程,直到所有的顶点都遍历完成
特点:不撞南墙不回头,空间复杂度是n,不具有最短路效应,因为如果数据量一旦过大,递归树会越来越大最终导致爆栈。对于DFS而言,只要有解即可适合一些思路比较奇怪,对空间要求高的题目。
DFS模板:
def check(参数):
if(满足条件):
return 1
return 0
def dfs(参数):
if (越界不合法情况):
return
if (终点边界,输出答案):
return
for (循环枚举所有方案):
if(未被标记):
(标记)
dfs(下一层)
(还原标记)
这个视频直接看起来:深度优先算法
介绍:广度优先遍历,指的是从图的一个未遍历的节点出发,先遍历这个节点的相邻节点,再依次遍历每个相邻节点的相邻节点。放到树的遍历中而言,就是层序遍历(levelorder)
实现:因为需要保存相邻节点,所以我们需要使用到队列(queue)这个数据结构,由于其具有先入先出的特性,就可以遍历完一层又遍历下一层。
特点:从根节点开始向下一层一层的搜索,具有最短路的特点,适用于需要找最短路的题目
BFS模板:
#M:map地图 G:go走过路径 q=deque():双向队列 q.popleft() 头节点从左边出队
from collections import deque #导入数据结构deque
q=deque([(0,0)]) #1.化队列,迷宫开始位置坐标(0,0)
n,m=map(int,input().split())
M=[[int(i) for i in input()] for j in range(n)] #模拟地图
G=[['']*m for i in range(n)] #记录到从起点到该点走过路径的方向(根据题意设置)
while q:
x,y=q.popleft()
if x==n-1 and y==m-1: #2.设置结束条件,输出答案
print(len(G[-1][-1]))
print(G[-1][-1])
break
for i,j,k in [(1,0,'D'),(0,-1,'L'),(0,1,'R'),(-1,0,'U')]:#3.下一步路,遍历可以走的四个方向,得到下一个坐标(a,b)。
a,b=x+i,y+j
if 0<=a<n and 0<=b<m and M[a][b]==0: #3.判断合理,判断坐标是否越界,是否之前遍历过。
M[a][b]=1
q.append((a,b))#4.迭代赋值:判断合理就进行标记,然后添加到队列最后,路径表上增加此刻的方向。
G[a][b]=G[x][y]+k
注意:([(0,0)])有三层,先外面套一层deque()函数小括号,然后里面夹一层列表中括号[ ],最后再加一层代表坐标点小括号().
和上面视频是一家子:广度优先算法
树:如果一个无向连通图不包含回路(连通图中不存在环),那么就是一个树。
最小生成树:一个图中可能存在多条相连的边,我们**一定可以从一个图中挑出一些边生成一棵树。**这仅仅是生成一棵树,还未满足最小,当图中每条边都存在权重时,这时候我们从图中生成一棵树(n - 1 条边)时,生成这棵树的总代价就是每条边的权重相加之和。
应用举例:要在n个城市之间铺设道路,主要目标是要使这 n 个城市的任意两个之间都可以连通,但铺设费用很高,且各个城市之间铺设费用不同,因此另一个目标是要**使铺设总费用最低。*这就需要找到*带权的最小生成树。
最小生成树模板:
#这里用到并查集的知识点
n=int(input())
edge=[]#edge[i]=[a,b,c]代表i这条边连接了a,b,权值为c
ans=0#权值和
edge.sort(key=lambda x:x[2])#按边权升序排序
for x in edge:#遍历所有边
a,b,c=x[0],x[1],x[2]
if find(a)!=find(b) and j<=n-1:#两顶点不连通且当前边数小于n-1
ans+=x[2]#权值累加
union(a,b)
j+=1
print(ans)
应用场景:有向图,检测环,有向无环图一定有拓扑序列。
1.一件事情必须在另一件事情完成之后才能进行,这种十分强调事件的先后顺序的问题可以使用拓扑排序解决。例如,我们的升学,只有上完小学才上中学,上了中学才能上大学,上了大学才能上研究生等。
2.任何一个有向无环图一定有拓扑序列,因此拓扑序列可以用来检测环
基本思想:
- 从有向图中选一个无前驱(入度为0)的顶点输出;
- 将此顶点和以它为起点的弧删除;
- 重复上述2个步骤直到不存在无前驱的顶点。
- 若此时输出的顶点数小于有向图的顶点数,则说明有向图中存在回路,否则输出的顶点的顺序即为一个拓扑序列。
实现:那么对于代码来说,我们需要运用深搜的思想来执行拓扑排序的操作,1.找到图中入度为0的顶点 压栈 2.处于栈顶的顶点出栈 并加入列表(另外创建的存储结构,用于存储拓扑序列中的元素) 访问该顶点的所有邻居,如果其存在入度为0的邻居,压栈,执行操作2,如果不存在,回溯。
拓扑排序模板:
#准备工作上需要一个字典:用于存放连接关系
def topsort(graph):
#初始化所有点的入度为0
indegrees=dict((i,0) for i in graph.keys())
#传入入度大小
for i in graph.keys():
for j in graph[i]:
indegrees[j]+=1#'a':'cd',代表a指向c和d,那么c和d的入度增加1
#获取入度为0的顶点
stack=[i for i in indegrees.keys() if indegrees[i]==0]
#创建拓扑序列
seq=[]
while stack:#深搜
tmp=stack.pop()#出栈
seq.append(tmp)#加入拓扑序列
for k in graph[tmp]:#遍历邻居,邻居入度-1,邻居入度为0入栈
indegrees[k]-=1
if indegrees[k]==0:
stack.append(k)
if len(seq)==len(graph):
#无环
print(seq)#输出拓扑序列
else:
print('存在环')#当存在环,最终余下的点入度都不为0,Seq个数少于总顶点
#此处会存在一些入度不为0的结点,他们组成环。
graph={
'a':'cd',
'b':'df',
'c':'e',
'd':'efg',
'e':'g',
'f':'g',
'g':''
}
topsort(graph)
#['b', 'a', 'd', 'f', 'c', 'e', 'g']
如果还是不怎么明白,我知道你很急但是你别急。我相信你看完这个视频,你瞬间会通透的:拓扑排序视频
介绍:用来找每个点两两之间的最短距离(多源最短路径),是典型的动态规划。既可以是有向图也可以是无向图,权重可以为负。
缺点:时间复杂度高
优点:比起狄克斯特拉算法,简单地多,代码量少,容易上手
floyd模板:
n=int(input())#这个根据题意设置,表示结点个数
edge=[[float('inf')]*n for i in range(n)]
#初始化所有边权为无穷大
#根据题意更新edge[i][j]
#更新的时候,如果有无向图需要edge[i][j]=edge[j][i]这样设置,否则不用
#三重循环 结束
for k in range(n): #以k为中转站
for i in range(n):
for j in range(n):
edge[i][j]=min(edge[i][j],edge[i][k]+edge[k][j])
如果不是很明白,我相信你看完这个视频会恍然大悟:floyd视频讲解
介绍:既可以是无向图也可以有向图,权值必须非负,求某个顶点到其他顶点的最短距离(单源)
缺点:写起来会稍微麻烦一点比起Floyd 东西比较多
优点:跑的挺快的,数据量比较大的时候也能用Python解决
狄克斯特拉算法模板:
n=int(input())#根据题意 n代表结点个数
edge={}
#edge[i]={x:费用1,y:费用1....} 存结点之间的费用
cost=[]
#cost[i]代表结点i到出发点的最短开销
s=set()
#集合s用于存储处理过的结点
def find_node():#find_node函数用于寻找未被处理过的最便宜的结点
node,spend=None,float('inf')
for i in range(n):
if cost[i]<=spend and i not in s:#
node,spend=i,cost[i]
return node
while node:#只要还有结点未被处理
for i in edge[node]:#遍历node的邻居
cost[i]=min(cost[i],cost[node]+edge[node][i]) #cost[node](起点到node的费用)+node到节点的费用,取最小值。
s.add(node)
node=find_node
print(#根据你的需要输出出发点到某个结点i的费用cost[i])
#此外补充一个,如果题还要我们求最短路径上边的个数
#只需外加一个字典pre 每当更新cost[i]时,创建一个键值对
#最后通过终点逆向遍历统计次数即边数 输出即可
来看看这个视频吧:狄克斯特拉视频讲解
如果觉得博主的文章还不错的话,请三连支持一下博主哦