暑假第一周笔记

Wee1

心得:本周共学习了两个大的图论知识点,其中也可以套许多图论算法解决问题,最难的还是最先确定模型和建图的过程

Part1 二分图

1. 定义

二分图是指图中有两个部分 $x$ 和 $y$,且图中每条边连接的两个顶点一定是一个位于 $x$ ,一个位于 $y$ .

2.匹配

所谓匹配就是一个图的边集,集合中任意两条边都没有公共顶点

例如这张图$M ${(A,C),(B,F)}就是一个匹配:

这里还有一个概念,未盖点 它的意义为如果点$v$不与任何一条属于$M$的边相关联,则称点$v$是一个未盖点,比如上图中的点$D,E$都是相对于$M$的未盖点

在这基础上,还引出两个概念:最大匹配和完全匹配

  • 最大匹配就是图中这样的边数最多的集合
  • 完全匹配则是图中的每个顶点都与匹配中的某条边相关联,也称作完备匹配

3.求最大匹配的算法

这才是二分图的精髓

怎么求最大匹配呢? 目前有两种方法。

  • 先在二分图中找出所有的匹配,再在此保留匹配数最大的。但时间复杂度为边数的指数级函数,因此我们需要更高效的算法
  • 匈牙利算法

在讲匈牙利算法之前,又先引出一个概念:增广路

增广路的定义(也称增广轨或交错轨):若 $P$ 是图 $G$ 中一条连通两个未匹配顶点的路径,并且属于匹配边集$M$的边和不属$M$的边(即已匹配和未匹配的边)在$P$上交替出现,则称$P$为相对于$M$的一 条增广路径。

增广路的起点和终点都是未盖点(未匹配点), 并且属于$M$的边和不属于$M$的边交替出现 把$P$中原来属于$M$边从$M$中删除,把P中原来不属于$M$边加入 到$M$中,变化后得到的新的匹配$M’$恰好比原匹配多一条边

所以匈牙利算法就是通过寻找增广路来得到最大匹配

代码:

#include<bits/stdc++.h>
using namespace std;
const int N=10000,M=10000;
bool road[N],Map[N][M];//map[x][y]=true表示点x和点y有边相连
//road[i]记录点i是否已在当前增广路中,防止死循环
int link[N],n,tot,m;//link[i]记录增广路上与i相连的前一个节点的编号,即记录已求出的匹配
//简单的说link[i]用于记录匹配集合中的边
bool Find(int v){
    int i=1;//find查找从v点出发是否有可增广路
    for(i=1;i<=m;i++){//可改用邻接表 //枚举在下半部分图中与v点相关联的点
        if(Map[v][i]&&(!road[i])){//如果该点不在增广路上
            road[i]=1;//把i标记为已讨论,防止死循环
            if(link[i]==0||Find(link[i])){//i是未匹配点(未盖点) 或者 从i的匹配点出发有可增广路
                link[i]=v;//修改与i匹配的点为v
                return true;//则从v出发可找到增广路,返回true;
            }//则从v出发可找到增广路,返回true;
        }
    }
    return false;//如果从v出发没有增广路,返回false
}
int main(){
    scanf("%d%d",&n,&m);//首先读入图结构到map数组中
    for(int i=1;i<=n;i++){
        int x;
        scanf("%d",&x);
        for(int j=1;j<=x;j++){
            int h;
            scanf("%d",&h);
            Map[i][h]=1;
        }
    }
    for(int i=1;i<=n;i++){
        for(int j=1;j<=m;j++)road[j]=0;//依次从上半部图的点出发,寻找增广路
        if(Find(i))++tot;//每找到一条增广路,匹配数加1(最多n个匹配)
    }
    printf("%d\n",tot);//输出最大匹配数
    return 0;
}

算法复杂度为 $O(nm)$,因为要讨论$n$个节点,每个节点又要讨论$m$条边

与最大匹配相关的一些定理

König 定理: 一个二分图的最大匹配数等于这个图的最小点覆盖数

最小点覆盖: 假如选了一个点就相当于覆盖了和它相关联的所有边。最少需要选择多少个点才能覆盖图中所有的边,这就是最小点覆盖数

二分图的最大独立集 = 顶点数 - 二分图的最大匹配数

二分图的最小顶点覆盖 = 二分图的最大匹配数

二分图的最小路径覆盖 = 顶点数 - 二分图的最大匹配数

DAG的最小路径覆盖 = 顶点数n - 对应二分图最大匹配数

DAG的最小路径覆盖

DAG$(有向无环图的缩写)$的最小路径覆盖是指,在该图中选出最少的路 径条数,使得图中所有点都被覆盖,且路径间公共无交点。

那解法是什么呢?

我们可以考虑把$n$的点拆成$2n$个点,$x$号点拆为$x$和$x'$两个点,原图中$x$连向$y$的边就从$x$出发,连接一条边到$y$

例如:

心得体会

二分图的模板不难,但建图的思路很难想,需要勤加练习增强

Part 2 连通性

如果无向图 G 的子图是连通图,则称该子图为连通子图。

最大连通子图:将任何不在这个子图中的点加入该子图,它将不再连通。

也就是平常说的连通块。

连通分量:无向图中的最大连通子图。

