前言
数据结构,一门数据处理的艺术,精巧的结构在一个又一个算法下发挥着他们无与伦比的高效和精密之美,在为信息技术打下坚实地基的同时,也令无数开发者和探索者为之着迷。
也因如此,它作为博主大二上学期最重要的必修课出现了。由于大家对于上学期C++系列博文的支持,我打算将这门课的笔记也写作系列博文,既用于整理、消化,也用于同各位交流、展示数据结构的美。
此系列文章,将会分成两条主线,一条“数据结构基础”,一条“数据结构拓展”。“数据结构基础”主要以记录课上内容为主,“拓展”则是以课上内容为基础的更加高深的数据结构或相关应用知识。
欢迎关注博主,一起交流、学习、进步,往期的文章将会放在文末。
随着学习的深入,相信各位读者的野心已经仅满足于计算出任意两点间的最短路径。那么更进一步的,在这一节我们要讨论关于最短路径的计数问题。在计算出任意两点间的最短路径长度的基础上,我们还需要统计出这些最短路径的方案数。
这个问题,乍一看感觉有些不同寻常,因为不管怎么说,方案数相较于长度来说总还是不好把握的。我们在思考统计方案数的算法的时候总是容易囿于对起点到终点路径规划决策的模拟。
朴素的解决思路多是先在纸上推演从一起点到终点的路径,一条一条的画出最短路径。小规模的时候,找出答案不是难事,但是一旦数据量较大,笔算就会漏洞百出,更何况一旦准备将其付诸于程序,就像是秀才遇到兵,发现根本无从下手。。。。
所以解决问题的关键就是要跳出朴素的思维方式,用一种新的视角来审视这个问题。不妨先从回顾计算最短路径长度的算法开始。
floyd算法求最短路径
任意两点间的最短路径问题,解决的方案是选择不停地枚举中间点来对路径进行“松弛”,让各点之间不断挑选更优的路径从而将距离边的更短。
如下图,对于点对14来说,可以经由中间点2,走1->2->4这条更短的路径。
当然,能用来更新最短路径的中间点也可以不是直接相连的顶点,例如下图:
点对15的最短路径可能由3更新成为5。不过在此之前,13和35的距离应该分别被2和4更新成为2.
可以看到,在这个问题中,有个关键的状态定义,点对 ( i , j ) (i,j) (i,j)之间的最短路径长度 d i j d_{ij} dij。
这个定义让我们只关心点与点之间的路径长度关系而非具体路径。
进一步的,根据这个状态定义,可以找到状态间转移方案:经过中间点k的点对ij的最短路径就是:
d i j = m i n ( d i k + d k j , d i j ) d_{ij}=min(d_{ik}+d_{kj},d_{ij}) dij=min(dik+dkj,dij)
那么在算法的初始,将最短路径长度初始化:
d i i = 0 d_{ii}=0 dii=0
d i j = i n f ( i j 之 间 没 有 边 相 连 ) d_{ij}=inf\ (ij之间没有边相连) dij=inf (ij之间没有边相连)
d i j = l e n i j ( i j 之 间 有 长 度 为 l e n i j 的 边 链 接 ) d_{ij}=len_{ij}\ (ij之间有长度为len_{ij}的边链接) dij=lenij (ij之间有长度为lenij的边链接)
于是这个最短路径长度的算法问题就可以解了,步骤如下:
- 依次枚举所有中间节点k,更新其余点对
- 枚举非中间节点的点对ij
- 使用中间节点k对ij之间的最短路径长度进行松弛
代码实现如下:
int dis[N][N];//最短路径数组,ij最短路径为dis[i][j]
void floyd(int n){
for(int k = 1;k <= n;k++){
//枚举中间节点
for(int i = 1;i <= n;i++){
//枚举非中间节点的点对
if(i == k)continue;
for(int j = i + 1;j <= n;j++){
if(j == k)continue;
if(dis[i][j] > dis[i][k] + dis[k][j]){
//松弛最短路径
dis[i][j] = dis[i][k] + dis[k][j];
dis[j][i] = dis[i][k] + dis[k][j];
}
}
}
}
}
两点间最短路径方案数
要统计最短路径的方案数,没有最短路径长度肯定是不行的,所以以下的算法要建立在floyd算法之上。也就是说,方案数问题要在floyd的过程中顺手解决
前面说过,要解决最短路径的方案数统计问题。我们要跳出朴素的思维方案,借鉴最短路径长度的建模分析方法。
不知各位是否注意到了上文在分析最短路径算法时标记出了三个模块,他们分别是:
状 态 、 转 移 、 初 始 化 状态、转移、初始化 状态、转移、初始化这是解决路径长度问题的关键所在,也就是常常提到floyd用到的动态规划思想。
现在我们可以从动态规划的角度借助这三个模块依葫芦画瓢来分析最短路径方案数的问题。(当然这里对状态和转移的描述未必在学术上严谨,主要是便于分析解决问题。)
定状态
依照最短路径的状态 d i j d_{ij} dij,我们定义:
点 对 ( i , j ) 之 间 的 最 短 路 径 方 案 数 = c i j 点对(i,j)之间的最短路径方案数=c_{ij} 点对(i,j)之间的最短路径方案数=cij(c取自count单词首字母)
有了这个定义,在任意两点间最短路方案数方面,用一个状态便可以忽略具体的路径方案,将方案数量一言以蔽之。
找转移
定义了状态之后,我们迫切的想知道在使用中间点k松弛点对ij时,最短路径的方案数状态会如何转移。
首先一点,路径的计算应该满足乘法原理,即:
点 对 ( i , j ) 经 过 点 k 的 最 短 路 径 方 案 数 = c i k × c j k 点对(i,j)经过点k的最短路径方案数=c_{ik}\times c_{jk} 点对(i,j)经过点k的最短路径方案数=cik×cjk
举个例子,如下图:
对于中间节点4,点对17之间的最短路径方案数为 2 × 2 = 4 2\times2=4 2×2=4(14方案数为2,47方案数为2)
乘法原理这里很好理解,就不再赘述了。下面的问题就是在进行floyd算法过程中,计算出来的方案数要怎么进行转移,这个问题要拆成三种情况来讨论:
- 当经过k不能使得i到j已知路径更短,即 d i j < d i k + d k j d_{ij} < d_{ik}+d_{kj} dij<dik+dkj,此次松弛无效,不改变方案数 c i j c_{ij} cij
- 当经过k的最短路径长度等于已知路径长度,即 d i j = d i k + d k j d_{ij} = d_{ik}+d_{kj} dij=dik+dkj,i到j的方案应该加上从k走的方案数,即 c i j = c i j + c i k × c k j c_{ij}=c_{ij}+c_{ik}\times c_{kj} cij=cij+cik×ckj
- 当经过k的最短路径长度小于已知路径长度,即 d i j > d i k + d k j d_{ij} > d_{ik}+d_{kj} dij>dik+dkj,i到j之前的方案应该作废而只算从k走的方案数,即 c i j = c i k × c k j c_{ij}=c_{ik}\times c_{kj} cij=cik×ckj
于是,状态转移的问题也就解决了
初始化
方案数的初始化方法就是给每个有边的点对之间只能通过这条边到达,方案数置1,否则不能到达,置0
实现
有了上面的铺垫,下面就可以大刀阔斧的开始计算最短路径的方案数了。
步骤如下:
- 初始化方案数矩阵
- 执行floyd算法,在松弛过程中同时转移方案数
-
- 当经过k不能使得i到j已知路径更短,即 d i j < d i k + d k j d_{ij} < d_{ik}+d_{kj} dij<dik+dkj,此次松弛无效,不改变方案数 c i j c_{ij} cij
-
- 当经过k的最短路径长度等于已知路径长度,即 d i j = d i k + d k j d_{ij} = d_{ik}+d_{kj} dij=dik+dkj,i到j的方案应该加上从k走的方案数,即 c i j = c i j + c i k × c k j c_{ij}=c_{ij}+c_{ik}\times c_{kj} cij=cij+cik×ckj
-
- 当经过k的最短路径长度小于已知路径长度,即 d i j > d i k + d k j d_{ij} > d_{ik}+d_{kj} dij>dik+dkj,i到j之前的方案应该作废而只算从k走的方案数,即 c i j = c i k × c k j c_{ij}=c_{ik}\times c_{kj} cij=cik×ckj
实现起来也不难,改动一下上文的代码:
int dis[N][N];//最短路径长度矩阵
int cnt[N][N];//最短路径计数矩阵
void floyd(int n){
for(int k = 1;k <= n;k++){
//枚举中间节点
for(int i = 1;i <= n;i++){
//枚举非中间节点点对
if(i == k)continue;
for(int j = i + 1;j <= n;j++){
if(j == k)continue;
if(dis[i][j] == dis[i][k] + dis[k][j]){
//当经过中间点最短距离相等,方案数相加
cnt[i][j] += cnt[i][k] * cnt[k][j];
cnt[j][i] += cnt[i][k] * cnt[k][j];
}else if(dis[i][j] > dis[i][k] + dis[k][j]){
//当经过中间点最短距离更小,之前方案数归零,计经过k的方案数
dis[i][j] = dis[i][k] + dis[k][j];
dis[j][i] = dis[i][k] + dis[k][j];
cnt[i][j] = cnt[i][k] * cnt[k][j];
cnt[j][i] = cnt[i][k] * cnt[k][j];
}
}
}
}
}
让我们来拟定一个需求,测试这个场景:
给定n个顶点和m条边,求出任意两点间的最短路径长度和方案数
#include<iostream>
using namespace std;
int dis[105][105];
int cnt[105][105];
const int inf = 1 << 29;
void floyd(int n){
for(int k = 1;k <= n;k++){
//枚举中间节点
for(int i = 1;i <= n;i++){
//枚举非中间节点点对
if(i == k)continue;
for(int j = i + 1;j <= n;j++){
if(j == k)continue;
if(dis[i][j] == dis[i][k] + dis[k][j]){
//当经过中间点最短距离相等,方案数相加
cnt[i][j] += cnt[i][k] * cnt[k][j];
cnt[j][i] += cnt[i][k] * cnt[k][j];
}else if(dis[i][j] > dis[i][k] + dis[k][j]){
//当经过中间点最短距离更小,之前方案数归零,计经过k的方案数
dis[i][j] = dis[i][k] + dis[k][j];
dis[j][i] = dis[i][k] + dis[k][j];
cnt[i][j] = cnt[i][k] * cnt[k][j];
cnt[j][i] = cnt[i][k] * cnt[k][j];
}
}
}
}
}
int main(){
int n,m;
scanf("%d%d",&n,&m);
for(int i = 1;i <= n;i++){
//初始化距离和计数矩阵
for(int j = 1;j <= n;j++){
dis[i][j] = inf;
cnt[i][j] = 0;
}
dis[i][i] = 0;
}
for(int i = 0,x,y,l;i < m;i++){
//读入m条边
scanf("%d%d%d",&x,&y,&l);
dis[x][y] = l;
dis[y][x] = l;
cnt[x][y] = 1;
cnt[y][x] = 1;
}
floyd(n);//floyd算法计算长度和计数
/*打印长度信息和计数信息*/
printf("最短路径长度矩阵:\n");
for(int i = 1;i <= n;i++){
for(int j = 1;j <= n;j++){
printf("%-3d",dis[i][j] == inf ? -1 : dis[i][j]);
}
printf("\n");
}
printf("最短路径计数矩阵:\n");
for(int i = 1;i <= n;i++){
for(int j = 1;j <= n;j++){
printf("%-3d",cnt[i][j]);
}
printf("\n");
}
}
进行一个测试样例:
经过指定顶点的最短路径数
好的,现在经过改进过的floyd算法,我们可以统计出任意两点间的最短路径数量了。那么接下来我们还想知道另一个问题:如何统计两点间经过指定中间顶点的最短路径方案数
其实有了前面的铺垫,这个问题就变得很简单,我们模仿上面的套路,不妨先给个状态定义:
点 对 ( i , j ) 之 间 经 过 中 间 顶 点 k 的 最 短 路 径 方 案 数 = c i j k 点对(i,j)之间经过中间顶点k的最短路径方案数=c_{ij}^k 点对(i,j)之间经过中间顶点k的最短路径方案数=cijk
那么状态的转移如下:
- 当ij之间存在经过k的最短路径,有 d i j = d i k + d k j d_{ij} =d_{ik}+d_{kj} dij=dik+dkj,方案数 c i j k = c i k × c k j c_{ij}^k=c_{ik}\times c_{kj} cijk=cik×ckj(还记的上文的乘法原理吗?)
- 当ij之间不存在经过k的最短路径,即 d i j < d i k + d k j d_{ij} <d_{ik}+d_{kj} dij<dik+dkj,方案数 c i j k = 0 c_{ij}^k=0 cijk=0
于是乎这个问题就解决了~
往期博客
- 【数据结构基础】数据结构基础概念
- 【数据结构基础】线性数据结构——线性表概念 及 数组的封装
- 【数据结构基础】线性数据结构——三种链表的总结及封装
- 【数据结构基础】线性数据结构——栈和队列的总结及封装(C和java)
- 【算法与数据结构基础】模式匹配问题与KMP算法
- 【数据结构与算法基础】二叉树与其遍历序列的互化 附代码实现(C和java)
- 【数据结构与算法拓展】 单调队列原理及代码实现
- 【数据结构基础】图的存储结构
- 【数据结构与算法基础】并查集原理、封装实现及例题解析(C和java)
- 【数据结构与算法拓展】二叉堆原理、实现与例题(C和java)
- 【数据结构与算法基础】哈夫曼树与哈夫曼编码(C++)
- 【数据结构与算法基础】最短路径问题
- 【数据结构与算法基础】堆排序原理及实现
- 【数据结构与算法基础】最小生成树算法原理及实现
- 【数据结构基础】图的遍历方法与应用
- 【数据结构基础】矩阵的存储结构,数组,三元组表及十字链表
- 【数据结构与算法基础】拓扑排序与AOV网络
- 【数据结构与算法基础】AOE网络与关键路径
- 【数据结构与算法基础】树与二叉树的互化
参考资料:
- 《数据结构》(刘大有,杨博等编著)
- 《算法导论》(托马斯·科尔曼等编著)
- 《图解数据结构——使用Java》(胡昭民著)
- OI WiKi
来源:oschina
链接:https://my.oschina.net/u/4301815/blog/4812619