图论小专题A
大意失荆州。今天考试一到可以用Dijkstra水过的题目我竟然没有做出来,这说明基础还是相当重要。考虑到我连Tarjan算法都不太记得了,我决定再过一遍蓝皮书,对图论做一个小的总结。图论这个部分可能会分几次发布出来。
0 图的储存
一般而言,在代码中我们会用\(M\)表示边的个数,\(N\)表示点的个数。
- 邻接矩阵:用一个矩阵\(A_{ij}\)表示点\(i,j\)的距离或连通性。
- 邻接表:用
vector
类型edge
储存。其中edge[i][j]
表示以\(i\)为起点的第\(j\)条边。 - 链式前向星:非常重要的一种储存方式。是一种类似“哈希表”的结构,其中用
head
记录每个点对应的边链表的表头,每个边记录边链表的下一条边。
无向边可以用两条方向相反的有向边代替。
1 最短路
1.1 单源最短路问题(SSSP,Single Source Shortest Path)
对于任意一个点\(i\),求其到某一个固定点的距离\(dis_i\)。
在求解最短路问题中,有一个核心的“松弛操作relax”:
\]
它是所有SSSP算法的基础。
如果有\(d(i,j)>d(i,k)+d(k,j)\),我们就说\(k\)能松弛\((i,j)\)。
Dijkstra算法
贪心算法。其使用条件是没有负边。
这里采用了一个贪心的策略:注意到全局最优的SP是不可能再被松弛的,否则就跟这个性质不符了。我们每次就用这个最优的SP去松弛其他的SP,从而使得每一个SP都逐步变得最优,即不可松弛。
这个算法的代码如下:
inline void dijkstra()
{
memset(dis, 0x3f, sizeof(dis));
memset(used, 0, sizeof(used));
dis[S] = 0;
while(1)
{
int transferNode = getTransferNode();
if(transferNode == -1)
return;
for(rg int e = head[transferNode]; e; e = edge[e].next)
{
int to = edge[e].to;
checkMin(dis[to], dis[transferNode] + edge[e].len);//用最小值更新dis[to]
}
used[transferNode] = true;
}
}
其中getTransferNode()
是找到dis
最小的中转点。
inline int getTransferNode()
{
int transferNode = -1;
number disMin = Inf;//number 是typedef过的long long
for(rg int i = 1; i <= N; ++ i)
{
if(!used[i] && dis[i] < disMin)
{
disMin = dis[i];
transferNode = i;
}
}
return transferNode;
}
由于要枚举中转点和每个与中转点相连的点,上面代码的时间复杂度是\(O(N^2)\)的。
当然,我们也可以用二叉堆(priority_queue)来维护\(dis\)数组。这样可以在\(O(N\log N)\)的时间内完成算法。上述getTransferNode
部分代码如下:
priority_queue< pair<number, int> > heap;
inline int getTransferNode()
{
int transferNode = -1;
while(heap.size() > 0)
{
transferNode = heap.top().second;
heap.pop();
if(used[transferNode])
continue;
used[transferNode] = true;
return transferNode;
}
return -1;
}
主过程代码如下:
inline void dijkstra()
{
memset(dis, 0x3f, sizeof(dis));
memset(used, 0, sizeof(used));
dis[S] = 0;
heap.push(make_pair(0, S));
while(1)
{
int transferNode = getTransferNode();
if(transferNode == -1)
return;
for(rg int e = head[transferNode]; e; e = edge[e].next)
{
int to = edge[e].to;
if(dis[to] > dis[transferNode] + edge[e].len)
{
dis[to] = dis[transferNode] + edge[e].len;
heap.push(make_pair(-dis[to], to));
}
}
used[transferNode] = true;
}
}
Bellman-Ford和SPFA
由于负边权会导致\(dis_i>dis_j+d(j,i)\),最小的\(dis\)反而可能会更新出一个比它更小的\(dis\)。这样,一个点可能会不断被更新,这个全局最优的贪心策略就不成立了。
在之前的Dijkstra算法上,我们再考虑如何进一步优化。
如果对于任意的边\((i,j)\),均有\(dis_i\leq dis_j+d(i,j)\),那么这个\(dis\)就是所求的答案。这个不等式叫做三角形不等式。因此,我们可以考虑这样一种做法:我们由“点松弛”变为“边松弛”,使得每条边满足三角型不等式。
Bellman-Ford的算法流程大致就是:不断地扫描并更新每一条边,使得每一条边都满足三角形不等式,直到没有边可以再更新。
由于Bellman-Ford算法是固定的\(O(NM)\),在实际运行上速度有时不如Dijkstra,这里就不张贴这个代码了。我们考虑队列优化版的Bellman-Ford算法:SPFA(Shortest Path Fast Algorithm)算法。这个算法的命名其实和“快速排序”这个名字一样一根筋。正因如此,SPFA这个名字只在中国流行。
仔细想想,根据Dijkstra算法的推论,是不是只有全局最优值才能更新其他值?我们可以类似地,每次只对刚更新完的\(dis\)点所相连的边进行松弛。我们可以考虑建立一个队列,当一个节点被更新之后,就将其加入队列,并在之后考虑它所相连的边是否能被更新。
由于每一次入队和出队都代表一次边的更新,而一个节点可能入队出队若干次,故算法的时间复杂度下限为\(\Omega(M)\),上限为\(O(MN)\)。
算法的代码如下:
queue<int> q;
bool state[maxN];
#define IN_QUEUE true
#define NOT_IN_QUEUE false
inline void SPFA()
{
memset(dis, 0x3f, sizeof(dis));
memset(state, NOT_IN_QUEUE, sizeof(state));
dis[S] = 0; state[S] = IN_QUEUE; q.push(S);
while(!q.empty())
{
int cur = q.front(); q.pop();
state[cur] = NOT_IN_QUEUE;
for(rg int e = head[cur]; e; e = edge[e].next)
{
int to = edge[e].to;
if(dis[to] > dis[cur] + edge[e].len)
{
dis[to] = dis[cur] + edge[e].len;
if(state[to] == NOT_IN_QUEUE)
{
state[to] = IN_QUEUE;
q.push(to);
}
}
}
}
}
#undef IN_QUEUE
#undef NOT_IN_QUEUE
SPFA算法接近上界的条件是图是疏密图或网格图。如果图中没有负边权,我们可以用优先队列队优化这个算法。此时算法时间复杂度是比较稳定的\(O(M\log N)\),但是往往就不如Dijkstra优了。
在实际应用的过程中,要根据实际条件判断如何使用。图中往往有样例数据、数据范围、子任务等要素。可以通过这些来判断图是否稀疏,从而分门别类地使用对应算法。
SPFA算法的一大优势在于可以判断是否有负环。根据\(O(NM)\)的时间复杂度上限可以知道,如果一个点被更新大于等于\(n\)次,说明这个图一定存在负环。此时存在一条无穷小的路径,SSSP也就没有意义了。
1.2 最短路衍生算法
SPFA的过程也可以用dfs实现。但是显然的,由于SPFA往往需要对若干个点更新多次,如果使用dfs,不仅可能会超时,还有可能因迭代过多而发生栈溢出。但是,dfs的SPFA有一个非常明显的长处:找负环。
和bfs的做法不同,我们将队列改为栈,每次将当前节点入栈,搜索完后将当前节点出栈。只有可以被松弛的节点才有搜索的必要。
代码很简单,如下:
queue<int> q;
bool state[maxN];
#define IN_STACK true
#define NOT_IN_STACK false
inline bool SPFA(int curNode)
{
state[curNode] = IN_STACK;
for(rg int e = head[curNode]; e; e = edge[e].next)
{
int to = edge[e].to;
if(dis[to] > dis[curNode] + edge[e].len)
{
dis[to] = dis[curNode] + edge[e].len;
if(state[to] == IN_STACK || SPFA(to) == true)
return true;
}
}
state[curNode] = NOT_IN_STACK;
return false;
}
再主程序中对每一个节点进行一次SPFA,直到找到负环为止。
对于随机图,我们可以先运行一个SPFA_INIT:首先选定一个初始点,每次扩展到一个节点时,随机地选择一条可松弛的边进行下一步扩展;如果不存在可松弛边,就结束这一过程。
对每个点进行一次SPFA_INIT,然后再用SPFA对每个点进行扩展,这样效率会有所提高。
更近一步的,我们还可以使用一个IDFS_SPFA,即加深迭代搜索。每次选取搜索的深度分别为\(1,2,4,8,16,\cdots\)是一个不错的选择。这些过程都比较简单,就是在原代码上进行简单的改装。代码就不再粘贴。
1.3 多源最短路径算法(MSSP)
Floyd算法
非常经典的算法。其实质是动态规划算法。
通过类比,我们发现松弛操作\(d(i,j)=\min\{d(i,j),d(i,k)+d(k,j)\}\)与区间DP十分类似。这意味着\(i\)和\(j\)两个状态变量不适合用来划分阶段。
我们设\(F(k,i,j)\)表示以\(1,2,\cdots,k\)点作为中转点,\(i,j\)之间的最短距离。我们可以在选前\(k-1\)个点的基础上,选择第\(k\)个点,并看其是否更优。有方程:
\]
注意这个方程满足两个性质:
- 转移\(F(k,i,j)=F(k-1,i,j)\)成立。
- 第\(k\)阶段刚计算出的状态量\((i,j)\)不会被再次引用。
由此,我们可以删去\(k\)维,得到状态转移方程:
\]
注意这里\(k\)是阶段,要放在最外层循环。最后可以算出任意两点的最短路。
有\(N\)个阶段,每个阶段\(N^2\)个状态,所以转移的时间复杂度为\(O(N^3)\)。
Floyd算法还可以判断点的连通性:只要把状态表示成\(F(k,i,j)\)表示成“以前\(k\)个点为中转点,能否让\(i,j\)连通”。转移方程同理进行对应修改即可。这个问题和“传递闭包”有关,“闭包”的概念可以在网络上或者离散数学教材上找到,并不是一个很高深的概念。
Dijkstra算法?
从理论上来讲,可以用\(dis_{i,j}\)表示以\(i\)为源点,\(j\)到源点的最短路,然后做\(N\)次Dijkstra算法。如果采用二叉堆优化,理论上可以用\(O(N^2\log N)\)的时间完成处理。但是至于为什么在网络上并没有有关介绍,是常数原因还是其他?我本人也不清楚。如果有哪位大佬知道原因或者做过有关测试,欢迎在下方留言。
图论小专题A的更多相关文章
- 图论小专题B
2 树 2.1 树的定义 一个只有\(N-1\)条边,且任意两个点连通的图叫做树.通过这样定义的树往往是一棵无根树,而我们通常会任意选定一个根节点使其变成有根树.有根树可以定义"父亲和儿子& ...
- 图论小专题C
3 负环及其应用 3.1 判定算法 判断负环只能用"边松弛"算法,也就是Bellman-Ford和SPFA算法.这两个算法都是\(O(NM)\)级别的.因为负环中一定存在一条负边, ...
- NOIp 图论算法专题总结 (1):最短路、最小生成树、最近公共祖先
系列索引: NOIp 图论算法专题总结 (1) NOIp 图论算法专题总结 (2) NOIp 图论算法专题总结 (3) 最短路 Floyd 基本思路:枚举所有点与点的中点,如果从中点走最短,更新两点间 ...
- 【总结】图论小总结【题解】P1330封锁阳关大学
[题解][总结]P1330 封锁阳光大学 &&图论小总结 这道题其实有一点点难度,不过我能经过思考做出来说明还是没有普及组\(D1T1\)难度的. 考虑一条边的两边要有且仅有一个点被选 ...
- NOIp 图论算法专题总结 (2)
系列索引: NOIp 图论算法专题总结 (1) NOIp 图论算法专题总结 (2) NOIp 图论算法专题总结 (3) 树链剖分 https://oi-wiki.org/graph/heavy-lig ...
- NOIp 图论算法专题总结 (3):网络流 & 二分图 简明讲义
系列索引: NOIp 图论算法专题总结 (1) NOIp 图论算法专题总结 (2) NOIp 图论算法专题总结 (3) 网络流 概念 1 容量网络(capacity network)是一个有向图,图的 ...
- 「日常训练」「小专题·图论」Domino Effect(1-5)
题意 分析 这题几乎就是一条dijkstra的问题.但是,如何考虑倒在中间? 要意识到这题求什么:单源最短路的最大值.那么有没有更大的?倒在中间有可能会使它更大. 但是要注意一个问题:不要把不存在的边 ...
- 「日常训练」「小专题·图论」 Cow Contest (1-3)
题意 分析 问题是要看出来这是个floyd闭包问题.我没看出来- - 分析之后补充. 代码 // Origin: // Theme: Graph Theory (Basic) // Date: 080 ...
- 「日常训练」「小专题·图论」 Frogger (1-1)
题意 分析 变形的dijkstra. 分析题意之后补充. 代码 // Origin: // Theme: Graph Theory (Basic) // Date: 080518 // Author: ...
随机推荐
- spark算子篇-repartition and coalesce
我们知道 RDD 是分区的,但有时候我们需要重新设置分区数量,增大还是减少需要结合实际场景,还有可以通过设置 RDD 分区数来指定生成的文件的数量 重新分区有两种方法:repartition and ...
- T100——取得系统参数值,如关帐日期
CALL cl_get_para(g_enterprise,g_site,'S-MFG-0031') RETURNING l_para_data 用此方法获取关帐日期
- phpexcel 生成大于26列数据
function excelExport2($fileName = '', $headArr = [], $data = [], $widths=[]) { ob_clean(); // $fileN ...
- SSD性能测试
Tested by CrystalDiskMark 7 * MB/s = 1,000,000 bytes/s [SATA/600 = 600,000,000 bytes/s]* KB = 1000 b ...
- word、ppt转换为pdf
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.T ...
- bash 中 () {} [] [[]] (()) 的解释
bash 中 () {} [] [[]] (()) 的解释 来源 https://www.cnblogs.com/fhefh/archive/2011/04/16/2017895.html bash ...
- 第二十六篇 jQuery 学习8 遍历-父亲兄弟子孙元素
jQuery 学习8 遍历-父亲兄弟子孙元素 jQuery遍历,可以理解为“移动”,使用“移动”还获取其他的元素. 什么意思呢?老师举一个例子: 班上30位同学,我是新来负责教这个班学生的老师 ...
- shell与其他语言不同点
1.定义变量时,变量名不加美元符号($,PHP语言中变量需要),如: your_name="w3cschool.cn" 注意,变量名和等号之间不能有空格,这可能和你熟悉的所有编程语 ...
- mysql8安装
1.先卸载当前系统中已安装的mariadb rpm -qa | grep mariadb rpm -e --nodeps 文件名 2.安装mysql依赖包 yum install gcc gcc-c+ ...
- Linux下MySql基本操作命令
(1).切换至MySql目录下[假设MySql安装路径为:/home/mysql/bin] cd /home/mysql/bin (2).连接MySql mysql -u用户名 -p,回车后输入密码 ...