グラフ理論 -- 最短経路の応用

    この記事では主にダイクストラアルゴリズムとフロイドアルゴリズムに基づいた最短経路に関するいくつかのトピックを記録します. ダイクストラは負のエッジの重みを考慮していないため、この記事では負のエッジの問題は考慮しません。


目次

ダイクストラの最適化

P1119 災害復興

P1629 手紙を配達する郵便配達員 

P1144 最短パス数

P1462 オーグリマーへの道

P1522 [USACO2.4] カウツアー


 

ダイクストラの最適化

このアルゴリズムは、最短経路問題における最も古典的なアルゴリズムの 1 つであり、単一ソース最短経路問題、つまり 2 点間の最短経路を解くために使用されます。実際、1 点からの最短経路を見つけることができます。他のすべての点に。従来の記述複雑さは O(n^2) です。以前に書いたブログを参照してください:グラフ理論---最小スパニング ツリー、最短パス_czc131 のブログ-CSDN ブログ_最小スパニング ツリー最短パス

解決プロセスを確認します。まず、現在の距離で最短かつ最も明確な最短経路を見つけます。次に、この点に基づいて他の点から開始点までの距離を更新します。

最適化のポイントは 2 つあります。

  1. 現在の距離が最小となる点を見つけるには、このときのヒープ最適化が考えられますが、実際の応用では、もちろん STL のプライオリティキューを使用します。キュー内のポイント数と開始点までの距離を保存します。キューに入るとき、優先キューは距離に応じて自動的に最も小さいものをキューの先頭に置きます。このとき、キューの先頭、それは最短距離である必要があります。vis Array マークを設定します。チームの現在の先頭が最短パスとしてマークされている場合は、それをポップオフします。それ以外の場合は、更新中に最小距離が変更された場合は更新します。プロセスの場合、それが現在の最短パスになる可能性があるため、キューに追加する必要があります。
  2. 更新プロセスです。この最適化は通常、隣接行列を使用して保存することを目的としており、同時にエッジが少なくなります。隣接マトリックス ストレージの場合、多くの走査は更新中に実際には無効になり、現時点ではエッジ ストレージの方が効率的です。連鎖した前方スターを導入します。連鎖した表現はリンクされたリストであり、順方向とはポインタが前を指すことを意味します。各エッジに対して、次のように定義される構造が確立されます。
struct Edge
{
	int to, next;
	double len;
}e[M];
int last[N], cnt;

Edge はエッジを表し、to はこのエッジが接続されている点を表し、next はこの点の開始点でもある前のエッジの位置を表します。各点での最後のエッジの位置を格納する最後の配列を定義します。開始点として、リンクされたリストと同様に、先頭ポインタである cnt はエッジの数を記録します。エッジを追加するプロセスは言うまでもありません。

最短経路の更新プロセスはオリジナルと同様で、最短点を取り出し、この点に接続されているすべての点を更新します。

k から他のすべての点までの最短パスを計算すると、コードは次のようになります。

void dksa(int k)
{
	dis[k] = 0;
	q.push({ k, 0 });
	while(!q.empty())
	{
		node x = q.top();
		q.pop();
		int d = x.d;
		if (vis[d])continue;//如果已经处理过了那么不需要处理了
		vis[d] = 1;
		for (int i = last[d]; i; i = e[i].next)
			if (dis[e[i].to] > dis[d] + e[i].len)
			{
				dis[e[i].to] = dis[d] + e[i].len;
				q.push({ e[i].to,dis[e[i].to] });
			}
	}
}

関連するアプリケーションをいくつか紹介します

P1119 災害復興

