算法笔记1(python)

本文是关于并查集算法的学习笔记,详细介绍了并查集的设计思想、动态连接问题、连通关系,并深入探讨了如何优化查找和合并操作,包括加权法和路径压缩法。还提及了并查集在渗滤问题中的应用。
摘要由CSDN通过智能技术生成

算法笔记

使用视频BV1u441127b5,随时更新。

1. 并查集

1.1 算法设计

问题建模
寻找算法
时空复杂度
迭代

1.1.1 动态连接问题

n个物体的连接,有一个连接命令连接两个物体,如何寻找两个物体间是否有连接?
union表示连接关系,connected表示查询命令
连通性的例子可以很大,在这一节,首先探究是否连通,之后探讨具体路径是什么。

1.1.2 连通关系

定义一个关系是连通的:满足自反性、对称性和传递性。在一个图中,将彼此相连的元素称为一个连通分量,每个连通分量都互斥,且并集等于整个集合。

1.2 寻找算法

1.2.1 定义类与API

定义一个class UF,其union运算合并的是两个元素的所在的连通分量;其connected运算返回是否。通过元素的总数,将这个类初始化:

class UF:
	def __init__(self, N):
		self.nums = N
	def union(self, p, q):
		Pass
	def connected(self, p, q):
		Pass

上面的代码就是一个在python中的UF类,这个类包含两个方法,查找connected和合并union。接下来的两节就围绕如何更快更好地实现这两个方法进行。
在此之前,首先定义一个API,使得程序可以从写有连接数对的txt文档中读取连接关系,txt的第一行写的是元素总数,其余的每一行写有一个数对,表明两个元素是相连的,例如,当txt的内容如下时:

10
4 3
3 8
6 5

可以这样处理:

with open('connections.txt', 'r', encoding='utf-8') as f:
	connections = f.readlines()
	N = int(connections[0])
	uf = UF(N)
	for ct in connections[1:]:
		p, q = list(map(int, ct.strip().split(' ')))
		uf = uf.union(p, q)

此处注意,p和q使用整型或是字符串,其实并没有什么大的影响,只要注意在代码当中统一即可。这里是按照使用int整型,实际上也可使用字符串。

以上使用python自带的open方法来操作txt,另一个常用来操作txt数据的是numpy库,但是此处,使用np.loadtxt('name.txt', dtype=np.int)不行的,如下图所示的代码:

import numpy as np


ct = np.loadtxt('example.txt', dtype=np.int)

会报错:

ValueError: Wrong number of columns at line 2

原因是numpy要求txt中每一行都应有统一的格式。

但是,用pandas可以的,原因是pandas可以跳行。

import pandas as pd


# N已知
uf = UF(N)
df=pd.read_table('/data01/mingyu19/example.txt', sep=' ', header=None, skiprows=[0])
for i in range(len(df))  # 或in range(df.shape[0]):
	p, q = df.iloc[i]
	uf.union(p, q)

以上代码跳过了txt不规整的第一行,将后续的内容以dataframe的格式读入。如果已知N值的话可以这样使用,否则,从文件中读取第一行的N值可能还是需要用刚才with open()的方法,或者总之需要再次打开一遍文件专门读取第一行的元素总数,比较繁琐。

1.2.2 快速查找

第一种思路是对于给每个连通集标号,不同的节点就有不同的编号,处于同一个连通集内的节点,其编号相同。
这种逻辑结构使得查找连通元素变得十分简单,而合并则需要将两个连通集中的元素的编号统一。当数据集很大时,有过多的值需要变化,时间复杂度较高,一次union的操作时间复杂度为 O ( N ) O(N) O(N)

class UF:
	def __init__(self, N):
		self.nums = N
		self.data = {}
		
	def union(self, p, q):
		value_p = self.data[str(p)]
		value_q = self.data[str(q)]
		for key in self.data:
			if self.data[key] == value_q:
				self.data[key] = value_p
		return self.data
		
	def connected(self, p, q):
		p, q = str(p), str(q)
		if p in self.data and q in self.data:
			return self.data[p]==self.data[q]
		else:
			return False

需要注意的是,字典的键key使用了字符串,是这一数据类型的要求。
此外,可以使用列表,算法上没有区别,之后的展示将以列表形式给出,字典形式可以同理类推。

class UF:
	def __init__(self, N):
		self.nums = N
		self.data = [i for i in range(N)]
		
	def union(self, p, q):
		i_p = self.data[p]
		i_q = self.data[q]
		for index in self.data:
			if index == i_q:
				self.data[index] = i_p
		return self.data
		
	def connected(self, p, q):		
		return self.data[p]==self.data[q]		

虽然名为快速查找,但是这种方法实际上很慢—— O ( N ) O(N) O(N),对于整体的操作则是 O ( N 2 ) O(N^2) O(N2)

1.2.3 快速合并

应对1.2.2中合并的速度缺陷,一个直观的思路就是甩掉一层for循环。可以通过树的方式组织数据,使得合并时只要连接相应连通树的根节点,非常快速。
在算法上的实现主要依赖以下性质——

