图的概念:
逻辑结构:多对多
graph 图 g
vertex 顶点 v
edge 边\弧 e
图G是由两个集合V和E构成,G = (V,E),其中V是顶点的有限非空集合,E是V中顶点偶对的有限集,这些顶点偶对称为边(弧)
重边:两个顶点不只有一条边
自边:顶点自身形成一条边
图的分类:
不同的维度:
1.方向性:有向图和无向图
2.环:带环图和无环图
//环:从一个点x出发,沿着边走,最终可以回到x
注:有向无环图(DAG)
3.边权:无权图 带权图
图的基本术语:
1.简单图:若不存在顶点到自身的边,且同一条边不重复出现,则称这样的图为简单图
2.无向图:在图中,如果代表边的顶点偶对是无序的,则为无向图
3.有向图:在图中,如果代表边的顶点偶对是有序的,则为有向图
//有向边(弧)
//起点:弧尾
//终点:弧头
//注:无向图可以看成有向图
4.完全图:图中每两个顶点都有一条边
//完全有向图:n(n-1)条边
//完全无向图:n(n-1)/2条边
5.端点,邻接点;
端点:边的两个顶点
邻接点:上述两个顶点互为邻接点
6.顶点的度,入度和出度
//对于无向图而言
度:该点所连的边数
//对于有向图而言
入度;入边的个数
出度:出边的个数
度:出度+入度(一定为偶数)
7.子集:与子树类似
8.路径,路径长度:
路径:顶点序列
路径长度:边的数目
简单路径:一条路径上除开始点和结束点可以相同外,其余顶点均不相同(路径中不含环)
9.回路,环
欧拉环路:经过图中各边一次且恰好一次的环路
哈密尔顿环路:经过图中各顶点一次且恰好一次的环路
10.连通,连通图和连通分量(针对无向图)
1.连通:如果顶点x和y之间存在可以相互抵达的路径,则称两个顶点连通
2.连通图:图中任意两个顶点都连通
3.连通分量:无向图中的极大连通子图称为图的连通分量
注:连通图只有一个极大连通子图(自身)
非连通图有多个极大连通子图
注:极大连通子图(连通分量)
极小连通子图(图的生成树):在保证各点连通的情况下,边最少
11.强连通图和强连通分量(针对有向图,含义同上)
12.稠密图和稀疏图
一个图接近完全图时,称为稠密图,当一个图含有较少边数时,称为稀疏图,一般以nlogn作为分界线(n为节点个数)
13.权和网
边上带有权的图称为带权图,也称之为网
//注:自边权值看作零,两个点之间没有连接权值看作无穷大
14.连通图的生成树(针对无向图)
连通图的生成树是一个极小的连通子图
图的存储:
1.边集数组
2.邻接矩阵
3.邻接表
4.十字链表
5.多重邻接表
有向图/无向图:边集数组,邻接矩阵,邻接表
有向图:十字链表
无向图:多重邻接表
做题:邻接矩阵,链式前向星(邻接表) //链式前向星后续再学习
存储图:
(1)边集数组:不太方便,效率低,但是简单
存点:数组存点集
存边:起点,终点,边权
(2)邻接矩阵:如果为稀疏图,存在较大空间浪费
存点:数组存点集
存边:二维数组(邻接矩阵),下标就是点的编号
//无权图可以用bool类型二维数组
有向图;i->j j->i e[i][j] != e[j][i]
无向图:i->j j->i e[i][j] == e[j][i]
(3)邻接表:方便算出度,不方便算入度(对于有向图而言)
//为了方便计算入度,可以建立逆邻接表,但不方便计算出度
//二者结合形成十字链表
存点:数组存点集
存边:
1.无向图
把每个顶点的邻接点构成线性表,因为线性表大小不定,所以使用链表
2.有向图
邻接表:顶点后面挂的单链表,由其出边邻接点构成
逆邻接表:顶点后面挂的单链表,由其入边邻接点构成
(4)十字链表:结构复杂,操作简单
把有向图的邻接表和逆邻接表结合起来
结构复杂,使用简单
(5)多重邻接表
同上
//注:做题常用邻接矩阵和邻接表
图的遍历:从图中某点出发,把所有点访问且仅访问一次
1.深度优先遍历(DFS)----->递归/栈
1.右手原则:在没有碰到重复顶点的情况下,分叉口始终是向右边走,每路过一个顶点就做一个记号
2.左手原则:在没有碰到重复顶点的情况下,分叉口始终是向左边走,每路过一个顶点就做一个记号
步骤:
1.首先选定一个未被访问的顶点V作为起始顶点,并将其标记为已经访问过
2.然后搜索与顶点V邻接的所有顶点,判断这些顶点是否被访问过,如果有未被访问过的顶点W,再选取与顶点W邻接的未被访问的一个顶点进行访问,依次重复进行。当一个顶点的所有的邻接顶点都被访问过时,则依次回退到最近被访问的顶点。若该顶点还有其他邻接点未被访问,则从这些未被访问的顶点中取出一个并重复上述过程,直到与起始顶点V相邻接的所有顶点都被访问过为止。
3.若此时图中依然有顶点未被访问,则再选取其中一个顶点作为起始顶点并进行遍历,转第二步,反之,则遍历结束。//即可能该图为非连通图,有多个极大连通子图
2.广度优先遍历(BFS)----->队列
注:图遍历序列不唯一
遍历时间复杂度(DFS与BFS一样)
邻接矩阵(n^2) //n为点的个数
邻接表(n+m) //n为点的个数,m为边的个数
BFS优缺点:
缺点:在搜索过程中,BFS必须要保存搜索过程中的状态,用于判重
优点:一般用于找无权图最短路径
DFS优缺点:
缺点:容易超时/或爆栈
优点:使用空间少,一般用于找所有的解
边集数组:
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
using namespace std;
class Edge
{
public:
int start;//起点
int end;//终点
int w;//权值
};
class Graph
{
public:
Edge* e;//边集
int* vertex;//点集
int size;//点的个数
public:
Graph(int sz):size(sz)
{
vertex = new int[sz];
e = new Edge[sz * (sz - 1)];//若为无向图,则(sz * (sz - 1)) / 2
if (vertex == nullptr || e == nullptr)
{
cout << "开辟失败" << endl;
}
}
void Find(int x)//找邻接点
{
for (int i = 0; i < size * (size - 1); ++i)
{
if (e[i].start == x)
{
cout << e[i].end << endl;
}
if (e[i].end == x)
{
cout << e[i].start << endl;
}
}
}
};
int main()
{
int a(4);
Graph g(a);
return 0;
}
邻接矩阵:
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
using namespace std;
//有权无向图为例
class Graph
{
public:
int** edge;//邻接矩阵
int* vertex;//点集
int size;//点的个数
int size2;//边的个数
public:
Graph(int sz,int sz2) :size(sz),size2(sz2)
{
vertex = new int[sz];
edge = new int* [sz];
if (vertex == nullptr || edge == nullptr)
{
cout << "开辟失败" << endl;
}
for (int i = 0; i < sz; ++i)
{
edge[i] = new int[sz];
if (edge[i] == nullptr)
{
cout << "开辟失败" << endl;
}
}
}
void Find(int x)//找邻接点,有权无向图,权值大于零
{
for (int i = 0; i < size; ++i)
{
if (edge[x][i] > 0)
{
cout << i << endl;
}
}
}
};
int main()
{
int a(4);
Graph m(a,a);
return 0;
}
邻接表:
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
using namespace std;
//无向带权图为例
class Node
{
public:
int adjv;//邻接点编号
int m_w;//权值
Node* next;
};
class Graph_Node
{
public:
int vertex_i;//点的编号
Node* first;//不使用头节点,直接使用头指针
};
class Graph
{
public:
int size1;//点的个数
int size2;//边的个数
Graph_Node* g;//邻接表
public:
Graph(int sz1,int sz2):size1(sz1),size2(sz2)
{
g = new Graph_Node[sz1];
if (g == nullptr)
{
cout << "开辟失败" << endl;
}
//初始化点
for (int i = 0;i < sz1; ++i)
{
cin >> g[i].vertex_i;
g[i].first = nullptr;
}
//输入边和权值
int x(0);
int y(0);
int w(0);
for (int i = 0; i < sz2; ++i)
{
cin >> x >> y >> w;
//y为x邻接点
Node* s = new Node;
s->adjv = y;
s->m_w = w;
//进行头插
s->next = g[x].first;
g[x].first = s;
//x为y邻接点(开辟一块新的空间)
s = new Node;
s->adjv = x;
s->m_w = w;
//进行头插
s->next = g[y].first;
g[y].first = s;
}
}
};
int main()
{
return 0;
}
十字链表:
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
using namespace std;
//十字链表节点结构
class Link_Node
{
public:
//存储点的数据,若下标与数据不对应,则先找到数据对应下标
int tail_v;//弧尾(起点),加入到弧尾出边单链表
int head_v;//弧头(终点),加入到弧头入边单链表
int wi;//权值
Link_Node* tail_out_next;//弧尾出边单链表下一个节点
Link_Node* head_in_next;//弧头入边单链表下一个节点
};
class Vertex_Node
{
public:
int vertex_i;//点的编号
Link_Node* first_out;//出边单链表头指针
Link_Node* first_in;//入边单链表头指针
};
class Graph
{
public:
int size1;//点的个数
int size2;//边的个数
Vertex_Node* v;//十字链表实现
public:
Graph(int sz1,int sz2):size1(sz1),size2(sz2)
{
v = new Vertex_Node[sz1];
if (v == nullptr)
{
cout << "开辟失败" << endl;
}
else
{
//初始化点
for (int i = 0; i < sz1; ++i)
{
cin >> v[i].vertex_i;
v[i].first_in = nullptr;
v[i].first_out = nullptr;
}
//进行两次头插
//输入边和权值
int x(0);
int y(0);
int w(0);
for (int i = 0; i < sz2; ++i)
{
cin >> x >> y >> w;
Link_Node* s = new Link_Node;
s->tail_v = x;
s->head_v = y;
s->wi = w;
//第一次头插
//s插入到弧尾x出边单链表中
s->tail_out_next = v[x].first_out;
v[x].first_out = s;
//第二次头插
//s插入到弧头y入边单链表中
s->head_in_next = v[y].first_in;
v[y].first_in = s;
}
}
}
};
int main()
{
Graph g(4, 6);
int ans(0);
int x(0);
cin >> x;
Link_Node* p = g.v[x].first_out;
while (p != nullptr)
{
ans++;
p = p->tail_out_next;
}
cout << ans;
return 0;
}
/*
1 0 3
0 1 4
1 2 2
2 0 5
0 3 6
2 3 8
2
*/
多重邻接表:
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
using namespace std;
//多重邻接表
//无向图邻接表一条边存储两次,浪费空间
class Link_Node
{
public:
//存储点的数据,若下标与数据不对应,则先找到数据对应下标
//两个端点
int vi;
int vj;
int wi;//权值
Link_Node* vi_next;//vi单链表下一个节点
Link_Node* vj_next;//vj单链表下一个节点
};
class Vertex_Node
{
public:
int vertex_i;//点的编号
Link_Node* first;//边单链表头指针
};
class Graph
{
public:
int size1;//点的个数
int size2;//边的个数
Vertex_Node* v;//多重邻接表实现
public:
Graph(int sz1,int sz2):size1(sz1),size2(sz2)
{
v = new Vertex_Node[sz1];
if (v == nullptr)
{
cout << "开辟失败" << endl;
}
else
{
//初始化点
for (int i = 0; i < sz1; ++i)
{
cin >> v[i].vertex_i;
v[i].first = nullptr;
}
//输入边和权值
int x(0);
int y(0);
int w(0);
for (int i = 0; i < sz2; ++i)
{
cin >> x >> y >> w;
Link_Node* s = new Link_Node;
s->vi = x;
s->vj = y;
s->wi = w;
//第一次头插(先vi)
s->vi_next = v[x].first;
v[x].first = s;
//第二次头插(后vj)
s->vj_next = v[y].first;
v[y].first = s;
}
}
}
};
int main()
{
Graph g(4, 5);
int ans(0);
int x(0);
cin >> x;
Link_Node* p = g.v[x].first;
while (p != nullptr)
{
ans++;
if (p->vi == x)
{
p = p->vi_next;
}
else
{
p = p->vj_next;
}
}
cout << ans << endl;
return 0;
}
/*
0
1
2
3
0 1 3
0 3 5
0 2 5
1 3 6
3 2 6
*/
BFS:
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
using namespace std;
//队列实现,循环队列
//将点的下标入队
template<class T>
class m_queue
{
public:
T* data;
int size;
int front;
int rear;
public:
m_queue(int sz):size(sz)
{
data = new T[sz];
if (data == nullptr)
{
cout << "开辟失败" << endl;
}
else
{
rear = 0;//指向队尾元素下一个元素
front = 0;
}
}
void En_queue(T k)
{
//牺牲一个位置与判空区分
if ((rear + 1) % size == front)
{
cout << "队列已满" << endl;
}
else
{
data[rear] = k;
//cout << k << "入队" << endl;
//实现循环效果
rear = (rear + 1) % size;
}
}
T De_queue()
{
int x(0);
//保存出队元素
if (front == rear)
{
cout << "队空" << endl;
return -1;
}
else
{
x = data[front];
front = (front + 1) % size;
//cout << x << "出队" << endl;
return x;
}
}
bool m_empty()
{
if (front == rear)
{
return true;
}
else
{
return false;
}
}
};
//以一个无权无向图为基础 进行BFS
//存储方式:邻接表
class Edge_Node
{
public:
int adj_v;//邻接点下标
Edge_Node* next;
};
class Graph_Node
{
public:
char vertex_i;//顶点编号
Edge_Node* first;//边链表
};
class Graph
{
public:
int size1;//点的个数
int size2;//边的个数
Graph_Node* g;//邻接表
bool* visit;//记录是否访问过
public:
Graph(int sz1,int sz2):size1(sz1),size2(sz2)
{
g = new Graph_Node[sz1];
visit = new bool[sz1];
if (g == nullptr || visit == nullptr)
{
cout << "开辟失败" << endl;
}
else
{
//初始化
for (int i = 0; i < size1; ++i)
{
cin >> g[i].vertex_i;
g[i].first = nullptr;
visit[i] = false;
}
//读入数据
int x(0);
int y(0);
for (int i = 0; i < size2; ++i)
{
cin >> x >> y;
//两次头插
Edge_Node* s = new Edge_Node;
//y为邻接点
s->adj_v = y;
s->next = g[x].first;
g[x].first = s;
s = new Edge_Node;
//x为邻接点
s->adj_v = x;
s->next = g[y].first;
g[y].first = s;
}
}
}
//为了避免重复入队,一入队便标记
void BFS()
{
//声明队列
m_queue<int> q(100);
//存储出队元素
int x(0);
Edge_Node* p = nullptr;
for (int i = 0; i < size1; ++i)
{
if (visit[i] == false)
{
//以下标为i点做起点
//把i点入队
q.En_queue(i);
visit[i] = true;
while (!q.m_empty())
{
x = q.De_queue();
//访问
cout << g[x].vertex_i << endl;
//下标为x点邻接点入队
p = g[x].first;
while (p != nullptr)
{
if (visit[p->adj_v] == false)
{
q.En_queue(p->adj_v);
visit[p->adj_v] = true;
}
p = p->next;
}
}
}
}
}
};
int main()
{
Graph g(9, 15);
g.BFS();
return 0;
}
//也可以将点的编号入队,但如果将编号入队,
//需要先根据编号找到对应下标
/*
测试用例:
9
15
ABCDEFGHI
0 1
0 5
1 6
5 6
2 1
1 8
2 8
6 7
2 3
3 8
3 7
3 4
4 7
4 5
3 6
*/
DFS:
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
using namespace std;
//以一个无权无向图为基础 进行DFS
//存储方式:邻接矩阵
class Graph
{
public:
int** edge;//邻接矩阵
char* vertex;//点集数组
bool* visit;//存储已经访问过的点(与点集数组下标对应)
int size1;//点的个数
int size2;//边的个数
public:
Graph(int sz1, int sz2) :size1(sz1), size2(sz2)
{
vertex = new char[sz1];
visit = new bool[sz1];
edge = new int* [sz1];
if (visit == nullptr || edge == nullptr || vertex == nullptr)
{
cout << "开辟失败" << endl;
}
for (int i = 0; i < sz1; ++i)
{
edge[i] = new int[sz1];
if (edge[i] == nullptr)
{
cout << "开辟失败" << endl;
}
}
for (int i = 0; i < sz1; ++i)
{
cin >> vertex[i];
visit[i] = false;
}
int x(0);
int y(0);
//假设x,y之间有一条边
for (int i = 0; i < sz2; ++i)
{
cin >> x >> y;
edge[x][y] = 1;
edge[y][x] = 1;
}
}
//以下标为零的点开始遍历
//DFS和DFSg两层循环避免图为非连通图时只遍历一个极大连通子图
void DFS()
{
for (int i = 0; i < size1; ++i)
{
if (visit[i] == false)
{
//标记为已经访问
visit[i] = true;
DFSg(i);
}
}
}
void DFSg(int x)
{
//访问点x
cout << vertex[x] << endl;
//再访问x邻接点
for (int i = 0; i < size1; ++i)
{
//没访问过且为x邻接点
if (visit[i] == false && edge[x][i] == 1)
{
//标记为已经访问
visit[i] = true;
DFSg(i);
}
}
}
};
int main()
{
Graph g(9, 15);
g.DFS();
return 0;
}
/*
测试用例:
9
15
ABCDEFGHI
0 1
0 5
1 6
5 6
2 1
1 8
2 8
6 7
2 3
3 8
3 7
3 4
4 7
4 5
3 6
*/