トピックリンク:災害後の復興 - 羅谷 

    この質問でさらに興味深いのは、村が次々に修復されており、修復されないと通行できないということです。が修復されるということは、この地点を通過できるということなので、この地点を利用して他の地点間の距離を緩和できないかというと、実はこれがフロイドアルゴリズムの応用です。クエリの毎日、その日より前に修復されたすべてのポイントを使用して、他のすべてのポイント間の最短パスを更新し、クエリされたポイント間の距離を出力します。

    最初に間違えたのですが、アップデート時に修理した村だけが更新されてしまいましたが、実はこれは間違いで、未修理の村は通行できないだけで、現在修理中の村を使用する条件では、実は村間の最短道が変わっています、到達できないので注意が必要です。

    輸出する場合は、2つの村に到達できるかどうか、また、修復しない場合は片方の村を通行できるかどうかを判断することを忘れないでください。

コードは以下のように表示されます。

#include<stdio.h>
#include<algorithm>
using namespace std;
#define Inf 0x3f3f3f3f
#define N 201
int dis[N][N];//存储任意点之间的距离
int flag[N];//标记当前村庄是否已经修复完毕
int n, m;

struct Village
{
	int t;//村庄修复时间
	int num;//村庄号
}v[N];
int now;//表示更新到的索引


int cmp(Village x, Village y)
{
	return x.t < y.t;
}

void update(int day)
{
	for (int i = now; i < n && v[i].t <= day; i++)
		flag[i] = 1;//修复好当前村庄
	while (v[now].t <= day && now<n)
	{
		int k = v[now].num;
		//printf("k=%d\n", k);
		for (int i = 0; i < n; i++)
			for (int j = 0; j < n ; j++)//两个点都修复好才更新距离
				dis[i][j] = min(dis[i][j], dis[i][k] + dis[k][j]);
		now++;
	}
}

int main()
{
	scanf("%d%d", &n, &m);
	for (int i = 0; i < n; i++)
	{
		scanf("%d", &v[i].t);
		v[i].num = i;
	}
	sort(v, v + n, cmp);//将村庄按照修复时间排序
	int x, y, l;
	for (int i = 0; i < n; i++)
		for (int j = 0; j < n; j++)
			dis[i][j] = Inf;
	for (int i = 0; i < n; i++)
		dis[i][i] = 0;//自己到自己是0
	for (int i = 1; i <= m; i++)
	{
		scanf("%d%d%d", &x, &y, &l);
		dis[x][y] = min(dis[x][y], l);
		dis[y][x] = dis[x][y];
	}
	int q; scanf("%d", &q);
	int day = 0;//表示当前更新到的天数
	update(day);
	int t;
	for (int i = 1; i<= q; i++ )
	{
		scanf("%d%d%d", &x, &y, &t);
		if (t > day)
		{
			day = t;//更新到t这一天
			update(day);
		}
		if (!flag[x]||!flag[y]||dis[x][y]==Inf)//没有路或者未修复
			printf("-1\n");
		else printf("%d\n", dis[x][y]);
	}
	return 0;
}

P1629 手紙を配達する郵便配達員 

 トピックリンク:郵便配達員が手紙を配達する - 羅谷

    この問題は、最初の点からすべての点までの最短経路を計算することと、すべての点から最初の点までの最短経路を計算することです。最初のものは計算が簡単で、二番目のものはマップを構築するだけで済みます。逆にすると、問題も最初の点からすべての点までの最短距離に変換され、答えを出力して 2 つを加算します。 

コードは以下のように表示されます。

#include<stdio.h>
#include<algorithm>
#include<queue>
using namespace std;
#define Inf 0x3f3f3f3f
#define N 1001
struct Edge
{
	int to,next;
	int len;
}e1[500001],e2[500001];
int last1[N], last2[N],cnt;
int dis1[N], dis2[N];
bool vis1[N], vis2[N];
int n, m;

struct Node
{
	int d, dis;
	inline bool operator <(const Node &x)const
	//重载运算符把最小的元素放在堆顶(大根堆)
	{
		return dis > x.dis;//这里注意符号要为'>'
	}
};
priority_queue<Node>q;

void add(Edge e[], int last[], int from, int to, int len)
{
	e[cnt].to = to;
	e[cnt].next = last[from];
	last[from] = cnt;
	e[cnt].len = len;
}

