基础图论

树与图的存储

树是一种特殊的图,是无环连通图

图分为有向图和无向图(边是否有方向)(无向图可以看成特殊的有向图)

有向图的存储分为邻接矩阵和邻接表

邻接矩阵:开一个二维数组,g[a][b]存储a->b这条边的信息(布尔值或者权重)
邻接矩阵不能存储重边,如果有,就保留一条边
用的比较少,适合存储稠密图,n^2

邻接表:用的多,有n个点,就开n个单链表
(与拉链法的哈希表类似)

  1. 邻接矩阵:一般开二维数组g[a][b]存储边a->b
    存储稠密图(m>n^2)
  2. 邻接标:存储稀疏图(m<<n^2)
int h[N], e[N], ne[N], idx;

// 添加一条边a->b
void add(int a, int b){
    e[idx] = b, ne[idx] = h[a], h[a] = idx ++ ;
}

// 初始化
idx = 0;
memset(h, -1, sizeof h);

树与图的遍历

深度优先遍历dfs

基础图论-D. 树的重心
在这里插入图片描述

#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 100010, M = N * 2;
int n;
int h[N], e[M], ne[M], idx;//根链表定义变量一样,h[N]是head,有n个链表
bool st[N];
int ans = N;//全局答案

//链表插入操作
void add(int a, int b) {
	e[idx] = b;
	ne[idx] = h[a];
	h[a] = idx++;
}

//返回以u为根的子树中点的数量
int dfs(int u) {
	st[u] = true;//标记一下,已经被搜过了
	int sum = 1, res = 0;
	for (int i = h[u]; i != -1; i = ne[i]) {
		int j = e[i];
		if (!st[j]) {//如果还没有被搜过
			int s = dfs(j);//s表示当前这个子树的大小
			res = max(res, s);//子树中最大的
			sum += s;//以这个儿子为根节点的子树是以u为根节点的一部分
		}
	}
	res = max(res, n - sum);//以u为根节点的子树和剩余的连通块取最大值,
	//res存的就是把u删掉之后,各个连通块中点数的最大值
	ans = min(ans, res);//就是求res中的最小值,也就是数的重心
	return sum;
}

int main() {
	cin >> n;
	memset(h, -1, sizeof h);
	for (int i = 0; i < n - 1; i++) {//构图
		int a, b;
		cin >> a >> b;
		add(a, b), add(b, a);//无向边
	}
	dfs(1);
	cout << ans << endl;
	return 0;
}

宽度优先遍历bfs

在这里插入图片描述

#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N  = 100010;
int n, m;
int h[N], e[N], ne[N], idx;//邻接表
int d[N], q[N]; //d是距离,q是队列

void add(int a, int b) {
	e[idx] = b;
	ne[idx] = h[a];
	h[a] = idx++;
}

int bfs() {
	int hh = 0, tt = 0;
	q[0] = 1; //第一个元素是起点1
	memset(d, -1, sizeof d);
	d[1] = 0;
	while (hh <= tt) {
		int t = q[hh++];//每一次取出队头
		for (int i = h[t]; i != -1; i = ne[i]) {//扩展一下当前这个点
			int j = e[i];//j来表示当前这个点可以到的地方
			if (d[j] == -1) {//如果j没有被扩展过的话
				d[j] = d[t] + 1;//就扩展j这个点,更新距离
				q[++tt] = j;//把他加到队列里去
			}
		}
	}
	return d[n];//返回1号点到n号点的最短距离
}

int main() {
	cin >> n >> m;
	memset(h, -1, sizeof h);
	for (int i = 0; i < m; i++) {//读入所有的边
		int a, b;
		cin >> a >> b;
		add(a, b);//插入所有的边
	}
	cout << bfs() << endl;
	return 0;
}

拓扑排序

图的拓扑序列只针对有向图
有向无环图被称为拓扑图
一个点的入度是指有多少条边是指向自己
一个点有几条边出去就是这个点的出度
一个有向无环图一定至少存在一个入度为0的点

基础图论-E. 有向图的拓扑序列
在这里插入图片描述

#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 100010;
int n, m;
int h[N], e[N], ne[N], idx;
int q[N], d[N];
//q是宽搜队列,d是这个点的入度

