首页 > 编程语言 >最短路算法及其常见扩展应用

最短路算法及其常见扩展应用

时间:2022-11-30 22:45:49浏览次数:42  
标签:head dist int 短路 扩展 cnt tot 算法

第一节——最短路问题

基本概念:由于无向边可以看作两条相反的有向边,于是我们默然按照有向边的形式讨论
存图方式:

  1. 邻接矩阵:空间复杂度\(O(n^2)\),优点:\(O(1)\)查找\(x->y\)的边是否存在,方便
scanf("%d%d%d",&u,&v,&w);
a[u][v]=w;//邻接矩阵
  1. 邻接表:空间复杂度\(O(m)\)
void add(int u,int v,int w){//u->v的边权为w的边
	nxt[++tot]=head[u],ver[tot]=v,cost[tot]=w,head[u]=tot;
}
//遍历时:
for(int i=head[u];i;i=nxt[i])……
  1. vector,这个和邻接表相比无任何突出特点,故略去
    邻接表是最常用的

单源最短路问题(SSSP)

为了方便叙述,下面没特殊说明默认\(dist\)数组为存储距离的数组,\(cost_{u,v}\)表示边\(u->v\)的长度,默认使用邻接表存图,默认\(u\)为父节点,\(v\)为子节点

Dijkstra算法

应用于非负权图上,基于贪心思想
它的过程是这样的

  1. 寻找到所有未被标记的的节点中\(dist\)值最小的,将其取出并标记,记为\(u\)
  2. 遍历从\(u\)出发的所有边,将这些边所连后继节点\(v\)的\(dist\)值全部用\(dist_u+cost_{u,v}\)更新,
  3. 重复上述步骤直到全部节点被标记
    性质:由于是非负权图,我们取出的\(u\)节点的\(dist\)一定是无法被更新的节点,此时的\(dist\)值就是起点到\(u\)的最短距离
    上述算法的复杂度为\(O(n^2)\)
    我们发现,算法的瓶颈在于第一步,对于这种情况,我们可以采取二叉堆优化
    详细地说,我们采用一个小根堆,以\(dist\)值为第一关键字进行优化,那么我们的步骤就可以变为
    1.取出堆顶节点\(u\),若被标记,则继续取出堆顶直到堆顶无标记为止(懒惰删除法)
    2.更新\(u\)的所有后继节点 ,如果被成功更新,则将\(v\)入队

3.直到堆为空时,算法结束
实现细节
1.如果懒得重载运算符,建议把\(dist\)值跟随节点入队时取反,拿出来时再取反,这就可以吧默认大根堆改为小根堆
2.时刻记得memset(dist,0x3f,sizeof dist);

代码如下:

// 堆优化Dijkstra算法,O(mlogn)
const int N = 100010, M = 1000010;
int head[N], ver[M], edge[M], Next[M], d[N];
bool v[N];
int n, m, tot;
// 大根堆(优先队列),pair的第二维为节点编号
// pair的第一维为dist的相反数(利用相反数变成小根堆,参见0x71节)
priority_queue< pair<int, int> > q;

void add(int x, int y, int z) {
	ver[++tot] = y, edge[tot] = z, Next[tot] = head[x], head[x] = tot;
}

void dijkstra() {
	memset(d, 0x3f, sizeof(d)); // dist数组
	memset(v, 0, sizeof(v)); // 节点标记
	d[1] = 0;
	q.push(make_pair(0, 1));
	while (q.size()) {
		// 取出堆顶
		int x = q.top().second; q.pop();
		if (v[x]) continue;
		v[x] = 1;
		// 扫描所有出边
		for (int i = head[x]; i; i = Next[i]) {
			int y = ver[i], z = edge[i];
			if (d[y] > d[x] + z) {
				// 更新,把新的二元组插入堆
				d[y] = d[x] + z;
				q.push(make_pair(-d[y], y));
			}
		}
	}
}

