优先深度搜索判断曲线相交_用深度优先搜索(DFS)解决多数图论问题

前言

本文大概是作者对图论大部分内容的分析和总结吧,\(\text{OI}\)和语文能力有限,且部分说明和推导可能有错误和不足,希望能指出。

创作本文是为了提供彼此学习交流的机会,也算是作者在忙碌的中考后对此部分的复习和延伸吧。

本文顾名思义是探讨\(\text{DFS}\)在图论中的重要作用,可能心情比较好会丢个链接作拓展,下面就步入正文。

目录

1 基础篇

\(1.1\) 图的定义和深度优先搜索

\(1.2\) 图的连通分量和二分图染色

2 进阶篇

\(2.1\) 割顶和桥

\(2.2\) 无向图的双连通分量(\(\text{BCC}\))和有向图的强连通分量(\(\text{SCC}\))

\(2.3\) 二分图匹配问题

关键字

深度优先搜索(\(\text{DFS}\))、图的遍历、连通分量、二分图染色、二分图匹配、割顶、桥、双连通分量、强连通分量、\(\text{Tarjan}\)、增广路。

1 基础篇

总言:这里是\(\text{PJ}\)内容,相对来说较为简单。

1.1 图的定义和深度优先搜索

这一部分比较简单,大佬可以直接跳过~

在\(\text{OI}\)中图被抽象成点和边,边连接着两个顶点,可分成无向边和有向边,所有的点和边组在一起构成一个图,记作\(G=\),\(G\)表示图,\(V,E\)分别表示点集和边集。如下图所示,都可称作图。

图的存储主要有两种:邻接矩阵和邻接表。

邻接矩阵:就是用矩阵的行和列来记录两个结点之间是否有边相连,如果有边\(u \rightarrow v\),则\(e[u,v]=1\),否则为\(0\)。

优点:访问速度\(\text{O}(1)\)。

缺点:占用内存\(\text{O}(n^2)\)。

int e[maxn][maxn]; // 邻接矩阵

void add(int u, int v) { // 添加新边

e[u][v] = e[v][u] = 1; // 无向图

e[u][v] = 1; // 有向图

}

例如中间的图,邻接矩阵即为$$\begin{bmatrix} \text{u\v} & V1 & V2 & V3 & V4 & V5 & V6 \ V1 & 0 & 1 & 0 & 0 & 0 & 0 \ V2 & 0 & 0 & 1 & 0 & 0 & 0 \ V3 & 1 & 0 & 0 & 0 & 0 & 0 \ V4 & 0 & 0 & 0 & 0 & 1 & 0 \ V5 & 0 & 0 & 0 & 1 & 0 & 0 \ V6 & 0 & 0 & 0 & 0 & 0 & 1 \end{bmatrix}$$  邻接矩阵:就是通过链表的形式将与当前结点有关联的结点连起来。

优点:所需内存大小只与边的多少有关。

缺点:随机访问某条边的速度较慢。不过如果按顺序遍历目标结点速度很快。

// 实现1 : STL

vector e[maxn];

void add(int u, int v) {

e[u].push_back(v);

e[v].push_back(u); // 无向图时使用

}

// 实现2 : 前向星

struct Edge {

int u, v, pre; // e[i]表示第i+1条边,pre表示链接,若为-1则说明已经指向表头

} e[maxn * maxn];

int G[maxn], m; // G[i]表示所构成的i结点有关的结点构成的链的最后一条边,m表示边数

void init() {

m = 0;

memset(G, -1, sizeof(G)); // 清空G数组

}

void add(int u, int v) {

e[m++] = (Edge){u, v, G[u]}; // 添加新边,新边指向边G[u]

G[u] = m-1; // 将G[u]指向新边

// 处理无向图用以下

e[m++] = (Edge){v, u, G[v]};

G[v] = m-1;

}

// summary : 方案2比方案1好在常数较小

// 方案2中边的链接顺序相较于读入顺序相反。如果要一致可以改链接方式

例如最后一个图中,链接的情况:$$\begin{array}{ll} V1 \rightarrow 2 \ V2 \rightarrow 1 \rightarrow 3 \rightarrow 5 \ V3 \rightarrow 2 \rightarrow 4 \rightarrow 6 \ V4 \rightarrow 3 \ V5 \rightarrow 2 \ V6 \rightarrow 3 \end{array}$$  接着再说深搜(\(\text{DFS}\))和遍历。深搜顾名思义就是一直往下搜索,遇到阻碍再回头一步,再继续向下,直到所有的情况都搜索过。

深搜用于遍历图的话,好处很多,比如说代码短小精悍且复杂度为线性。对于上面最后一个图,如果起点在\(1\)号结点,那么访问的顺序:\(1\rightarrow 2 \rightarrow 3 \rightarrow 6 \rightarrow 4 \rightarrow 5\)。

// 在此代码之后全部都采用前向星存储图

bool vis[maxn]; // 是否访问过某结点

void dfs(int u) {

vis[u] = 1; // 访问过的标记

cout << u; // 输出遍历顺序

for (register int i = G[u]; ~i; i = e[i].pre) { // 遍历邻接表,~i表示当i=-1时结束

int v = e[i].v; // 边指向的结点

// do something before dfs

if (!vis[v]) dfs(v); // 若未访问过指向的结点,访问

// do something after dfs

}

}

// 这个代码展现了dfs的基本框架,下文及以后的dfs基本上与此大同小异

1.2 图的连通分量和二分图染色

连通分量:在无向图中,如果从结点\(u\)可以到达结点\(v\),那么结点\(v\)必然可以到达结点\(u\)(对称性);如果从结点\(u\)可以到达结点\(v\),而结点\(v\)可以到达结点\(w\),则结点\(u\)一定可以到达结点\(w\)(传递性),再加上原地不动的话,结点自身可以到达自身(自反性),这些结点满足等价关系,可以组成一个等价类,我们把这些相互可达的结点称作一个连通分量(\(\text{CC, connected component}\))。例如下面的图,有\(3\)个连通分量,分别为\(\{1,2,3,4\},\{5,6,7\},\{8\}\)。

原理:找到一个未标记的点,然后将所有能够直接或间接到达的结点全部标记。不断重复其操作。

int cc[maxn], cc_cnt; // 记录结点所在连通分量的编号,同时若cc不为0,则说明该结点被访问过

void dfs(int u) {

cc[u] = cc_cnt; // 标记连通分量的编号

for (register int i = G[u]; ~i; i = e[i].pre) {

int v = e[i].v;

if (!cc[v]) dfs(v); // 继续访问

}

}

void work() {

cc_cnt = 0; // 清空连通分量数

memset(cc, 0, sizeof(cc)); // 清空 标号&&访问

for (register int i = 1; i <= N; i++)

if (!cc[i]) { // 没被标记

cc_cnt++; // 新的连通分量

dfs(i); // 将所有能访问到的连通分量访问

}

}

二分图:如果一个图\(G=\),将\(V\)分成\(X\)和\(Y=V-X\),能使得\(E\)中任意一条边,两个端点分别在\(X\)集和\(Y\)集中,则此图为二分图。下图的左图即为二分图,而右图不是。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值