void add(int a, int b) {//邻接表
	e[idx] = b;
	ne[idx] = h[a];
	h[a] = idx++;
}

bool topsort() {
	int hh = 0, tt = -1;
	for (int i = 1; i <= n; i++)
		if (!d[i]) //如果这个点不存在入度
			q[++tt] = i;//就把这个点加到队列里
	while (hh <= tt) {
		int t = q[hh++];//取出队头元素
		for (int i = h[t]; i != -1; i = ne[i]) {//拓展队头元素
			int j = e[i];//找到出边
			d[j]--;//删掉入边
			if (d[j] == 0) //如果这个点的入度全部被删掉了
				q[++tt] = j;//就让这个点入队
		}
	}
	//判断所有点是否已经全部入队
	return tt == n - 1; //返回队列里元素的数量是否等于n-1
}

int main() {
	cin >> n >> m;
	memset(h, -1, sizeof h);
	for (int i = 0; i < m; i++) {
		int a, b;
		cin >> a >> b;
		add(a, b); //插入一条a->b的有向边
		d[b]++;//b的入度加一
	}
	if (topsort()) {//如果存在拓扑序
		for (int i = 0; i < n; i++)
			printf("%d ", q[i]);
		puts("");
	} else {
		puts("-1");
	}
	return 0;
}

最短路

最短路分为 单源最短路 和 多源汇最短路 (源点就是起点汇点就是终点)
单源最短路: 求一个点到其他所有的点的最短路,只有一个起点
多源汇最短路: 任选两个点,求一个点到另一个点的最短路(两个点不确定)很多不同起点

n是点数,m是边数
m远小于n的平方的图称为稀疏图,反之是稠密图
一. 单源最短路分为 所有边权都是正数 和 存在负边权:

  1. 所有边权都是正数: 朴素Dijkstra算法O(n^2)(稠密图)和 堆优化版的Dijkstra算法O(mlogn)(稀疏图)
  2. 存在负权边: Bellman-Ford O(nm)(求不超过k条边)和 SPFA 一般 O(m),最坏O(nm)

二. 多源汇最短路: Floyd算法 O(n^3)

朴素dijkstra算法

基础图论-F. Dijkstra求最短路 I
在这里插入图片描述

#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 510;
int n, m;//该图为稠密图,一般来说用邻接矩阵
int g[N][N];//g[a][b]是点a到b的距离
int dist[N];//dist是这个点到起点的距离
bool st[N];//表示每个点的最短路是否已经确定了

int dijkstra() {
	memset(dist, 0x3f, sizeof dist);//初始距离都是正无穷
	dist[1] = 0;
	for (int i = 0; i < n; i++) {//迭代n次
		int t = -1;
		for (int j = 1; j <= n; j++)//找没有确定最短路的点中距离最小的那一个点
			if (!st[j] && (t == -1 || dist[t] > dist[j]))
				t = j;
		st[t] = true;//把t加到集合里去
		for (int j = 1; j <= n; j++)//用t更新其他点的距离
			dist[j] = min(dist[j], dist[t] + g[t][j]);
	}
	if (dist[n] == 0x3f3f3f3f)//说明1和n是不连通的
		return -1;
	return dist[n];
}

int main() {
	scanf("%d%d", &n, &m);
	memset(g, 0x3f, sizeof g);//初始距离都是正无穷
	while (m--) {
		int a, b, c;
		scanf("%d%d%d", &a, &b, &c);
		g[a][b] = min(g[a][b], c);//如果存在重边就保留长度最短的那条边
	}
	int t = dijkstra();
	printf("%d\n", t);
	return 0;
}

堆优化版dijkstra

基础图论-G. Dijkstra求最短路 II
在这里插入图片描述

#include <iostream>
#include <cstring>
#include <algorithm>
#include <queue>
using namespace std;
typedef pair<int, int>PII;
const int N = 150010;
int n, m;
int h[N], w[N], e[N], ne[N], idx; //稀疏图,用邻接表存图,w是边的权重
int dist[N];
bool st[N];

void add(int a, int b, int c) {
	e[idx] = b;
	w[idx] = c;//存边的权重
	ne[idx] = h[a];
	h[a] = idx++;
}

