Coursera - Algorithm (Princeton) - 课程笔记 - Week 1

Week 1

并查集 Union-find

  • 开发一个有效算法的步骤
    1. 给问题建模(寻找基本操作)
    2. 寻找一个合适的算法
    3. 算法是否足够快?内存是否合适?
    4. 如果不是,则寻找原因
    5. 寻找一条解决的方法
    6. 不断迭代直到满意

动态连接性 Dynamic Connectivity

  • 动态连接性,即并查集
  • 给定一组N个对象,用0到N-1进行标识,两个命令
    • 并集命令,将连个对象连接到一起
    • 查找命令/连通性查询,两个对象之间是否有路径存在
  • 对连接进行建模,假设“连接”是一个等价关系
    • 自反性,p连接到p
    • 对称性,如果p连接到q,那么q连接到p
    • 传递性,如果p连接到q,q连接到r,那么p连接到r
  • 连通分量(Connected components),互相连接的对象的最大集合
  • 命令的实现思路如下:
    • 对查找命令,检查两个对象是否在相同分两种
    • 并集命令,用两个分量的并集替代两个对象所在的连个连通分量
  • 目标:为并查集设计高效的数据结构
    • 物体个数N可能很大
    • 操作数量M可能很大
    • 查询命令和并集命令可能混杂在一起

快速查找 Quick Find

  • 一种贪心算法

  • 数据结构:

    • 一个用于存储对象索引(id)的整数数组,长度为N

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XEXEtPHK-1587744539312)(C:\Users\carys\AppData\Roaming\Typora\typora-user-images\1541329925302.png)]

    • p和q使连通的,当且仅当二者对应的位置存着相同的id
  • 查找操作,即检查p和q对应的数组位是否为相同id,是则连通,否则不连通

  • 并集操作,将所有id值等于p对应位的id值的项均改为p所对应id值

  • 初始状态下,每个位置上的id值即各个对象本身的索引(初始时均不连通)

  • 代价模型:

    • 初始化,需要遍历整个数组(N)
    • 合并时,同样需要便利整个数组(N)
    • 查找时,只需要常数次访问数组(1)
    • 问题:合并的代价太高(达到了指数级)
public class QuickFindUF
{
    private int[] id;
    
    public QuickFindUF(int N)
    {
        id = new int[N];
        for (int i = 0; i < N; i++)
        id[i] = i;
    }
    
    public boolean connected(int p, int q)
    { return id[p] == id[q]; }
    
    public void union(int p, int q)
    {
        int pid = id[p];
        int qid = id[q];
        for (int i = 0; i < id.length; i++)
        if (id[i] == pid) id[i] = qid;
    }
}

快速合并 Quick Union

  • 一种“懒策略”,即尽量避免计算直到不得不进行计算
  • 数据结构:
    • 一个用于存储对象索引(id)的整数数组,长度为N
    • 把连通分量看作一棵树,那么id值得含义就是i的父节点
    • i的根节点(Root),即id[id[id[...id[]...]]]
    • 根节点,即id值为自己的节点
  • 查找操作:检查p和q的根节点是否相同,相同根节点的点是连通的
  • 合并操作:将p的根节点设为q的根节点
  • 代价模型:
    • 初始化,需要便利整个数组(N)
    • 查找,不定(最坏需要便利整个数组,N)
    • 合并,包含查找操作(N)
    • 问题:树可能会非常高,导致查找非常的麻烦
public class QuickUnionUF
{
    private int[] id;
    
    public QuickUnionUF(int N)
    {
        id = new int[N];
        for (int i = 0; i < N; i++) id[i] = i;
    }
    
    private int root(int i)
    {
        while (i != id[i]) i = id[i];
        return i;
    }
    
    public boolean connected(int p, int q)
    {
    	return root(p) == root(q);
    }
    
    public void union(int p, int q)
    {
        int i = root(p);
        int j = root(q);
        id[i] = j;
    }
}

对快速合并的改进

  • 由上,快速查找和快速合并均不支持很大的动态连接性问题
  • 使用“加权”的快速合并,这样在合并时可以避免合并较高的树,而得到更高的树
  • 跟踪每一棵树的大小,即树中对象的个数
  • 通过将更大的树的根作为更小的树的根实现平衡
  • 数据结构:
    • 和快速合并相同
    • 额外维护一个数组sz[i],计算以i为根的树的节点个数
  • 查找操作:检查根节点是否相同
  • 合并操作:查询两棵树的大小,将小树的根连接到大树的根上,同时更新sz数组,将i树连接到j树上,需要将sz[i]加到sz[j]
