Tag Archives: SPFA

LeetCode Path with Maximum Probability

1514. Path with Maximum Probability

You are given an undirected weighted graph of n nodes (0-indexed), represented by an edge list where edges[i] = [a, b] is an undirected edge connecting the nodes a and b with a probability of success of traversing that edge succProb[i].

Given two nodes start and end, find the path with the maximum probability of success to go from start to end and return its success probability.

If there is no path from start to endreturn 0. Your answer will be accepted if it differs from the correct answer by at most 1e-5.

Example 1:

Input: n = 3, edges = [[0,1],[1,2],[0,2]], succProb = [0.5,0.5,0.2], start = 0, end = 2
Output: 0.25000
Explanation: There are two paths from start to end, one having a probability of success = 0.2 and the other has 0.5 * 0.5 = 0.25.

Example 2:

Input: n = 3, edges = [[0,1],[1,2],[0,2]], succProb = [0.5,0.5,0.3], start = 0, end = 2
Output: 0.30000

Example 3:

Input: n = 3, edges = [[0,1]], succProb = [0.5], start = 0, end = 2
Output: 0.00000
Explanation: There is no path between 0 and 2.

Constraints:

  • 2 <= n <= 10^4
  • 0 <= start, end < n
  • start != end
  • 0 <= a, b < n
  • a != b
  • 0 <= succProb.length == edges.length <= 2*10^4
  • 0 <= succProb[i] <= 1
  • There is at most one edge between every two nodes.

给定一个无向图,边表示概率,问从start到end的最大概率是多少。

借此题复习一下若干最短路径算法吧。

首先朴素的DFS,代码如下:

class Solution {
private:
	void DFS(const vector<vector<double>> &graph, vector<int> &visited, int start, int end, double curprob, double &maxprob) {
		
		if (start == end) {
			maxprob = max(maxprob, curprob);
			return;
		}

		int n = graph.size();
		for (int i = 0; i < n; ++i) {
			if (visited[i] == 0 && graph[start][i] >= 0) {
				visited[i] = 1;
				DFS(graph, visited, i, end, curprob*graph[start][i], maxprob);
				visited[i] = 0;
			}
		}

	}
public:
	double maxProbability(int n, vector<vector<int>>& edges, vector<double>& succProb, int start, int end) {
		vector<vector<double>> graph(n, vector<double>(n, -1.0));
		for (int i = 0; i < edges.size(); ++i) {
			graph[edges[i][0]][edges[i][1]] = succProb[i];
			graph[edges[i][1]][edges[i][0]] = succProb[i];
		}
		vector<int> visited(n, 0);
		visited[start] = 1;
		double maxprob = 0;
		DFS(graph, visited, start, end, 1, maxprob);
		return maxprob;
	}
};

本代码提交TLE。

其次, 朴素的迪杰斯特拉算法。迪杰斯特拉算法思路很简单,维护两个集合,一个集合S是已经找到最短路径的,另一个集合U是还未找到最短路径的。每次,从U选一个距离最小的节点u,这个节点已经是最短路径了,加入到S中。然后更新与u相连的其他节点的最短路径。如此循环往复。代码如下:

// 朴素迪杰斯特拉
class Solution {

public:
	double maxProbability(int n, vector<vector<int>>& edges, vector<double>& succProb, int start, int end) {
		vector<unordered_map<int, double>> graph(n, unordered_map<int, double>());
		for (int i = 0; i < edges.size(); ++i) {
			graph[edges[i][0]][edges[i][1]] = succProb[i];
			graph[edges[i][1]][edges[i][0]] = succProb[i];
		}

		vector<int> visited(n, 0);
		visited[start] = 1;

		vector<double> ans(n, 0);
		for (int i = 0; i < n; ++i) {
			ans[i] = graph[start][i];
		}

		while (true) {
			int maxid = -1;
			double maxprob = 0;
			for (int i = 0; i < n; ++i) {
				if (visited[i] == 0 && ans[i] > maxprob) {
					maxid = i;
					maxprob = ans[i];
				}
			}
			if (maxid == -1 || maxid == end)break; // 遇到end提前结束

			visited[maxid] = 1;


			for (unordered_map<int, double>::iterator it = graph[maxid].begin(); it != graph[maxid].end(); ++it) {
				int next = it->first;
				double prob = it->second;

				if (visited[next] == 0 && (maxprob*prob > ans[next])) {
					ans[next] = maxprob * prob;
				}

			}
		}

		return ans[end];
	}
};

很遗憾,上述代码在最后一个样例上TLE了。