根节点的编号是其本身的值


因此在此基础上,在类中增加一个“寻根”的代码:

class UF:
	def __init__(self, N):
		self.nums = N
		self.data = [i for i in range(N)]
		
	def union(self, p, q):
		r_p = self.root(p)
		r_q = self.root(q)
		self.data[q] = r_p
		return self.data
		
	def connected(self, p, q):		
		return self.root(p)==self.root(q)
		
	def root(self, i):
		while self.data[i] != i:
			i = self.data[i]
		return i		

这种写法表面上去除了一个for循环,但是引入了一个while循环,因此虽然在合并时可能会变快(因为通常无需严格地遍历整个数组,而只是一部分),但是在查找connected时变慢了。

1.2.4 提升方法
1.2.4.1 加权法

如果将小树连在大树上而不是相反,就能在一定程度上避免树过高,这可以通过跟踪每棵树的元素个数实现。
之所以选择跟踪叶子节点个数而不是跟踪高度,是因为树的节点越多,数据点就越有可能落在树上。即便是一颗大而矮的树,如果放在下面,也会因为增加了太多元素的寻根距离而影响时间复杂度。因此大的树在上,比小的树在上更有利。
加权法使用空间换时间,另开一个新的数组,存储每个根节点对应的树的叶子节点个数。对于union方法,先考察两棵树的大小,再进行连接:将小的树的根连接到大的树的根部,成为大的树的叶子节点。
代码改变了union方法——

class UF:
	def __init__(self, N):
		self.nums = N
		self.data = [i for i in range(N)]
		self.sz = [1 for i in range(N)]  # 数字1
		
	def union(self, p, q):
		r_p = self.root(p)
		r_q = self.root(q)
		if sz[r_p] > sz[r_q]:
			self.data[r_q] = r_p
			self.sz[r_p] += self.sz[r_q]
		else:
			self.data[r_p] = r_q
			self.sz[r_q] += self.sz[r_p] 
		return self.data, self.sz
		
	def connected(self, p, q):		
		return self.root(p)==self.root(q)
		
	def root(self, i):
		while self.data[i] != i:
			i = self.data[i]
		return i		

经过这样的操作,操作的耗时与节点深度有关,时间复杂度为 O ( l o g 2 N ) O(log_2N) O(log2N)

1.2.4.2 路径压缩法

在寻找某个节点的根节点的过程中,会遇到该节点的根并不是整棵连通树的根的情况,对于这种不直连根节点的中间节点,可以将这个节点的父节点改为整棵树的根节点,从而压缩路径。
在实现时,为了简单操作,可以做一些妥协:只将这个节点的父节点替换为其祖父节点,也就是只缩短一层路径。对于那些直连根节点的叶子,经过操作不受影响。
代码改变了root方法——

class UF:
	def __init__(self, N):
		self.nums = N
		self.data = [i for i in range(N)]
		self.sz = [1 for i in range(N)]  # 数字1
		
	def union(self, p, q):
		r_p = self.root(p)
		r_q = self.root(q)
		if sz[r_p] > sz[r_q]:
			self.data[r_q] = r_p
			self.sz[r_p] += self.sz[r_q]
		else:
			self.data[r_p] = r_q
			self.sz[r_q] += self.sz[r_p] 
		return self.data, self.sz
		
	def connected(self, p, q):		
		return self.root(p)==self.root(q)
		
	def root(self, i):
		while self.data[i] != i:
			self.data[i] = self.data[self.data[i]]
			i = self.data[i]
		return i		

经过树展平,这个算法的时间复杂度已经非常接近于线性。可惜,完全线性的算法是不存在的。

1.2.5 应用
1.2.5.1渗滤

在这里插入图片描述
现定义一个系统,具有 N ∗ N N*N NN个方格,这些方格可能是开放的(白色)或闭塞的(黑色),每个方格开放的概率是 p p p. 一个系统是渗滤的,当从矩形的上边到下边存在由白色方格连接的通路。当这种通路不存在时,系统是不渗滤的。

显然的,当开放概率很低是,系统将是闭塞的;当开放概率很高时,系统将是开放的。开放概率与系统开放的关系呈陡峭的S形曲线,当开放概率在0.593左右,系统的开放概率从接近于0陡峭上升至接近于1.

要想求解这个值,需要蒙特卡洛仿真,是快速并查集的一种延伸应用。

1.3.1 算法导论引言

1.3.2 观察:3-总和问题

3-总和问题:给出一个数组,寻找其中的三个元素使得和为零,找到所有组合。
暴力法:

class ThreeSum():
	def __init__(a):
		self.a = a
		self.len = len(a)

	def count(a):
		num = 0
		for i in range(self.len):
			for j in range(i+1, self.len):
				for k in range(j+1, self.len):
					if i+j+k == 0:
						num += 1
		return num

如果想要测量程序运行的时间可以使用time库中的time.time()time.clock()
实际上,程序运行的时间可以精确测量,只要计算出每个语句的执行次数就可以。比如1-SUM问题和2-SUM问题。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值