图论模板总结
参考:
https://www.zhangshengrong.com/p/YjNKpGrLXW/
相关练习总结参考
https://blog.csdn.net/ghost_him/article/details/124024682
链表存储
用数组模拟链表,用链表存储树和图
首先看如何用数组存储链表和遍历:
定义:
e[N]
每个节点的值
en[N]
节点的下一个节点的下标
idx
存储当前可以放入节点位置的索引,也就是当前用到的点
head
存储头结点
链表的初始化:
void init()
{
head = -1;
idx = 0;
}
头插法插入节点:
void add_to_head(int a)
{
e[idx] = a;
ne[idx] = head;
head = idx ;
idx++;
}
在位置K之后插入节点:
void add_to_k(int k,int x){
e[idx] = x;
ne[idx] = ne[k];
ne[k] = idx;
idx++;
}
删除第K个位置后面的节点
void remove(int k){
ne[k] = ne[ne[k]];
}
遍历链表的值
for(int i=head; i!=-1; i=ne[i]){
cout<<e[i]<<" ";
}
#include<iostream>
using namespace std;
const int N=100010;
int e[N],ne[N],idx,head;
// e[i] 第i个结点的值,ne[i]第i个结点的下一个节点的下标(next指针);
//head 头结点,idx 当前 可以放入节点位置的索引
//初始化
void init(){
head = -1;
idx=0;
}
//向头结点处插入值
void add_to_head(int x){
e[idx] = x;
ne[idx] = head;
head = idx;
idx++;
}
//向k位置插入值
void add_to_k(int k,int x){
e[idx] = x;
ne[idx] = ne[k];//当前节点指向k的下一个节点
ne[k] = idx;//k节点指向当前节点
idx++;
}
//删除k位置下一个的节点
void remove(int k){
ne[k] = ne[ne[k]];
}
int main(){
int m;
cin>>m;
init();
while(m--){
char p;
int k,x;
cin>>p;
if(p=='H'){
cin>>x;
add_to_head(x);
}
if(p=='D'){
cin>>k;
if(k!=0)
remove(k - 1);
else head = ne[head];//删除头节点
}
if(p=='I'){
cin>>k>>x;
add_to_k(k-1,x);
}
}
for(int i=head; i!=-1; i=ne[i]){
cout<<e[i]<<" ";
}
return 0;
}
图的存储
树是一种特殊的图,与图的存储方式相同。
对于无向图中的边ab,存储两条有向边a->b, b->a。
因此我们可以只考虑有向图的存储。
(1) 邻接矩阵:g[a][b] 存储边a->b
(2) 邻接表:
定义:
int h[N], e[N], ne[N], idx;
1.用 h
数组保存各个节点能到的第一个节点的编号。开始时,h[i] 全部为 -1。
2.用 e
数组保存节点编号,ne
数组保存 e
数组对应位置的下一个节点所在的索引。
3.用idx
保存下一个 e
数组中,可以放入节点位置的索引
//邻接表
const int N = 100010, M = N * 2; //无向图n条边时,最多2n个idx,因为每条边在邻接表中会出现两次
int h[N], e[M], ne[M], idx;//n个链表头,e每一个结点的值,ne每一个结点的next指针
int w[M] //边有权值的情况
//头插法插入 a->b
void add(int a,int b){
e[idx] = b;
ne[idx] = h[a];
h[a] = idx;
idx++;
}
//或者 如果插入带权值的边
void add(int a,int b, int c){
e[idx] = b;
w[idx] = c;//加入权值
ne[idx] = h[a];
h[a] = idx++;
}
//初始化
idx = 0;
memset(h, -1, sizeof(h));
图的遍历
深度优先遍历
时间复杂度 O(n+m), n 表示点数,m 表示边数;
深度优先遍历的步骤
- 访问顶点V
- 依次从顶点V的未被访问的邻节点出发,进行深度优先搜索,直至和V有路径相通的顶点都被访问到。
- 对于连通图进行遍历时,从一个顶点出发即可访问图中所有的顶点。
- 对于非连通图进行遍历时,若图中尚有顶点未被访问,则另选一未曾访问的顶点作为起始点,进行深度优先搜索,直至所有顶点都被访问
// 需要标记数组st[N], 遍历节点的每个相邻的点
int dfs(int u){
st[u] = true; // st[u] 表示点u已经被遍历过
for (int i = h[u]; i != -1; i = ne[i])
{
int j = e[i];
if (!st[j]) dfs(j);
}
}
广度优先遍历
从顶点V出发广度优先搜索的步骤
- 访问顶点V
- 依次访问顶点V的各个未被访问的临接点(横向访问)
- 从V的这些邻接点出发依次访问他们的邻接点,致使“先被访问的顶点的邻接点先于"后访问的顶点的邻接点"被访问(一般可以借助队列实现),直至图中所有已被访问的顶点的邻接点均被访问。
- 对于非连通图进行遍历时,若图中尚有顶点未被访问,则另选一未曾访问的顶点作为起始点,进行广度优先搜索,直至所有顶点都被访问
queue<int> q;
st[1] = true;//表示1号点已经遍历过
q.push(1);
while(q.size()){
int t = q.front();//队头出队,找该点能到的点
q.pop();//遍历完就出队列
for(int i=h[t]; i!=-1; i = ne[i]){ //遍历所有t节点能到的点,i为节点索引
int j = e[i]; //通过索引i得到t能到的节点编号
if(!st[j]){
st[j] = true;//访问标记
q.push(j);//节点入队
}
}
}
拓扑排序
BFS 的写法就叫「拓扑排序」
使用一个队列来进行广度优先搜索。开始时,所有入度为 0 的节点都被放入队列中,它们就是可以作为拓扑排序最前面的节点,并且它们之间的相对顺序是无关紧要的。
在广度优先搜索的每一步中,我们取出队首的节点 u
.我们将 u放入答案中;
我们移除 u 的所有出边,也就是将 u 的所有相邻节点的入度减少 1。如果某个相邻节点 v 的入度变为 0,那么我们就将 v 放入队列中。
在广度优先搜索的过程结束后。如果答案中包含了这 n 个节点,那么我们就找到了一种拓扑排序,否则说明图中存在环,也就不存在拓扑排序了。
int h[N],e[N],ne[N],idx;
int d[N];//记录每个点的入度
int seq[N];//拓扑排序的序列
void add(int a,int b){
e[idx] = b;
ne[idx] = h[a];
h[a] = idx;
idx++;
}
void topsort(){
queue<int> q;
for(int i=1;i<=n;i++){
if(!d[i]){//入度为0
q.push(i);
}
}
int k=0;
while(q.size()){
int t = q.front();
q.pop();
seq[k++] = t;//入队
for(int i = h[t]; i!=-1; i = ne[i]){
int j = e[i];
if(--d[j]==0){//入度-1后如果为0就加入队列
q.push(j)
}
}
}
}
用数组模拟队列:
bool topsort()
{
int hh = 0, tt = -1;
// d[i] 存储点i的入度
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];
if (-- d[j] == 0)
q[ ++ tt] = j;
}
}
// 如果所有点都入队了,说明存在拓扑序列;否则不存在拓扑序列。
return tt == n - 1;
}
作者:yxc
链接:https://www.acwing.com/blog/content/405/
来源:AcWing