强连通图:在有向图中,如果这个图的每个点都有路径到达其他任何点,称这个图为“强连通图”。

强连通子图:字面意义。

最大强连通子图:定义同“最大连通子图”

强连通分量(Strong Connected Component, SCC):图中的最大强连通子图。

求强连通分量算法:

Tarjan 算法

在 Tarjan 算法中为每个结点 $u$ 维护了以下几个变量:

  1. $\textit{dfn}_u$:深度优先搜索遍历时结点 $u$ 被搜索的次序。
  2. $\textit{low}_u$:在 $u$ 的子树中能够回溯到的最早的已经在栈中的结点。设以 $u$ 为根的子树为 $\textit{Subtree}_u$。$\textit{low}_u$ 定义为以下结点的 $\textit{dfn}$ 的最小值:$\textit{Subtree}_u$ 中的结点;从 $\textit{Subtree}_u$ 通过一条不在搜索树上的边能到达的结点。

一个结点的子树内结点的 dfn 都大于该结点的 dfn。

从根开始的一条路径上的 dfn 严格递增,low 严格非降。

按照深度优先搜索算法搜索的次序对图中所有的结点进行搜索,维护每个结点的 dfnlow 变量,且让搜索到的结点入栈。每当找到一个强连通元素,就按照该元素包含结点数目让栈中元素出栈。在搜索过程中,对于结点 $u$ 和与其相邻的结点 $v$($v$ 不是 $u$ 的父节点)考虑 3 种情况:

  1. $v$ 未被访问:继续对 $v$ 进行深度搜索。在回溯过程中,用 $\textit{low}_v$ 更新 $\textit{low}_u$。因为存在从 $u$ 到 $v$ 的直接路径,所以 $v$ 能够回溯到的已经在栈中的结点,$u$ 也一定能够回溯到。
  2. $v$ 被访问过,已经在栈中:根据 low 值的定义,用 $\textit{dfn}_v$ 更新 $\textit{low}_u$。
  3. $v$ 被访问过,已不在栈中:说明 $v$ 已搜索完毕,其所在连通分量已被处理,所以不用对其做操作。

对于一个连通分量图,我们很容易想到,在该连通图中有且仅有一个 $u$ 使得 $\textit{dfn}_u=\textit{low}_u$。该结点一定是在深度遍历的过程中,该连通分量中第一个被访问过的结点,因为它的 dfn 和 low 值最小,不会被该连通分量中的其他结点所影响。

因此,在回溯的过程中,判定 $\textit{dfn}_u=\textit{low}_u$ 是否成立,如果成立,则栈中 $u$ 及其上方的结点构成一个 SCC。

实现

int dfn[N], low[N], dfncnt, s[N], in_stack[N], tp;
int scc[N], sc;  // 结点 i 所在 SCC 的编号
int sz[N];       // 强连通 i 的大小

void tarjan(int u) {
  low[u] = dfn[u] = ++dfncnt, s[++tp] = u, in_stack[u] = 1;
  for (int i = h[u]; i; i = e[i].nex) {
    const int &v = e[i].t;
    if (!dfn[v]) {
      tarjan(v);
      low[u] = min(low[u], low[v]);
    } else if (in_stack[v]) {
      low[u] = min(low[u], dfn[v]);
    }
  }
  if (dfn[u] == low[u]) {
    ++sc;
    while (s[tp] != u) {
      scc[s[tp]] = sc;
      sz[sc]++;
      in_stack[s[tp]] = 0;
      --tp;
    }
    scc[s[tp]] = sc;
    sz[sc]++;
    in_stack[s[tp]] = 0;
    --tp;
  }
}

Kosaraju 算法

过程

该算法依靠两次简单的 DFS 实现:

第一次 DFS,选取任意顶点作为起点,遍历所有未访问过的顶点,并在回溯之前给顶点编号,也就是后序遍历。

第二次 DFS,对于反向后的图,以标号最大的顶点作为起点开始 DFS。这样遍历到的顶点集合就是一个强连通分量。对于所有未访问过的结点,选取标号最大的,重复上述过程。

两次 DFS 结束后,强连通分量就找出来了,Kosaraju 算法的时间复杂度为 $O(n+m)$。

实现

// g 是原图,g2 是反图

void dfs1(int u) {
  vis[u] = true;
  for (int v : g[u])
    if (!vis[v]) dfs1(v);
  s.push_back(u);
}

void dfs2(int u) {
  color[u] = sccCnt;
  for (int v : g2[u])
    if (!color[v]) dfs2(v);
}

void kosaraju() {
  sccCnt = 0;
  for (int i = 1; i <= n; ++i)
    if (!vis[i]) dfs1(i);
  for (int i = n; i >= 1; --i)
    if (!color[s[i]]) {
      ++sccCnt;
      dfs2(s[i]);
    }
}

Part 3 心得体会

本周学习的都是图论算法,有些概念和知识点在课上不是很明白,下来查找资料才慢慢理解透了,还有做题的时候也不是很快就能想到这道题考什么,需要和哪些以前学过知识点融合起来,并且有时还会忘了旧的知识点,要重头复习,总之我会继续努力的。

                                                                                      ————by wilbur
posted @ 2023-07-15 15:35  CQWYB  阅读(3)  评论(0编辑  收藏  举报  来源