深入浅出:四种常用的最短路算法+两种常用生成树算法+网络流常用算法大礼包

写了不少题,写着写着就忘了,主要复习算法的思路,没有原理哈


我比较喜欢结构体,所以一般都会用

#define inf 10000000
#define MAX 2005  //点的最大数目
#define p pair<int,int>

struct edge {
	int to, cost;
	edge(int a, int b) { to = a, cost = b; }
};

vector<edge> G[MAX];
int vis[MAX], d[MAX];

void addEdge(int from, int to, int cost) {
	G[from].push_back(edge(to, cost));
}

一、最短路径

1:Dijkstra & 堆优化 & why not 负权边?

Dijkstra算法算是贪心思想实现的,首先把起点到所有点的距离存下来找个最短的,然后松弛一次再找出最短的,所谓的松弛操作就是,遍历一遍看通过刚刚找到的距离最短的点作为中转站会不会更近,如果更近了就更新距离,这样把所有的点找遍之后就存下了起点到其他所有点的最短距离。

//s:起始点
void dijkstra(int s) {
	//创建pair类型的小顶堆,以到s的距离为度量
	priority_queue<p, vector<p>, greater<p> >q;
	memset(vis, 0, sizeof(vis));
	fill(d, d + MAX, inf);
	d[s] = 0; q.push(p(0, s));
	while (!q.empty()) {
		int id = q.top().second, dis = q.top().first; 
		q.pop(); vis[id] = 1;
		//清除过时数据
		if (dis > d[id])continue;
		for (unsigned i = 0; i < G[id].size(); i++) {
			edge e = G[id][i];
			//如果这个点还没有visit,而且当前的路径更短
			if (!vis[e.to] && d[e.to] > d[id] + e.cost) {
				d[e.to] = d[id] + e.cost;
				q.push(p(d[e.to], e.to));
			}
		}
	}
}

Dijkstra 为什么不能用于存在负权边的图?

需要知道的是,其实dijkstra是基于这样一个假设,假设 s s 是起点, s v s\rightarrow v 的最短路径一定不会经过与 s s 的距离大于 d ( s , v ) d(s,v) 的点,在正权值的图里这是对的,但是显然对于负权值这是错的:我们使用dij的话,会选择 v v 进行更新,并打上标记,然后就不会再更新 v v ,但是负权边的存在,使得 s a v s\rightarrow a \rightarrow v 反而更短。也就是说我们不应该剥夺节点再次被更新的权力,于是bellman-ford来了
在这里插入图片描述


2:Bellman-Ford:迭代与松弛

Bellman-ford 算法比dijkstra算法更具普遍性,因为它对边没有要求,可以处理负权边与负权回路。缺点是时间复杂度过高,高达O(VE), V为顶点数,E为边数。

其主要思想:对所有的边进行n-1轮有且只有n-1)松弛操作,因为在一个含有n个顶点的图中,任意两点之间的最短路径最多包含n-1边。换句话说,第1轮在对所有的边进行松弛后,得到的是源点最多经过一条边到达其他顶点的最短距离;第2轮在对所有的边进行松弛后,得到的是源点最多经过两条边到达其他顶点的最短距离;第3轮在对所有的边进行松弛后,得到的是源点最多经过一条边到达其他顶点的最短距离

直观理解迭代与松弛

//s:起始点
void BellmanFord(int s) {
	fill(d, d + MAX, inf);
	d[s] = 0;
	while (true) {
		bool update = false;
		for (int i = 0; i < n; i++) {//n是点的数目
			for (unsigned j = 0; j < G[i].size(); j++) {
				edge e = G[i][j];
				if (d[i] != inf && d[e.to] > d[i] + e.cost) {
					d[e.to] = d[i] + e.cost;
					update = true;
				}
			}
		}
		if (!update) break;
	}
}

为了给每个点不断被更新的机会,我们每次while都要遍历所有存在的边,然后进行松弛,当然如果 i i 点还没有被遍历过,我们是不需要处理从他开始的所有边的。上面已经分析了,我们需要且恰好需要 n 1 n-1 次迭代的过程,所有这就可以成为判断负权边的依据了。迭代的主体部分不变,我们进行 n n 次循环,如果到第 n n 迭代依然有边进行了松弛,那么显然只可能是负权图。

//s:起始点
bool BellmanFord(int s) {
	fill(d, d + MAX, inf);
	d[s] = 0;
	bool sign = true;
	for (int k = 0; k < n && sign; k++) {
		for (int i = 0; i < n && sign; i++) {//n是点的数目
			for (unsigned j = 0; j < G[i].size(); j++) {
				edge e = G[i][j];
				if (d[i] != inf && d[e.to] > d[i] + e.cost) {
					d[e.to] = d[i] + e.cost;
					if (k == n - 1) { sign = false; break; }
				}
			}
		}
	}
	return sign;
}

2(2): 进阶版:Spfa & why 队列?