void dksa(int st, Edge e[], int last[], int dis[],bool vis[])
{
	dis[st] = 0;//重置不要忘记
	q.push({ st,0 });
	while (!q.empty())
	{
		Node node = q.top();
		q.pop();
		//printf("d=%d\n", node.d);
		int d = node.d;
		if (!vis[d])
		{
			vis[d] = 1;
			for (int i = last[d]; i; i = e[i].next)
			{
				if (dis[e[i].to] > dis[d] + e[i].len)
				{
					dis[e[i].to] = dis[d] + e[i].len;
					q.push({ e[i].to ,dis[e[i].to] });
				}
			}
		}
	}
}

int main()
{
	scanf("%d%d", &n, &m);
	int x, y, len;
	for (int i = 1; i <= m; i++)
	{
		scanf("%d%d%d", &x, &y, &len);
		cnt++;
		add(e1, last1, x, y, len);
		add(e2, last2, y, x, len);//反向建图
	}
	for (int i = 1; i <= n; i++)
		dis1[i] = Inf,dis2[i] = Inf;
	dksa(1, e1, last1,dis1,vis1);
	dksa(1, e2, last2, dis2,vis2);
	int ans = 0;
	for (int i = 2; i <= n; i++)
		ans += dis1[i] + dis2[i];
	printf("%d\n", ans);
	return 0;
}

P1144 最短パス数

 トピックリンク:最短経路のカウント - 羅谷

    この問題は、各点への最短経路の数を計算する必要があります。点の最短経路は、前の点から渡されます。d[i] が i 点であると仮定すると、少なくとも d[i] ステップが必要です。次に、d[i]=sum(d[j]==d[i]-1)、つまり、この点の前の点からこの点までのステップ数を渡し、それらを合計します。はこの時点での最短パスの数です。このプロセスの本質は bfs です。

コードは以下のように表示されます。

#include<stdio.h>
#include<algorithm>
#include<vector>
#include<queue>
using namespace std;
#define mod 100003
#define N 1000001
vector<int>v[N];
//queue<int>q;
int que[N];
int quetmp[N];
int n, m;
int ans[N];
bool vis[N];
void bfs(int x)
{
	ans[x] = 1;
	vis[x] = 1;
	int num = 0;
	que[++num] = x;
	while (num)
	{
		//printf("haha\n");
		//printf("num=%d\n", num);
		for (int i = 1; i <= num; i++)
		{
			x = que[i];
			//printf("x=%d\n", x);
			for (int i = 0; i < v[x].size(); i++)
				if (!vis[v[x][i]])//只给当前层的下一层传递,已经算过的点不会再算了,因为路径长度只会更长
					ans[v[x][i]] = (ans[v[x][i]] + ans[x]) % mod;
		}
		int num2 = 0;
		for (int i = 1; i <= num; i++)//整理接下来的队列
		{
			int x = que[i];
			for(int i=0;i<v[x].size();i++)
				if (!vis[v[x][i]])
				{
					quetmp[++num2] = v[x][i];
					vis[v[x][i]] = 1;
				}
		}
		num = num2;
		for (int i = 1; i <= num2; i++)
			que[i] = quetmp[i];
	}
}

int main()
{
	scanf("%d%d", &n, &m);
	int x, y;
	for (int i = 1; i <= m; i++)
	{
		scanf("%d%d", &x, &y);
		v[x].push_back(y);
		v[y].push_back(x);
	}
	bfs(1);
	for (int i = 1; i <= n; i++)
		printf("%d\n", ans[i]);
	return 0;
}

P1462 オーグリマーへの道