int dijkstra() {
	memset(dist, 0x3f, sizeof dist);
	dist[1] = 0;
	priority_queue<PII, vector<PII>, greater<PII>>heap;//堆优化,优先队列
	heap.push({0, 1});//表示1号点的距离是0
	while (heap.size()) {
		auto t = heap.top();//找到当前堆里最小的点
		heap.pop();
		int ver = t.second, distance = t.first;//ver表示点的编号,distance表示到起点的距离
		if (st[ver])//表示这个点已经出现过了
			continue;
		for (int i = h[ver]; i != -1; i = ne[i]) {//用当前这个点来更新其他点
			int j = e[i]; //用j来存点的编号
			if (dist[j] > distance + w[i]) {//如果当前的距离大于从t过来的距离了话
				dist[j] = distance + w[i];
				heap.push({dist[j], j});//把j这个点放到优先队列里
			}
		}
	}
	if (dist[n] == 0x3f3f3f3f)
		return -1;
	return dist[n];
}

int main() {
	scanf("%d%d", &n, &m);
	memset(h, -1, sizeof h);//邻接表初始化表头为空结点
	while (m--) {//构建邻接表
		int a, b, c;
		scanf("%d%d%d", &a, &b, &c);
		add(a, b, c);
	}
	int t = dijkstra();
	printf("%d\n", t);
	return 0;
}

Bellman-Ford算法

基础图论-H. 有边数限制的最短路
在这里插入图片描述

#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 510, M = 10010;
int n, m, k;
int dist[N], backup[N]; //backup用来备份,存上一次迭代的结果

struct Edge {//结构体存所有边
	int a, b, w;//a,b表示起点和终点,w是权重
} edges[M];

int bellman_ford() {
	memset(dist, 0x3f, sizeof dist);
	dist[1] = 0;
	for (int i = 0; i < k; i++) {//求不超过k条边的最短路,就迭代k次
		memcpy(backup, dist, sizeof dist);//备份dist数组,避免更新串联
		for (int j = 0; j < m; j++) {
			int a = edges[j].a, b = edges[j].b, w = edges[j].w;
			dist[b]  = min(dist[b], backup[a] + w);//用备份数组来更新
		}
	}
	if (dist[n] > 0x3f3f3f3f / 2)
		return -1;
	return dist[n];
}

int main() {
	scanf("%d%d%d", &n, &m, &k);
	for (int i = 0; i < m; i++) {//读入m条边
		int a, b, w;
		scanf("%d%d%d", &a, &b, &w);
		edges[i] = {a, b, w};
	}
	int t = bellman_ford();
	if (t == -1)//最短路不存在
		puts("impossible");
	else
		printf("%d\n", t);
	return 0;
}

spfa 算法(队列优化的Bellman-Ford算法)

spfa可解决有负权,也可以解决没有负权的问题
dijkstra算法大部分题也可以用spfa算法过掉,除非出题人卡数据(概率很小)
基础图论-I. spfa求最短路
在这里插入图片描述

#include <iostream>
#include <cstring>
#include <queue>
#include <algorithm>
using namespace std;
const int N = 100010;
int n, m;
int h[N], w[N], e[N], ne[N], idx;
int dist[N];
bool st[N];//st存当前这个点是否在队列当中

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

int spfa() {
	memset(dist, 0x3f, sizeof dist);//初始化所有点距离为无穷大
	dist[1] = 0;
	queue<int>q;//队列用来存储所有待更新的点
	q.push(1);
	st[1] = true;//st存当前这个点是否在队列当中,防止队列里存重复的点
	while (q.size()) {
		int t = q.front();
		q.pop();//删掉该点
		st[t] = false;//点从队列里出来,标记一下
		for (int i = h[t]; i != -1; i = ne[i]) {//更新t的所有临边
			int j = e[i];//j表示当前这个点
			if (dist[j] > dist[t] + w[i]) {
				dist[j] = dist[t] + w[i];
				if (!st[j]) {//判断j是否在队列里去,如果j不在队列里
					q.push(j);//才j加到队列里
					st[j] = true;
				}
			}
		}
	}
	if (dist[n] == 0x3f3f3f3f)
		return -1;
	return dist[n];
}