int i = root(p);
int j = root(q);
if (i == j) return;
if (sz[i] < sz[j]) { id[i] = j; sz[j] += sz[i]; }
else { id[j] = i; sz[i] += sz[j]; }
  • 运行时间:
    • 查找操作,时间与p和q的深度成正比(最坏 lg ⁡ N \lg N lgN
    • 合并操作,在给定根的情况下是常数次时间(包含查找, lg ⁡ N ) \lg N) lgN
    • 树中任意节点的深度上限是 lg ⁡ ( N ) \lg(N) lg(N)
  • 路径压缩(path compression):在计算了p的根节点之后,将其id值直接设为根节点
    • 两趟实现:在root方法中再加一个for循环修改每一个检查的节点的id值
    • 一趟实现:将查找路径上每一个节点id值设为其祖父节点(查询路径折半)
private int root(int i)
{
    while (i != id[i])
    {
        id[i] = id[id[i]];
        i = id[i];
    }
    return i;
}
  • 论证:从一个空的数据结构开始,在N个对象上的任意M个合并-查找操作序列,最多访问 c ( N + M lg ⁡ ∗ N ) c(N+M\lg^*N) c(N+MlgN)次数组(lg*,把N变成1需要做对数操作的次数)
  • 带路径压缩的加权快速合并(WQUPC)在实际应用中是线性的
  • 经论证,不存在绝对线性的算法

并查集的应用

  • 渗滤(Percolation):很多物理系统的模型

    • n × n n \times n n×n的方形网格,小方格称为位
    • 每个位开放的概率为p
    • 一个系统是渗滤的,当且仅当顶部和底部被开放的位相连
  • 当N很大时,理论保证存在一个尖锐阈值p*,这个概率的高和低决定了这个系统是否渗滤

  • 解决方法:蒙特卡洛仿真(Monte Carlo Simulation)

    • 初始化整个网络为闭合的
    • 随机地开放位,并检查是否变得渗滤,持续这个过程直到系统渗滤
    • 开放位的比例就是对p*的估计值
  • 使用动态连接性方案解决渗滤阈值估计问题

    • 为每一个位创建一个对象,标号为 0 到 N 2 − 1 0到N^2-1 0N21
    • 如果开放位相连,就够成一个分量
    • 一个系统连通,当且仅当底行与顶行有位连接
    • 简化技巧:顶和底分别引入一个虚拟节点,系统渗滤当且仅当两个虚拟节点连通
    • 开放一个位,检查并连接所有相连的分量(四个邻域)
    • 阈值大约为0.592746

算法的分析

  • 分析算法的原因:
    • 预测算法的性能
    • 比较算法
    • 在最坏情况下算法的性能下限
    • 算法的一些理论基础
    • 基本目标:避免性能bug
  • 分析的科学方法:
    • 观察自然世界的一些特征——观察程序在计算机上的运行时间
    • 假设一个模型与观察到的现象相一致
    • 使用这一假设预测某些事件——预测更大问题规模或者另一台计算机上的运行时间
    • 通过更长远的观察验证假设
    • 不断验证,直到假设与观察一致
  • 科学方法原则:
    • 实验必须是可重现的
    • 假设一定是可证伪的

观察 Observation

  • 如何对运行程序计时(Java)
Stopwatch stopwatch = new Stopwatch();
...
double time = stopwatch.elapsedTime();
  • 暴力解法往往在大量输入时出现用时过长的问题

  • 经验分析法:用不同大小的输入测量程序运行时间

  • 标准尺度:用于绘制运行时间到输入大小的图像

  • 双对数尺度(log-log scale):用于绘制运行时间的对数到输入大小的对数的图像,双对数能够将图像呈现成直线的样式。针对这条直线,又 lg ⁡ ( T ( N ) ) = b lg ⁡ ( N ) + c \lg(T(N))=b\lg(N)+c lg(T(N))=blg(N)+c,则 T ( N ) = a N b , a = 2 c T(N)=aN^b,a=2^c T(N)=aNb,a=2c

  • 如果该直线的斜率为B,则这个函数正比于 N B N^B NB,这称为幂定律

  • 有了具体拟合参数,就能够对更大规模实际不可测(时间过长)的情形进行预测

  • 加倍假设对存在幂定律的关系中快速估计b,即加倍输入运行程序,然后计算N和2N时间的比率,可以发现当N到达到一定大小时,会收敛到指定值,即N的指数

  • 系统独立的因素:算法和输入数据共同决定了幂定律中的b的值

  • 系统依赖因素:硬件(CPU,内存……)、软件(编译器,解释器……),系统(OS,网络……)

  • 上二者共同决定了幂定律中的a的值