トピックリンク: Orgrimmarへの道 - 羅谷 

    この問題は、始点 1 から終点 n までに必要な最大値の最小値を計算する必要があり、一見すると非常に複雑です。まず第一に、都市を通過するには通行料を支払う必要があり、道路を歩くには血液控除を支払う必要があることを明確にする必要があります。

    金額が大きいほど、より多くの都市に行くことができ、より多くの道路を選択でき、終点に到達できる可能性が高まることを考慮すると、ここには明らかな単調性があるため、計算する際には二分法的な金額は使用しないでください。最短経路に従わない都市を通過する場合、つまり 2 点の結果より通行料が高い場合、血の減算を右の辺として扱い、血の減算の最小値を満たしているかどうかを確認して答えを求めます。つまり、最短経路は血液量よりも小さいということです。

コードは以下のように表示されます。

#include<stdio.h>
#include<algorithm>
#include<queue>
using namespace std;
#define ll long long
#define N 10100
#define M 540000 //无向图要双倍数量!!!
#define Inf 0x7f7f7f7f
ll dis[N];//分别是1出发和n出发
bool vis[N];
int fee[N];

struct Edge
{
	int to, next;
	int len;
}e[M];
int last[N], cnt;
int n, m, b;

struct Node
{
	int d;
	ll dis;
	inline bool operator <(const Node &x)const
	//重载运算符把最小的元素放在堆顶(大根堆)
	{
		return dis > x.dis;//这里注意符号要为'>'
	}
};
priority_queue<Node>q;

void add(int from, int to, int len)
{
	e[++cnt].to = to;
	e[cnt].next = last[from];
	last[from] = cnt;
	e[cnt].len = len;
}

bool check(ll Max)
{
	for (int i = 1; i <= n; i++)
	{
		dis[i] = Inf;
		vis[i] = 0;
	}
	dis[1] = 0;
	q.push({ 1,0 });
	while (!q.empty())
	{
		Node x = q.top();
		int d = x.d;
		q.pop();
		if (vis[d])continue;
		vis[d] = 1;
		for (int i = last[d]; i; i = e[i].next)
		{
			if (fee[e[i].to] > Max)continue;//这个点不可以经过
			//printf("%d -> %d\n", d, e[i].to);
			if (dis[e[i].to] > dis[d] + (ll)e[i].len)
			{
				dis[e[i].to] = dis[d] + (ll)e[i].len;
				q.push({ e[i].to,dis[d] + (ll)e[i].len});
			}
		}
	}
	if (dis[n] <= b)return 1;//计算最小血量能不能达到终点
	else return 0;
}

int main()
{
	scanf("%d%d%d", &n, &m, &b);
	int x, y, z;
	for (int i = 1; i <= n; i++)
		scanf("%d", fee + i);//每个城市的费用
	for (int i = 1; i <= m; i++)
	{
		scanf("%d%d%d", &x, &y,&z);
		add(x, y, z);
		add(y, x, z);
	}
	ll l = fee[1], r = Inf;
	while (l <= r)
	{
		ll mid = (l + r) / 2;
		if (check(mid))r = mid - 1;
		else l = mid + 1;
	}
	if (l == Inf + 1)printf("AFK\n");
	else printf("%lld\n", l);
	return 0;
}

P1522 [USACO2.4] カウツアー

 トピックリンク: [USACO2.4] カウツアー - 羅谷   

    この質問のデータ量は少なく、比較的複雑な内容にも耐えることができますが、次のように手順が比較的面倒です。

  1. まず、dfs を実行してさまざまな牧草地をマークし、領域を分割します。つまり、染色操作です。
  2. Floyd を 1 回実行してすべての点間の最短経路を計算し、次に同じ牧草地内の最大道路の最大値を各牧草地の直径として列挙して計算します。
  3. 任意の 2 つの牧草地を接続した後、直径の最小値を計算する必要があるため、すべての点の組み合わせ (i, j) が列挙されます。異なる牧草地に属している場合は、これら 2 つの点を接続して、新しい牧草地の直径を確認します。この直径には 2 つの可能性があります: 1 つは新しく接続された側を通過しない場合、元の 2 つの牧草地の直径の大きい方の値です。新しい側を通過する場合は、牧草地からの最大距離になります。 i に属する牧草地点 i に属する牧草地 j に属する牧草地 j までの最大距離に i と j の間の距離を加えたもの。
  4. 上記のエッジを見つける過程で動的に答えを更新し、最終的に結果を出力します。