int main() {
	scanf("%d%d", &n, &m);
	memset(h, -1, sizeof h);
	while (m--) {
		int a, b, c;
		scanf("%d%d%d", &a, &b, &c);
		add(a, b, c);
	}
	int t = spfa();
	if (t == -1)
		puts("impossible");
	else
		printf("%d\n", t);
	return 0;
}

floyd算法

基础图论-J. Floyd求最短路
在这里插入图片描述

#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 210, INF = 1e9;
int n, m, Q;//Q是询问个数
int d[N][N];//d[x][y]表示x点到y点的距离

void floyd() {
	for (int k = 1; k <= n; k++)
		for (int i = 1; i <= n; i++)
			for (int j = 1; j <= n; j++)
				d[i][j] = min(d[i][j], d[i][k] + d[k][j]);
}

int main() {
	scanf("%d%d%d", &n, &m, &Q);
	for (int i = 1; i <= n; i++)//初始化邻接矩阵
		for (int j =  1; j <= n; j++)
			if (i == j)
				d[i][j] = 0;
			else
				d[i][j] = INF;
	while (m--) {
		int a, b, w;
		scanf("%d%d%d", &a, &b, &w);
		d[a][b] = min(d[a][b], w);//保证重边最小
	}
	floyd();
	while (Q--) {
		int a, b;
		scanf("%d%d", &a, &b);
		if (d[a][b] > INF / 2)
			puts("impossible");
		else
			printf("%d\n", d[a][b]);
	}
	return 0;
}

求最小生成树

朴素版prim算法

基础图论-K. Prim算法求最小生成树
在这里插入图片描述

#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 5100, INF = 0x3f3f3f3f;
int n, m;
int g[N][N];//稠密图,邻接矩阵
int dist[N];
bool st[N];

int prim() {

	memset(dist, 0x3f, sizeof dist);//距离初始化成正无穷
	int res = 0;//最小生成树里边所有边的长度之和
	for (int i = 0; i < n; i++) {
		int t = -1;//每一次找到 当前集合外所有点当中 距离 集合最近的点
		for (int j = 1; j <= n; j++)
			if (!st[j] && (t == -1 || dist[t] > dist[j]))
				t = j;//t存当前距离集合最小的点

//如果不是第一个点,并且当前距离最近的点到集合的距离时正无穷,说明这个点是不连通的,就说明不存在最小生成树
		if (i && dist[t] == INF)
			return INF;

		if (i)//否则只要不是第一条边,就把距离加到答案里去
			res += dist[t];

		for (int j = 1; j <= n; j++)//用t来更新其他点到集合的距离
			dist[j] = min(dist[j], g[t][j]);
		//dijkstra算法写的是dist[j] =dist[t] +g[t][j]

		st[t] = true;//把当前这个点加到集合里去,表明已经加到树里去了
	}
	return res;
}

int main() {
	cin >> n >> m;

	memset(g, 0x3f, sizeof g);

	while (m--) {
		int a, b, c;
		cin >> a >> b >> c;
		g[a][b] = g[b][a] = min(g[a][b], c);//,无向图,有重边
	}

	int t = prim();

	if (t == INF)
		cout << "orz" << endl;
	else
		cout << t << endl;

	return 0;
}

Kruskal算法

基础图论-P. 【模板】最小生成树
在这里插入图片描述

//最小生成树
//克鲁斯卡尔算法kruskal

#include <iostream>
#include <algorithm>
using namespace std;
const int N = 200010;
int n, m;
int p[N];//p[x]表示x的父节点

/*
operator的符号与下面return 要用的符号一致,operator的目的就是重载"<"或者其他符号。
至于(const node &a)const 大概就是不能改变原有的值,只是换顺序。直接照着写就行
*/

struct Edge {//结构体存所有边
	int a, b, w;
	bool operator< (const Edge &W)const {//重载小于号,方便按照权重来排序
		return w < W.w;
	}
} edges[N];

int find(int x) {//返回x的祖宗节点
	if (p[x] != x)
		p[x] = find(p[x]);
	return p[x];
}

