最小生成树、最短路 笔记

原理

Part1. 建图

实现方法 邻接矩阵 邻接表 链式前向星 边集数组
空间 \(O(n^2)\) \(O(n+m)\) \(O(n+m)\) \(O(m)\)
优点 适合于稠密图,方便得到出入度、一条边是否存在 各种图,对一个点的出边排序时十分常用 各种图,边带有编号 关注边的信息,常用于 kruskal
缺点 复杂度高,不能处理重边 不好计算节点入度、删除节点,不能处理反向边 无法快速查询一条边的存在,无法对一个点的出边排序 搜索整张图时要用 \(O(nm)\) 对每个点遍历所有边,复杂度高

邻接矩阵

int n, m, e[N][N];
int main()
{
	cin >> n >> m;
	for (int i = 1; i <= m; i ++)
	{
		int x, y, w;
		cin >> x >> y >> w;
		e[x][y] = w;
		e[y][x] = w;
	}
	for (int i = 1; i <= n; i ++)
	{
		for (int j = 1; j <= n; j ++) cout << e[x][y] << ' ';
		cout << '\n';
	}		
}

邻接表(vector)

struct edge
{
	int y, w;
};
vector<edge> e[N];
int n, m;
int main()
{
	cin >> n >> m;
	for (int i = 1; i <= m; i ++)
	{
		int x, y, w;
		cin >> x >> y >> w;
		e[x].push_back({y, w});
		e[y].push_back({x, w});
	}
	for (int i = 1; i <= n; i ++)
		for (int j = 0; j < e[i].size(); j ++)
			printf("%d %d %d\n", i, e[i][j].y, e[i][j].w);		
}

链式前向星

struct edge
{
	int to, w, next;
}e[M];
int n, m, top, h[N];
void add(int x, int y, int w)
{
	e[++top] = {y, w, h[x]};
	h[x] = top;
}
int main()
{
	cin >> n >> m;
	for (int i = 1; i <= m; i ++)
	{
		int x, y, w;
		cin >> x >> y >> w;
		add(x, y, w);
		add(y, x, w);
	}
	for (int i = 1;i <= n; i ++) 
		for (int j = h[i]; j; j = e[j].next)
			printf("%d %d %d\n", i, e[j].to, e[j].w);
		
}

边集数组

struct edge
{
	int x, y, w;
}e[2*M];
int n, m;
int main()
{
	cin >> n >> m;
	for (int i = 1; i <= m; i ++)
	{
		int x, y, w;
		cin >> x >> y >> w;
		e[i] = {x, y, w};
		e[i+m] = {y, x, w};
	}
	for (int i = 1; i <= n; i ++)
		for (int j = 1; j <= m << 1; j ++)
			if (e[j].x == i)  
				printf("%d %d %d\n", i, e[j].y, e[j].w);
}

Part2. 最小生成树

算法 暴力 prim heap(小根堆) - prim kruskal
实现方法 邻接表、链式前向星 邻接表、链式前向星 边集数组
时间 \(O(n^2)\) \(O((n+m)\log n)\) \(O(m\log m)\)

暴力 prim

bool prim(int s)
{
	for (int i = 0; i <= n; i ++) dist[i] = inf;
	dist[s] = 0;
	for (int i = 1; i <= n; i ++)
	{
		int x = 0;
		for (int j = 1; j <= n; j ++)
			if (!vis[j] && dist[j] < dist[x]) x = j;
		vis[x] = true;
		ans += dist[x];
		if (dist[x] != inf) cnt ++;
		for (int j = h[x]; j ; j = e[j].next)
		{
			int y = e[j].to, w = e[j].w;
			if (w < dist[y]) dist[y] = w;
		}
	}
	return (cnt == n ? 1 : 0);
}


heap(小根堆) - prim

bool prim(int s)
{
	for (int i = 1; i <= n; i ++) dist[i] = inf;
	dist[s] = 0;
	q.push({0, s});
	while(!q.empty())
	{
		int x = q.top().second;
		q.pop();
		if (vis[x]) continue;
		vis[x] = true;
		ans += dist[x];
		cnt ++;
		for (int j = h[x]; j ; j = e[j].next)
		{
			int y = e[j].to, w = e[j].w;
			if (w < dist[y]) 
			{
				dist[y] = w;
				q.push({dist[y], y});
			}
		}
	}
	return (cnt == n ? 1 : 0);
}

kruskal

bool kruskal()
{
	int cnt = 0;
	for (int i = 1; i <= n; i ++) fa[i] = i;
	sort(e + 1, e + m + 1, cmp);
	for (int i = 1; i <= m; i ++)
	{
		int x = e[i].x, y = e[i].y, w = e[i].w;
		if (find(x) != find(y))
		{
			merge(x, y);
			ans += w;
			cnt ++;
		}
	}
	return (cnt == n - 1 ? 1 : 0);
}

Part3. 最短路

