文章目录
树与图的存储
树是一种特殊的图,是无环连通图
图分为有向图和无向图(边是否有方向)(无向图可以看成特殊的有向图)
有向图的存储分为邻接矩阵和邻接表
邻接矩阵:开一个二维数组,g[a][b]存储a->b这条边的信息(布尔值或者权重)
邻接矩阵不能存储重边,如果有,就保留一条边
用的比较少,适合存储稠密图,n^2
邻接表:用的多,有n个点,就开n个单链表
(与拉链法的哈希表类似)
- 邻接矩阵:一般开二维数组
g[a][b]
存储边a->b
存储稠密图(m>n^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
#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的点
#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的平方的图称为稀疏图,反之是稠密图
一. 单源最短路分为 所有边权都是正数 和 存在负边权:
- 所有边权都是正数: 朴素Dijkstra算法O(n^2)(稠密图)和 堆优化版的Dijkstra算法O(mlogn)(稀疏图)
- 存在负权边: Bellman-Ford O(nm)(求不超过k条边)和 SPFA 一般 O(m),最坏O(nm)
二. 多源汇最短路: Floyd算法 O(n^3)
朴素dijkstra算法
#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
#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算法
#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算法
#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算法
#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算法
//最小生成树
//克鲁斯卡尔算法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;
}
匈牙利算法
//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;
}