《算法笔记》读书记录DAY_46

CHAPTER_10  提高篇(4)——图算法专题

10.4.2Dijkstra算法

(接上篇)

上面的题目实际是一个多标尺的最短路径问题。何为多标尺?即题目给出了多个衡量标准,为了输出最终结果,我们不但要考虑道路的距离(第一标尺),还需要考虑每个点的点权(第二标尺)和最短路径的数量(第三标尺)。这种情况我们必须对Dijkstra算法做一些改动才能将其他标尺考虑进来,正如上题所做的处理,我们设置数组weight[]和num[]来记录其他标尺的量,并在更新d[]的值的时候用判断条件来更新这些值。

这种方式成功地让我们解决了上个例题,但是对于一些逻辑更复杂的多标尺最短路径问题,只使用Dijkstra算法就不一定能正确结果(原因是不一定满足最右子结构),或者即便能算出,但因逻辑的复杂导致很容易写错。

这里介绍一种更通用、有模板化的解决此类问题的方式——Dijkstra+DFS。这个方法的思路很简单:先在Dijkstra算法中记录下所有最短路径(只考虑距离),然后从这些最短路径中选出一条第二标尺的最优路径。

(1)使用Dijkstra算法记录所有的最短路径

前面已经介绍了记录一条最短路径的方法——设置pre[]数组。但是考虑到有多条最短路径的情况,每个结点可能存在多个前驱结点,我们要将数组设置成vector类型 “vector<int> pre[maxn]"。这样对每个结点v来说,pre[v]就是一个变长数组vector,里面存放v的所有最短路径的前驱结点。

在本处的Dijkstra算法,只需要考虑距离这一个因素而专心于pre数组的求解。在一开始pre数组不需要赋初值。在更新d[v]的过程中,如果d[u]+Adj[u][j].distance<d[v],说明以u为中介点可以使d[v]更优,此时需要令v的前驱结点为u。并且即便原先pre[v]中存放了若干结点,此时也要全部清空,然后再添加结点u。之后,如果d[u]+Adj[u][j].distance==d[v],说明以u为中介可以找到一条相同距离的路径,因此v的前驱结点需要再原先的基础上添加u。

整个过程的完整代码如下:

vector<int> pre[maxn];         //记录每个顶点的最短路径的前驱 
int d[maxn];                   //记录最短路径 
int n;                         //顶点数 
bool vis[maxn]={0};            //实现SET

struct node {
	int v;
	int pathW;
}; 

vector<node> Adj[maxn];                               //邻接表 

void Dijkstra() {
	fill(d,d+n,INF); 
	d[s]=0;                                           //起点的d[s]为0
	for(int i=0;i<n;i++) {
		int u=-1, MIN=INF;
		for(int j=0;j<n;j++) {                        //找到未访问的最短路径的点 
			if(vis[j]==0&&d[j]<MIN) {
				u=j;
				MIN=d[j];
			}
		}
		if(u==-1)
			return;
		vis[u]=1;
		for(int j=0;j<Adj[u].size();j++) {
			int v=Adj[u][j].v;
			if(vis[v]==0&&d[u]+Adj[u][j].pathW<d[v]) { //以u为中介到v更短 
				d[v]=d[u]+Adj[u][j].pathW;             //优化d[v]    
				pre[v].clear();
				pre[v].push_back(u);                   //令v的前驱为u 
			}
			else if(d[u]+Adj[u][j].pathW==d[v]) {
				pre[v].push_back(u);                    //令v的前驱为u 
			}
		}
	} 
}

(2)遍历所有最短路径,找出一条使第二标尺最优的路径

在用Dijkstra算法得到pre[]数组之后,我们用递归来获得最短路径。前面已经提到,Dijkstra算法最终会生成一棵最短路径树,而这棵最短路径树的信息其实就隐含在pre数组里。

我们只要对某一点v的pre[v]不断递归获取其最短路径上的前驱结点,遍历的过程就会形成一棵递归树,这棵树的根节点就是顶点v,叶子节点都是起点S。当遍历这棵树时,每次到达叶子节点就会产生一条完整的从S到V的最短路径。因此,每得到一条完整的最短路径,就可以对这条路径计算其第二标尺的值(例如累加边权或是点权),令其与当前第二标尺的最优值进行比较。如果比当前最优值更优,则更新最优值,并用这条路径覆盖当前最优路径。这样,当所有路径都遍历完以后,就可以得到最优第二标尺和最优路径。

通过上面的分析,我们如下编写DFS函数模板:

int optValue;                //第二标尺最优值
vector<int> pre[maxn];       //通过Dijkstra生成的pre[]
vector<int> path,tempath;    //最优路径、临时路径 

void DFS(int v) {                    //v为当前访问点 
	//递归边界
	if(v==s) {                       //如果到达了起点S,即叶子结点 
		tempath.push_back(v);        //将起点S加入临时路径最后面
		int value;                   //存放这条最短路径的第二标尺值
		计算路径tempath上的value
		if(value优于optValue) {
			optValue=value;
			path=tempath;            //更新第二标尺最优值和最优路径 
		}
		tempath.pop_back();          //将刚加入的结点删除
		return;
	}
	//递归式
	tempath.push_back(v);            //将当前结点加入临时路径最后面
	for(int i=0;i<pre[v].size();i++) {
		DFS(pre[v][i]);              //向下递归
	}
	tempath.pop_back();               //遍历完所有前驱结点,将当前结点删除 
}

读者会发现,上面DF代码中只有少许部分是需要根据实际情况填充的,即计算tempath上的value和value优于optValue两条语句。这些地方一般会涉及点权和边权的计算。还有一点需要注意,由于递归的原因,tempath和path中的路径结点是逆序的,因此访问结点需要倒着进行。

猜你喜欢

转载自blog.csdn.net/jgsecurity/article/details/121093316