竞赛学习-图论基础

1. 图的基本概念

图的相关概念可结合视频学习

图的基本知识     图的进阶知识

图的定义:

有向图和无向图:

简单图和多重图:

稠密图和稀疏图:

顶点的度:

顶点 v 的度是指与它相关联的边的条数,记作 deg(v)。由该顶点发出的边称为顶点的出度,到达该顶 点的边称为顶点的⼊度。

• 无向图中,顶点的度等于该顶点的⼊度(indev)和出度(outdev),即 deg(v) = indeg(v) =
outdeg(v)。

• 有向图中,顶点的度等于该顶点的⼊度与出度之和,其中顶点 v 的⼊度 indeg(v) 是以 v 为终点的有
向边的条数,顶点 v 的出度 outdeg(v) 是以 v 为起始点的有向边的条数,deg(v) = indeg(v) +
outdeg(v)。

路径:

简单路径和回路:

路径长度和带权路径长度:

某些图的边具有与它相关的数值,称其为该边的权值。这些权值可以表⽰两个顶点间的距离、花费的代价、所需的时间等。一边将该种带权图称为网络


对于不带权的图,⼀条路径的路径⻓度是指该路径上的边的条数。
对于带权的图,⼀条路径的路径⻓度是指该路径上各个边权值的总和

子图:

G1_1 和 G1_2 为⽆向图 G1 的⼦图,G1_1 为 G1 的生成子图。
G2_1 和 G2_2 为有向图 G2 的⼦图,G2_1 为 G2 的生成子图。

连通图与联通分量:

生成树:

连通图的⽣成树是包含图中全部顶点的⼀个极⼩连通⼦图。若图中顶点数为 n,则它的⽣成树含有 n -1 条边。对⽣成树⽽⾔,若砍去⼀条边,则会变成⾮连通图,若加上⼀条边则会形成⼀个回路。

图的存储和遍历:


图的存储有两种:邻接矩阵和邻接表:
• 其中,邻接表的存储⽅式与树的孩⼦表⽰法完全⼀样。因此,⽤ vector 数组以及链式前向星就能实
现。
• 而邻接矩阵就是⽤⼀个⼆维数组,其中 edges[i][j] 存储顶点 i 与顶点 j 之间,边的信
息。

邻接矩阵:

邻接矩阵,指⽤⼀个矩阵(即⼆维数组)存储图中边的信息(即各个顶点之间的邻接关系),存储顶点之间
邻接关系的矩阵称为邻接矩阵。
对于带权图⽽⾔,若顶点 vi和vj 之间有边相连,则邻接矩阵中对应项存放着该边对应的权值,若顶
点 vi和 vj不相连,则⽤∞来代表这两个顶点之间不存在边。

对于不带权的图,可以创建⼀个⼆维的 bool 类型的数组,来标记顶点 vi 和 vj 之间有边相连。

#include<iostream>
#include<cstring>
using namespace std;

const int N = 1010;
int n, m;//顶点个数  边数
int edges[N][N];
int main()
{
	memset(edges, -1, sizeof edges);
	cin >> n >> m;
	for (int i = 1; i <= m; i++)
	{
		int a, b, c; cin >> a >> b >> c;
		edges[a][b] = c;
		//a-b有一条边,权值为c

		//如果是无向图的话,需要反存
		edges[b][a] = c;
		
	}

	return 0;
}
vector 数组:

和树的存储⼀模⼀样,只不过如果存在边权的话,我们的 vector 数组⾥⾯放⼀个结构体或者是 pair 即可。

const int N = 1e5 + 10;
typedef pair<int, int>PII;
int n, m;
vector<PII>edges[N];
int main()
{
	cin >> n >> m;
	for (int i = 1; i <= n; i++)
	{
		int a, b, c; cin >> a >> b >> c;
		edges[a].push_back({ b,c });
		//如果是无向图的话,需要反存
		edges[b].push_back({ a,c });
	}
}
链式前向星:

和树的存储⼀模⼀样,只不过如果存在边权的话,我们多创建⼀维数组,⽤来存储边的权值即可。