int main() {
	cin >> n >> m;
	// 构建邻接表
	for (int i = 1; i <= m; i++) {
		int x, y, z;
		scanf("%d%d%d", &x, &y, &z);
		add(x, y, z);
	}
	// 求单源最短路径
	dijkstra();
	for (int i = 1; i <= n; i++)
		printf("%d\n", d[i]);
}

它的复杂度是\(O((m+n)\log n)\)

Bellman-ford与SPFA

在国际上,\(SPFA\)其实被称为队列优化的\(Bellman-ford\)算法,这两种算法也是求解单源最短路径问题的算法,与\(dijkstra\)算法不同的是,这两种算法在负权图上一样可以正常工作,由于\(SPFA\)是\(Bellman-ford\)算法的优化形式,下面就只讲述\(SPFA\)算法
首先,在一张有向图上,若是对于任意边\((u,v,w)\)都有:\(dist[u]+w\ge dist[v]\),(此不等式又叫三角形不等式),那么我们所求的\(dist\)数组就是最短路径,而\(SPFA\)算法便是通过若干次更新迭代,使所有边都满足三角形不等式,下面我们介绍它的详细流程

  1. 建立一个队列,最初队里只有起点\(s\),\(dist[s]=0\)其余均为正无穷
  2. 取出队头\(u\),将所有队头出发的后继节点\(v\)进行更新,若更新成功,则将\(v\)入队
  3. 重复第二步直到队列为空
    代码模板
// SPFA算法
const int N = 100010, M = 1000010;
int head[N], ver[M], edge[M], Next[M], d[N];
int n, m, tot;
queue<int> q;
bool v[N];

void add(int x, int y, int z) {
	ver[++tot] = y, edge[tot] = z, Next[tot] = head[x], head[x] = tot;
}

void spfa() {
	memset(d, 0x3f, sizeof(d)); // dist数组
	memset(v, 0, sizeof(v)); // 是否在队列中
	d[1] = 0; v[1] = 1;
	q.push(1);
	while (q.size()) {
		// 取出队头
		int x = q.front(); q.pop();
		v[x] = 0;
		// 扫描所有出边
		for (int i = head[x]; i; i = Next[i]) {
			int y = ver[i], z = edge[i];
			if (d[y] > d[x] + z) {
				// 更新,把新的二元组插入堆
				d[y] = d[x] + z;
				if (!v[y]) q.push(y), v[y] = 1;
			}
		}
	}
}

int main() {
	cin >> n >> m;
	// 构建邻接表
	for (int i = 1; i <= m; i++) {
		int x, y, z;
		scanf("%d%d%d", &x, &y, &z);
		add(x, y, z);
	}
	// 求单源最短路径
	spfa();
	for (int i = 1; i <= n; i++)
		printf("%d\n", d[i]);
}

在随机图的情况下,SPFA算法的时间复杂度是\(O(kn)\),其中k是一个较小的常数,但可以构造数据卡,使其退化到\(O(nm)\)级别,并且之前我在知乎上看到一篇文章,几乎所有的SPFA算法优化都在上面被卡了
同样,正是因为SPFA使得三角形不等式绝对收敛,以至于其可以在负权图上工作,当位于正权图的时候,一样可以采取堆优化,最后得到的算法与dijkstra算法一模一样,一个是基于贪心的非负权算法,一个是基于三角形不等式收敛的通用算法,在非负权图上却是殊途同归

特殊情形下的线性算法

  1. 在\(DAG\)中,我们可以按照拓扑序或者记忆化搜索\(O(n)\)递推出最短路
  2. 在边权只为0/1的情况下,我们可以不使用优先队列,直接将dijkstra算法里的优先队列替换为双端队列,对于一个新加入的节点,与其父节点连边的边权是1就插入队尾,是0就插入队头
  3. 形式上来说,单源最短路径问题很类似与优先队列BFS,本质上来说是一致的,所以类似于\(A^*\)算法的估价函数一样可以使用

全源最短路径算法:Floyd