优化版本的迪杰斯特拉算法。朴素迪杰斯特拉算法最耗时的是while循环内的两个for循环,第二个for循环是对找到的u节点的边进行循环,这个无法再优化,主要优化第一个for循环,即在U集合中找距离最短的u节点的过程。这里可以用优先队列来存储U中的所有节点。但是需要注意的是去重,比如题目中的第一个图,对于节点2,节点0弹出pq时,会访问节点2并把节点2加入到pq中;当节点1弹出pq时,又会访问节点2并把节点2加入到pq中,此时pq中出现了两个节点2,只不过prob不一样。所以pq弹出时可能会出现相同的节点,不过由于pq的性质,同样是2,先弹出来的肯定是概率最大的路径(最短路径),所以每次pq弹出时,更新visited数组,后续如果弹出的节点已经visited了,则不需要做第二个for循环了,直接continue。比如题目中的第一个图,0访问2时,压pq的是(id=2,prob=0.2);1访问2时,压pq的是(id=2,prob=0.25)。所以(id=2,prob=0.25)先于(id=2,prob=0.2)弹出,并设置visited[2]=1。当(id=2,prob=0.2)弹出时,visited[2]已经==1了,此时continue。


// 优化的迪杰斯特拉
struct P {
	int id_;
	double prob_;
	P(int id, double prob) :id_(id), prob_(prob) {};

	// priority_queue默认是最大堆,即小于就是小于
	bool operator<(const P& p) const {
		return this->prob_ < p.prob_;
	}
};

class Solution {

public:
	double maxProbability(int n, vector<vector<int>>& edges, vector<double>& succProb, int start, int end) {
		vector<unordered_map<int, double>> graph(n, unordered_map<int, double>());
		for (int i = 0; i < edges.size(); ++i) {
			graph[edges[i][0]][edges[i][1]] = succProb[i];
			graph[edges[i][1]][edges[i][0]] = succProb[i];
		}

		vector<int> visited(n, 0);

		vector<double> ans(n, 0);
		ans[start] = 1;

		priority_queue<P> pq;
		pq.push(P(start, 1));

		while (!pq.empty()) {
			P cur = pq.top();
			pq.pop();
			
			if (cur.id_ == end)break; // 提前结束

			if (visited[cur.id_] == 1)continue;
			visited[cur.id_] = 1;

			for (unordered_map<int, double>::iterator it = graph[cur.id_].begin(); it != graph[cur.id_].end(); ++it) {
				int next = it->first;
				double prob = it->second;
				if (cur.prob_*prob > ans[next]) {
					ans[next] = cur.prob_*prob;
					pq.push(P(next, ans[next]));
				}
			}

		}
		return ans[end];
	}
};

本代码提交AC,用时428MS。

SPFA算法。SPFA算法的思想可以参考之前的一道题: http://code.bitjoy.net/2014/12/28/hihocoder-1093/

简单来说,SPFA就是带剪枝的BFS。它和迪杰斯特拉算法非常像,迪杰斯特拉算法是每次找U中距离最小的一个节点u,此时u的最短距离已经确定了,后续不会再更新了。因为迪杰斯特拉算法每次加入S的节点都是最优的,所以必须要线性(朴素迪杰斯特拉算法)或者用优先队列的方法从U中找到最小距离节点u。而SPFA算法就更简单一点,该算法不要求每次找到最优的节点u,因为SPFA是渐进逼近最优解的过程,它不需要线性查找,也不需要优先队列,它只需要一个简单的队列即可,每次把可以更新的节点u加入到队列中,然后BFS更新与u相连的其他节点。之前更新过的节点,后续还有可能更新。代码如下:

// SPFA
class Solution {

public:
	double maxProbability(int n, vector<vector<int>>& edges, vector<double>& succProb, int start, int end) {
		vector<unordered_map<int, double>> graph(n, unordered_map<int, double>());
		for (int i = 0; i < edges.size(); ++i) {
			graph[edges[i][0]][edges[i][1]] = succProb[i];
			graph[edges[i][1]][edges[i][0]] = succProb[i];
		}

		vector<int> visited(n, 0);
		visited[start] = 1;

		vector<double> ans(n, 0);
		ans[start] = 1;

		queue<int> q;
		q.push(start);
		while (!q.empty()) {
			int cur = q.front();
			q.pop();
			visited[cur] = 0;

			for (unordered_map<int, double>::iterator it = graph[cur].begin(); it != graph[cur].end(); ++it) {
				int next = it->first;
				double prob = it->second;
				if (ans[cur] * prob > ans[next]) {
					ans[next] = ans[cur] * prob;
					if (visited[next] == 0) {
						visited[next] = 1;
						q.push(next);
					}
				}
			}
		}

		return ans[end];
	}
};

本代码提交AC,用时392MS。

hihoCoder 1138-Islands Travel

