本系列博客为《数据结构》(C语言版)的学习笔记(上课笔记),仅用于学习交流和自我复习
数据结构合集链接: 《数据结构》C语言版(严蔚敏版) 全书知识梳理(超详细清晰易懂)
图(graph)并不是指图形图像(image)或地图(map)。通常来说,我们会把图视为一种由“顶点”组成的抽象网络,网络中的各顶点可以通过“边”实现彼此的连接,表示两顶点有关联。注意上面图定义中的两个关键字,由此得到我们最基础最基本的2个概念,顶点(vertex)和边(edge)。
如上图所示,节点(vertex)用红色标出,通过黑色的边(edge)连接。
与结点关联的边数,在有向图中为入度与出度之和。
出度:在有向图中以这个结点为起点的有向边的数目。(可形象的理解为离开这个结点的边的数目)
入度:在有向图中以这个结点为终点的有向边的数目。(可形象的理解为进入/指向这个结点的边的数目)
任意一个图的总度数等于其边数的2倍
如果在同一无向图中两个结点存在一条路径相连,则称这两个结点连通。
如果无向图中任意两个结点都是连通的,则称之为连通图。
如果有向图中任意两个结点之间存在两条路径(即(i,j)两点中,既从i到j有一条路径,j到i也有一条路径),则两点是强连通的。当一个图中任意两点间都是强连通的,则该图称之为强连通图。
在强连通图中,必定有一条回路经过所有顶点。
强连通分量:非强连通图有向图中的最大子强连通图。
起点与相同的路径,又叫“环”。
任意两点间都存在边使其相连的无向图或任意两点间都存在两条不同边的有向图称作完全图
N个顶点的完全图:
有向 有n(n-1)条边
无向 有n(n-1)/2条边
无向完全图
n个结点,一共有 C ( n , 2 ) C(n,2) C(n,2)条边
有向完全图
n ( n − 1 ) n(n-1) n(n−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; //带权图
}
}
邻接表:
#include
using namespace std;
#define debug(x) cout<<"# "<
typedef long long ll;
const ll mod=2147483647;
const ll N=1e4+7;
vector<ll>G[N];//graph
/*
如果边上有属性(带权图)
struct edge
{
ll to,cost;
};
vectorG[N];
*/
int main()
{
ll V,E;//V个顶点和E条边
scanf("%lld %lld",&V,&E);
for(int i=0;i<E;++i)
{
ll s,t;//从s到t
scanf("%lld %lld",&s,&t);
G[s].push_back(t);
}
/*
**********各种对图的操作
*/
return 0;
}
#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;
优点 :空间效率高,容易寻找顶点的邻接点;
缺点 :判断两顶点间是否有边或弧,需搜索两结点对应的单链表,没有邻接矩阵方便。
如果说邻接表是不好写但效率好,邻接矩阵是好写但效率低的话,前向星就是一个相对中庸的数据结构。前向星固然好些,但效率并不高。而在优化为链式前向星后,效率也得到了较大的提升(主要是看着舒服)。
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;
---------------
}
//这样我们就可以遍历全部的点了!!
搜索引擎的两种基本抓取策略 —深度优先/广度优先
两种策略结合=先广后深 +权重优先
先把这个页面所有的链接都抓取一次再根据这些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;
}
深度优先遍历,就是在每个点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小节的内容不要求掌握,我就是看着有关联就放到这里的,都是竞赛相关的内容,有兴趣可以看一下,都比较简单
按照上述的深度优先遍历的过程,以每一个结点第一次被访问的顺序,依次赋值1~N的整数标记,该标记就被称为时间戳。
标记了每一个结点的访问顺序。
一般来说,我们在对树的进行深度优先时,对于每个节点,在刚进入递归时和回溯前各记录一次该点的编号,最后会产生一个长度为 2 N 2N 2N的序列,就成为该树的 D F S DFS DFS序。
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 DFS DFS序的特点时:每个节点的 x x x的编号在序列中恰好出现两次。设这两次出现的位置时 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 x x为根的子树的 D F S DFS DFS序。
dfs序可以把一棵树区间化,即可以求出每个节点的管辖区间。
对于一棵树的dfs序而言,同一棵子树所对应的一定是dfs序中连续的一段。
放一个博客。
dfs序的七个基本问题
树中各个节点的深度是一种自顶向下的统计信息
起初,我们已知根节点深度是 0 0 0.若节点x的深度为 d [ x ] d[x] d[x],则它的子结点 y y y 的深度就是 d [ y ] = d [ x ] + 1 d[y]=d[x]+1 d[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);
}
}
树的重心是自底向上统计的
树的重心也叫树的质心。对于一棵树n个节点的无根树,找到一个点,使得把树变成以该点为根的有根树时,最大子树的结点数最小。
【树形DP】树的重心详解+多组例题详解
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){//更新
ans = max_part;//记录重心对应的max_part的值
id = u;//记录重心位置
}
}
若在一个无向图中的一个子图中任意两个点之间都存在一条路径(可以相互到达),并且这个子图是“极大的”(不能在扩展),则称该子图是原图的一个联通块
如下代码所示,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);
}
}
用邻接矩阵来表示图,遍历图中每一个顶点都要从头扫描该顶点所在行,时间复杂度为 O ( n 2 ) O(n^2) O(n2)。
用邻接表来表示图,虽然有 2e 个表结点,但只需扫描 e 个结点即可完成遍历,加上访问 n个头结点的时间,时间复杂度为 O ( n + e ) O(n+e) O(n+e)。
结论:
稠密图适于在邻接矩阵上进行深度遍历;
稀疏图适于在邻接表上进行深度遍历。
树与图的广度优先遍历,顺便求d数组(树结点的深度/图结点的层次)。
void bfs(){
memset(d,0,sizeof d);
queue<int>q;
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);
}
}
}
广度优先遍历是一种按照层次顺序访问的方法。
它具有两个重要的性质:
如果使用邻接矩阵,则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
本ACMer狂喜
极小连通子图:该子图是G 的连通子图,在该子图中删除任何一条边,子图不再连通。
生成树:包含图G所有顶点的极小连通子图(n-1条边)。
首先明确:
使用不同的遍历图的方法,可以得到不同的生成树
从不同的顶点出发,也可能得到不同的生成树。
按照生成树的定义,n 个顶点的连通网络的生成树有 n 个顶点、n-1 条边。
目标:
在网的多个生成树中,寻找一个各边权值之和最小的生成树。
K r u s k a l Kruskal Kruskal算法可以简单理解为按边贪心。
P r i m Prim Prim算法是以更新过的节点的连边找最小值
Prim算法适用于稠密图
Kruskal适用于稀疏图
每次选择权值最小的边,若该边两点没有加入集合,就将他加入。
起初每个点的都是一个独立的集合,把边权从小到达排序,按照边权枚举边,用并查集判断两个是否在同一个集合,如果在一个集合就跳过当前边,反之就联通这两个集合。
时间复杂度: 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<int,int> PII;
const int N = 2e5+7;
struct node{
int x,y,z;
bool operator<(node &t)const{
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);
}
每次选择当前点所连的边的最小值,然后把它连起来
有些类似 D i j k s t r a Dijkstra Dijkstra就是一个
普通版本的时间复杂度为 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<int,int> 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<PII,vector<PII>,greater<PII> >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;
}
经典的最短路算法,基于贪心思想的,适用于非负权值图的经过优先队列或者线段树优化后的 O ( m l o g n ) O(mlogn) O(mlogn)的优秀算法。(m是边数,n是点数)
其实也超级简单,就是从起点开始,用一个dis数组存从起点到每一个点的最短距离,每次在当前点更新dis数组(可能经过当前点u到达的v点的总距离dis[u]+edge[v]是小于dis[v]就更新),然后往下走。
最后得到一个dis数组。
然后就是代码了。
Dijkstra算法时间复杂度为 O ( n m ) O(nm) O(nm),算法瓶颈是每次查找值最小的路径,下面代码使用优先队列,直接实现自动排序,将整个算法的时间复杂度优化到 O ( m l o g n ) O(mlogn) O(mlogn),普通的C语言写法可以写一个循环, O ( n ) O(n) O(n)来每次查找当前点的最小路径。
#include
using namespace std;
#define debug(x) cout<<"# "<
typedef long long ll;
const ll mod=2147483647000;
const ll N=500007;
struct Edge
{
ll v,w,next;//v:目的地,w:距离,next:下一个节点
}G[N];
ll head[N],cnt,n,m,s;
ll dis[N];//存距离
inline void addedge(ll u,ll v,ll w)//链式前向星存图
{
cnt++;
G[cnt].w=w;
G[cnt].v=v;
G[cnt].next=head[u];
head[u]=cnt;
}
struct node
{
ll d,u;//d是距离u是起点
bool operator<(const node& t)const//重载运算符
{
return d>t.d;
}
};
inline void Dijkstra()
{
for(register int i=1;i<=n;++i)dis[i]=mod;//初始化
dis[s]=0;
priority_queue<node>q;//堆优化
q.push((node){0,s});//起点push进去
while(!q.empty())
{
node tmp=q.top();q.pop();
ll u=tmp.u,d=tmp.d;
if(d!=dis[u])continue;//松弛操作剪枝
for(register int i=head[u];i;i=G[i].next)//链式前向星
{
ll v=G[i].v,w=G[i].w;
if(dis[u]+w<dis[v])//符合条件就更新
{
dis[v]=dis[u]+w;
q.push((node){dis[v],v});//沿着边往下走
}
}
}
}
int main()
{
scanf("%lld %lld %lld",&n,&m,&s);
for(register int i=1;i<=m;++i)
{
ll x,y,z;
scanf("%lld %lld %lld",&x,&y,&z);
addedge(x,y,z);//建图
}
Dijkstra();
for(register int i=1;i<=n;++i)
printf("%lld ",dis[i]);
printf("\n");
return 0;
}
我根据自己的理解改了一点ppt
这里的关键路径实际上跟最短路正好相反。
这里每到达一个结点(事件)就必须要花所有指向该结点的边的权值最大(活动可以同时进行),即所有边(活动)都完成才能触发该事件。
计算其实非常简单,算好 V e ( j ) Ve(j) Ve(j)(取最大值)和 V l ( j ) V l (j) Vl(j)(取最小值),关键是把握好下面的公式:
事件的最早发生时间 ( V e ( j ) ) (Ve(j)) (Ve(j))
事件的最迟发生时间 ( V l ( j ) ) (V l (j)) (Vl(j))
活动的最早开始时间: e ( a i ) = V e ( j ) e( ai ) = Ve( j ) e(ai)=Ve(j)
活动的最迟开始时间: l ( a i ) = V l ( k ) − d u t ( j , k ) l( ai ) = V l( k ) - dut( j , k ) l(ai)=Vl(k)−dut(j,k)
然后就是我最喜欢的代码环节了:
实例:求事件结点的最早发生时间
Status Topologicalsort( ALGraph G, Stack &T)
{ FindinDegree(G,indegree); // 对各顶点求入度,建立入度为零的栈 S,
Initstack(T);count = 0;
ve [ 0 .. G.vexnum - 1 ] = 0;
while (!StackEmpty(S))
{ Pop(S,j);Push(T,j); ++count;
for (p=G.vertices[i]. firstarc; p; p=p->nextarc);
{ k = p->adjnexr;
if (!(- - indegree [ k ])) Push(S, k);
if (ve[ j ]+ *( p->info)> ve[ k ] )
ve[ k ] = ve[ j ] + *( p->info); }
}
}
if (count < G.vexnum)return ERROR;
else return OK;
} // 栈 T 为求事件的最迟发生时间的时候用。
实例:求事件结点的最迟发生时间
1.下面( )方法可以判断出一个有向图是否有环。 (2分)
A.深度优先遍历
B.拓扑排序
C.求最短路径
D.求关键路径
答案:B
对于有向图的拓扑排序,
2.在图采用邻接表存储时,求最小生成树的Prim算法的时间复杂度为()
A. O ( n ) O(n) O(n)
B. O ( n + e ) O(n+e) O(n+e)
C. O ( n 2 ) O(n^2) O(n2)
D. O ( n 3 ) O(n^3) O(n3)
Prim算法的时间复杂度
邻接表存储 | O ( n + e ) O(n+e) O(n+e) |
---|---|
邻接矩阵 | O ( n 2 ) O(n^2) O(n2) |
3.图中有关路径的定义是
A.由顶点和相邻顶点序偶构成的边所形成的序列
B.由不同顶点所形成的序列
C.由不同边所形成的序列
D.上述定义都不是
答案:A
A(正确). 序偶:两个具有固定次序的客体组成一个序偶。由顶点和相邻顶点序偶构成的边的序列-----一条边对应两个端点,每条边的两个端点之间都有序偶关系----则一系列边的序列,构成有次序关系的一系列顶点的序列-----路径的定义:一个vp vi1 vi2 … vq的顶点序列就是一条路径。 所以很清楚了,A的确能反映路径。
B. 路径分为简单路径和复杂路径,该选项只是简单路径的性质。
C. 这一系列的边之间是否有连接关系?如果只是很多不相连的线段呢?
4.若从无向图的任意一个顶点出发进行一次深度优先搜索可以访问图中所有的顶点,则该图一定是( )图。
A.非连通 B.连通 C.强连通 D.有向
答案:B
解释:即从该无向图任意一个顶点出发有到各个顶点的路径,所以该无向图是连通图。
强连通图是指的所有的点都连通。
5.n个顶点的连通图用邻接距阵表示时,该距阵至少有( )个非零元素。
A.n B.2(n-1) C.n/2 D.n2
答案:B
6.关键路径是事件结点网络中( )。 (2分)
A.从源点到汇点的最长路径
B.从源点到汇点的最短路径
C.最长回路
D.最短回路
答案:A
关键路径在实际应用中被当做“参考路径”,即deadline 长度最长的路径
7.下列关于AOE网的叙述中,不正确的是( )。
A.所有的关键活动提前完成,那么整个工程将会提前完成
B.关键活动不按期完成就会影响整个工程的完成时间
C.任何一个关键活动提前完成,那么整个工程将会提前完成
D.某些关键活动提前完成,那么整个工程将会提前完成
答案:C