コードは以下のように表示されます。

#include<stdio.h>
#include<algorithm>
#include<vector>
#include<math.h>
using namespace std;
#define Inf 0x3f3f3f3f
#define N 200
#define M N*N
double dis[N][N];//任意两点之间的最短距离
int color[N];//存储每个点所属的牧场类别
int colorNum;//记录牧场总数
double maxr[N];//记录每个牧场的直径
bool vis[N];
int n;

struct Edge
{
	int to, next;
	double len;
}e[M];
int last[N], cnt;

struct ZuoBiao
{
	int x, y;
}zuobiao[N];

double len(int x, int y)
{
	return sqrt(pow(zuobiao[x].x - zuobiao[y].x, 2) + pow(zuobiao[x].y - zuobiao[y].y, 2));
}

void add(int from, int to)
{
	e[++cnt].to = to;
	e[cnt].next = last[from];
	last[from] = cnt;
	e[cnt].len = sqrt(len(from, to));
}

void addColor(int x,int k)
{
	color[x] = k;
	for (int i = last[x]; i; i = e[i].next)
		if (!color[e[i].to])
			addColor(e[i].to, k);
}

int main()
{
	scanf("%d", &n);
	for (int i = 1; i <= n; i++)
		scanf("%d%d", &zuobiao[i].x, &zuobiao[i].y);
	for (int i = 1; i <= n; i++)
		for (int j = 1; j <= n; j++)
		{
			if (i == j)continue;
			dis[i][j] = Inf;//两点之间的最短距离
		}
	char ch;
	for (int i = 1; i <= n; i++)
		for (int j = 1; j <= n; j++)
		{
			scanf(" %c", &ch);
			if (ch == '1')
			{
				add(i, j);
				dis[i][j] = len(i, j);
			}
		}
	for (int i = 1; i <= n; i++)
		if (!color[i])
		{
			colorNum++ ;
			addColor(i,colorNum);//dfs染色
		}
	//计算最短路,同时记录每个牧场的直径
	for (int i = 1; i <= colorNum; i++)
		maxr[i] = 0;
	for (int k = 1; k <= n; k++)
		for (int i = 1; i <= n  ; i++)
			for (int j = 1; j <= n; j++)
			{
				if (i == j)continue;
				dis[i][j] = min(dis[i][j], dis[i][k] + dis[k][j]);
			}
	//记录每个牧场直径
	for (int i = 1; i <= n; i++)
		for (int j = 1; j <= n && j != i; j++)
			if (color[i] == color[j] && dis[i][j] != Inf)//同一色块并且有边
				maxr[color[i]] = max(maxr[color[i]], dis[i][j]);//如果是同一牧场那么更新直径
	double ans = Inf;//答案
	//枚举不同组合
	for(int i=1;i<=n;i++)
		for (int j = 1; j < i; j++)
		{
			if (color[i] == color[j])continue;//同一个牧场不考虑
			double ans1=0, ans2=0;//分别是第一个牧场经过i的最短路最大和第二个牧场经过j的最短路最大
			for (int k = 1; k <= n; k++)
			{
				if (color[k] == color[i] && dis[i][k] != Inf)
					ans1 = max(ans1, dis[i][k]);
				if (color[k] == color[j] && dis[j][k] != Inf)
					ans2 = max(ans2, dis[j][k]);
			}
			//考虑两种情况,不经过i,j,就是tmp,经过i,j,此时可能在i的也可能在j的
			double tmp = ans1 + ans2 + len(i, j);
			ans = min(ans,max(max(maxr[color[i]],maxr[color[j]]), tmp));
		}
	printf("%.6lf\n", ans);
	return 0;
}

 ❀❀❀散りばめの花❀❀❀

おすすめ

転載: blog.csdn.net/weixin_60360239/article/details/128527691