int main() {
	cin >> n >> m;

	for (int i = 0; i < m; i++) {
		int a, b, w;
		cin >> a >> b >> w;
		edges[i] = {a, b, w};
	}

	sort(edges, edges + m);//把所有边按权重排序

	for (int i = 1; i <= n; i++)//初始化并查集
		p[i] = i;

	int res = 0, cnt = 0;
	for (int i = 0; i < m; i++) { //从小到大枚举所有边
		int a = edges[i].a, b = edges[i].b, w = edges[i].w;
		a = find(a), b = find(b);
		if (a != b) {//判断a与b是否连通,就是判断两个祖宗节点是否一样
			p[a] = b;
			res += w;//res存的是最小生成树当中所有树边的权重之和
			cnt++;//cnt存的是当前加入了多少条边
		}
	}
	if (cnt < n - 1)
		puts("impossible");
	else
		cout << res << endl;
	return 0;
}

染色法判别二分图

如果是一个二分图, 那么当且仅当图中不含奇数环由于图中不含奇数环, 所以染色过程中一定没有矛盾
基础图论-L. 染色法判定二分图
在这里插入图片描述

#include <iostream>
#include <cstring>
using namespace std;
const int N = 100010, M = 200010;//无向图
int n, m;
int h[N], e[M], ne[M], idx;
int color[N];

void add(int a, int b) {
	e[idx] = b;
	ne[idx] = h[a];
	h[a] = idx++;
}

bool dfs(int u, int c) {
	color[u] = c;//先记录一下,当前u的颜色是c
	for (int i = h[u]; i != -1; i = ne[i]) {//从前往后遍历一下当前这个点所有的邻点
		int j = e[i];//j存储当前这个点的编号
		if (!color[j]) {//如果当前这个点没有染过颜色的话
			if (!dfs(j, 3 - c))//就把他染一下,染成另外一种颜色,就是1要变成2,2要变成1
				return false;
		} else if (color[j] == c)//如果当前j已经染过颜色,并且j的颜色与当前颜色相同的话
			return false;//就说明有矛盾
	}
	return true;//成功没有矛盾就返回true
}

int main() {
	cin >> n >> m;
	memset(h, -1, sizeof h);
	while (m--) {
		int a, b;
		cin >> a >> b;
		add(a, b);
		add(b, a);
	}
	bool flag = true;
	for (int i = 1; i <= n; i++)
		if (!color[i]) {//如果当前这个点没有被染过颜色,就把他染一下
			if (!dfs(i, 1)) {//判断是否有矛盾发生,如果返回false,就说明有矛盾发生
				flag = false;
				break;
			}
		}
	if (flag)//如果flag是true,说明整个过程没有矛盾发生,就说明是二分图
		puts("Yes");
	else
		puts("No");
	return 0;
}

匈牙利算法

基础图论-M. 二分图的最大匹配
在这里插入图片描述

//P58二分图的最大匹配
//匈牙利算法
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 510, M = 100010;
int n1, n2, m;//可以把n1看成男生,n2看成女生
int h[N], e[M], ne[M], idx;
int match[N];//右边这些点对应左边的点
bool st[N];//判重,每一次不要重复搜一个点

void add(int a, int b) {
	e[idx]  = b;
	ne[idx] = h[a];
	h[a]  = idx++;
}

bool find(int x) {//判断一个男生能否找到一个合适的女生
	for (int i = h[x]; i != -1; i = ne[i]) {//枚举这个男生所有看上的女生
		int j = e[i];//j表示集合当中点的编号
		if (!st[j]) {//如果这个妹子之前没有考虑过
			st[j] = true;
			if (match[j] == 0 || find(match[j])) {
				//如果这个妹子还没有匹配任何男生,或者说她匹配到了某个男生,
				//但是可以为那个男生找到下家,那么这个女生就空出来了
				match[j] = x;//当前这个妹子就可以匹配这个男生
				return true;
			}
		}
	}
	return false;//匹配不到,就返回false
}

int main() {
	cin >> n1 >> n2 >> m;
	memset(h, -1, sizeof h);
	while (m--) {
		int a, b;
		cin >> a >> b;
		add(a, b);
	}
	int res = 0;//res存的是当前匹配的数量
	for (int i = 1; i <= n1; i++) {//从前往后依次分析一下每个男生该找那个妹子
		memset(st, false, sizeof st);//分析之前,先把所有妹子清空,表示这些妹子还没有考虑过
		if (find(i))//如果这个男生成功找到一个妹子,
			res++;//答案+1
	}
	cout << res << endl;
	return 0;
}
  • 21
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值