//链式前向星
const int N = 1e5 + 10;
int h[N], e[2 * N], w[2 * N], ne[2 * N], id;
//h[a]存储与a节点相连的最后一个下标   e[id]存储第id个 另一个端点b
//w[id]存储第id个权值     ne[id]存储上一个下标
int n, m;
void add(int a, int b, int c)
{
	id++;
	e[id] = b;
	w[id] = c;
	ne[id] = h[a];
	h[a] = id;

}
int main()
{
	cin >> n >> m;
	for (int i = 1; i <= m; i++)
	{
		int a, b, c; cin >> a >> b >> c;
		add(a, b, c);
		//无向图反存
		add(b, a, c);
	}

}

图的遍历分两种:DFS 和 BFS,和树的遍历方式以及实现⽅式完全⼀样。因此,可以仿照树这个数据 结构来学习。

DFS和BFS:

1.邻接矩阵

#include<iostream>
#include<cstring>
using namespace std;

const int N = 1010;
int n, m;//顶点个数  边数
int edges[N][N];

bool st[N];
void dfs(int u)  //u表示第一个端点,v表示第二个端点
{
	cout << u << endl;
	st[u] = true;

	for (int v = 1; v <= n; v++)
	{
		if (edges[u][v] != -1 && st[v] == false)//也可写为!st[v]
		{
			dfs(v);
		}
	}
}
void bfs(int u)
{
	queue<int>q;
	q.push(u);
	st[u] = true;

	while (q.size())
	{
		int t = q.front(); q.pop();
		cout << t << endl;

		for (int i = 1; i <= n; i++)
		{
			if (edges[t][i] != -1 && !st[i])
			{
				q.push(i);
				st[i] = true;
			}
		}
	}
}
int main()
{
	memset(edges, -1, sizeof edges);
	cin >> n >> m;
	for (int i = 1; i <= m; i++)
	{
		int a, b, c; cin >> a >> b >> c;
		edges[a][b] = c;
		//a-b有一条边,权值为c

		//如果是无向图的话,需要反存
		edges[b][a] = c;
		
	}

	return 0;
}

2.vector数组

const int N = 1e5 + 10;
typedef pair<int, int>PII;
int n, m;
vector<PII>edges[N];

bool st[N];
void dfs(int u)
{
	cout << u << endl;
	st[u] = true;

	for (auto t : edges[u])
	{
		int v = t.first; int w = t.second;
		if (!st[v])
		{
			dfs(v);
		}
	}
}

void bfs(int u)
{
	queue<int>q;
	q.push(u);
	st[u] = true;

	while (q.size())
	{
		int a = q.front(); q.pop();
		cout << a << endl;

		for (auto t : edges[a])
		{
			int b = t.first; int c = t.second;
			if (!st[b])
			{
				q.push(b);
				st[b] = true;
			}
		}
	}
}
int main()
{
	cin >> n >> m;
	for (int i = 1; i <= n; i++)
	{
		int a, b, c; cin >> a >> b >> c;
		edges[a].push_back({ b,c });
		//如果是无向图的话,需要反存
		edges[b].push_back({ a,c });
	}
}

3.链式前向星

const int N = 1e5 + 10;
int h[N], e[2 * N], w[2 * N], ne[2 * N], id;
//h[a]存储与a节点相连的最后一个下标   e[id]存储第id个 另一个端点b
//w[id]存储第id个权值     ne[id]存储上一个下标
int n, m;

void add(int a, int b, int c)
{
	id++;
	e[id] = b;
	w[id] = c;
	ne[id] = h[a];
	h[a] = id;

}

bool st[N];
void dfs(int u)
{
	cout << u << endl;
	st[u] = true;

	for (int i = h[u]; i; i = ne[i])//i先被赋值为最后一个与u相连的节点的下标,i不为0,i更新为上一个节点下标
	{
		int v = e[i];
		if (!st[v])
		{
			dfs(v);
		}
	}
}
void bfs(int u)
{
	queue<int>q;
	q.push(u);
	st[u] = true;

	while (q.size())
	{
		int a = q.front(); q.pop();
		cout << a << endl;

		for (int i = ne[a]; i; i = ne[i])
		{
			int b = e[i]; int c = w[i];
			if (!st[b])
			{
				q.push(b);
				st[b] = true;
			}
		}
	}
}
int main()
{
	cin >> n >> m;
	for (int i = 1; i <= m; i++)
	{
		int a, b, c; cin >> a >> b >> c;
		add(a, b, c);
		//无向图反存
		add(b, a, c);
	}

}

最小生成树:


