图(树)论解析(修订版)

图论1

目录

1.存储图、遍历图

存储图、遍历图

存储图(认识图)

先来认识什么是图,图是一种多对多的数据结构,比如说有1,2,3这三个点,且1与2,1与3,2与3之间都有一条没有方向的边联通,这就是一个无向图,连通图。如果1,2,3这三个点,1有一条通向2的单向边,2有一条通向3的有向边,这就是一个有向图,连通图(有向图用强)。

在一张简单图中,也就是没有自环、重边的图。什么意思?自环就是自己到自己毫无意义,重边也是,1到2已经有一条无向边了,2到1又有一条无向边,这种就是重边。概念都知道了,那么接着说,在一张简单图中如果任意两点之间都有一条无向边或两条有向边,也就是任意两点之间都能直接到达的图,叫做完全图

一张空的图与一张完全图互为补图,什么是补图?对于一张图

G
它的补图就是完全图-G

怎么减?把两个图都有的边删去,剩下的那张图是G的补图,也可以说这两张图互为补图。

先说这么多,那么在c++中如何存储图?

1、邻接矩阵

假设有n个点那么就建一个n*n的数组

对于两个点u,v如果这两个点之间有连边(无向)那么

G_{u,v}=1 并且 G_{v,u}=1

u,v如果这两个点之间有连边(有向,u可以去v,v不能去u)那么

G_{u,v}=1

好处是查询一条边是否存在时间复杂度仅为

O(1)

但是所需的空间复杂度为

O(n^2)

代码示例:

#include<bits/stdc++.h>
using namespace std;
const int N=1005; //点的个数 
int g[N][N]; //图
int main()
{
	int n,m;
	cin>>n>>m; //n是点数,m是边数
	while(m--) //请确保m在后面不会用到
	{
		int u,v,w; //u,v是点,w是边权看题目有些要有些不用,相当于过路费 
		cin>>u>>v; //cin>>u>>v>>w;
		g[u][v]=1;
		g[v][u]=1; //如果是有向图请删除这一行 
	} 
	return 0;
 } 

2、邻接表

一个新知识:vector

动态数组,根据你所需存储的元素数量开空间,具体原理是:先找一块大小为1的地方如果不够存,找一块大小为2的地方,并把之前的元素复制进去,还存不下找一块大小为4的地方,不断*2,所以在存

2^n+1

个元素时比较浪费。

邻接表的原理就是每一行是一个点,与他相连的点加入他这一行。

好处:大大减少空间复杂度

但是查询是否存在一条边要

O(n)

的时间复杂度

代码示例

#include<bits/stdc++.h>
using namespace std;
const int N=1005; //点的个数上限 
vector<int> g[N]; //图,你可以把两个维度都开成动态的 
int main()
{
	int n,m;
	cin>>n>>m; //n是点数,m是边数
	while(m--) //请确保m在后面不会用到
	{
		int u,v,w; //u,v是点,w是边权看题目有些要有些不用,相当于过路费 
		cin>>u>>v; //cin>>u>>v>>w;
		g[u].push_back(v); //如果不知道这行是干嘛的可以查OI-wiki.org 
		g[v].push_back(u); //如果是有向图请删除这一行 
	} 
	return 0;
 } 

新手掌握这两种就够了


遍历图

事先声明:这里会分为BFS和DFS两种方法,如果有不懂的可以查询OI-wiki,在这个网站里点到图论相关里找。

在遍历图的时候BFS和DFS的时间复杂度近乎一致,不像平常BFS快很多。他们的时间复杂度都是

O(v+e)也就是O(n+e)也就是O(n+m)

都是点数+边数,因为每条边、每个点都只会访问一遍。

dfs

深度优先搜索,原理是从一个点(如1)开始把他能到的所有点访问并标记,具体:

标记1,搜索下一个点比如说是3,那就标记3,再在3那里往下搜……

对于一张连通图只需要一次就能搜到所有的点。树是边数最少的连通图,关于最小生成树就是拓展的内容了

代码示例:

int vis[100005]; //标记数组,0是未访问,1是已访问
void dfs(int u)
{
	for(auto it : g[u]) //迭代器,vector知识
	{
		int nu=it;
        if(!vis[nu]) //没被访问过
        {
            vis[nu]=1; //标记
			dfs(it); //访问
        }
	}
}

