大意失荆州。今天考试一到可以用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”:

\[ d(i,j) =\min\{d(i,j), d(i,k)+d(k,j)\}
\]

它是所有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)=\min\{F(k - 1,i,j), F(k - 1,i,k)+F(k - 1,k,j)\}
\]

注意这个方程满足两个性质:

  • 转移\(F(k,i,j)=F(k-1,i,j)\)成立。
  • 第\(k\)阶段刚计算出的状态量\((i,j)\)不会被再次引用。

    由此,我们可以删去\(k\)维,得到状态转移方程:

\[ F(i,j)=\min\{F(i,j), F(i,k)+F(k,j)\}
\]

注意这里\(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)\)的时间完成处理。但是至于为什么在网络上并没有有关介绍,是常数原因还是其他?我本人也不清楚。如果有哪位大佬知道原因或者做过有关测试,欢迎在下方留言。

最新文章

  1. k近邻(KNN)复习总结
  2. java 连接数据库之一个完整的函数
  3. MySQL语句中的转义字符----引号
  4. 047医疗项目-模块四:采购单模块—采购单审核提交(Dao,Service,Action三层)
  5. bzoj 3389
  6. Python基础(4)--字符串
  7. Uva----------(11078)Open Credit System
  8. UVa 294 (因数的个数) Divisors
  9. 【转】1.5 起步 - 初次运行 Git 前的配置
  10. hdoj 1071 The area
  11. Android开发框架
  12. passwnger
  13. 固定IP和绑定了MAC,可以在设置无线路由器供笔记本电脑和平板上网吗?
  14. SQL中两种表复制语句
  15. 【Android】创建Popwindow弹出菜单的两种方式
  16. Windows系统下远程Linux系统
  17. 具体的了解“&amp;gt;/dev/null 2&amp;gt;&amp;amp;1”
  18. kubernetes之flannel
  19. Oracle Metric sequence load elapsed time
  20. ElasticSearch查询 第二篇:文档更新

热门文章

  1. 基于keepalived搭建mysql双主高可用
  2. web框架链接
  3. EasyUI_前台js_分页
  4. Docker 部署mysql、tomcat笔记
  5. form表单相关
  6. OO方式实现ALV: cl_salv_table
  7. 修改Vue中的 v-html 内的元素无效问题
  8. Spring面试题整理
  9. PCB检查步骤
  10. 复习rem