1 并查集
class Node:
def __init__(self,value):
self.val = value
self.next = None
class UnionSett:
def __init__(self):
# V->节点 对应关系
self.nodes = dict()
# 充当往上找的行为{b:b的父节点}
self.parents = dict()
# 只有一个点,它是代表点,才有记录{a:4} 被指向4次
self.sizeMap = dict()
def UnionSet(self,lists): # 集合初始化
for i in range(len(lists)):
node = Node(lists[i]) # 按照我这种方式,数据创建了节点,next指向空
self.nodes.update({lists[i]:node}) # 11对应{自己的值:自己创建的节点}
self.parents.update({node:node}) # 一开始,父节点是自己
self.sizeMap.update({node:1})
# 从点cur开始,一直往上找,找到不能再往上的代表点,返回
def findFather(self,cur):
path = []
while cur != self.parents.get(cur):
path.append(cur) # 栈内放cur节点,栈全是路径节点
cur = self.parents.get(cur) # cur向上移一位
# cur头节点
while path:
self.parents.update({path.pop():cur}) # 经过所有的节点全部改为指向代表点为直接父亲
return cur
# a样本和b样本是否是一个集合
def isSameSet(self,a,b):
# 若果ab不在节点集合中
if a not in self.nodes or b not in self.nodes:
return False
# 如果ab在集合中,如果a往上不能再往上的代表点和b对应的是同一个节点,返回True
return self.findFather(self.nodes.get(a)) == self.findFather(self.nodes.get(b))
def union(self,a,b): # 将a,b集合合成同一个集合
# 若果ab不在节点集合中
if a not in self.nodes or b not in self.nodes:
return
# aHead指向a的代表点
aHead = self.findFather(self.nodes.get(a))
bHead = self.findFather(self.nodes.get(b))
if aHead !=bHead: # 如果a和b的代表点不是同一个代表点,如果一样说明已经在一个集合中了
aSetSize = self.sizeMap.get(aHead) # aHead是a的代表点,所以能在sizemap中拿到代表点被指向的数
bSetSize = self.sizeMap.get(bHead)
if aSetSize >= bSetSize: # 小集合挂大集合,b挂到a上
self.parents.update({bHead:aHead}) # bHead的父亲直接改成aHead
self.sizeMap.update({aHead:aSetSize + bSetSize})
self.sizeMap.pop(bHead) # sizeMap中只有一个节点,删除bhead,现在代表点是a
else:
self.parents.update({aHead: bHead}) # aHead的父亲直接改成bHead
self.sizeMap.update({bHead: aSetSize + bSetSize})
self.sizeMap.pop(aHead) # sizeMap中只有一个节点,删除ahead,现在代表点是b
b = [1,2,3,4]
a = UnionSett()
a.UnionSet(b)
print(a.isSameSet(1,2))
findFather函数
例子:
四个学生实际上为一个人,只是各种平台的账号不同
class Node:
def __init__(self,value):
self.val = value
self.next = None
class UnionSett:
def __init__(self):
# V->节点 对应关系
self.nodes = dict()
# 充当往上找的行为{b:b的父节点}
self.parents = dict()
# 只有一个点,它是代表点,才有记录{a:4} 被指向4次
self.sizeMap = dict()
def UnionSet(self,lists): # 集合初始化
for i in range(len(lists)): # len(lists) == 4
node = Node(lists[i]) # 按照我这种方式,数据创建了节点,next指向空
self.nodes.update({lists[i]:node}) # 11对应{自己的值:自己创建的节点}
self.parents.update({node:node}) # 一开始,父节点是自己
self.sizeMap.update({node:1})
# 从点cur开始,一直往上找,找到不能再往上的代表点,返回
def findFather(self,cur):
path = []
while cur != self.parents.get(cur):
path.append(cur) # 栈内放cur节点,栈全是路径节点
cur = self.parents.get(cur) # cur向上移一位
# cur头节点
while path:
self.parents.update({path.pop():cur}) # 经过所有的节点全部改为指向代表点为直接父亲
return cur
# a样本和b样本是否是一个集合
def isSameSet(self,a,b): # a,b分别接受了一个用户
# 若果ab不在节点集合中
if a not in self.nodes or b not in self.nodes:
return False
# 如果ab在集合中,如果a往上不能再往上的代表点和b对应的是同一个节点,返回True
return self.findFather(self.nodes.get(a)) == self.findFather(self.nodes.get(b))
def union(self,a,b): # 将a,b集合合成同一个集合,a,b分别接受了一个用户,准备将这两个用户合并
# 若果ab不在节点集合中
if a not in self.nodes or b not in self.nodes:
return
# aHead指向a的代表点
aHead = self.findFather(self.nodes.get(a))
bHead = self.findFather(self.nodes.get(b))
if aHead !=bHead: # 如果a和b的代表点不是同一个代表点,如果一样说明已经在一个集合中了
aSetSize = self.sizeMap.get(aHead) # aHead是a的代表点,所以能在sizemap中拿到代表点被指向的数
bSetSize = self.sizeMap.get(bHead)
if aSetSize >= bSetSize: # 小集合挂大集合,b挂到a上
self.parents.update({bHead:aHead}) # bHead的父亲直接改成aHead
self.sizeMap.update({aHead:aSetSize + bSetSize})
self.sizeMap.pop(bHead) # sizeMap中只有一个节点,删除bhead,现在代表点是a
else:
self.parents.update({aHead: bHead}) # aHead的父亲直接改成bHead
self.sizeMap.update({bHead: aSetSize + bSetSize})
self.sizeMap.pop(aHead) # sizeMap中只有一个节点,删除ahead,现在代表点是b
def getSetNum(self):
return len(self.sizeMap)
class Student:
def __init__(self,a,b,c):
self.a = a
self.b = b
self.c = c
# 如果两个user,a字段一样或b字段一样或c子段一样就认为是一个人
# 请合并users,返回合并之后的用户数量
def mergeUsers(users): # users = [student1,student2,student3,student4]
unionFind = UnionSett() # 创建并查集类型
unionFind.UnionSet(users) # 初始化并查集
mapA = dict() # {a字段的值:哪个用户有这个值}
mapB = dict() # {a字段的值:哪个用户有这个值}
mapC = dict() # {a字段的值:哪个用户有这个值}
for user in users: # user = student1
if user.a in mapA: # 如果该用户a字段在mapA中,student1.a在不在mapA中
unionFind.union(user,mapA.get(user.a)) # 就将用户和用户的a字段并为一个集合
else: # 不在,是新字段
mapA.update({user.a:user})
if user.b in mapB: # 如果该用户b字段在mapB中
unionFind.union(user,mapB.get(user.b)) # 就将用户和用户的b字段并为一个集合
else: # 否则是新字段
mapB.update({user.b:user})
if user.c in mapC: # 如果该用户c字段在mapC中
unionFind.union(user,mapC.get(user.c)) # 就将用户和用户的c字段并为一个集合
else: # 否则是新字段
mapC.update({user.c:user})
# 向并查集询问,合并之后,还有多少个集合?
return unionFind.getSetNum()
student1 = Student('123','345','567')
student2 = Student('123','777','888')
student3 = Student('111','777','999')
student4 = Student('545','781','999')
users = [student1,student2,student3,student4]
print(mergeUsers(users))
2 图
(1)由点的集合和边的集合构成
(2)虽然存在有向图和无向图的概念,但实际上都可以用有向图来表示
(3)边上可能带有权值
常见的表达方法:
(1)邻接表法;
(2)邻接矩阵法
(3)…
邻接表
邻接矩阵:
把图算法统一转化为自己熟悉的结构:
2.1 点、边、图结构描述
# 点结构描述
class Node:
def __init__(self,value):
self.value = value # 点的value值,点的编号、点的名称、点的值
self.ins = 0 # 入度,有多少连向自己
self.outs = 0 # 自己出去的边有多少
self.nexts = [] # 直接邻居集合,Node集合,注意这里是指点出发直接指向叫直接邻居,被指不算,也就是outs
self.edges = [] # 从点出发的边 组成的集合
# 边结构描述
class Edge: # 有向边,有向边可以表示无向边
def __init__(self,weight,froms,to):
self.weight = weight # 权重
self.froms = froms # Node类型,指向自己的点
self.to = to # 指向别的点的边
# 图结构描述
class Graph:
def __init__(self):
self.nodes = dict() # 编号: 编号为1的点是什么
self.edges = set() # 所有的边都在里面,set是去重集合,这里只放key(边类型),不需要对应关系
2.2 转化为自己的图结构
最重要是能什么算法都可以转化过来
# 转为图结构,普通矩阵,邻接矩阵都可以转为这个图结构
# matrix 所有的边
# matrix 是 N*3的矩阵
# [weight,from节点上面的值,to节点指向谁的值]
def creatGraph(matrix):
graph = Graph() # 建立空图,点集和边集都没有
for i in range(len(matrix)):
# matrix[0][0],matrix[0][1],matrix[0][2]
weight = matrix[i][0] # 接收权重
froms = matrix[i][1]
to = matrix[i][2]
if froms not in graph.nodes: # 如果当前点编号在图的点集和中
graph.nodes.update({froms:Node(froms)})
if to not in graph.nodes: # 如果指向的点不在图的点集和中
graph.nodes.update({to:Node(to)})
# 现在已经有from编号所对应得点和to编号所对应得点
# 拿出这两个点
fromNode = graph.nodes.get(froms) # 我将编号对应的点拿出来
toNode = graph.nodes.get(to) # 得到指向的那个点
# 一条边,有froms点,to点,和权重,一条边就表示好了
newEdge = Edge(weight,fromNode,toNode) # 将点权重,自己点和指向的点都放到边集和中
fromNode.nexts.append(toNode) # 在点类型中,加入直接邻居数组
fromNode.outs = fromNode.outs + 1 # 入度出度分别加1
toNode.ins = toNode.ins + 1
fromNode.edges.append(newEdge) # 将边的信息放入这个点中
graph.edges.add(newEdge) # 将边的信息放入到大图边集和中
return graph
图的算法题如何搞定:
(1)先用自己最熟练的方式,实现图结构的表达;
(2)在自己熟悉的结构上,实现所有常用的图算法作为模板;
(3)把算法题提供的图结构转化为自己熟悉的图结构,再调用模板或改写即可。
2.3 图的宽度优先遍历
宽度优先遍历 二叉树用队列,图用hashset
- 利用队列实现
- 从源节点开始依次按照宽度进队列,然后弹出
- 每弹出一个点,把该节点所有没有进过队列的邻接点放入队列
- 直到队列变空
b的直接邻居没有a
# 从node出发,进行宽度优先遍历
def bfs(node): # 传入Node类型
if node is None:
return
queue = []
sets = set()
queue.append(node)
sets.add(node)
while queue:
cur = queue.pop(0) # 将元素弹出队列
print(cur.value) # 这个可以根据你的业务变化
for i in cur.nexts: # 遍历当前节点的邻居
if i not in sets:
sets.add(i)
queue.append(i)
2.4 图的深度优先遍历
用栈和set
- 利用栈实现
- 从源节点开始把节点按照深度放入栈,然后弹出
- 每弹出一个点,把该节点下一个没有进过栈的邻接点放入栈
- 知道栈变空
# 从node出发,进行深度优先遍历
def dfs(node):
if node is None:
return
stack = []
sets = set()
stack.append(node) # 节点入栈
sets.add(node)
print(node.value)
while stack:
cur = stack.pop() # 弹出节点赋给cur
for i in cur.nexts: # i遍历节点的直接邻居
if i not in sets:
stack.append(cur) # 栈先加入当前节点,再加入指向的节点
stack.append(i)
sets.add(i) # set中加入指向的节点
print(i.value)
break
2.5 图的拓扑排序算法
- 在图中找到所有入度为0的点输出
- 把所有入度为0的点在图中删除,继续找入度为0的点输出,周而复始
- 图的所有点都被删除后,依次输出的顺序就是拓扑排序
要求:有向图且没有环
应用:事件安排、编译顺序
# 图的拓扑排序
def sortedTopology(graph):
# key 某一个点node
# value 剩余的入度
inMap = dict() # {node:这个node还剩多少入度}
# 剩余入度为0的点,才能进这个队列
zeroInQueue = []
# 把所有点集拿出来,在图类里,nodes是一个字典类型,里面是{编号;点}
for node in graph.nodes.values: # values不是点的值而是点,而是{key:values}
inMap.update({node:node.ins}) # 原始图放入,每个点原始入度为剩余入度
if node.ins == 0: # 必有一个剩余入度为0的点,第一批入度的点
zeroInQueue.append(node)
result = [] # 拓扑排序的结果,依次加入result
while zeroInQueue:
cur = zeroInQueue.pop(0)
result.append(cur)
for next in cur.nexts:
inMap.update({next:inMap.get(next) - 1}) # 所有的邻居入度-1
if inMap.get(next) == 0:
zeroInQueue.append(next)
return result
2.6 最小生成树算法Kruskal
- 总是从权值最小的边开始考虑,依次考虑权值依次变大的边
- 当前的边要么进入最小生成树的集合,要么丢弃
- 如果当前的边进入最小生成树的集合中不会形成环,就要当前边
- 如果当前的边进入最小生成树的集合中会形成环,就不要当前边
- 考察完所有边之后,最小生成树的集合也得到了
用并查集算法
import heapq
# 最小生成树——Kruskal算法
# 本来图就是有结点的,小修改一下并查集,算法思路一致
class UnionFind:
def __init__(self): # 取消封装节点步骤
self.fatherMap = dict() # {Node:Node}
self.sizeMap = dict() # {Node:整形}
def makeSets(self,nodes):
self.fatherMap.clear()
self.sizeMap.clear() # 清空这两个集合
for node in nodes: # 遍历每一个节点
self.fatherMap.update({node:node}) # 一开始父节点是自己
self.sizeMap.update({node:1}) # 当前节点被指向次数为1
def findFather(self,n):
path = []
while n is not self.fatherMap.get(n):
path.append(n)
n = self.fatherMap.get(n)
while path:
self.fatherMap.update({path.pop():n})
return n
def isSameSet(self,a,b):
return self.findFather(a) == self.findFather(b)
def union(self,a,b):
if a == None or b == None:
return
aDai = self.findFather(a)
bDai = self.findFather(b)
if aDai is not bDai: # 如果ab代表点不是同一个点,说明不在一个集合中
aSetSize = self.sizeMap.get(aDai)
bSetSize = self.sizeMap.get(bDai)
if aSetSize <= bSetSize:
self.fatherMap.update({aDai:bDai})
self.sizeMap.update({bDai,aSetSize + bSetSize})
self.sizeMap.pop(aDai)
else:
self.fatherMap.update({bDai:aDai})
self.sizeMap.update({aDai,aSetSize + bSetSize})
self.sizeMap.pop(bDai)
def KruskalMST(graph):
unionFind = UnionFind()
unionFind.makeSets(graph.nodes.values()) # 把并查集初始化,建立起来
priorityQueue = []
for edge in graph.edges: # 所有边都进入优先级队列
heapq.heappush(priorityQueue,edge)
result = set()
while priorityQueue:
edge = priorityQueue.pop(0)
heapq.heapify(priorityQueue)
if unionFind.isSameSet(edge.froms,edge.to): # 这条边的两个点是不是在同一个集合中
result.add(edge) # 不在就加入
unionFind.union(edge.froms,edge.to) # 使这两个点组成一个集合
return result
优先队列每弹出一个值,就需要修复一下,但添加元素好像是自动修复的
2.7 最小生成树算法Prim
import main
import heapq
# 最小生成树——prim算法
def primMST(graph):
# 解锁的边进入小根堆
priorityQueue = []
# 哪些点被解锁出来了
nodeSet = set()
result = set() # 依次挑选的边在result里
for node in graph.nodes.values(): # 遍历图的每一个点,字典加括号
# node 是开始点
if node not in nodeSet: # 如果点不存在就把点放入点合集
nodeSet.add(node)
for edge in node.edges: # 由一个点,解锁所有相连的边
heapq.heappush(priorityQueue,edge)
while priorityQueue:
edge = priorityQueue.pop(0) # 弹出解锁的边中,最小的边
toNode = edge.to # 可能的一个新的点,得到最小权重边指向的点
if toNode not in nodeSet: # 不含有的时候,就是新的点
nodeSet.add(toNode)
result.add(edge)
for nextEdge in toNode.edges: # 下一个点的附件边全解锁
# 把下一个点的边加入到优先级队列中
heapq.heappush(priorityQueue,nextEdge) # 边加边修复优先级,保证第一个边为最小权重
return result
2.8 Dijkstra算法
- Dijkstra算法必须指定一个源点
- 生成一个源点到各个点的最小距离表,一开始只有一条记录,即原点到自己的最小距离为0,源点到其他所有点的最小距离都为正无穷大
- 从距离表中拿出没拿过记录里的最小记录,通过这个点发出的边,更新源点到各个点的最小距离表,不断重复这一步
- 源点到所有的点记录如果都被拿过一遍,过程停止,最小距离表得到了
# Dijkstra算法
def Dijkstra1(start):
# 从start出发到所有点的最小距离
# key:从start出发到达key
# value:从start出发到达key的最小距离
# 如果在表中,没有T的记录,含义是从start出发到T这个点的距离为正无穷
distanceMap = dict()
distanceMap.update({start:0})
# 已经求过距离的节点,存在selectedNodes中,以后再也不碰
selectedNodes = set() # 放到这个表中,表示已经被锁
# 得到最小距离的点,并且不被锁
minNode = getMinDistanceAndUnselectedNode(distanceMap,selectedNodes)
while minNode: # minNode是连接点
# 原始点 -> minNode(跳转点)最小距离distance
distance = distanceMap.get(minNode) # 取到最小值
for edge in minNode.edges: # 取边
toNode = edge.to # 取出指向下一个节点的权重,边的权重
if toNode not in distanceMap:
distanceMap.update({toNode:distance + edge.weight}) # 第一次建立距离
else: # 有记录,看看能不能更新距离
distanceMap.update({edge.to,
min(distanceMap.get(toNode),distance + edge.weight)})
selectedNodes.add(minNode)
minNode = getMinDistanceAndUnselectedNode(distanceMap,selectedNodes)
return distanceMap
def getMinDistanceAndUnselectedNode(distanceMap,touchedNodes): # (dict,set)
minNode = None
minDistance = 999999999 # 取一个整数最大值
for i in distanceMap:
node = i # 默认取key
distance = distanceMap.get(i)
# 没有碰过的点 并且 记录最小
if node in touchedNodes and distance < minDistance:
minNode = node
minDistance = distance
return minNode
改进后的代码(用堆)
# Dijkstra2算法
# 改进,利用堆排序,并且新加三种方法,新加,更新和忽视
class NodeHeap:
def __init__(self):
# 实际的堆结构
self.nodes = []
# {某一个节点node:上面数组中的位置},如果位置是-1,说明之前进过堆
self.heapIndexMap = dict()
# {某一个节点,value 从源节点出发到该节点的目前最小距离}
self.distanceMap = dict()
# 堆上有多少个点,记录位置
self.size = 0
# 是否进入过堆结构
def isEntered(self,node):
return node in self.heapIndexMap
# 判断一个节点是否在堆上
def inHeap(self,node):
# 如果在堆上且堆上位置不等于-1,说明在堆上
return self.isEntered(node) and self.heapIndexMap.get(node) != -1
# 有一个点叫node,现在发现了一个从源节点出发达到node的距离为distance
# 判断要不要更新,如果需要的话就更新
# node有一条新距离是distance
# 如果没有记录就新加,有老记录的话要比对最小值作为它的距离,其他忽视
def addOrUpdateOrIgnore(self,node,distance):
if self.inHeap(node): # 节点在堆上
self.distanceMap.update({node:min(self.distanceMap.get(node),distance)}) # 更新
self.insertHeapify(node,self.heapIndexMap.get(node)) # 从现在的位置,往上修复
if not self.isEntered(node): # 从来没有进入堆
self.nodes.append(node)
self.heapIndexMap.update({node:self.size})
self.distanceMap.update({node:distance})
self.insertHeapify(node,self.size) # 插入节点并修复成小根堆
self.size = self.size + 1
# 如果节点进来过,但不在堆上就直接忽视,直接返回
# 既要在堆上交换,也要在堆位置表上交换
def swap(self,index1,index2):
self.heapIndexMap.update({self.nodes[index1]:index2}) # 使heapIndexMap保持小根堆
self.heapIndexMap.update({self.nodes[index2],index1})
tmp = self.nodes[index1] # 使节点数组保证小根堆
self.nodes[index1] = self.nodes[index2]
self.nodes[index2] = tmp
# 小根堆
def pop(self):
nodeRecord = NodeRecord(self.nodes[0],self.distanceMap.get(self.nodes[0])) # 记录点
self.swap(0,self.size - 1) # 第一位和最后一位做交换
self.heapIndexMap.update({self.nodes[self.size - 1]:-1}) # -1是标志,表示曾经进来过
self.distanceMap.pop(self.nodes[self.size - 1])
# c语言和c++就需要把原本堆顶点析构
self.nodes[self.size - 1] = None # 节点数组减少一个
self.size = self.size - 1 # 节点数量减少
self.heapify(0,self.size) # 重新调整为小根堆,毕竟删除了数
return nodeRecord
def insertHeapify(self,node,index): # 插入小根堆需要根据数值,进行顺序插入
while self.distanceMap.get(self.nodes[index]) < self.distanceMap.get(self.nodes[(index - 1) / 2]):
self.swap(index,(index - 1) / 2)
index = (index - 1) / 2
def heapify(self,index,size):
left = index * 2 + 1
while left < size:
if (left + 1 < size and
self.distanceMap.get(self.nodes[left + 1]) <
self.distanceMap.get(self.nodes[left])):
smallest = left + 1
else:
smallest = left
if self.distanceMap.get(self.nodes[smallest]) >= self.distanceMap.get(self.nodes[index]):
smallest = index
if smallest == index:
break
self.swap(smallest,index)
index = smallest
left = index * 2 + 1
def isEmpty(self):
return self.size == 0
class NodeRecord:
def __init__(self,node,distance):
self.node = node
self.distance = distance
def Dijkstra2(start):
nodeHeap = NodeHeap() # 建立堆结构,初始化
nodeHeap.addOrUpdateOrIgnore(start,0) # 把起始点传过去
result = dict() # 结果是个字典类型
while nodeHeap.isEmpty(): # 起始点已经进入,开始遍历剩下的点
record = nodeHeap.pop() # record是NodeRecord类型
cur = record.node # 对当前节点进行操作
distance = record.distance
for edge in cur.edges:
nodeHeap.addOrUpdateOrIgnore(edge.to,edge.weight + distance)
result.update({cur:distance})
return result