简单来说就是在一张图上要求出任意两点间的最短路径,我们使用Floyd算法,时间复杂度为\(O(n^3)\),一般使用邻接矩阵存图
使用动态规划的思想,设\(D[k,i,j]\)表示i到j的路径只经过节点编号小于k的路径的最短长度,则有状态转移方程

\[D[k,i,j]=\min \lbrace D[k-1,i,j],D[k-1,i,k]+D[k-1,k,j]\rbrace$$,类似于01背包那样,k这一维度可以省略 值得一提的是,在动态规划算法中,对k的枚举才是阶段,i,j只是附加状态而已。于是循环顺序应该是$k->i->j$ ``` int d[310][310]; int n, m; int main() { cin >> n >> m; // 把d数组初始化为邻接矩阵 memset(d, 0x3f, sizeof(d)); for (int i = 1; i <= n; i++) d[i][i] = 0; for (int i = 1; i <= m; i++) { int x, y, z; scanf("%d%d%d", &x, &y, &z); d[x][y] = min(d[x][y], z); } // floyd求任意两点间最短路径 for (int k = 1; k <= n; k++) for (int i = 1; i <= n; i++) for (int j = 1; j <= n; j++) d[i][j] = min(d[i][j], d[i][k] + d[k][j]); // 输出 for (int i = 1; i <= n; i++) { for (int j = 1; j <= n; j++) printf("%d ", d[i][j]); puts(""); } } ``` ## Floyd与传递闭包 传递闭包问题是这样的:给定$n$个元素和$m$个二元关系,关系具有传递性,求推出最多的关系。 建立邻接矩阵$d$,如果$i,j\text{有关系,那么令}d[i,j]=1$,特别的,令$d[i,i]=1$,其余均为0,对d执行$Floyd$即可求出传递闭包关系,它的原理是Floyd本身的递推式就是在做关系的传递(仅属个人见解) ``` // Floyd传递闭包 #include <iostream> #include <cstdio> #include <cstring> #include <algorithm> using namespace std; bool d[310][310]; int n, m; int main() { cin >> n >> m; for (int i = 1; i <= n; i++) d[i][i] = 1; for (int i = 1; i <= m; i++) { int x, y; scanf("%d%d", &x, &y); d[x][y] = d[y][x] = 1; } for (int k = 1; k <= n; k++) for (int i = 1; i <= n; i++) for (int j = 1; j <= n; j++) d[i][j] |= d[i][k] & d[k][j]; } ``` ## 最短路算法的灵活运用 ### 题目1:[通信线路](https://www.acwing.com/problem/content/342/) 题意:在无向图上求出一条1到n的路径,使得第K+1大的边权尽量小,共有N个点P条边 介绍两种解法,两种思想 1.**分层图最短路** 具体的,我们利用动态规划的思想,设$d[i,j]$表示1到i的路径中第j+1大的边权的最小值,于是对于一条边$(u,v,w)$我们可以用$\max(w,d[u,k])$去更新$d[v,k]$,用$d[u,k]$去更新$d[v,k+1]$。在我的另一篇文章中曾经提到过DP实际上也是对状态空间进行的一张有向无环图的遍历,显然这个方程有后效性,但是我们可以使用SPFA去消去后效性,因为三角形不等式会令其不断收敛,但又因为全是正权图的原因就可以使用堆优化。 如果从图论的角度来看,无疑我们可以把点的状态抽象为二维的,用二元组$(u,k)$代表一个节点,那么就等同于节点$(u,k)$与节点$(v,k)$有着一条边权为$w$的边,与节点$(v,k+1)$有着一条边权为0的边,这是一个具有N * K个点,P * K条边的**广义最短路问题,又被称为分层图最短路** ``` #define x second.first #define y second.second #define mp make_pair int head[1050],ver[105000],cost[105000],nxt[105000],tot,n,m,k,dist[1050][1050],vis[1050][1050]; priority_queue<pair<int,pair<int,int> > >q; void add(int u,int v,int w) { ver[++tot]=v,cost[tot]=w,nxt[tot]=head[u],head[u]=tot; } void dijkstra(){ memset(dist,0x3f,sizeof(dist)); dist[1][0]=0; q.push(mp(0,mp(1,0))); while (!q.empty()) { int u=q.top().x,dep=q.top().y; q.pop(); if (vis[u][dep])continue; vis[u][dep]=1; for (int i=head[u];i;i=nxt[i]) { int v=ver[i],w=cost[i]; if (dist[v][dep]>max(dist[u][dep],w)) { dist[v][dep]=max(dist[u][dep],w); q.push(mp(-dist[v][dep],mp(v,dep))); } if (dep < k && dist[v][dep+1]>dist[u][dep]) { dist[v][dep+1]=dist[u][dep]; q.push(mp(-dist[v][dep+1],mp(v,dep+1))); } } } } int main() { scanf("%d%d%d",&n,&m,&k); for (int i=1;i<=m;i++) { int u,v,w; scanf("%d%d%d",&u,&v,&w); add(u,v,w); add(v,u,w); } dijkstra(); printf("%d\n",dist[n][k]==0x3f3f3f3f?-1:(k!=n?dist[n][k]:0));//最后一组数据:N=2K=2????不是K<N吗 } ``` 2.**二分转化判定** 很明显的单调性,我们可以二分第K+1大的边权的最小值,设为$mid$ 然后我们把所有边权大于$mid$的边权看作1,小于等于的看作0,使用双端队列bfs在$O(n)$时间内求出最短路,当最短路超过k的时候上界收敛,否则下界收敛 ``` // 解法二:二分答案+双端队列BFS const int MAX_N = 1005, MAX_M = 20005; int head[MAX_N], ver[MAX_M], edge[MAX_M], nxt[MAX_M], tot; int n, m, k, d[MAX_N]; deque<int> q; // 插入一条从x到y长度z的有向边 void add(int x, int y, int z) { tot++;ver[tot] = y; edge[tot] = z;nxt[tot] = head[x],head[x]=tot; // 在head[x]这条链的开头插入新点 } bool solve(int t) { memset(d, 0x7f, sizeof(d)); d[1] = 0; q.push_back(1); while (!q.empty()) { int x = q.front(); q.pop_front(); for (int i = head[x]; i; i = nxt[i]) { int y = ver[i], z = edge[i] > t ? 1 : 0; if (d[y] > d[x] + z) { d[y] = d[x] + z; if (z == 0) q.push_front(y); else q.push_back(y); } } } return d[n] <= k; } int main() { cin >> n >> m >> k; for (int i = 1; i <= m; i++) { int x, y, z; scanf("%d%d%d", &x, &y, &z); add(x, y, z); add(y, x, z); } int L = 0, R = 1000001; while (L < R) { int mid = (L + R) >> 1; if (solve(mid)) R = mid; else L = mid + 1; } if (L == 1000001) puts("-1"); else cout << L << endl; } ``` 这道题告诉我们的是分层图的最短路,**借用动态规划思想解决最短路径问题**,还有第x大的最小值是二分,可以抽象为二维节点,也可以把每一个节点的边都建立出来 ### 题目2:[道路与航线](https://www.acwing.com/problem/content/344/) 题目描述: 分析:本题是一个带有负权的单源最短路径问题,但是SPFA被卡了,于是我们需要运用图上的性质:道路都是双向的,航线都是单向的,且如果一条航线从A->B,那么不可能存在一条道路使得B->A,这句话的意思就是说任何一条航线都是这张图的一个割边,并且只有这个割边的边权为负数,对于这样的图,我们可以**拆图**,将这张图拆为一个个连通块和割边,详细的说,我们最开始无视所有的航线,只进行对道路的计算,无疑,这些道路全是正权,我们可以使用dijkstra算法求出一个个连通块内的最短路,又由于割边的存在,我们把每一个连通块看成一个点,那么这张图就成了一个$DAG$,我们就可以使用线性的动态规划求出答案,这个顺序需要我们预处理出拓扑序 流程如下: 1. 只加入道路,也就是双向边 2. 使用深度优先搜索划分连通块 3. 统计每个连通块的入度,记作$in$ 4. 建立一个用于拓扑排序的队列,其中只存储入度为0的连通块编号,当然也包含起点所在的连通块编号 5. 取出队头连通块执行$dijkstra$ (1).将连通块所有点加入优先队列,取出$d$最小的节点$u$进行更新 (2).若队头被扩展过,重复执行(1) (3).扫描$u$的所有出边$(u,v,w)$,用$d[u]+w<d[v]更新d[v]$ (4).如果u,v再同一连通块内并且d[v]被更新,则将v加入堆 (5).如果不在同一连通块内,将$v$所在连通块的入度减少一,直到减少到零,此时就将v所在联通快加入拓扑排序的队列中 (6).重复执行1至5直到优先队列为空 6. 重复执行5直到队列为空 代码如下 ``` #include<iostream> #include<cstdio> #include<queue> #include<cstring> #include<vector> #define int long long using namespace std; queue<int>q; priority_queue<pair<int,int> >p; int t,r,n,m,P,s,cnt,head[300005],nxt[300005],ver[300005],cost[300005],tot,vis[300005],c[300005],deg[300005],kind[300005],dist[300005]; vector<int>a[300005]; void add(int u,int v,int w,int k){ nxt[++tot]=head[u],ver[tot]=v,cost[tot]=w,head[u]=tot,kind[tot]=k;//1是单向边,0是有向边 } void dfs(int u,int id){ c[u]=id; a[id].push_back(u); for(int i=head[u];i;i=nxt[i]){ if(kind[i])continue; if(!c[ver[i]])dfs(ver[i],id); } } void dijkstra(int id){//求编号为id的联通块内最短路 int len=a[id].size(); for(int i=0;i<len;i++){ p.push(make_pair(-dist[a[id][i]],a[id][i])); } while(p.size()){ int u=p.top().second,w=dist[u];p.pop(); if(vis[u])continue; vis[u]=1; for(int i=head[u];i;i=nxt[i]){ int v=ver[i],z=cost[i]; if(c[v]==c[u]&&dist[v]>w+z){ dist[v]=w+z; p.push(make_pair(-dist[v],v)); } if(c[v]!=c[u]){ dist[v]=min(w+z,dist[v]); deg[c[v]]--; if(!deg[c[v]])q.push(c[v]); } } } } signed main(){ int maxx=0; scanf("%lld%lld%lld%lld",&n,&m,&P,&s); for(int i=1;i<=m;i++){ int u,v,w; scanf("%lld%lld%lld",&u,&v,&w); add(u,v,w,0); add(v,u,w,0); } for(int i=1;i<=P;i++){ int u,v,w; scanf("%lld%lld%lld",&u,&v,&w); add(u,v,w,1); } for(int i=1;i<=n;i++){ if(!c[i])dfs(i,++cnt); } for(int u=1;u<=n;u++){ for(int i=head[u];i;i=nxt[i]){ if(kind[i]){ deg[c[ver[i]]]++; } } } for(int i=1;i<=cnt;i++){ if(!deg[i])q.push(i); } for(int i=1;i<=n;i++)dist[i]=0x3f3f3f3f3f3f3f3f; dist[s]=0; while(q.size()){ int k=q.front(); q.pop(); dijkstra(k); } for(int i=1;i<=n;i++){ if(dist[i]<=n*10000)printf("%lld\n",dist[i]); else puts("NO PATH"); } return 0; } ``` 这道题告诉我们,合理运用题目性质,对于图的明显性质,我们可以将一张图分为几个块,使用不同的算法进行处理,本题就是将连通块内使用dijkstra,然后缩点成DAG使用线性递推直接求解 ### 题目三[Sightseeing](https://www.acwing.com/problem/content/385/) 题意简述:给定一张有向图,求起点S到终点F的最短路径条数和次短路径条数 对于这个问题的维护,我们只需要把dijkstra算法稍稍改造一下就可以了 我们开一个数组$cnt$,表示路径条数,同时$dist,cnt$都是二维,第二维只能是0/1表示最短路/次短路 下面我们简单谈谈它的维护 首先在优先队列里我们记录当前状态是由次短路还是最短路更新,记为k,那么在我们执行正常dijkstra算法的时候就会出现四种情况 1.$dist[v][0]>dist[u][k]+w$,此时令$dist[v][1]=dist[v][0],dist[v][0]=dist[u][k],cnt[v][1]=cnt[v][0],cnt[v][0]=cnt[u][k]$,并将两个都插入队列 2.$dist[v][1]>dist[u][k]+w>dist[v][0]$,此时令$dist[v][1]=dist[u][k]+w$, 3.$dist[v][0]=dist[u][k]+w$ 4.$dist[v][1]=dist[u][k]+w$ 只有在这四种情况下才会更新 ``` const int N=1010,M=100010; int T,t,n,m,s,head[N],ver[M],cost[M],nxt[M],tot,d[N][2],cnt[N][2],vis[N][2]; struct node{ int id,k,d; bool operator<(const node& node)const { return d>node.d; } }; priority_queue<node>q; void add(int u,int v,int w){ ver[++tot]=v,cost[tot]=w,nxt[tot]=head[u],head[u]=tot; } int dijkstra(){ memset(vis,0,sizeof vis); memset(d,0x3f,sizeof d); d[s][0]=0,cnt[s][0]=1; q.push({s,0,0}); while(q.size()){ int u=q.top().id,k=q.top().k,z=d[u][k];q.pop(); if(vis[u][k])continue; vis[u][k]=true; for(int i=head[u];i;i=nxt[i]){ int v=ver[i],w=cost[i]; if(d[v][0]>z+w){ d[v][1]=d[v][0],cnt[v][1]=cnt[v][0]; q.push({v,1,d[v][1]}); d[v][0]=z+w,cnt[v][0]=cnt[u][k]; q.push({v,0,d[v][0]}); } else if(d[v][0]==z+w)cnt[v][0]+=cnt[u][k]; else if(d[v][1]>z+w){ d[v][1]=z+w,cnt[v][1]=cnt[u][k]; q.push({v,1,d[v][1]}); } else if(d[v][1]==z+w)cnt[v][1]+=cnt[u][k]; } } //printf("%d %d %d %d\n",d[t][0],cnt[t][0],d[t][1],cnt[t][1]); return cnt[t][0]+(d[t][1]==d[t][0]+1)*cnt[t][1]; } int main(){ scanf("%d",&T); while(T--){ tot=0; memset(head,0,sizeof head); scanf("%d%d",&n,&m); for(int i=1;i<=m;i++){ int u,v,w; scanf("%d%d%d",&u,&v,&w); add(u,v,w); } scanf("%d%d",&s,&t); printf("%d\n",dijkstra()); } return 0; } ``` ### 例题4:[排序](https://www.luogu.com.cn/problem/P1347) #### 题目描述 一个不同的值的升序排序数列指的是一个从左到右元素依次增大的序列,例如,一个有序的数列 $A,B,C,D$ 表示$A<B,B<C,C<D$。在这道题中,我们将给你一系列形如 $A<B$ 的关系,并要求你判断是否能够根据这些关系确定这个数列的顺序。 #### 输入格式 第一行有两个正整数 $n,m$,$n$ 表示需要排序的元素数量,$2\leq n\leq 26$,第 $1$ 到 $n$ 个元素将用大写的 $A,B,C,D\dots$ 表示。$m$ 表示将给出的形如 $A<B$ 的关系的数量。 接下来有 $m$ 行,每行有 $3$ 个字符,分别为一个大写字母,一个 `<` 符号,一个大写字母,表示两个元素之间的关系。 #### 输出格式 若根据前 $x$ 个关系即可确定这 $n$ 个元素的顺序 `yyy..y`(如 `ABC`),输出 `Sorted sequence determined after xxx relations: yyy...y`. 若根据前 $x$ 个关系即发现存在矛盾(如 $A<B,B<C,C<A$),输出 `Inconsistency found after x relations.` 若根据这 $m$ 个关系无法确定这 $n$ 个元素的顺序,输出 `Sorted sequence cannot be determined.` (提示:确定 $n$ 个元素的顺序后即可结束程序,可以不用考虑确定顺序之后出现矛盾的情况) #### 提示 $2 \leq n \leq 26,1 \leq m \leq 600$。 很明显的,这是一道传递闭包问题,对于$i<j$的式子,令$f[i][j]=1$,其余的全部置为0,使用Floyd即可求出传递闭包,然后若$\exists i,j\in[1,n],使得f[i][j]=f[j][i]$则矛盾,确定所有的顺序即$\forall i,j\in[1,n]有f[i][j]+f[j][i]=1$ 套传递闭包即可。至于确定顺序之后输出,我们只需要将有多少个数小于当前数统计出来,这就是实质上的排名 注意这里不可以使用二分法,是因为二分很可能出现前面可以确定顺序后面发生矛盾却是二分到了矛盾的地方 ``` int e[55][55],d[55][55],n,m; pair<int,char>ans[105]; int floyd(){ memcpy(e, d, sizeof(e)); for(int k=1;k<=n;k++) for(int i=1;i<=n;i++) for(int j=1;j<=n;j++){ e[i][j]|=e[i][k]&e[k][j]; if(e[i][j]&&e[j][i])return -1; } for(int i=1;i<=n;i++) for(int j=1;j<=n;j++){ if(e[i][j]==e[j][i]&&!e[i][j]&&i!=j)return 0; } return 1; } int main(){ while(~scanf("%d%d",&n,&m)&&n&&m){ int op=1; memset(d,0,sizeof d); for(int i=1;i<=m;i++){ char a[3]; scanf("%s",a); d[(int)a[0]-64][(int)a[2]-64]=1; if(op==1){ int x=floyd(); if(x==1){ printf("Sorted sequence determined after %d relations: ",i); for(int i=1;i<=n;i++){ ans[i].first=0; for(int j=1;j<=n;j++){ if(!e[i][j])ans[i].first++; } ans[i].second=(char)(i+64); } sort(ans+1,ans+n+1); for(int i=1;i<=n;i++){ printf("%c",ans[i].second); } puts("."); op=0; } else if(x==-1){ printf("Inconsistency found after %d relations.\n",i); op=0; } } } if(op){ puts("Sorted sequence cannot be determined."); } } } ``` ### 例题5[观光之旅](https://www.acwing.com/problem/content/346/) 这是经典的**最小环问题** 给定一张无向图,求图中一个至少包含 3 个点的环,环上的节点不重复,并且环上的边的长度之和最小。 该问题称为无向图的最小环问题。 你需要输出最小环的方案,若最小环不唯一,输出任意一个均可。 这里我们使用Floyd算法,因为Floyd算法外层循环的k代表节点编号不超过k,于是一个环可以表示为:$f[i][j]+a[i][k]+a[k][j]$,当然,这是还没有进行第k层递推的时候,我们就相当于是枚举了k在环上的左右边,当然不相连的时候就是无穷大。至于方案递归输出就可 这样枚举完取最小值得到的环是满足以下条件的最小环 1.经过k 2.由编号不超过k的节点构成 由对称性可知,这样做不会影响结果 ``` int ans[10005],num,cnt,d[105][105],a[105][105],h[105][105],m,n; long long sum=0x3f3f3f3f; void get_ans(int i,int j){ if(h[i][j]==0)return ; get_ans(i,h[i][j]); ans[++num]=h[i][j]; get_ans(h[i][j],j); }//求方案 void floyd(){ for(int k=1;k<=n;k++){ for(int i=1;i<k;i++) for(int j=i+1;j<k;j++) if((long long)d[i][j]+a[i][k]+a[k][j]<sum){ sum=(long long)d[i][j]+a[i][k]+a[k][j]; num=0; ans[++num]=i; get_ans(i,j); ans[++num]=j; ans[++num]=k; } for(int i=1;i<=n;i++) for(int j=1;j<=n;j++){ if(d[i][j]>d[i][k]+d[k][j])h[i][j]=k,d[i][j]=d[i][k]+d[k][j];//注意这里必须严格 } } } int main(){ scanf("%d%d",&n,&m); memset(a,0x3f,sizeof a); for(int i=1;i<=n;i++)a[i][i]=0; for(int i=1;i<=m;i++){ int u,v,w; scanf("%d%d%d",&u,&v,&w); a[u][v]=a[v][u]=min(a[u][v],w); } memcpy(d,a,sizeof a); floyd(); if(sum==0x3f3f3f3f)puts("No solution."); else{ int k=n+1,p=n+1; for(int i=1;i<=num;i++)if(ans[i]<p)k=i,p=ans[i];//顺序输出 for(int i=k;i<=k+num-1;i++){ printf("%d ",ans[(i-1)%num+1]); } puts(""); } return 0; } ``` 有向图的最小环更简单,我们枚举起点$s=1\sim n$,对此进行单源最短路径算法,并且第一次取出s对节点进行更新之后把d[s]改为无穷大,当s第二次被取出的时候这个长度就是最小环长度 ### [牛站](https://www.acwing.com/problem/content/347/) 给定一张由 T 条边构成的无向图,点的编号为 1∼1000 之间的整数。 求从起点 S 到终点 E 恰好经过 N 条边(可以重复经过)的最短路。 2≤T≤100, 2≤N≤106 注意: 数据保证一定有解。 发现边很少,于是我们将点离散化,使用邻接矩阵存图 定义矩阵运算:设矩阵A,B,定义A*B=C,其中$C[i][j]=\min_{k=1}^n\lbrace A[i][k]+B[k][j] \rbrace$ 若矩阵A就是我们存图的邻接矩阵,那么$A^m[i][j]$就是从i到j的经过m条边的最短路,于是我们使用快速幂即可得到答案,因为这个新矩阵乘法很明显具备结合律 ``` #include<iostream> #include<cstdio> #include<map> #include<cstring> using namespace std; map<int,int>H; int a[105][105],n,m,s,t,b[10005],cnt,S,E; void mul(int a[][105],int b[][105]){ int c[105][105]; memset(c,0x3f,sizeof c); for(int i=1;i<=cnt;i++) for(int j=1;j<=cnt;j++) for(int k=1;k<=cnt;k++) c[i][j]=min(c[i][j],a[i][k]+b[k][j]); for(int i=1;i<=cnt;i++) for(int j=1;j<=cnt;j++) a[i][j]=c[i][j]; } void power(int a[][105],int m){ int ans[105][105]; memset(ans,0x3f,sizeof ans); ans[s][s]=0; while(m){ if(m&1)mul(ans,a); mul(a,a); m>>=1; } for(int i=1;i<=cnt;i++) for(int j=1;j<=cnt;j++) a[i][j]=ans[i][j]; } int main(){ // freopen("relays.12.in","r",stdin); scanf("%d%d%d%d",&n,&m,&S,&E); memset(a,0x3f,sizeof a); for(int i=1;i<=m;i++){ int u,v,w; scanf("%d%d%d",&w,&u,&v); int x=u,y=v; if(H[u])u=H[u]; else H[u]=++cnt,u=cnt; if(H[v])v=H[v]; else H[v]=++cnt,v=cnt; if(x==S)s=u; if(y==S)s=v; if(x==E)t=u; if(y==E)t=v; a[u][v]=a[v][u]=min(w,a[u][v]); } power(a,n); printf("%d\n",a[s][t]); } ``` ## 总结 三个最短路算法必须掌握 然后就是经典问题 1.次短路及路径方案统计 2.最小环问题 3.传递闭包问题 经典思想 1.分层图 2.二分答案转判定 3.拆分路径统计答案 4.拆图分别计算\]

标签:head,dist,int,短路,扩展,cnt,tot,算法
From: https://www.cnblogs.com/oierpyt/p/16940024.html

相关文章