一个具有n个顶点的连通图,其生成树为包含n-1条边和所有顶点的极小连通子图。对于生成树来说,若砍去一条边就会使得图不连通;若增加一条就会形成回路。

Prim算法:O(n^2+m)

核心:不断加点。
Prim 算法构造最小生成树的基本思想:
1. 从任意一个点开始构造最小生成树;
2. 将距离该树权值最小且不在树中的顶点,加入到生成树中。然后更新与该点相连的点到生成树的最短距离;
3. 重复操作n次,直到所有顶点都加入为止。

prim流程-->n次(找最小值--☑判断是否联通☑--最小值入树--更新最小值)

邻接矩阵--代码实现:

最小生成树【模板】(下述为两种实现代码)

1.邻接矩阵实现
const int N = 5010;     //https://www.luogu.com.cn/problem/P3366
int edges[N][N];
int n, m;

bool st[N];//标记某个点到生成树的最短距离
int dist[N];//标记某个点是否加入生成树
int prim()
{
	memset(dist, 0x3f, sizeof dist);

	int ret = 0;
	dist[1] = 0;//葱节点1开始造树
	for (int i = 1; i <= n; i++)//循环加入n个点
	{
		int t = 0;//用t来标记最短距离点的下标

		for (int j = 1; j <= n; j++)//遍历找最小值
		{
			if (!st[j] && dist[j] < dist[t])
				t = j;
		}
		//判断是否联通
		if (dist[t] == 0x3f3f3f3f)return 0x3f3f3f3f;
		st[t] = true;
		ret += dist[t];
		
		//更新最短路径
		for (int j = 1; j <= n; j++)
		{
			if (edges[t][j] < dist[j])dist[j] = edges[t][j];
		}



	}
	return ret;
}
int main()
{
	cin >> n >> m;
	memset(edges, 0x3f, sizeof edges);//初始化

	for (int i = 1; i <= m; i++)
	{
		int a, b, c; cin >> a >> b >> c;
		edges[a][b] = min(edges[a][b], c); //重复路劲、环
		edges[b][a] = min(edges[b][a], c);

	}
	int ret = 0;
	ret = prim();
	if (ret == 0x3f3f3f3f)cout << "orz" << endl;
	else cout << ret << endl;

}

vector数组--代码实现:

const int N = 5010,INF=0x3f3f3f3f;
typedef pair<int, int>PII;
vector<PII>edges[N];
int n, m;

int dist[N];
bool st[N];
int prim()
{
	memset(dist, 0x3f, sizeof dist);
	int ret = 0;
	dist[1] = 0;

	for (int i = 1; i <= n; i++)//n个顶点入树
	{
		int t = 0;
		for (int j = 1; j <= n; j++)
		{
			if (!st[j] && dist[t] > dist[j])
				t = j;
		}

		if (dist[t] == INF)return INF;//判断是否不联通
		st[t] = true;
		ret += dist[t];

		for (auto x : edges[t])
		{
			int b = x.first; int c = x.second;
			if (dist[b] > c)dist[b] = c;

		}
	}
	return ret;
}
int main()
{
	cin >> n >> m;
	//memset(edges, 0x3f, sizeof edges);  //vector数组不需要,因为后续 获取边时均合法
	for (int i = 1; i <= m; i++)
	{
		int a, b, c; cin >> a >> b >> c;
		edges[a].push_back({ b,c });//此处不用重边取小,prim算法会自动取小
		edges[b].push_back({ a,c });

	}

	int ret = prim();
	if (ret == INF)cout << "orz" << endl;
	else cout << ret << endl;
}

Kruskal算法:O(mlog m)

核心:不断加边
1. 所有边按照权值排序;
2. 每次选出权值最⼩且两端顶点不连通的⼀条边,直到所有顶点都联通。

最小生成树【模板】

Kruskal流程:初始化--进行边操作--☑判断是否联通☑

const int N = 5010,M=2e5+10,INF=0x3f3f3f3f;
int fa[N];//并查集
int n, m;
struct node
{
	int a, b, c;
}a[M];  //m组
int find(int x)
{
	if (x == fa[x])return x;
	return fa[x] = find(fa[x]);

	//return fa[x]==x?x:fa[x]=find(fa[x]);
}
int cmp(node& a, node& b)
{
	return a.c < b.c;
}
int kk()
{
	sort(a + 1, a + m + 1, cmp);//对边的权值进行排序

	int ret = 0;
	int cnt = 0;//对边进行计数,用于判断是否联通
	for (int i = 1; i <= m; i++)
	{
		int x = a[i].a; int y = a[i].b; int c = a[i].c;
		int fx = find(x); int fy = find(y);
		if (fx != fy)
		{
			cnt++;
			ret += c;
			fa[fx] = fy;
		}
	}
	return cnt == n - 1?ret : INF;//此处是判断是否联通
}

