本系列博客为《数据结构》(C语言版)的学习笔记(上课笔记),仅用于学习交流和自我复习html
1、 图的基本定义和术语
彻底图:任意两个点都有一条边相连ios
无向彻底图web
n个结点,一共有C
(
n
,
2
)
C(n,2)C(n,2)条边算法
有向彻底图数组
n
(
n
−
1
)
n(n-1)n(n−1)网络
2、图的三种存储结构
1.邻接矩阵表示法
所谓邻接矩阵存储结构就每一个顶点用一个一维数组存储边的信息,这样全部点合起来就是用矩阵表示图中各顶点之间的邻接关系。所谓矩阵其实就是二维数组。数据结构
int g[N][N];
int main() {
int n, m; //n个点 m条边
scanf("%d%d", &n, &m);
int u, v; //从u到v
for (int i = 0; i < m; ++i) {
scanf("%d%d", &u, &v);
g[u][v] = 1;
//g[v][u] = 1;//无向图要建双边
//g[u][v] = w; //带权图
}
}
2.邻接表(链式)表示法
#define MVNum 100//最大顶点数
typedef struct ArcNode{ //边结点
int adjvex; //该边所指向的顶点的位置
struct ArcNode * nextarc; //指向下一条边的指针
OtherInfo info; //和边相关的信息
}ArcNode;
typedef struct VNode{
VerTexType data; //顶点信息
ArcNode * firstarc; //指向第一条依附该顶点的边的指针
}VNode, AdjList[MVNum]; //AdjList表示邻接表类型
typedef struct{
AdjList vertices; //邻接表
int vexnum, arcnum; //图的当前顶点数和边数
}ALGraph;
优势 :空间效率高,容易寻找顶点的邻接点;app
缺点 :判断两顶点间是否有边或弧,需搜索两结点对应的单链表,没有邻接矩阵方便。svg
3.邻接矩阵和邻接表的区别
4.链式前向星
若是说邻接表是很差写但效率好,邻接矩阵是好写但效率低的话,前向星就是一个相对中庸的数据结构。前向星当然好些,但效率并不高。而在优化为链式前向星后,效率也获得了较大的提高(主要是看着舒服)。
struct node
{
int v,nex,val,u;
}e[N];
int head[N],cnt;
inline void add(int u,int v,int val)//从u到v,从父节点到子节点
{
e[++cnt].nex=head[u];
e[cnt].val=val;//无关紧要
e[cnt].v=v;
e[cnt].u=u;//无关紧要
head[u]=cnt;
}
遍历全部结点方法:
for(int i=head[u];i;i=e[i].nex)
{
int v=e[i].v;
---------------
}
//这样咱们就能够遍历所有的点了!!
3、图的遍历
搜索引擎的两种基本抓取策略 —深度优先/广度优先
两种策略结合=先广后深 +权重优先
先把这个页面全部的连接都抓取一次再根据这些URL的权重来断定URL的权重高,就采用深度优先,URL权重低,就采用宽度优先或者不抓取 。
我把我以前写的博客的内容所有直接搬过来啦 ,下面的可能会有点难度
0x21.搜索 - 树与图的遍历、拓扑排序
注:如下图的创建都是使用链式前向星建图。
int head[N],ver[N],nex[N],edge[N],tot;
void add(int u,int v,int val){//链式前向星建图
ver[++tot] = v;
edge[tot] = val;
nex[tot] = head[u];
head[u] = tot;
}
1.)树与图的深度优先遍历及树的一些性质
1.树与图的深度优先遍历
深度优先遍历,就是在每一个点x上面的的多条分支时,任意选择一条边走下去,执行递归,直到回溯到点x后再走其余的边
int vis[N];//标记每个点的状态
void dfs(int u){
vis[u] = 1;
for(int i = head[u];i;i = nex[i]){
int v = ver[i];
if(vis[v])
continue;
dfs(v);
}
}
注:下面的2,3,4,5,6小节的内容不要求掌握,我就是看着有关联就放到这里的,都是竞赛相关的内容,有兴趣能够看一下,都比较简单
2.时间戳
按照上述的深度优先遍历的过程,以每个结点第一次被访问的顺序,依次赋值1~N的整数标记,该标记就被称为时间戳。
标记了每个结点的访问顺序。
3.树的DFS序(树链剖分前驱知识)
通常来讲,咱们在对树的进行深度优先时,对于每一个节点,在刚进入递归时和回溯前各记录一次该点的编号,最后会产生一个长度为2
N
2N2N的序列,就成为该树的D
F
S
DFSDFS序。
int a[N],cnt;
int dfs(int u){
a[++cnt] = u;//用a数组存DFS序
vis[u] = 1;
for(int i = head[u]; i;i = nex[i]){
int v = ver[i];
if(vis[v])
continue;
dfs(v);
}
a[++cnt] = u;
}
D
F
S
DFSDFS序的特色时:每一个节点的x
xx的编号在序列中刚好出现两次。设这两次出现的位置时L
[
x
]
,
R
[
x
]
L[x],R[x]L[x],R[x],那么闭区间[
L
[
x
]
,
R
[
x
]
]
[L[x],R[x]][L[x],R[x]]就是以x
xx为根的子树的D
F
S
DFSDFS序。
dfs序能够把一棵树区间化,便可以求出每一个节点的管辖区间。
对于一棵树的dfs序而言,同一棵子树所对应的必定是dfs序中连续的一段。
放一个博客。
dfs序的七个基本问题
4.树的深度
树中各个节点的深度是一种自顶向下的统计信息
起初,咱们已知根节点深度是0
00.若节点x的深度为d
[
x
]
d[x]d[x],则它的子结点 y
yy 的深度就是d
[
y
]
=
d
[
x
]
+
1
d[y]=d[x]+1d[y]=d[x]+1
int dep[N];
void dfs(int u){
vis[u] = 1;
for(int i = head[u];i;i = nex[i]){
int v = ver[i];
if(vis[v])
continue;
dep[v] = dep[u]+1;//父结点 u 到子结点 v 递推
dfs(v);
}
}
5.树的重心与s
i
z
e
sizesize
树的重心是自底向上统计的
树的重心也叫树的质心。对于一棵树n个节点的无根树,找到一个点,使得把树变成以该点为根的有根树时,最大子树的结点数最小。
int vis[N];
int Size[N];
int ans = INF;
int id;
void dfs(int u){
vis[u] = 1;
Size[u] = 1;//子树的大小
int max_part = 0;
for(int i = head[u];i;i = nex[i]){
int v = ver[i];
if(vis[v])
continue;
dfs(v);
Size[u] += Size[v];
max_part = max(max_part,Size[v]);//比较儿子的size由于这里是假设以u为重心
}
max_part = max(max_part,n-Size[u]);//n为整棵树的结点数
if(max_part
ans = max_part;//记录重心对应的max_part的值
id = u;//记录重心位置
}
}
6.图的连通块划分
若在一个无向图中的一个子图中任意两个点之间都存在一条路径(能够相互到达),而且这个子图是“极大的”(不能在扩展),则称该子图是原图的一个联通块
以下代码所示,cnt是联通块的个数,v记录的是每个点属于哪个联通块
通过连通块划分,能够将森林划分出每一颗树,或者将图划分为各个连通块。
int cnt;
void dfs(int u){
vis[u] = cnt;//这里存的是第几颗树或者是第几块连通图
for(int i = head[u];i;i = nex[i]){
int v = ver[i];
if(vis[v])
continue;
dfs(v);
}
}
int main()
{
for(int i = 1;i<=n;++i){
if(!vis[i])//若是是颗新树就往里面搜
++cnt,dfs(i);
}
}
DFS算法效率分析
用邻接矩阵来表示图,遍历图中每个顶点都要从头扫描该顶点所在行,时间复杂度为O
(
n
2
)
O(n^2)O(n2)。
用邻接表来表示图,虽然有 2e 个表结点,但只需扫描 e 个结点便可完成遍历,加上访问 n个头结点的时间,时间复杂度为O
(
n
+
e
)
O(n+e)O(n+e)。
结论:
稠密图适于在邻接矩阵上进行深度遍历;
稀疏图适于在邻接表上进行深度遍历。
2.)树与图的广度优先搜索
树与图的广度优先遍历,顺便求d数组(树结点的深度/图结点的层次)。
void bfs(){
memset(d,0,sizeof d);
queueq;
q.push(1);
d[1] = 1;
while(q.size()){
int u = q.front();
q.pop();
for(int i = head[u];i;i = nex[i]){
int v = ver[i];
if(d[v])continue;
d[v] = d[u]+1;
q.push(v);
}
}
}
广度优先遍历是一种按照层次顺序访问的方法。
它具备两个重要的性质:
在访问完全部的第i层结点后,才会访问第i+1层结点。
任意时刻,队列中只会有两个层次的结点,知足“两段性”和“单调性”。
BFS算法效率分析
若是使用邻接矩阵,则BFS对于每个被访问到的顶点,都要循环检测矩阵中的整整一行( n 个元素),总的时间代价为O
(
n
2
)
O(n^2)O(n2)。
用邻接表来表示图,虽然有 2e 个表结点,但只需扫描 e 个结点便可完成遍历,加上访问 n个头结点的时间,时间复杂度为O
(
n
+
e
)
O(n+e)O(n+e)。
练习
答案:
深度:3,6,5,1,2,4
广度:3,6,2,5,1,4
4、图的应用
本ACMer狂喜
1.最小生成树
极小连通子图:该子图是G 的连通子图,在该子图中删除任何一条边,子图再也不连通。
生成树:包含图G全部顶点的极小连通子图(n-1条边)。
首先明确:
使用不一样的遍历图的方法,能够获得不一样的生成树
从不一样的顶点出发,也可能获得不一样的生成树。
按照生成树的定义,n 个顶点的连通网络的生成树有 n 个顶点、n-1 条边。
目标:
在网的多个生成树中,寻找一个各边权值之和最小的生成树。
K
r
u
s
k
a
l
KruskalKruskal算法能够简单理解为按边贪心。
P
r
i
m
PrimPrim算法是以更新过的节点的连边找最小值
1.K
r
u
s
k
a
l
KruskalKruskal算法
每次选择权值最小的边,若该边两点没有加入集合,就将他加入。
起初每一个点的都是一个独立的集合,把边权从小到达排序,按照边权枚举边,用并查集判断两个是否在同一个集合,若是在一个集合就跳过当前边,反之就联通这两个集合。
时间复杂度:O
(
m
l
o
g
m
)
O(mlogm)O(mlogm)
给出C++代码:
#include
#include
#include
#include
#include
#include
#define over(i,s,t) for(register int i = s;i <= t;++i)
#define lver(i,t,s) for(register int i = t;i >= s;--i)
//#define int __int128
#define lowbit(p) p&(-p)
using namespace std;
typedef long long ll;
typedef pair PII;
const int N = 2e5+7;
struct node{
int x,y,z;
bool operator
return z < t.z;
}
}edge[N];
int fa[N],n,m,ans;
int Find(int x){
if(x == fa[x])return x;
return fa[x] = Find(fa[x]);
}
int main()
{
cin>>n>>m;
over(i,1,m)
scanf("%d%d%d",&edge[i].x,&edge[i].y,&edge[i].z);
sort(edge + 1,edge + 1 + m);
over(i,1,n)
fa[i] = i;
over(i,1,m){
int x = Find(edge[i].x);
int y = Find(edge[i].y);
if(x == y)continue;
fa[x] = y;
ans += edge[i].z;
}
printf("%d\n",ans);
}
2.P
r
i
m
PrimPrim算法
每次选择当前点所连的边的最小值,而后把它连起来
有些相似D
i
j
k
s
t
r
a
DijkstraDijkstra
普通版本的时间复杂度为O
(
n
2
)
O(n^2)O(n2)
堆优化的算法时间复杂度为O
(
n
l
o
g
n
)
O(nlogn)O(nlogn)
给出C++代码:
#include
#include
#include
#include
#include
#include
#include
#define over(i,s,t) for(register int i = s;i <= t;++i)
#define lver(i,t,s) for(register int i = t;i >= s;--i)
//#define int __int128
#define lowbit(p) p&(-p)
using namespace std;
typedef long long ll;
typedef pair PII;
const int N = 4e5+7;
int ver[N],nex[N],edge[N],head[N],tot;
int n,m,ans;
int dis[N];
int vis[N],cnt;
void add(int u,int v,int val){
ver[++tot] = v;
edge[tot] = val;
nex[tot] = head[u];
head[u] = tot;
}
priority_queue,greater >q;
void prim(){
dis[1] = 0;
q.push({0,1});
while(q.size()&&cnt != n){
int d = q.top().first,u = q.top().second;
q.pop();
if(vis[u])continue;
cnt++;
ans += d;
vis[u] = 1;
for(int i = head[u];i;i = nex[i]){
int v = ver[i];
if(edge[i] < dis[v])
dis[v] = edge[i],q.push({dis[v],v});
}
}
}
int main()
{
memset(dis,0x3f,sizeof dis);
scanf("%d%d",&n,&m);
over(i,1,m){
int x,y,z;
scanf("%d%d%d",&x,&y,&z);
add(x,y,z);
add(y,x,z);
}
prim();
printf("%d\n",ans);
return 0;
}
2.最短路
3.拓扑排序
4.关键路径