普林斯顿·算法Part1 && 2
一个人比较缓慢,大多数在于信息不全面、思维碰撞缺乏,在B站上也没有找到相关群体
故此,留一个群号,希望可以共同进步,相互监督,940774008
导论
Algorithm: method for solving a problem. 解决问题的方法
Data structure: method to store information. 存储问题相关信息的方法
最早起源追溯:Euchlid 欧几里得 公元前300
正式确立为一门学科:Church and Turing 丘奇和图灵 in 1930s
Why Study:
To solve problems that could not otherwise be addressed. 解决只有算法能解决的问题
For intellectual stimulation. 启发智力
Great algorithms are the poetry of computation — Francis Sullivan
注:部分算法是由本科生发明的,并且还有很多算法正在等待发现
eg.动态连通性问题
开发流程
- 建立问题模型Model
- 查找解决该问题的基本basic操作(复杂操作=基本操作的线性组合)
- 时间消耗 与 内存空间 的取舍(不同的倾向的算法有不同的用途)
- 通过实践验证模型,如果结果不是想要的?想办法找出问题的根源(debug)
- 提出新的算法
- 循环以上步骤直至满意为止
算法入门
案例·动态连通性问题
数学模型·总结性质
自反性:P点连接自身
对称性:P连通Q,则Q也联通P
传递性:P联通Q,Q联通R,则P也联通R
= = 是不是突然就想到 Math·集合?
算法模型·总结基操
Connected components ==> 相互连接的点的最大集合·Set
查询是否联通操作 ==> 检查这两个点是否在同一个component
联通操作 ==> 将包含两个点的component替换为它们的并集union
快速查找 ==> 查找包含p点的component
abstract public calss UF
UF(int N) //Number of objects N can be Huge!
void union(int p,int q) //Add connection between p and q
boolean connected(int p,int q) //Are p and q connected?
int find(int p) //Component identifier for p (0 to N-1)
int count()
算法实现·不断优化
惊人的现实: No linear-time algorithm exists. 不存在线性增长的算法,所以就别浪费太多的脑力。差不多就行
①QuickFind
使用数组,如果查询某一个节点所属 集合 直接通过 arr(节点index) 即可,find方法的时间复杂度为O(1)
但其union方法 调用一次的时间复杂度为 O(N),N为节点个数,遍历整个数组,更改所有连接后为同一个集合的节点
②QuickUnion
改进思想:QuickFind 是一种 一维线型思想更改所有受到union方法影响的节点数组项,那么能否只修改 两个 节点数组项(进行连接的两个节点的两个根节点·二维平面树型思想)
③QuickUnionWeight
改进思想:QuickUnion 在Union方法上做了较大改进,但find方法也从 O(1) 变为了 O(N)【逐渐逼近N => 约等于N】,原因为这个树在极端情况下会成为一个瘦高树【竖着的线型】,find时间需要遍历所有节点 => 遍历整个数组, 也就是另一种慢。那么如何将find方法时间复杂度进行改进?—— 约束 瘦高树的形成 => 也就是确保每次都是小树接入大树,而不是大树的根节点改为小树的根节点。通过另一个 weight权值数组 记录当前子树的已连接的节点个数。
④QuickUnionWeightPathCompress
改进思想:进一步优化 瘦高树——为什么不完全展平?将每个节点都指向根节点 => 树层级只为2 => MySQL B+Tree 3层
数据结构·存储信息
Integer Index Array[N] 整数索引数组
Quick-find => 如何表示p点与q点连接——当且仅当 p && q 索引拥有相同的数组项 id (component Id)
Quick-Union =>
数据结构·初始化:将所有索引的数组项都设置为自身,表明:初始状态下各个点都是相互独立的
![image-20200117103843598](i\conn_DataStructure.png)
时空复杂度·改进源泉
1次调用 [ 空间复杂度为 S[N] ]
M union-find ops on N objects
lg* N 【迭代对数函数】:使N变为1所需取2的对数的次数,通常来说:lg*N <= 5(N=2^65536)
带权 时间复杂度 Log2 N 证明
假定:包含名为X节点的树 一直为 小树(最差情况)
关系:X节点的深度 与 其所在树的节点总数增长的关系为:Deepth + 1 <=> 包含X的新小树节点总数较原总数增长 2 倍
( 至少甚至更多,因为大树节点总数 >= 小树节点总数 )
推算:若初始包含X的小树 仅为 X,深度为1,且节点总数为 N,那么 2^? = N => ? = Log2 N / lgN 次
测试设计
在进行深层次的代码构建前,应想好,如何设计 输入与输出
确保API的可用性——确保任何一种算法的实现都执行我们期望它进行的操作
真实代码实现
import scala.collection.mutable
import scala.io.StdIn
/**
* @Project Algorithms
* @Author INBreeze
* @Date 2020/1/17 10:52
* @Description
*/
object Test {
def main(args: Array[String]): Unit = {
val N = StdIn.readInt()
val uf: UF = new QuickFindUF(N)
while (StdIn.readInt()!= -1){
val p = StdIn.readInt()
val q = StdIn.readInt()
if(!uf.isConnected(p,q)){
uf.union(p,q)
println(p+" "+q)
}
}
}
}
abstract class UF {
abstract def isConnected(p: Int, q: Int): Boolean
abstract def union(p: Int, q: Int): Unit
abstract def find(p: Int): Int
abstract def count(): Int
}
// 最快查找
class QuickFindUF(N: Int) extends UF {
private var i = -1
private val id: Array[Int] = Array.fill(N) {
i += 1
i
}
def isConnected(p: Int, q: Int): Boolean = id(p) == id(q)
/**
* 调用一次的时间复杂度为 n
* 调用n次的时间复杂度为 n*n = n^2^
*
* @param p 被修改的数组项
* @param q 连接的另一个点
*/
def union(p: Int, q: Int): Unit = {
if (id(p) != id(q)) {
id.map {
value =>
if (value == id(p)) id(q)
else value
}
}
}
def find(p: Int): Int = id(p)
def count(): Int = {
val countSet = mutable.Set[Int]()
id.foreach(countSet + _)
countSet.size
}
}
class QuickUnion(N: Int) extends UF {
private var i = -1
private val id: Array[Int] = Array.fill(N) {
i += 1
i
}
private def root(i: Int): Int = {
var res = i
while (id(res) != res) {
res = id(res)
}
res
}
def isConnected(p: Int, q: Int): Boolean = root(p) == root(q)
/**
* 最快合并,只需要 极少次 访问数组,但缺点是当 N 的量级足够大时,树会很瘦高
* 一次调用的时间复杂度也将逐渐逼近 N
*
* @param p 被修改根节点的点
* @param q 连接的另一点
*/
def union(p: Int, q: Int): Unit = id(root(p)) = root(q)
def find(p: Int): Int = root(p)
def count(): Int = {
var res = 0
id.foreach(
value =>
if (value == id(value))
res += 1
)
res
}
}
class QuickUnionWight(N: Int) extends UF {
private var i = -1
private val id: Array[Int] = Array.fill(N) {
i += 1
i
}
private val weight: Array[Int] = Array.fill(N)(1)
/**
* 优化:为什么不将整个树展平? 将每个节点都指向根节点
* @param i 当前节点索引
* @return 当前节点的根节点索引
*/
private def root(i: Int): Int = {
var res = i
while (id(res) != res) {
//将父节点的父节点索引 赋值给 当前节点【将当前节点指向祖父节点】
id(i)=id(id(i))
res = id(res)
}
res
}
def isConnected(p: Int, q: Int): Boolean = root(p) == root(q)
/**
* 通过将小树合并至大树的根节点,有效的防止 瘦高巨长树的出现
* 这样Find的时间就不会出现 时间复杂度逼近 N 的情况 而是变成 log2 N / lgN
* N=1000 TimeCast=10 N=100w TC=20 N=10ww TC=30
*
* @param p 被修改根节点的点
* @param q 连接的另一点
*/
def union(p: Int, q: Int): Unit = {
val i = root(p)
val j = root(q)
if (i != j) {
//左小右大:修改左子树根节点数组项,修改右子树权重数组项[改进④路径压缩]
if (weight(i) < weight(j)) {
id(i) = j
weight(j) += weight(i)
}
else {
id(j) = i
weight(i) += weight(j)
}
}
}
def find(p: Int): Int = root(p)
def count(): Int = {
var res = 0
id.foreach(
value =>
if (value == id(value))
res += 1
)
res
}
}
实际应用
渗透问题・Percolation
When N is large, theory guarantees a sharp threshold p*. 概率为多大时,该模型将会渗透
・p > p: almost certainly percolates.
・p < p*: almost certainly does not percolate.
将渗透问题 转换为 动态联通问题,通过 蒙特卡罗模拟 百万次 得出 近似逼近结论:0.593,
开发算法的一般步骤
Steps to developing a usable algorithm.
・Model the problem. //1.数学模型
・Find an algorithm to solve it. //2.算法模型+简单实现
・Fast enough? Fits in memory? //3.时空复杂度分析
・If not, figure out why. //4.时空优化
・Find a way to address the problem. //5.创建新算法实现
・Iterate until satisfied. //6.循环迭代
The scientific method.
Mathematical analysis.