hihoCoder 1138-Islands Travel #1138 : Islands Travel 时间限制:10000ms 单点时限:1000ms 内存限制:256MB 描述 There are N islands on a planet whose coordinates are (X1, Y1), (X2, Y2), (X3, Y3) …, (XN, YN). You starts at the 1st island (X1, Y1) and your destination is the n-th island (XN, YN). Travelling between i-th and j-th islands will cost you min{|Xi-Xj|, |Yi-Yj|} (|a| denotes the absolute value of a. min{a, b} denotes the smaller value between a and b) gold coins. You want to know what is the minimum cost to travel from the 1st island to the n-th island. 输入 Line 1: an integer N. Line 2~N+1: each line contains two integers Xi and Yi. For 40% data, N<=1000,0<=Xi,Yi<=100000. For 100% data, N<=100000,0<=Xi,Yi<=1000000000. 输出 Output the minimum cost. 样例输入 3 2 2 1 7 7 6 样例输出 2


本题实质上是单源最短路径问题,只是两点间的距离需要自己算。我先后尝试了Dijkstra算法和朴素SPFA算法,都超时,后来参考这篇博客,在SPFA之前需要对数据进行预处理。 原始SPFA时,点(x,y)和其余n-1个点的边都需要尝试,这样计算量就很大,但是因为距离函数是min{|Xi-Xj|, |Yi-Yj|}这样的,和点(x,y)“距离”最近的点(x’,y’)是那些x’和x最近或者y’和y最近的点。所以点(x,y)实际上只需要尝试4条边,分别是靠近x的前后两个点和靠近y的上下两个点。 所以我们需要对所有点分别按x和y排序,然后重新构造一个图,这个图中,每个点只和另外4个点有边,这样使得图的复杂度大幅下降,再使用SPFA就不会超时了。 SPFA的介绍可以看我之前的题目。 完整代码如下: [cpp] #include<iostream> #include<cstdio> #include<cmath> #include<algorithm> #include<cstdlib> #include<queue> #include<cstring> #include<vector> using namespace std; const int kMaxN = 100005, kINF = 0x3f; int n; typedef struct P { int x, y; }; typedef struct Node { int value; int id; bool operator <(const Node & t)const { return value < t.value; } }; Node X[kMaxN], Y[kMaxN]; P points[kMaxN]; int dist[kMaxN]; int used[kMaxN]; vector<vector<int>> path(kMaxN); inline int GetLength(int a, int b) { return min(abs(points[a].x – points[b].x), abs(points[a].y – points[b].y)); } int Spfa() { used[1] = 1; memset(dist, kINF, sizeof(dist)); dist[1] = 0; queue<int> Q; Q.push(1); while (!Q.empty()) { int u = Q.front(); Q.pop(); used[u] = 0; for (int i = 0; i < path[u].size(); i++) { int v = path[u][i]; if (dist[u] + GetLength(u, v) < dist[v]) { dist[v] = dist[u] + GetLength(u, v); if (used[v] == 0) { Q.push(v); used[v] = 1; } } } } return dist[n]; } int main() { scanf("%d", &n); for (int i = 1; i <= n; i++) { scanf("%d %d", &points[i].x, &points[i].y); X[i].value = points[i].x; X[i].id = i; Y[i].value = points[i].y; Y[i].id = i; } sort(X + 1, X + n + 1); sort(Y + 1, Y + n + 1); for (int i = 2; i <= n; i++) { path[X[i].id].push_back(X[i – 1].id); path[X[i – 1].id].push_back(X[i].id); } for (int i = 2; i <= n; i++) { path[Y[i].id].push_back(Y[i – 1].id); path[Y[i – 1].id].push_back(Y[i].id); } printf("%d\n", Spfa()); return 0; } [/cpp] 本代码提交AC,用时867MS,内存20MB。]]>

hihoCoder 1093-最短路径·三:SPFA算法