数学模型 Mathematical Models

  • 运行时间的数学模型:对所有的操作的cost×频率的代数和
    • 需要分析程序以确定一组操作
    • cost取决于机器本身以及编译器本身
    • 频率取决于算法和输入数据
  • 在实践中,精确的数学模型往往使可行的
  • 图灵认为,我们只需要对最大开销的操作计数进行粗略估计就可以了
  • 简化1:代价模型,我们选择开销最大的或最频繁的操作,使用cost乘频率作为运行时间的代理变量,一般假设运行时间使常数倍的该运行时间
  • 简化2:波浪号表示,估计运行时间为输入N的一个函数,同时忽略推导式中的低阶项
    • 如果N很大,这些项并不重要
    • 如果N很小,我们就完全忽略
    • 波浪号表示 f ( N ) ∼ g ( N ) , lim ⁡ N → ∞ f ( N ) g ( N ) = 1 f(N)\sim g(N), \lim\limits_{N\to\infin}\frac{f(N)}{g(N)}=1 f(N)g(N),Nlimg(N)f(N)=1
  • 对离散求和,使用积分近似替换
    • 1 + 2 + … + N = ∑ i = 1 N i ∼ ∫ x = 1 N x d x = 1 2 N 2 1+2+\ldots+N=\sum\limits_{i=1}^{N}i\sim \int_{x=1}^Nxdx = \frac12N^2 1+2++N=i=1Nix=1Nxdx=21N2
    • 1 + 1 2 + 1 3 + … + 1 N = ∑ i = 1 N 1 i ∼ ∫ x = 1 N 1 x d x = ln ⁡ N 1+\frac12+\frac13+\ldots+\frac1N=\sum\limits_{i=1}^{N}\frac1i\sim \int_{x=1}^{N}\frac1xdx=\ln N 1+21+31++N1=i=1Ni1x=1Nx1dx=lnN
    • 3-sum三重循环 ∑ i = 1 N ∑ j = i N ∑ k = j N 1 ∼ ∫ x = 1 N ∫ y = x N ∫ z = y N d z d y d x ∼ 1 6 N 3 \sum\limits_{i=1}^{N}\sum\limits_{j=i}^{N}\sum\limits_{k=j}^{N}1\sim\int_{x=1}^{N}\int_{y=x}^{N}\int_{z=y}^{N}dzdydx\sim\frac16N^3 i=1Nj=iNk=jN1x=1Ny=xNz=yNdzdydx61N3

增长阶数 Order-of-growth

  • 常用的一些函数: 1 , log ⁡ N , N , N log ⁡ N , N 2 , N 3 , 2 N 1,\log N, N, N\log N, N^2, N^3, 2^N 1,logN,N,NlogN,N2,N3,2N

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-K2S21e5X-1587744539318)(C:\Users\carys\AppData\Roaming\Typora\typora-user-images\1541509341306.png)]

  • 二叉搜索
public static int binarySearch(int[] a, int key)
{
	int lo = 0, hi = a.length-1;
	while (lo <= hi)
    {
        int mid = lo + (hi - lo) / 2;
        if (key < a[mid]) hi = mid - 1;
        else if (key > a[mid]) lo = mid + 1;
        else return mid;
    }
	return -1;
}
  • 二叉搜索数学分析
    • 定理:二叉搜索至少要用 1 + log ⁡ N 1+\log N 1+logN次比较才能在N长的有序数组中找到或找不到一个元素
  • 一般情况下,更低的增长阶数意味着更快的实际运行

算法的理论 Theory of Algorithms

  • 分析的类型:
    • 最好情况,代价下界,由”最简单“的输入确定,提供了所有输入的目标结果
    • 最坏情况,代价上届,由”最难“的输入确定,提供了所有输入的保证结果
    • 平均情况,随机输入的代价期望,需要定义一个”随机“的输入模型,提供了一个评估性能的方式
  • 根据实际需求设计输入模型:
    • 方法1,设计时考虑最坏情况
    • 方法2,随机化,依赖于某种概率条件下的保证
  • 算法目标:
    • 确立一个问题的“困难度”
    • 开发一个“最优化”的算法
  • 理论方法:
    • 在分析过程中去掉细节,只分析常数倍数的精度
    • 对输入模型,只关心最坏情况
  • 最优化算法:
    • 对任何输入在常数精度下的性能保证
    • 没有算法能够提供更好的性能保证
  • 算法理论常用符号:
    • Θ \Theta Θ,对增长阶数的逼近,其上下界都是常数乘以内部的值
    • O \Omicron O,增长阶数的上界,运行时间小于常数乘以内部值
    • Ω \Omega Ω,增长阶数的下界,运行时间大于常数乘以内部值
    • O ( ∗ ) = Ω ( ∗ ) \Omicron(*)=\Omega(*) O()=Ω()时(括号内为同一值),则认为其运行时间为 Θ ( ∗ ) \Theta(*) Θ()
  • 算法的设计步骤:
    • 开发一个算法
    • 证明下界,获得算法上界
    • 如果二者存在间隔,要么降低上界,要么提高下界(证明当前下界非平凡)

内存 Memory

  • 对齐空间,Java要求对象的存储占用必须是8的倍数
  • 对于非经典数据类型,其他的类型,除本身占用外,还存在着额外的空间占用
  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值