COMP9101 学习笔记 贪心算法在图中应用(Greedy Method applied to graphs)(更新完毕)

    技术2022-07-11  83

    本文为个人学习笔记(更新完毕) 如果觉得写的不错欢迎点赞收藏交流 如有疑问和建议欢迎留言或者私信

    目录

    1. 信号塔问题1.1 强连通分量和有向无环图1.2 拓扑排序 2. 最短路径问题2.1 Dijkstra 算法2.1.1 算法证明2.1.2 加强优先队列(an augmented priority queue)2.1.3 利用优先队列实现Dijkstra算法 3. 最小生成树问题3.1 最小生成树3.2 Kruskal 算法3.2.1 Union-Find数据结构Union-Find支持的操作:Union-Find的最简单应用: 3.2.2 利用Union-Find实现Kruskal算法

    1. 信号塔问题

    假设有n个信号塔用于播放海啸警告,坐标为(x,y),半径为r,信号塔发射型号后,辐射区域内的信号塔也会被激活。 给其中一些塔配备地震传感器,传感器可以使得该塔被激活。当所有塔激活便可以播放海啸警告。 怎样分配可以让地震传感器数量最少?

    1.1 强连通分量和有向无环图

    解决这个问题前回顾几个概念。

    给定有向图 G = ( V , E ) G = (V,E) G=(V,E), 怎么求 v ∈ V v \in V vV的强连通分量C?

    建立一个新有向图 G ′ = ( V , E ′ ) G' = (V, E') G=(V,E),V与G相同,E‘ 则是G中的E的反向。利用BFS寻找到点集合 D ⊆ V D \subseteq V DV, D中所有的点在 G G G中都可以从v出发寻到利用BFS寻找到点集合 R ⊆ V R \subseteq V RV, R中所有的点在 G ′ G' G中都可以从v出发寻到求得 C = D ∩ R C = D \cap R C=DR

    定义一个图: ∑ = ( S G , E ∗ ) \sum = (S_G, E^*) =(SG,E) 其中, S G S_G SG是G中所有强连通分量的集合,对任意 σ 1 , σ 2 ∈ S G   且 σ 1 ≠ σ 2 \sigma_1, \sigma_2 \in S_G \ 且 \sigma_1 \neq \sigma_2 σ1,σ2SG σ1=σ2都存在 σ 1 → σ 2 ∈ E ∗ \sigma_1\to\sigma_2 \in E^* σ1σ2E 注意, σ 1 → σ 2 \sigma_1 \to \sigma_2 σ1σ2表示存在点 u 1 ∈ σ 1 , u 2 ∈ σ 2 u_1 \in \sigma_1 , u_2 \in \sigma_2 u1σ1,u2σ2在E中有 u 1 → u 2 ∈ E u_1 \to u_2 \in E u1u2E

    很显然,图 ∑ \sum 是一个有向无环图(DAG),因此我们可以对其进行拓扑排序,拓扑排序后的DAG,所有edges都是从左至右。

    1.2 拓扑排序

    初始化 空表L 用于存放即将排序好的元素 初始化 S 用于存放所有的点 ‘’’

    while S not empty: 从S中移除点u 将u加入L尾部 for 对每个有e = (u,v)的v点: 从图中中移除e if v点没有edge了,插入S中 if 图还有edge,返回error(图像有环) else: return L

    ‘’’

    所以针对信号塔的问题,我们便可以通过先求出G的所有强连通分量,组成DAG,然后进行拓扑排序的方式解决。

    2. 最短路径问题

    有向图 G = ( V , E ) G = (V,E) G=(V,E), 每个edge都有非负权重w,给一个vertex v ∈ V v \in V vV,假设对所有 u ∈ V u \in V uV都有一条从v 到 u的路径,如何找到从v到所有 u ∈ V u \in V uV的最短路径?

    2.1 Dijkstra 算法

    首先我们要证明一点: 假定从点v到点z的最短路径为p, 对于路径p上的每个点w,从v到w的最短路径就是p路径在w的截断。

    利用反证法, 存在一个v到w最短路径p1不是这个p的截断,此时我们便可以移除从p上从v到w的截断,利用这个p1取而代之,得到新的最短路径p*,与条件是v到z的最短路径矛盾。

    以此为算法的贪心规则: 每一段都尽量找到v点到其他点的最短路径。我们有如下算法

    设置 S = {s} ,对每个S中的点u, 存储一个距离d(u),d(s) = 0 while S != V: 选择一个不在S中的点v, 使得从S到v至少有一条边e=(u,v)且 d'(v) = d(u) + length(e) 最小 将 u 加入 S并且 d(u) = d'(u) endwhile

    2.1.1 算法证明

    反证法, 假设G中存在从v到u的更短的路径,这条路径不由我们算法得出,根据我们的算法,我们选择的u都存在于S中,所以这条路径肯定不会全部在S中,我们假设这个不在S中的第一个点为点z,那从v到z的路径应该会比v到u更短,那我们的算法应该会选择z而不是u,矛盾。

    2.1.2 加强优先队列(an augmented priority queue)

    我们需要优先队列来更有效的改变队列中键的改变。 首先我们有一个heapbased 优先队列结构A,A[i] 左孩子 A[2i] 右孩子A[2i+1] 存在A中的元素形式为 (i, k(i)), k(i)为元素i的key 另外一个array P,与A同长,存取A中每个元素的位置A[P[i]] = (i,k(i))

    所以,当我们改变一个元素i的key,需要O(logn)操作: 首先查询 A[ P[i] ]然后改变A[ P[i] ] 元素中的 key,执行heappify操作保留堆属性。

    一个优选队列可以有效地插入元素,删除元素,改变元素key(changekey),取出具有最小key的元素(extractmin)

    2.1.3 利用优先队列实现Dijkstra算法

    起点为s,对于 u ∈ V   a n d   u ≠ s u \in V \ and\ u \neq s uV and u=s, 我们把 ( u , d ′ ( u ) ) (u, d'(u)) (u,d(u))放入优先队列, 执行extractmin找出 d ′ ( u ) d'(u) d(u)最小的点u加入S。 令点 w ∉ S w \notin S w/S是在优先队列中一个剩余的点, v ∈ S v \in S vS, 如果 ( v , w ) ∈ E (v,w) \in E (v,w)E, 则 d ′ ( w ) = m i n ( d ′ ( w ) , d ( v ) + l ( v , w ) ) d'(w) = min(d'(w), d(v) + l_{(v,w)}) d(w)=min(d(w),d(v)+l(v,w)),这一步就可能用到changeKey操作将w加入S,这个changeKey操作每条边最多出现一次

    总的来说使用优先队列,Dijkstra算法可以在n个节点m个边的图上,运行O(m)时间,加上n次Extractmin和m次changekey时间,一共是O(mlogn)时间

    3. 最小生成树问题

    3.1 最小生成树

    连通图G的子图,拥有与G相同的点和最小长度的边,该子图为G的最小生成树

    定理: 假设所有边费用不等,令S为G任意节点的非空真子集,令边e = (u,v)是一端在S,一端在V-S中的最小费用边,则所有的最小生成树都一定包含e 证明

    反证法,假设存在一个不包含e = (u,v)的最小生成树TT是生成树,所以必定存在一条从u到v的路径因为 u ∈ S   a n d   v ∉ S u \in S\ and\ v \notin S uS and v/S, 所以路径一定经过S假设该路径在S中的最后一个点为点p, 下一个为点 q ∉ S q \notin S q/S ( p , q ) ∈ T (p,q) \in T (p,q)T的费用一定大于e=(u,v)(e的假设)将e加入此路径,则形成了一个 u → p → q → v → u u\to p\to q\to v\to u upqvu的循环移除(p,q),形成了一个新的生成树T*根据假设,e = (u,v)的费用比(p,q)更小, T与T的差别在于后者少了(p,q)多了(u,v),所以T是总费用比T更小的生成树,与T是最小生成树的假设矛盾

    3.2 Kruskal 算法

    将所有边根据长度升序排序生成一个树,每一步增加一条边当一条边 e i e_i ei的加入不会构成环,则在第 i 轮加入,如果构成环,则放弃,直至所有边遍历完毕

    Kruskal算法会生成最小生成树,如果所有边长度不同,则生成的最小生成树唯一 证明

    设一条边 e = ( u , v ) e=(u,v) e=(u,v)被算法加入生成树在e加入前的一刻,从u点有路径通向某些结点,设这些结点集合S显然,此时 u ∈ S   b u t   v ∉ S u \in S \ but\ v \notin S uS but v/S,因为加入 ( u , v ) (u,v) (u,v)不会在S内构成环此时已经不存在一条 一端在S一端在S外 的边,因为如果这样的边存在且加入不会构成圈,那他肯定会被算法加入S因此, e = ( u , v ) e=(u,v) e=(u,v)便是一端在S一端在S外中最便宜的边,根据3.1.1的定理,它属于最小生成树。

    3.2.1 Union-Find数据结构

    Union-Find支持的操作:

    MakeUnionFind(S): 返回集合S上的一个Union-Find结构,其中所有元素都被放置在一个分离的单独的集合中: S = ( a , b , c ) → ( ( a ) , ( b ) , ( c ) ) S=(a,b,c) \to ((a),(b),(c)) S=(a,b,c)((a),(b),(c)) 运行时间: O(n), n=|S|

    Find(v): 给定元素v,返回元v所属的集合, 运行时间:O(logn)。某些实现对这个操作实际上只需要O(1)

    Union(A,B): A ∪ B A \cup B AB代替A,B集合,改变结构。运行时间:O(logn) 而k个连续Union操作运行时间为O(klogk)(接下来解释)

    我们只看k个连续Union操作运行时间,是因为平均分析下来每个操作只需要O(logk)的时间而不是O(logn)

    Union-Find的最简单应用:

    数组A: A [ i ] = j A[i] = j A[i]=j表示 i 属于标签为j的集合数组B: B [ i ] B[i] B[i] 表示标签为i的集合的元素数量 和 由该集合元素组成的链表的头尾元素的指针

    Union(i,j)操作被定义为:(元素数量少的被更新)

    如果标签i集合的元素数量大于等于标签j集合,则A中标签为 j 的元素都被改成i,B也随之更新如果标i集合的元素数量少于标签j集合, 则A中标签为 i 的元素都被改成j,B也随之更新

    根据定义,假设Union操作改变元素m的标签,则产生的新集合的元素数量至少是被更新前元素m所在集合的元素数量的两倍

    任何k个连续Union操作都会触及最多2k个S中的元素(应用在MakeUnionFind操作后产生的分离集合中),因此,一个经过k个连续Union操作后包含元素m的集合最多包含2k个元素。

    因为每个union操作都会将包含元素m的集合的大小至少扩充一倍, 最多扩充到2k个元素,这意味着包含元素m的集合的标签最多被改变log2k次。

    我们一共有最多2k个元素,所以总共经过k个连续union操作的后我们的A最多有 2 k × l o g 2 k 2k\times log2k 2k×log2k次的标签改变,B的话只有一些指针改变和集合大小改变。 因此,经过k个连续UNION操作的时间复杂度为O(klogk)

    3.2.2 利用Union-Find实现Kruskal算法

    G = ( V , E ) G = (V,E) G=(V,E), 有n个点,m条边。

    给m条边按长度排序,因为 m < = n 2 m<=n^2 m<=n2, 时间为O(mlogm)=O(mlogn)我们运行Kruskal算法,S是所有连通分量的分离集合(MakeUnionFind),然后通过(Union)操作将满足算法要求的连通分量合并,将S慢慢变成所有包含所有结点的连通分量集合。对于e=(u,v),我们通过Find(u)和Find(v)来判断u,v是否属于同一连通分量。如果 F i n d ( u ) = i ≠ F i n d ( v ) = j Find(u)=i \neq Find(v)=j Find(u)=i=Find(v)=j,我们就添加e进入最小生成树(执行Union(i,j),u,v的标签统一,从此同属一个连通分量)

    该算法执行2m次Find操作,每次操作O(1),共O(m);执行n-1次Union操作,共O(nlogn); 排序边共O(mlogn);总体时间复杂度为O(mlogn)

    Processed: 0.017, SQL: 9