bellmanFord虽好,但是顶不住复杂度太高了。仔细想想,我们做遍历的时候,首先要先置一个数组,这个数组记录了原点到任意点的最短路径距离,初始化的时候,只有原点自己是0,其他的都是正无穷,那么第一次遍历边,只有原点可达的节点可以更新这个数组,其余的遍历都是废操作。同理第二轮遍历,也只有第一轮访问过的节点可达的节点的边才可能更新这个数组,也就是说,我们有定理只有上一次迭代中松弛过的点才有可能参与下一次迭代的松弛操作。那么只要利用一个队列就可以保证这个顺序是正确的(当然你大可以用任何的数据结构,只是对于随机数据而言,队列的速度最快

spfa松弛实例

//s:起始点
void SPFA(int s) {
	memset(vis, 0, sizeof(vis));
	fill(d, d + MAX, inf);
	queue<int>q;
	d[s] = 0; vis[s] = 1; q.push(s);//s入队,并标记s是入队的点
	while (!q.empty) {
		int id = q.front(); q.pop(); vis[id] = 0;//id节点不在队列中
		for (unsigned i = 0; i < G[id].size(); i++) {
			edge e = G[id][i];
			if (d[e.to] > d[id] + e.cost) {
				d[e.to] = d[id] + e.cost;
				if (!vis[e.to]) {
					q.push(e.to);
					vis[e.to] = 1;//入队并标记
				}
			}
		}
	}
}

spfa虽然平时用着不是很舒畅,但是却是最小费用流的基础(最小费用流见下面)

3:Floyd:他死了

一个 O ( n 3 ) O(n^3) 的算法用到的地方其实不是很多,这里需要注意一点,有些题会卡 m i n min 函数这是人做的事情吗就是这个天坑

	int dp[N][N];//dp[i][i]=0,其余初始化边权,无边就inf
	for (int i = 0; i < n; i++) {
		for (int j = 0; j < n; j++) {
			for (int k = 0; k < n; k++) {
				if (dp[i][k] + dp[k][j] < dp[i][j])
					dp[i][j] = dp[i][k] + dp[k][j];
			}
		}
	}

二、最小生成树

对生成树而言,与最短路显然不同,这里的重点在于边,顶点反而显得不是那么重要,因此基础的数据存储需要换一种方式。

1:Prim 算法-给定初始点的最小生成树

Prim采取了贪心的思想,从给定点 s s ,构造生成树点集 V V ,那么每一步我们需要找到与 V V 集合距离最近的点,然后把他加进来。

#define MAX 1555
#define inf 1e9

int cost[MAX][MAX];	// cost[u][v]表示边e=(u,v)的权值( 不存在的情况下设为INF)
int mincost[MAX];   // 从集合X出发的边到每个顶点的最小权值
bool used[MAX];		// 顶点i是否包含在集合X中
int V;				// 顶点数

int prim(int s) {
	//1:初始化
	memset(used, 0, sizeof(used));
	fill(mincost, mincost + MAX, inf);
	mincost[s] = 0; int res = 0;

	while (true) {
		int v = -1;
		//2:找到没使用过的,而且当前距离T集合最近的
		for (int i = 0; i < V; i++) {
			if (!used[i] && (v == -1 || mincost[i] < mincost[v]))
				v = i;
		}
		if (v == -1)break;//找不到就结束
		used[v] = 1;
		res += mincost[v];
		//3:从这里开始再更新一遍最短距离
		for (int i = 0; i < V; i++)
			mincost[i] = min(mincost[i], cost[v][i]);
	}
}

2:Kruskal算法-并查集based

先构造一个只含 n 个顶点、而边集为空的子图,把子图中各个顶点看成各棵树上的根结点,之后,从图的边集 E 中选取一条权值最小的边,若该条边的两个顶点分属不同的树,则将其加入子图,即把两棵树合成一棵树,反之,若该条边的两个顶点已落在同一棵树上,则不可取,而应该取下一条权值最小的边再试之。依次类推,直到森林中只有一棵树,也即子图中含有 n-1 条边为止。分属不同的树这个操作就要用到并查集这个神奇的东西了。

#define inf 10000000
#define MAX 2005  //边的最大数目
#define p pair<int,int>

struct edge {
	int from, to, cost;//只需要存储边的关系
	bool operator<(const edge &  e) { return cost < e.cost; }
};
vector<edge>G;
int V, E;//顶点数目与边的数目
int root[MAX];

int find(int x) {
	if (x == root[x])return x;
	else return root[x] = find(root[x]);
}

void unite(int x, int y) {
	root[x = find(x)] = root[find(y)];//双跟合并
}
int Kruskal() {
	int res = 0;
	sort(G.begin(), G.end());
	for (int i = 0; i <= V; i++)root[i] = i;//并查集的初始化
	for (unsigned i = 0; i < G.size(); i++) {
		edge e = G[i];
		if (find(e.from) != find(e.to)) {//两个点不在一个集合中,加上该边不会导致环
			unite(e.from, e.to);//合并到一个集合
			res += e.cost;
		}
	}
	return res;
}

三、网络流

1:网络流基础:反向弧详解+最小割定理网络流变种问题
2:最小费用流基础

3:Dinic算法精讲

四、图的匹配–二分图,一般图以及二分图的边覆盖、独立集和顶点覆盖

精讲

发布了186 篇原创文章 · 获赞 13 · 访问量 9283

猜你喜欢

转载自blog.csdn.net/csyifanZhang/article/details/105269356
今日推荐