hihoCoder 1093-最短路径·三:SPFA算法 #1093 : 最短路径·三:SPFA算法 时间限制:10000ms 单点时限:1000ms 内存限制:256MB 描述 万圣节的晚上,小Hi和小Ho在吃过晚饭之后,来到了一个巨大的鬼屋! 鬼屋中一共有N个地点,分别编号为1..N,这N个地点之间互相有一些道路连通,两个地点之间可能有多条道路连通,但是并不存在一条两端都是同一个地点的道路。 不过这个鬼屋虽然很大,但是其中的道路并不算多,所以小Hi还是希望能够知道从入口到出口的最短距离是多少? 提示:Super Programming Festival Algorithm。 输入 每个测试点(输入文件)有且仅有一组测试数据。 在一组测试数据中: 第1行为4个整数N、M、S、T,分别表示鬼屋中地点的个数和道路的条数,入口(也是一个地点)的编号,出口(同样也是一个地点)的编号。 接下来的M行,每行描述一条道路:其中的第i行为三个整数u_i, v_i, length_i,表明在编号为u_i的地点和编号为v_i的地点之间有一条长度为length_i的道路。 对于100%的数据,满足N<=10^5,M<=10^6, 1 <= length_i <= 10^3, 1 <= S, T <= N, 且S不等于T。 对于100%的数据,满足小Hi和小Ho总是有办法从入口通过地图上标注出来的道路到达出口。 输出 对于每组测试数据,输出一个整数Ans,表示那么小Hi和小Ho为了走出鬼屋至少要走的路程。 样例输入 5 10 3 5 1 2 997 2 3 505 3 4 118 4 5 54 3 5 480 3 4 796 5 2 794 2 5 146 5 4 604 2 5 63 样例输出 172


对于边稀疏的图,SPFA算法可以更高效的求解单源最短路径问题。 假设要求S和T的最短路径,该算法使用一个队列,最开始队列中只有(S,0)—表示当前处于点S,从点S到达该点的距离为0,然后每次从队首取出一个节点(i, L)——表示当前处于点i,从点S到达该点的距离为L,接下来遍历所有从这个节点出发的边(i, j, l)——表示i和j之间有一条长度为l的边,在将(j, L+l)加入到队列之前,先判断队列中是否存在点j,如果存在(j,l’),则比较L+l和l’的大小关系,如果L+l>=l’,那么(j,L+l)这条路就没必要继续搜索下去了,所以不将(j,L+l)加入队列;如果L+l<l’,那么原来的(j,l’)没必要继续搜索下去了,把(j,l’)替换成(j,L+l)即可。如果原队列中不存在j点,则直接把(j,L+l)加入队列。 当队列为空时,(T,ans)就是S到T的距离为ans。所以SPFA在某种程度上来说,就是BFS+剪枝。 SPFA的原理不难,写代码的时候要注意几点:1)N的范围是N<=10^5,如果用数组存储的话,内存够呛,所以建议用vector动态分配;2)每次从队列中弹出一个点时,记得将其在队列中的标记清空,即used[i]=0;3)处理好数据结构问题,因为数据输入中2点之间的距离可能有多个值,你可以在输入的时候只存储最小值,你也可以为了方便先把所有数据都存起来,统一在SPFA算法中进行大小判断,本代码使用第二种策略。
完整代码如下:


#include <iostream>
#include <cstdio>
#include <vector>
#include <queue>
using namespace std;
const int MAX_N = 1e5 + 10;
const int INF = 1e6 + 10;
int n, m, s, t;
//int path[MAX_N][MAX_N];//数组太大,改用vector动态分配
vector<int> path[MAX_N]; //path[i][j]表示第i个点和第path[i][j]个点有路相同,其中j是输入的顺序
vector<int> dist[MAX_N]; //dist[i][j]表示和第i个点相连的第j个点的距离是dist[i][j],path[i][j]和dist[i][j]是通过相同的j来共享数据
int used[MAX_N]; //used[i]表示第i个点是否在队列中
int s_dist[MAX_N]; //s_dist[i]表示第i个点和起始点s的距离
int spfa()
{
    int q1 = s;
    used[q1] = 1;
    for (int i = 1; i <= n; i++)
        s_dist[i] = INF;
    s_dist[s] = 0;
    queue<int> Q;
    Q.push(q1);
    while (!Q.empty()) {
        q1 = Q.front();
        Q.pop();
        used[q1] = 0; //当弹出队列时记得设置used[i]=0
        int qs = path[q1].size(); //和q1点相连的点的个数
        for (int i = 0; i < qs; i++) {
            int u = path[q1][i];
            if (s_dist[q1] + dist[q1][i] < s_dist[u]) //如果u是q1的父节点,肯定不满足这一条,所以不会回溯
            {
                s_dist[u] = s_dist[q1] + dist[q1][i];
                if (used[u] == 0) {
                    used[u] = 1;
                    Q.push(u);
                }
            }
        }
    }
    return s_dist[t];
}
int main()
{
    //freopen("input.txt","r",stdin);
    scanf("%d%d%d%d", &n, &m, &s, &t);
    int u, v, len;
    while (m–) {
        scanf("%d%d%d", &u, &v, &len); //记录了所有数据,此处还可以优化
        path[u].push_back(v);
        path[v].push_back(u);
        dist[u].push_back(len);
        dist[v].push_back(len);
    }
    printf("%d\n", spfa());
    return 0;
}

本代码提交AC,用时208MS,内存19MB。