bfs

需要一个队列,里面存放的是等待访问的点,然后挨个访问。

打个比方:1,2,3,4这4个点,1到2有连边,1到3有连边,2到4有连边,3到4有连边。

从1开始,1入队,标记,访问1,删除1,2入队,3入队同时打上标记,访问2,删除2,4入队,打标记;访问3,删除3,因为4已经被标记了,所以不2次入队;最后访问4,删除4,因为2和3都被打上了标记所以不做任何操作;结束。

代码示例:

queue<int> q;
vector<int> g[5];
int vis[10005]; 
g[1].push_back(2); g[1].push_back(3);
g[2].push_back(4);
g[3].push_back(4);
bfs(1);
q.push(1);
void bfs()
{
	while(q.size()) //while(!q.empty())
	{
		auto hd=q.front(); //访问 
		q.pop(); //删除
		for(auto it:g[hd])
		{
			int nu=it;
	        if(!vis[nu]) //没被访问过
	        {
	            vis[nu]=1; //标记
				q.push(nu); //入队 
	        }
		}
	}
} 

一种特殊的图,有

n

个点与

n-1

条边组成

是个连通图,也就是任意两点之间都能互相到达。一条链也是树树是特殊的图,菊花图也是树,什么是菊花图?就是1通向2,1通向3,1通向4……

父亲节点与子节点

一棵树(从上往下看),它上面那个与它相连的点是他的父亲节点,同理也能知道什么是子节点。祖先节点是一种特殊的父节点,它不仅限于上面那一个,而是从根节点(没有父节点,最上面的)到他路径上的所有节点都是它的祖先节点。

兄弟节点:同1个父亲的若干个节点

根节点

一棵树一定有且仅有一个根节点。根节点是所有节点(自己除外)的祖先节点,根节点没有祖先。

叶子节点

叶子节点没有子节点,在一棵树的最下面,一棵树一定有叶子节点,数量不定。

树的深度

一棵树从根到每一个叶子节点所需的最多步数

树的直径宽度

树最宽的地方,同一层节点最多的地方,那一层有几个节点树的宽度就是几


树形图

树的样子,但是每条边是有向的,树与树形图就好像完全图与竞赛图的关系。

二叉树

每个节点最多有两个孩子,一般把左边那个孩子叫做左子,右边同理。

​ O

​ / \

​ O O

​ / \ / \

O O O O

左子树:以一个节点为例,他的左孩子为根的一棵树就是左子树,右边同理

本篇完,敬请期待第二篇。

满二叉树与完全二叉树

完全二叉树包含满二叉树,也就是说满二叉树是完全二叉树的一种特殊形态

理一下:树是特殊的图,二叉树是特殊的树,完全二叉树是特殊的二叉树,满二叉树是特殊的完全二叉树。

完全二叉树就是叶子节点只出现在最下面一层与次下层(倒数第二层),上面那颗就是。只有根结点的树是完全二叉树,空树也是完全二叉树(不确定,待指正)。同理这两个树也是二叉树。

满二叉树就是把一颗深度为

n

的二叉树放满,一共

2^n-1

个节点,上面那颗就是。

↑↑↑这里讲的只是最基础的内容

二叉树的遍历

不是c++中的遍历。

而是先序、中序、后序遍历

这个序怎么看呢?什么时候访问根就是什么序。

如:

根 左子树 右子树

就是先序

左子树 根 右子树

就是中序

后序一样。

怎么手写,不要求用代码实现。

最简单的方法(用先序举例):

写上根,然后划线

1______ _______

12_ 3_

……


根据先序/后序和中序求另外一个序

一个重要的知识点。

以先序,中序举例

假设先序是1 2 4 3 5

假设中序是4 2 1 3 5

先序确定根是1,中序确定左子树是4 2,右子树是3 5,然后回到先序,左子树4 2中2是根,在确定2只有左子树可以直接画上4;右边回到先序3是根,只有右子树5,直接画上。

​ 1

​ / \

​ 2 3

/ \

4 5

像这样画好以后,求后序方便许多

_ _1

_2_31

42351OK!

UPD:
2024/9/30:新增树的相关知识

我链式前向星是不配存图吗(doge

可以,其实邻接表就有一点链表的味道

有任何问题欢迎指出