算法 暴力 dijkstra heap(小根堆) - dijkstra SPFA floyd 跑 n 次 heap - dijkstra
类型 单源 单源 单源 多源 多源
实现方法 邻接表、链式前向星 ~ ~ 领接矩阵 ~
适用于 非负权图 非负权图、负权图跑最长路 任意图、判负环、跑最长路 任意图 非负权图
时间 \(O(n^2)\) \(O(m\log n)\)(严格上是\(O((n+m)\log n)\) \(O(m)\sim O(nm)\) \(O(n^3)\) \(O(nm\log n)\)

单源

暴力 dijkstra
void dijkstra()
{
	for (int i = 0; i <= n; i ++) dist[i] = inf;
	dist[s] = 0;
	for (int i = 1; i <= n; i ++)
	{
		int x = 0;
		for (int j = 1; j <= n; j ++)
			if (!vis[j] && dist[j] < dist[x]) x = j;
		vis[x] = true;
		for (int j = 0; j < e[x].size(); j ++)
		{
			int y = e[x][j].y, w = e[x][j].w;
			if (dist[x] + w < dist[y]) dist[y] = dist[x] + w;
		}
	}
}

heap(小根堆) - dijkstra
void dijkstra()
{
    for (int i = 0; i <= n; i ++) dist[i] = inf;
    dist[s] = 0;
    q.push({0, s});
    while (!q.empty())
    {
        pii t = q.top();
        q.pop();
        int x = t.second;
        if (vis[x]) continue;
        vis[x] = true;
        for (int i = h[x]; i ; i = e[i].next)
        {
            int y = e[i].to, w = e[i].w;
            if (dist[x] + w < dist[y])
            {
                dist[y] = dist[x] + w;
                q.push({dist[y], y});
            }       
        }
    }
}


SPFA

......

image

void spfa(int s)
{
	for (int i = 1; i <= n; i ++) dist[i] = inf;
	dist[s] = 0;
	q.push(s);
	vis[s] = true;
	while (!q.empty())
	{
		int x = q.front(); q.pop();
		vis[x] = false;
		for (int j = h[x]; j ; j = e[j].next)
		{
			int y = e[j].to, w = e[j].w;
			if (dist[x] + w < dist[y])
			{
				dist[y] = dist[x] + w;
//				cnt[y] = cnt[x] + 1;
//				if (cnt[y] >= n) return false; 有负环
				if (!vis[y])
				{
					q.push(y);
					vis[y] = true;
				}
			}
		}
	}
//	return true;
}

全源

floyd
void init()
{
	memset(f, inf, sizeof(f));
	for (int i = 1; i <= n; i ++) f[i][i] = 0;
}
void floyd()
{
	for (int k = 1; k <= n; k ++)
		for (int i = 1; i <= n; i ++)
			for (int j = 1; j <= n; j ++)
				f[i][j] = min(f[i][j], f[i][k] + f[k][j]);
}

运用

最小生成树题单

一、1. P1194 买礼物

思路 \(\scr{Solution}\)

这道题我调了两年半, \(0\to17\to35\to100pts\) ......

一眼:这不就是个板子吗?一点不需要动,跑最小生成树,只需要在答案累加上根节点的价值 \(c\) 就可以了......然而抱灵

再仔细看数据范围,原来它并没有保证 \(c > w_{i,j}\)不贴合实际,蒸乌鱼

也就是说,全图跑最小生成树并不一定是最优解,

那么显然,对于两个互相有关联的物品,要比较单买 \(2c\) 和 捆绑买 \(c+w\) 哪个费用更少

易得思路:通过比较 \(2c\)\(c+w\),跑部分图的最小生成树 + 单点集合

\(O(n^2\log n^2)~\) done.

#include <bits/stdc++.h>
using namespace std;
const int N = 510, M = 1e3 + 10, inf = 0x3f3f3f3f;
struct edge 
{
	int x, y, w;
}e[N*N];
int top, n, c, fa[N], ans;

bool cmp(edge a, edge b)
{
	return a.w < b.w;
}

int find(int x)
{
	return fa[x] == x ? x : fa[x] = find(fa[x]);
}

void merge(int x, int y)
{
	fa[find(x)] = find(y);
}

void kruskal()
{
	for (int i = 1; i <= n; i ++) fa[i] = i;
	sort(e + 1, e + top + 1, cmp);
	for (int i = 1; i <= top; i ++)
	{
		int x = e[i].x, y = e[i].y, w = e[i].w;
		if (find(x) != find(y) && c + w < 2 * c)
		{
			merge(x, y);
			ans += w;
		}
	}
}

int main()
{
	ios::sync_with_stdio(false);
	cin.tie(0); cout.tie(0);
	
	cin >> c >> n;
	for (int i = 1; i <= n; i ++)
		for (int j = 1; j <= n; j ++)
		{
			int x;
			cin >> x;
			if (i == j) continue;
			e[++top] = {i, j, (!x ? inf : x)};
		}
	kruskal();
	int res = ans;
	for (int i = 1; i <= n; i ++)
	{
		if (find(i) == i) res += c;
	}
	cout << res;
	
	return 0;		
}

粗心1. 本题没直接给出边数范围,我就惯性打 N, 连自己用 \(n^2\) 输入时都没注意;

点击查看代码
struct edge 
{
	int x, y, w;
}e[N];

粗心2. 震惊,一开始我甚至默认以为最小生成树集合的点是连续的,没过脑子吗?

点击查看代码
int last = 0, res = 0;
for (int i = 1; i <= n ; i ++)
{
	if (find(last) != find(i))
	{
		if (find(i) != i) res += ans + w;
		else res += w;
	}
	last = i;
}
cout << res;

总结 \(\scr{Summary}\)

  1. 题意一定要反复看,不可主观臆断,特别是数据范围和特殊条件;

  2. 做题思路一定要清晰,觉得模棱两可的想法不要急着实现,先保证正确性,再考虑优化时效;


最短路题单

二、1. P1629 邮递员送信

P1342 请柬(双倍经验)

思路 \(\scr{Solution}\)

50pts

在有向图中,有 \(v_i\in V\),求 \(\sum\limits_{i=2}^{n}w_{min}(1\to v_i)+\sum\limits_{i=2}^{n}w_{min}(v_i\to 1)\) .

易想到以 1 为起点跑一遍 dijkstra,再暴力枚举其他点每次再跑一遍 dijkstra。

但这样做复杂度为 \(O(nm\log n)\),在 T 的边缘反复横跳


考虑对暴力枚举进行优化,可以发现,每次跑 dijkstra 可以知道到所有点的最短路,而答案只需要累加该点到 1 的最短路,其他的都没有用。

问了下 syz 大佬怎么优化,有一种办法可以不用枚举 tql %%%

就是在反图上跑。而 反图就是将原有向图的所有边方向倒转

在反图上从 1 向其他点跑最短图就等价于在原图上其他点向 1 跑最短路。

\(O(m\log n)~\) done.

#include <bits/stdc++.h>
#define re register
#define int long long 
using namespace std;
typedef pair<int, int> pii;
const int N = 1e6 + 10, M = 1e6 + 10, inf = 0x3f3f3f3f;
struct edge1
{
	int to, w, next;
}e1[M];
struct edge2
{
	int to, w, next;
}e2[M];
int n, m, ans;
int top1, top2, h1[N], h2[N], dist[N];
bool vis[N];
priority_queue< pii, vector<pii>, greater<pii> > q;
  
void add1(int x, int y, int w)
{
	e1[++top1] = (edge1){y, w, h1[x]}; 
	h1[x] = top1;
}

void add2(int x, int y, int w)
{
	e2[++top2] = (edge2){y, w, h2[x]}; 
	h2[x] = top2;
}

void kruskal1(int s)
{
	memset(vis, false, sizeof(vis));
	for (re int i = 1; i <= n; i ++) dist[i] = inf;
	dist[s] = 0;
	q.push({0, s});
	while (!q.empty())
	{
		int x = q.top().second; q.pop();
		if (vis[x]) continue;
		vis[x] = true;
		for (re int i = h1[x]; i ; i = e1[i].next)
		{
			int y = e1[i].to, w = e1[i].w;
			if (dist[x] + w < dist[y])
			{
				dist[y] = dist[x] + w;
				q.push({dist[y], y});
			}
		}
	}
}

void kruskal2(int s)
{
	memset(vis, false, sizeof(vis));
	for (re int i = 1; i <= n; i ++) dist[i] = inf;
	dist[s] = 0;
	q.push({0, s});
	while (!q.empty())
	{
		int x = q.top().second; q.pop();
		if (vis[x]) continue;
		vis[x] = true;
		for (re int i = h2[x]; i ; i = e2[i].next)
		{
			int y = e2[i].to, w = e2[i].w;
			if (dist[x] + w < dist[y])
			{
				dist[y] = dist[x] + w;
				q.push({dist[y], y});
			}
		}
	}
}

signed main()
{
	ios::sync_with_stdio(false);
	cin.tie(0); cout.tie(0);
	
	cin >> n >> m;
	for (re int i = 1; i <= m; i ++)
	{
		int x, y, w;
		cin >> x >> y >> w;
		add1(x, y, w);
		add2(y, x, w);
	}
	kruskal1(1);
	for (int i = 1; i <= n; i ++) ans += dist[i];
	kruskal2(1);
	for (int i = 1; i <= n; i ++) ans += dist[i];
	cout << ans;
	
	return 0;
}

总结 \(\scr{Summary}\)

  1. 跑最短路时可以考虑 建反图 来转化思路,优化时效;
posted @ 2023-08-08 23:08  Zhang_Wenjie  阅读(58)  评论(0)    收藏  举报