int main()
{
	cin >> n >> m;
	for (int i = 1; i <= n; i++)fa[i] = i;

	for (int i = 1; i <= m; i++)
	{
		cin >> a[i].a >> a[i].b >> a[i].c;
	}

	int ret = kk();
	if (ret == INF)cout << "orz" << endl;
	else cout << ret << endl;



}

练习部分:

(一般以Kruskal算法为主,因为并查集较为熟悉,当mlogm明显大于n^2时使用Prim算法)

1.

第一题:P1194 买礼物

Kruskal算法:

const int N = 510;
int v, n;
int fa[N];//n个节点的关系
struct node
{
	int a, b, c;
}a[N*N];
bool cmp(node& a, node& b)
{
	return a.c < b.c;
}
int find(int x)
{
	return fa[x] == x ? x : fa[x] = find(fa[x]);
}
int kk()
{
	sort(a + 1, a + n * n + 1, cmp);
	int ret = 0;
	int cnt = 0;
	for (int i = 1; i <= n * n; i++)
	{
		int x = a[i].a; int y = a[i].b; int c = a[i].c;
		int fx = find(x); int fy = find(y);
		if (fx != fy)
		{
			ret += c;
			cnt++;
			fa[fx] = fy;
		}
		if(cnt==n-1) return ret;
	}
	
}
int main()
{
	cin >> v >> n;
	
	for (int i = 1; i <= n * n; i++)fa[i] = i;
	for (int i = 1; i <= n; i++)
	{
		for (int j = 1; j <= n; j++)
		{
			
			int tmp; cin >> tmp;
			int sum = (i - 1) * n + j;
			a[sum].a = i;
			a[sum].b = j;
			a[sum].c = v;//全部先初始化为最初标价v
			if(i!=j&&tmp!=0)a[sum].c = min(tmp, a[sum].c);//tmp只有在不为0时更新
		}
		
	}

	int ret = kk();
	cout << ret + v << endl;//路径和加上最初一个点的价格v
}

Prim算法:

const int N = 510;
int edges[N][N];
int v, n;
int dist[N];
bool st[N];
int prim()
{
	memset(dist, 0x3f, sizeof dist);
	dist[1] = 0;
	int ret = v;   //初始化时直接计入第一个点的价值

	for (int i = 1; i <= n; i++)//n个点入树
	{
		int t = 0;
		//找最小权值
		for (int j = 1; j <= n; j++)
		{
			if (!st[j] && dist[j] < dist[t])
				t = j;
		}
		ret += dist[t];
		st[t] = true;
		//此题不用判断是否联通

		//更新最短路径
		for (int j = 1; j <= n; j++)
		{
			dist[j] = min(dist[j], edges[t][j]);
		}
	}
	return ret;
}
int main()
{
	cin >> v >> n;
	for (int i = 1; i <= n; i++)
	{
		for (int j = 1; j <= n; j++)
		{
			edges[i][j] = v;int tmp; cin >> tmp;
			if (i != j && tmp != 0)edges[i][j] = min(edges[i][j], tmp);

		}
	}
	int ret = prim();
	cout << ret << endl;
}

prim流程-->n次(找最小值--判断是否联通--最小值入树--更新最小值)

2.

第二题:P2330 [SCOI2005] 繁忙的都市

//Kruskal算法
const int N = 310, M = 8010;
int n, m;
struct node
{
	int a, b, c;
}a[M];
int fa[N];//n个节点的关系
int find(int x)
{
	return fa[x] == x ? x : fa[x] = find(fa[x]);
}
bool cmp(node& a, node& b)
{
	return a.c < b.c;
}
int kk()
{
	//初始化
	for (int i = 1; i <= n; i++)fa[i] = i;
	sort(a + 1, a + m + 1, cmp);

	int ret = 0;//此处记得是max
	for (int i = 1; i <= m; i++)
	{
		int x = a[i].a; int y = a[i].b; int c = a[i].c;
		int fx = find(x), fy = find(y);
		if (fx != fy)
		{
			ret = max(c, ret);
			fa[fx] = fy;
		}
	}
	//判断联通--此题不需要
	return ret;
}
int main()
{
	cin >> n >> m;
	for (int i = 1; i <= m; i++)cin >> a[i].a >> a[i].b >> a[i].c;

	int ret = kk();
	cout << n - 1 << " "<<ret<<endl;
    return 0;
}
//Prim算法
#include<iostream>
#include<cstring>
#include<vector>
const int N = 310, M = 8010, INF = 0x3f3f3f3f;
typedef pair<int, int>PII;
int n, m;
vector<PII>edges[N];

int ret=0;
int dist[N];//存的是每个点到树的最短距离
bool st[N];
void Prim()
{
	memset(dist, 0x3f, sizeof dist);
	dist[1] = 0;
	for (int i = 1; i <= n; i++)//n个点入树
	{
		int t = 0;
		for (int j = 1; j <= n; j++)//遍历找最小值
		{
			if (!st[j] && dist[j] < dist[t])
				t = j;
		}
		//判断连通性---此题不需要
		//if (dist[t] == INF)return INF;

		//点入树
		ret = max(ret, dist[t]);
		st[t] = true;

		//更新最小值
		for (auto x:edges[t])
		{
			int b = x.first; int c = x.second;
			dist[b] = min(dist[b], c);
		}
	}
}
int main()
{
	cin >> n >> m;
	for (int i = 1; i <= m; i++)
	{
		int a, b, c; cin >> a >> b >> c;
		edges[a].push_back({ b,c });
		edges[b].push_back({ a,c });
	}

	Prim();
	cout << n - 1 << " " << ret << endl;
    return 0;
}
3.直接来一道省选题(与前两题的差别是,此题为有向图)

P2573 [SCOI2012] 滑雪

此题首先问的是最多可以到达多少个点,使用dfs,bfs均可解决。(顺带将1号点向外衍生的边全部存储)

第二问是在第一问的基础上求最短路径(从1号点开始),由数据范围可知不能用Prim算法(n^2-->1e10会导致超时),应该使用Kruskal算法,但是sort排序时不能只按照权值大小,此处排序的核心是先按照另一点b的高度,在点的高度b1,b2相同时再按照权值大小排序。

typedef long long LL;
typedef pair<int, int>PII;
const int N = 1e5 + 10, M = 1e6 + 10;
LL ret;
int h[N];
int fa[N];
int n, m, cnt;
vector<PII>edges[N];
int pos = 0;
int find(int x)
{
	return fa[x] == x ? x : fa[x] = find(fa[x]);
}
struct node
{
	int a, b, c;
}e[M];
bool st[N];//标记DFS中哪些点已经遍历过
void dfs(int i)
{
	cnt++;
	st[i] = true;
	for (auto x : edges[i])
	{
		int a = i, b = x.first, c = x.second;
		e[++pos].a = a; e[pos].b = x.first; e[pos].c = x.second;
		if (!st[b])dfs(b);
	}
}
bool cmp(node& x, node& y) //根据题意,先考虑高度,再考虑路径长度
{
	int a1 = x.a, b1 = x.b, c1 = x.c;
	int a2 = y.a, b2 = y.b, c2 = y.c;
	if (h[b1] != h[b2])return h[b1] > h[b2];
	else return c1<c2;
}
void kk()
{
	for (int i = 1; i <= n; i++)fa[i] = i;

	sort(e + 1, e + pos + 1, cmp);
	for (int i = 1; i <= pos; i++)
	{
		int x = e[i].a, y = e[i].b, c = e[i].c;
		int fx = find(x), fy = find(y);
		if (fx != fy)
		{
			ret += c;
			fa[fx] = fy;
		}
	}
}

int main()
{
	cin >> n >> m;
	for (int i = 1; i <= n; i++)cin >> h[i];
	for (int i = 1; i <= m; i++)
	{
		int a, b, c; cin >> a >> b >> c;
		if (h[a] > h[b])edges[a].push_back({ b,c });
		else if (h[a] == h[b])
		{
			edges[a].push_back({ b,c });
			edges[b].push_back({ a,c });
		}
		else edges[b].push_back({ a,c });
	}
	dfs(1);
	kk();
	cout << cnt << " " << ret << endl;
}

拓扑排序--单源最短路--多源最短路 后续更新

评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值