图的分类

作者: pdnbplus | 发布时间: 2024/06/17 | 阅读量: 176

图的分类 -- 潘登同学的图论笔记

无向图(我们着重讨论简单图)

图的数学语言

根据前面数据结构的引入, 我们可以清楚的知道:图的由顶点, 边组成;

  • 贯彻集合论的思想, 将点写成一个点集V, 边写成一个边集E; $$ V = {v_1,v_2,v_3,...,v_n}\ E = {e_1,e_2,e_3,...,e_m}\ $$
  • 还有他们的对应关系呢! 定义一个由$E→V^2$的映射$r$;

$$r(e) = {u,v} \subseteq V$$

所以我们以后就用$G = (V,E,r)$来描述无向图了

接下来是用来描述图的术语

  • 邻接

    假设$G = (V,E,r)$为无向图, 若G的两条边$e_1$和$e_2$都与同一个顶点相关联, 则称$e_1$和$e_2$是邻接的

  • 自环,重边

    假设$G = (V,E,r)$为无向图,如果存在$e$,有$r(e)={u,v}$且$u=v$, 那么称$e$为一个自环

    假设$G = (V,E,r)$为无向图,若G中关联同一对顶点的边多于一条,则称这些边为重边

  • degree(度)

    假设$G = (V,E,r)$为无向图,$v\in V$, 顶点v的度数dgree deg(v) 是G中与v关联的边的数目(说白了就是有多少个顶点跟v相连)

  • 孤立顶点, 悬挂点,奇度点,偶度点

    假设$G = (V,E,r)$为无向图,G中度数为零的顶点称为孤立零点,

    图中度数为1的顶点成为悬挂点,与悬挂点相关联的边称为悬挂边,

    图中度数为k的点成为k度点,度数为奇数的为奇数点,度数为偶数的称为偶数点

简单图:不存在自环和重边的无向图

  • 图的基本定理/握手定理

    • 假设$G = (V,E,r)$为无向图,则有$\sum deg(v) = 2|E|$; 即所有顶点的度数是边的两倍 > 这个的定理是显然的,因为一条边能连接两个顶点
    • 推论: 在任何无向图中,奇数度的点必为偶数个 > 这个也很好证明,因为总的度数是偶数, 那想所有顶点的度数之和为偶数, 偶数个奇数度的顶点之和才能是偶数
  • 例子: 问题如下: 假设有9个工厂, 求证:

    • (1)在他们之间不可能每个工厂都只与其他3个工厂有业务联系
    • (2)在他们之间不可能只有4个工厂与偶数个工厂有业务联系 > 证明留给读者喔!

在简单图范畴下的其他有特点的图

  • 零图,离散图 > G中全是孤立顶点, 也称0-正则图
  • 正则图 > 所有顶点度数都相同的图, 若所有顶点度数均为k, 则称k-正则图
  • 完全图 > 任意两个顶点都有边, 也称n-正则图
  • n-立方体图

    其实长的就跟立方体一样,但由于图是平面的,下面给出严谨的数学定义 如果图的顶点集v由集合{0,1}上的所有长为n的二进制串组成,两个顶点的相邻当且仅当他们的标号序列仅在一位数字上不同 如图绘制了多个立方体图 n-立方体图

  • 大家也可以尝试自己构建(推荐一个构建网络的小工具networkx,但是networkx不是本文的主要工具,故不详细讲解,感兴趣的朋友们自己了解哦)

  • 贴上代码
import networkx as nx
import matplotlib.pyplot as plt


G = nx.Graph()
points = {'100':['101', '110', '000'],
          '110':['111', '100', '010'],
          '000':['001', '010', '100'],
          '001':['101', '011', '000'],
          '011':['001', '111', '010'],
          '101':['111', '100', '001'],
          '111':['110', '101', '011'],
          '010':['110', '011', '000']}
for i in points:
    for j in points[i]:
        G.add_edge(i, j)
 # 设置节点的位置
left = ['100', '110']
middle_left = ['000', '010']
middle_right = ['001', '011']
right = ['101', '111']

options = {
        "font_size": 36,
        "node_size": 100,
        "node_color": "white",
        "edgecolors": "black",
        "edge_color": 'red',
        "linewidths": 5,
        "width": 5,
        'alpha':0.8,
        'with_labels':True
    }
pos1 = {n: (0, 5-3*i) for i, n in enumerate(left)}
pos1.update({n: (1, 4-i) for i, n in enumerate(middle_left)})
pos1.update({n: (2, 4-i) for i, n in enumerate(middle_right)})
pos1.update({n: (3, 5-3*i) for i, n in enumerate(right)})
plt.clf()
nx.draw(G, pos1, **options)
  • 一个3-立方体图如下: 3-立方体图

  • 圈图

    假设$V = {1,2,...,n} (n>2),E = {(u,v)| 1 ≤ u, v ≤ n, u-v ≡ 1(mods\quad n)}$ 则称简单图为轮图

  • 一个圈图如下:

圈图

  • 轮图

    假设$V = {0,1,2,...,n} (n>2), E = {(u,v)| 1 ≤ u, v ≤ n, u-v ≡ 1(mods\quad n) 或者 u=0, v>0}$ 则称简单图为轮图, 其实就是在圈图的基础上增加了一个顶点去连接所有点

  • 一个轮图如下:

轮图

二部图(很经典)

若简单图$G = (V,E,r)$的顶点V存在一个划分${V_1, V_2}$使得G中任意一条边的两端分别属于$V_1$和$V_2$,则称$G$是二部图

如果V1中每个顶点都与V2中每个顶点相邻, 则成为完全二部图

  • 一个二部图如下:

二部图

  • 一个完全二部图如下:

完全二部图

有向图

就是上面的无向图的边有了方向即边$e = {u, v}$是一个有序对

  • degree(度)

    上面已经介绍过度了, 但在有向图中我们要把他细分一下 $deg(v) = deg(v)^{(-)} + deg(v)^{(+)}$

    出度 : 以v为起始点的有向边的数目, 记为$deg(v)^{(+)}$

    入度: 以v为终点的有向边的数目, 记为$deg(v)^{(-)}$

  • 定理

    对任意有向图$G=(V,E,r)$, 有$\sum deg(v)^{(-)} = \sum deg(v)^{(+)} = |E|$

  • 赋权图

    假设$G=(V,E,r)$为图, $W是E到\mathcal{R}$的一个函数,则称G为赋权图,对于$e\in E,w(e)$称作边e的权重或者简称权

    其实就是之前实现图的数据结构中的cost

图的同构与子图

  • 同构

    设$G_1 = (V_1,E_1,r_1)$和$G_2 = (V_2,E_2,r_2)$是两个无向图

    如果存在$v_1$到$v_2$的双射$f$和$E_1$到$E_2$的双射$g$, 满足对任意的$e\in E_1$,若$r_1(e)={u,v}$,则$r_2(g(e))= {f(u), f(v)}$ ,则称$G_1和G_2$是同构的,并记为$G_1\cong G_2$

  • 一对全等图的图像如下:

一对全等图

一对全等图

点集之间的双射函数$f(v_1) = a, f(v_2) = b, f(v_3) = c, f(v_4) = d$

边集之间的双射函数$f(e_i) = E_i, i = 1,2,...,6$

  • 补图

    假设$G=(V,E)$是一个n阶简单图 令$E' = {(u,v)|u,v\in V, u \neq v, {u,v}\notin E}$ 则称$(V, E')$为G的补图, 记作$G' = (V,E')$

    若$G与G'$同构, 则称$G$为自补图

  • 子图

    设$G_1 = (V_1,E_1,r_1)$和$G_2 = (V_2,E_2,r_2)$是两个图; 若满足$V_2 ⊆ V_1, E1 ⊆ E2, 及r_2 = r_1|E_2$, 即对于$∀e∈E_2 有r_2(e) = r_1(e)$ ,则称$G_2是G_1$的子图

    • 当$V_1=V_2$时,称$G_2是G_1$的支撑子图(因为当把顶点看成构成这个图的支点,当支点与原本图都一样的时候就可以把他称为支撑子图)

    • 当$E_1 ⊆ E_2$ 或 $V_2 ⊆ V_1$ 时, 称$G_2是G_1$的真子图

    • 当$V_2 = V_1$ 且 $E_2 = E_1$或$E_2 = ∅$时, 称$G_2是G_1$的真子图

  • 导出子图

    设$G_2 = (V_2,E_2,r_2)$是$G_1 = (V_1,E_1,r_1)$的子图,若$E_2={e|r_1(e)={u,v}⊆V_2}$(即E2包含了图G中V2的所有边)

构造子图的几种方法

  • 在原图中删掉点和与他关联的边,(得到的图也叫删点子图)
  • 在原图中删掉边(得到的是删边子图)

道路、回路与连通性

  • 道路

    有向图$G=(V,E,r)$中一条道路Π 是指一个点-边序列$v_0,e_1,v_1,...,e_k,v_k$,满足对于所有$i=1,2,...,k$

    $r(e_i) = (v_i-1,v_i)$ 称Π是从$v_0$到$v_u$的道路

  • 回路

    如果上面的道路中$v_1 = v_k$, 就是回路

  • 简单道路/简单回路

    如果道路/回路中各边互异,则称Π是简单道路/简单回路

    如果道路/回路中,除$v_0,v_k$外,每个顶点出现一次, 则称$Π$是初级道路/初级回路(也称圈)

  • 定理

    假设简单图G中每个顶点的度数都大于1,则G中存在回路

证明: 假设$Π:v_0, v_1, ... ,v_{k-1},v_k$是图G中最长的一条初级道路,显然$v_0$和$v_k$都不会与不在道路$Π$上的任何顶点相邻,否则就会有一条更长的道路;

但是,$deg(v_0)>1$,因此必定存在$Π$上的顶点$v_i$与$v_0$相邻,于是产生了回路

  • 可达

    u,v是G中两个顶点,如果u=v或G中存在u到v的道路,则称u到v是可达的

  • 连通图

    假设G是无向图,如果图中任意两相异点之间都存在道路,则称G是连通的或者连通图

  • 连通分支(重要)

    假设无向图$G=(V,E)$的顶点集V在可达关系下的等价类为${v_1, v_2, ..., v_k}$,则G关于$v_i$的导出子图称作G的一个连通分支(其中$i=1,2,...,k$)

    如果形象理解连通分支呢? 我举一个通俗一点的例子好了

    就拿国家与国家之间的关系来理解好了, 一个国家可能有很大的疆域也可能只有很小的疆域; 但是在与其他的国家进行外交的时候,无论这个国家再小,都是以一个独立的身份与别国外交; 而无论某个国家的某个地区的实力再强大,也不能独立的与别国外交(即一个国家只会一个身份去与别国外交);

    而连通分支就是来描述这样一种情况, 在一个网络中, 有很多的顶点, 但是很多顶点都有共性(比如他们500年前是一家), 那么就可以把他们看成一个顶点,这样就方便分析了

    而这个共性具体来说就是; 互相可达(就是这一堆的任何顶点都可以达到另一个顶点(也要在网络内))

后面还会讲到怎么求解连通分支的kosaraju算法

  • 一个包含三个连通分支的图如下: 连通分支
  • 割边(桥)

    假设$G=(V,E,r)$是连通图, 若$e∈E$,且$G-e(从G中去掉边e)$不连通,则称$e$是图$G$中的一条割边或桥

    如上图的7→9就是桥, 因为去掉之后图就不连通了

  • 定理

    连通图中的边e是桥, 当且仅当e不属于图中任意的一条回路;(因为回路一定是有来又回,如果桥在回路里面,那把他去掉图仍然是连通的)

  • 有向图的连通

    假设G是有向图,如果忽略图中边的方向后得到的无向图是连通的,则称G为连通的;否则就是不连通的

  • 强连通, 单向连通

    有向连通图G, 对于图中任意两个顶点u和v,u到v 和 v到u都是可达的,则称G为强连通的; 如果图中任意两个顶点u和v, u到v 和 v到u至少之一是可达的,则称G为单向连通的

  • 有向无环图(DAG)

    有向图G,如果图中不存在有向回路,则称G为有向无环图

欧拉图

  • 欧拉图

其实欧拉图的来源是那个柯尼斯堡七桥问题,这不是本文重点,感兴趣的同学自行百度喔

通过图G中每条边一次且仅一次的道路称作该图的一条欧拉道路;(相当于一笔画)

通过图G中每条边一次且仅一次的回路称作该图的一条欧拉回路;(相当于头尾相连的一笔画)

存在欧拉回路的图称作欧拉图

  • 一些例子如下:

欧拉图

  • 欧拉图的充要条件 > 无向图G是欧拉图当且仅当G是连通的而且所有顶点都是偶数度

证明: (必要性)假设G是欧拉图,即图中有欧拉回路,即对于任意一个顶点,有入必有出,因此每个顶点都是偶数度;

(充分性)设G的顶点是偶数度, 采用构造法证明欧拉回路的存在性:

从任意点v0出发,构造一条简单回路C, 因为各顶点的度数都是偶数, 因此一定能够回到v0, 构造简单回路;(!!!想清楚了再往下看!!!)

下一步,在上面构造的简单回路中挑一点在剩余的点和边中构造简单回路,重复该过程,直到不能再构造为止.

  • 定理 > 无向图G存在欧拉道路当且仅当G是连通的而且G中奇数度顶点不超过两个

证明: (必要性)G中一条欧拉道路L,在L中除起点和终点之外,其余每个顶点都与偶数条边(一条入,一条出)相关联. 因此, G中至多有两个奇数度的顶点

(充分性) ①若G中没有奇数度的顶点u,v,由上面定理,存在欧拉回路,显然是欧拉道路

②若G中有两个奇数度顶点u,v,那么连接uv,根据上面定理,可知存在一个欧拉回路C,从C中去掉边uv,得到顶点为u,v的一条欧拉道路(当我证到这的时候,应该有妙蛙妙蛙的声音)

(算法)构造欧拉回路 Fleury(G)

前提:得存在

输入: 至多有两个奇数度顶点的图G=(V,E,r)

输出:以序列的形式呈现欧拉回路/道路Π

算法流程:

  • ① 选择图中一个奇数度的顶点v∈V,如果图中不存在奇数度顶点,则任取一个顶点v, 道路序列Π←v

  • ② $while |E| ≠ 0$

    • if 与v关联的边多余一条,则任取其中不是桥的一条边e:
    • else 选择该边e:
      • 假设e的两个端点是$v,u, Π←Π·e·u, v←u$
      • 删除边e及孤立顶点(如果有)
  • ③ 输出序列

定理

假设连通图G中有k个度为奇数的顶点,则G的边集可以划分成$k/2$条简单道路,而不能分解为$(k/2 - 1)$或更少条道路

证明: (前半句) 由握手定理知k必为偶数.将这k个顶点两两配对,然后增添互不相邻的$k/2$条边,得到一个无奇数度顶点的连通图$G'$,

那么由前面的定理知,$G'$存在欧拉回路$C$,在$C$中删去增添的$k/2$条边, 便得到了$k/2$条简单道路;

(后半句) 假设图G的边集可以分为q条简单道路,则在图G中添加q条边可以得到欧拉图G',因此图中所有顶点都是偶数度,而每添加一条边最多可以把两个奇数度顶点变为偶数度,即$2q≥k$

  • 无向图的欧拉道路--推广-->有向图 定理 > 有向图G中存在欧拉道路当且仅当G是连通的,而且G中每个顶点的入度都等于出度; > > 有向图G中存在欧拉道路,但不存在欧拉回路当且仅当G是连通的; > > 除两个顶点外,每个顶点的入度都等于出度,而且这两个顶点中一个顶点的入度比出度大1,另一个的入度比出度小1;

哈密顿图

  • 哈密顿图

    图中含有哈密顿回路的图

  • 哈密顿道路

    通过图G中每个顶点一次且仅一次的道路称作该图的一条哈密顿道路;

  • 哈密顿回路

    通过图G中每个顶点一次且仅一次的回路称作该图的一条哈密顿回路;

  • 注意

    1.图G中存在自环/重边不影响哈密顿回路/道路的存在性

    2.哈密顿图中一定不存在悬挂边

    3.存在哈密顿道路的图中不存在孤立顶点

  • 例子: 科学家排座问题

    由七位科学家参加一个会议,已知A只会讲英语;B会讲英、汉;C会讲英、意、俄;D会日、汉;E会德、意;

    F会法、日、俄;G会讲德、法;安排他们坐在一个圆桌,使得相邻的科学家都可以使用相同的语言交流;

这就是一个很经典的哈密顿图的例子,转换一下,就是把他们之间的关系用图来表示,再在图中找一条哈密顿回路;

将讲相同语言的科学家之间连一条边,构造图,如下:

科学家排座图

构图代码

# %%科学家排座问题
import networkx as nx
import matplotlib.pyplot as plt

G = nx.Graph()
a_dict = {'A':['English'],
          'B':['English', 'Chinese'],
          'C':['English', 'Italian', 'Russian'],
          'D':['Japanese', 'Chinese'],
          'E':['German', 'Italian'],
          'F':['French', 'Japanese', 'Russian'],
          'G':['German', 'French']}

# 用于储存相邻节点, key是语言, value是people_name
neighbors_dict = {}
for people_name in a_dict:
    G.add_node(people_name)
    for language in a_dict[people_name]:
        # 如果这个语言在neighbors_dict, 那么就把会这门语言的其他人
        # 与这个人之间连接一条边, 否则就把这个人会的语言加到dict中
        if language in neighbors_dict:
            for people_name_exist in neighbors_dict[language]:
                G.add_edge(people_name_exist, people_name, language=language)
            neighbors_dict[language].append(people_name)
        else:
            neighbors_dict[language] = [people_name]

options = {
        "font_size": 36,
        "node_size": 100,
        "node_color": "white",
        "edgecolors": "white",
        "edge_color": 'red',
        "linewidths": 5,
        "width": 5,
        'alpha':0.8,
        'with_labels':True
    }
pos = nx.spring_layout(G)
plt.clf()
nx.draw(G, pos, **options)

# 显示edge的labels
edge_labels = nx.get_edge_attributes(G, 'language')
nx.draw_networkx_edge_labels(G, pos, edge_labels=edge_labels, font_size=10, font_color='blue')

再寻找一条回路经过所有顶点,这里我们采用DFS(深度优先搜索)

DFS深度优先搜索

DFS的本质是暴力搜索算法,也称回溯算法,就是一种在找不到正确答案的时候回退一步,再继续向前的算法

算法的思想很简单,但是实际操作可能会有点不好理解;下面我们通过几个小案例来深入理解DFS算法

  • 例子1: > 全排列问题: 给出一个数字n,要求给出1-n的全排列,输出结果为列表

如给定数字3, 那结果就是[[1, 2, 3], [1, 3, 2], [2, 1, 3], [2, 3, 1], [3, 1, 2], [3, 2, 1]]

上代码!!!

# 回溯算法
def template(n):  # 主函数(设定他的目的是保证储存结果的result不会因为递归栈的关闭而丢失)
    result = []  # 这个保存结果

    def trace(path, choices):  # 递归函数
        '''
        path: 用于储存目前排列
        choices: 用于储存所有可选择的数字(1-n)
        '''
        if len(path) == n:  # 当path的长度等于输入的n时,结束算法,把结果加入结果列表
            result.append(list(path))
            return

        for item in choices:
            if item in path:  # 减枝操作(如果path里面有item这个数)
                continue
            path.append(item)  # 如果没有就把他加到目前的path中
            trace(path, choices)  # 递归调用自身
            path.pop()  # 撤销最后一次的选择继续向前找答案

    trace([], range(1, n+1))  # 调用函数trace
    return result

if __name__ == '__main__': 
    print(template(3))

观察到输出结果与上面预期一致,(回溯算法是后面很多算法的基础)

  • 例子2: > 给定一个55的矩阵,里面有零元素,要求在矩阵里面5个0,使得每一行每一列都有零 > > [[7, 0, 2, 0, 2],[4, 3, 0, 0, 0],[0, 8, 3, 5, 3],[11, 8, 0, 0, 4],[0, 4, 1, 4, 0]]

上代码!!!

#%%矩阵找零
import copy
def template(matrix):  # 主函数(设定他的目的是保证储存结果的result不会因为递归栈的关闭而丢失)
    result = []  # 这个保存结果

    def trace(path, choices):  # 递归函数
        '''
        path: dict 用于储存找到的零 key表示row values表示column
        choices: 矩阵matrix
        '''
        if len(path) == len(matrix):  # 当path的长度等于输入的n时,结束算法,把结果加入结果列表
            if path not in result:
                result.append(copy.deepcopy(path))
            return

        for row, items in enumerate(choices):
            for column, j in enumerate(items):
                if j == 0:  # 当元素为零时
                    if row in path.keys():
                        continue
                    if column in path.values():  # 减枝操作(如果path里面有column,说名这一列已经有零了)
                        continue
                    path[row] = column  # 如果没有就把他加到目前的path中
                    trace(path, choices)  # 递归调用自身
                    path.popitem()  # 撤销最后一次的选择继续向前找答案

    trace({}, matrix)  # 调用函数trace
    return result
if __name__ == '__main__':
    matrix = [[7, 0, 2, 0, 2],
              [4, 3, 0, 0, 0],
              [0, 8, 3, 5, 3],
              [11, 8, 0, 0, 4],
              [0, 4, 1, 4, 0]]
    print(template(matrix))

可以看到最终返回了两个结果:

[{0: 1, 1: 2, 2: 0, 3: 3, 4: 4}, {0: 1, 1: 3, 2: 0, 3: 2, 4: 4}]

表示的就是第0行选择第1列,第1行选择第2列,第2行选择第0列,第3行选择第3列,第4行选择第4列;带回矩阵验证发现符合条件

其实可以把例子2中的问题转换一下,又变成一个排列问题,仍然采用相同的方法求解

  • 例子2(变形): > 在[2,4],[3,4,5],[1],[4,5],[1,5]中找到1-5的一个排列(如2,3,1,4,5) > > 把所有可能的结果输出成列表

上代码!!!

#%%将矩阵找零问题转化
import copy
def template(a):
    result = []

    def trace(path,choices):
        if len(path) == len(choices):
            if path not in result:
                result.append(copy.deepcopy(path))
            return

        for row, items in enumerate(choices):
            if len(path) != row:  # path的元素的index对应choice的第几行
                continue  # 要是没有这一个if判断,path中储存的元素会乱
            for j in items:
                if j in path:
                    continue
                path.append(j)
                trace(path,choices)
                path.pop()  #删掉最后一个元素
    trace([],a)
    return result

if __name__ == '__main__':
    a = [[2,4],[3,4,5],[1],[3,4],[1,5]]
    print(template(a))

可以看到结果是一致的

  • 例子3:给定两个整数 n 和 k,返回范围 [1, n] 中所有可能的 k 个数的组合

上代码!!!

#%%77. 组合
# 给定两个整数 n 和 k,返回范围 [1, n] 中所有可能的 k 个数的组合。
class Solution(object):
    def combine(self, n, k):
        """
        :type n: int
        :type k: int
        :rtype: List[List[int]]
        """
        result = []
        def trace(path, choices, k):
            if len(path) == k:
                path = sorted(path)
                if path not in result:
                    result.append(path)
                return
            for items in choices:
                if items in path:
                    continue
                path.append(items)
                trace(path,choices, k)
                path.pop()

        trace([], range(1, n+1), k)
        return result

if __name__ == '__main__':
    print(Solution().combine(4, 2))

可以看到结果符合题意

经过上面几个例子的引入,同学们对回溯算法应该比较了解了,现在我们来完成科学家的排座问题吧!

科学家排座问题

  • 修改数据结构,因为在DFS中要从一个点出发,遍历所有节点,为避免重复,对节点的数据结构加上color这个属性,标识为white
  • 对应的新增方法来修改color,以及查看color, 当节点已经被走过来就把他标识为gray,就不会再去遍历他;

话不多说,上代码!!!

#%% 科学家排座问题
from Vertex import Vertex # 导入Vertex
from Graph import Graph  # 导入之前实现的Graph

class New_Vertex(Vertex):  # 某一个具体问题的数据结构需要继承原有数据结构
    def __init__(self, key):
        super().__init__(key)
        self.color = 'white'  # 新增类属性(用于记录节点是否被走过)

    # 新增类方法, 设置节点颜色
    def setColor(self, color):
        self.color = color

    # 新增类方法, 查看节点颜色
    def getColor(self):
        return self.color

class New_Graph(Graph):  # 继承Graph对象
    def __init__(self):
        super().__init__()

    # 重载方法  因为原先Graph中新增节点用的是Vertex节点,但现在是用New_Vertex
    def addVertex(self, key):   # 增加节点
        '''
        input: Vertex key (str)
        return: Vertex object
        '''
        self.numVertices = self.numVertices + 1
        newVertex = New_Vertex(key)   # 创建新节点
        self.vertList[key] = newVertex
        return newVertex

    # 新增方法
    def addUndirectedEdge(self, key1, key2, cost=1):
        self.addEdge(key1, key2, cost)
        self.addEdge(key2, key1, cost=1)

class Solution():   # 解决具体问题的类对象
    # 对科学家们创建图,之前有过相关操作
    def createGraph(self, a_dict):
        '''
        input: a_dict type: dict, key: people_name, value: language
        return: graph
        '''
        # 创建图
        graph = New_Graph()
        # 用于储存相邻节点, key是语言, value是people_name
        neighbors_dict = {}
        for people_name in a_dict:
            graph.addVertex(people_name)
            for language in a_dict[people_name]:
                # 如果这个语言在neighbors_dict, 那么就把会这门语言的其他人
                # 与这个人之间连接一条边, 否则就把这个人会的语言加到dict中
                if language in neighbors_dict:
                    for people_name_exist in neighbors_dict[language]:
                        graph.addUndirectedEdge(people_name_exist, people_name)
                    neighbors_dict[language].append(people_name)
                else:
                    neighbors_dict[language] = [people_name]
        return graph

    # 关键函数DFS,有
    def DFS(self, g, start):
       result = []
       # 深度优先搜索
       def trace(path, g, start):   # path是探索的路径 g是图,start是起始的节点
           start.setColor('gray')  # 将正在探索的节点设置为灰色
           path.append(start)
           # path中包含了所有顶点,且最后一个顶点到初始顶点要有路径就是最终结果
           if len(path) == len(g):
               if path[-1] in path[0].getConnections():
                   result.append(list(path))
               return
           else:
               for i in list(start.getConnections()):
                   if i.getColor() == 'white':  # 如果与他相邻的顶点还是白色,就探索他
                       trace(path, g, i)
                       path.pop()
                       i.setColor('white')    # 将已经撤销的结果的节点设置回白色

       trace([], g, start)
       return result

if __name__ == '__main__':
    a_dict = {'A':['English'],
              'B':['English', 'Chinese'],
              'C':['English', 'Italian', 'Russian'],
              'D':['Japanese', 'Chinese'],
              'E':['German', 'Italian'],
              'F':['French', 'Japanese', 'Russian'],
              'G':['German', 'French']}
    s = Solution()
    graph = s.createGraph(a_dict)
    path = s.DFS(graph, graph.getVertex('A'))
    # 因为是无向图,如果存在一个回路那一定会有两条路径
    for n in range(len(path)):
        for i in path[n]:
            print(i.getId(), end='-')
        print('\t')

科学家排座问题结果

看到最后输出了两个结果,表示科学家两两的相邻关系,带回题目,发现OK!!!

平面图

  • 平面图

    如果可以将无向图G画在平面上,使得除端点处外,各边彼此互不相交,则称G是具有平面性的图(简称平面图)

  • 欧拉公式

    $面f、边m、顶点数n,的关系$

    设G是一个面数为$f(n,m)$-连通平面图,则有 $$n-m+f = 2$$

当然啦,欧拉公式已经像呼吸一样被我们所接受了,所以我这里就不证这种(1+1=2)的证明了,放一个链接:

  • 推论 > 设G是一个面数为f的(n,m)平面图,且有L个连通分支,则 > $$n-m+f = L+1$$

证明: 假设这$L$个分支是$G_1,G_2, ... ,G_L$,并设$G_i$的顶点数、边数和面数分别为$n_i,m_i和f_i$;

显然有$\sum n_i = n$和$\sum m_i = m$; 此外, 由于外部面是各个连通分支共用的,因此$\sum f_i = f + L - 1$

由于每个Gi都是连通图,因此由欧拉公式有$n_i - m_i + f_i = 2$

于是有$n - m + f = ∑n_i + ∑m_i + ∑f_i + 1 - L = ∑(n_i - m_i + f_i) + 1 - L = 2L + 1 - L = L + 1$

  • e的细分

    假设$G=(V,E,r)$是无向图, $e∈E$,顶点u与v是边e的两端,e的细分是指在G中增加一个顶点w,删去边e,新增以u和w为端点的边$e_1$以及w和v为端点的边$e_2$

    就相当于原本有俩城市直接相连,然后现在找一个中继点,把这俩城市通过这个中继点连接

  • 库拉托夫斯基定理

    一个无向图是平面图当且仅当他不包含与k5 (5阶完全图) 或 k3,3(完全3,3二部图)的细分同构子图

  • 例子:

    • 求证: 彼得森图不是平面图 > 先介绍一下彼得森图,是一个由10个顶点和15条边构成的连通简单图,它一般画作五边形中包含有五角星的造型。

一个经典的彼得森图如下: 彼得森图

我们采用构图法证明:

左图是一个彼得森图,把中间的O点去掉,得到一个彼得森图的子图.

将这个子图看成一个K3,3的细分同构 (由K3,3构造中间的图的方式是: 去掉边BC,增加顶点A,去掉边DH、EI, 增加顶点F, G)

证明图

  • 上图代码
#%%彼得森图不是平面图的证明
plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = False
G = nx.Graph()
points = {'A':['B', 'C', 'O'],
          'B':['A', 'D', 'I'],
          'C':['A', 'E', 'H'],
          'D':['B', 'E', 'F'],
          'E':['C', 'D', 'G'],
          'F':['D', 'H', 'O'],
          'G':['E', 'I', 'O'],
          'H':['F', 'I', 'C'],
          'I':['G', 'H', 'B'],
          'O':['A', 'G', 'F']}

for i in points:
    for j in points[i]:
        G.add_edge(i, j)

points_1 = ['A']
points_2 = ['B', 'C']
points_3 = ['D', 'E']
points_4 = ['F', 'G']
points_5 = ['H', 'I']
points_6 = ['O']

options = {
        "font_size": 24,
        "node_size": 100,
        "node_color": "white",
        "edgecolors": "white",
        "edge_color": 'blue',
        "font_color": 'red',
        "linewidths": 5,
        "width": 5,
        'alpha':0.8,
        'with_labels':True
    }

pos = {n: (2, 5) for i, n in enumerate(points_1)}
pos.update({n: (1+2*i, 4.2) for i, n in enumerate(points_2)})
pos.update({n: (4*i, 3.3) for i, n in enumerate(points_3)})
pos.update({n: (0.5+3*i, 2) for i, n in enumerate(points_4)})
pos.update({n: (1.25+1.5*i, 1) for i, n in enumerate(points_5)})
pos.update({n: (2, 3) for i, n in enumerate(points_6)})



plt.figure(figsize=(16,8))
plt.subplot(131)
nx.draw(G, pos, **options)
G.remove_node('O')
plt.title('Petersen图')

plt.subplot(132)
pos.pop('O')
options['edge_color'] = 'red'
options['font_color'] = 'black'
nx.draw(G, pos, **options)
plt.title('Petersen图的子图')

plt.subplot(133)
G3_3 = G = nx.Graph()
points3_3 = {'B':['D', 'C', 'I'],
          'C':['B', 'E', 'H'],
          'D':['B', 'E', 'H'],
          'E':['C', 'D', 'I'],
          'H':['C', 'D', 'I'],
          'I':['E', 'H', 'B']}

for i in points3_3:
    for j in points3_3[i]:
        G3_3.add_edge(i, j)

pos1 = {n: (1+2*i, 4.2) for i, n in enumerate(points_2)}
pos1.update({n: (4*i, 3.3) for i, n in enumerate(points_3)})
pos1.update({n: (1.25+1.5*i, 1) for i, n in enumerate(points_5)})

options['edge_color'] = 'green'
options['font_color'] = 'blue'
nx.draw(G3_3, pos1, **options)
plt.title('Petersen图的子图的细分同构')
  • 可平面图与哈密顿图存在联系

    如果简单图G既是哈密顿图又是平面图,那么在G画为平面图后,

    G的不在哈密顿回路C中的边落在两个集合之一: C的内部或C的外部

  • (算法)哈密顿图的可平面性判断算法 QHPLanar(G)

输入: (n, m)-简单哈密顿图G

输出: True or False

① 将图G的哈密顿回路哈在平面形成一个环,使得C将平面划分为内部区域和外部区域

② 设G的不在C中的边$e_1,e_2,...,e_{m-n}$, 在新的图$G'$中构造顶点(顶点就是这个m-n个)

③ 若$e_i和e_j$在G的新画法中必须是交叉的,即他们二者无法同时画在C的内部或者外部,则在图$G'$的顶点$e_i和e_j$中连一条边

④ 若$G'$是二部图, 则输出T, 否则输出F

  • 因为涉及到了两个边的画法问题,潘登同学暂时不能做算法实现0.0

  • 算法例子: (判断G是否可平面)

左图是题目图G,判断是否可平面

中图是将图G画在平面上形成一个环, 右图是将e构造顶点形成新的图

发现新的图是一个二部图, 所以G可平面

算法例子

  • 上图代码
#%%可平面性的判定例子
G = nx.Graph()
points_1 = {'A':['B', 'C', 'D', 'G'],
          'B':['A', 'D', 'C'],
          'C':['A', 'E', 'B'],
          'D':['B', 'A', 'F', 'G'],
          'E':['C', 'F', 'G'],
          'F':['D', 'E', 'H'],
          'G':['E', 'A', 'D', 'H'],
          'H':['G', 'F', 'C']}
# 用于显示边
G_edge = nx.Graph()
points_edge = {'A':['D', 'G'],
              'B':['C'],
              'C':['B'],
              'D':['A', 'G'],
              'E':['F',],
              'F':['E',],
              'G':['A', 'D',],
              'H':['C']}

G_bin = nx.Graph()
points_bin = {'GA': ['CB', 'FE'],
              'GD': ['FE'],
              'DA': ['CB']}

for i in points_1:
    for j in points_1[i]:
        G.add_edge(i, j)

for i in points_edge:
    for j in points_edge[i]:
        G_edge.add_edge(i, j, name=i + j)

for i in points_bin:
    for j in points_bin[i]:
        G_bin.add_edge(i, j)

points_1 = ['A', 'B']
points_2 = ['C', 'D']
points_3 = ['E', 'F']
points_4 = ['G', 'H']

pos = {n: (1+i, 5) for i, n in enumerate(points_1)}
pos.update({n: (0.5+2*i, 4.2) for i, n in enumerate(points_2)})
pos.update({n: (0.5+2*i, 2.8) for i, n in enumerate(points_3)})
pos.update({n: (1+i, 2) for i, n in enumerate(points_4)})

options = {
        "font_size": 24,
        "node_size": 100,
        "node_color": "white",
        "edgecolors": "white",
        "edge_color": 'red',
        "font_color": 'black',
        "linewidths": 5,
        "width": 5,
        'alpha':0.8,
        'with_labels':True
    }
plt.figure(figsize=(16, 8))
plt.subplot(131)
nx.draw(G, **options)

plt.title('未处理过的图')

plt.subplot(132)
nx.draw(G, pos, **options)
edge_labels = nx.get_edge_attributes(G_edge, 'name')
nx.draw_networkx_edge_labels(G_edge, pos, edge_labels=edge_labels, 
                             font_size=20, font_color='blue')
plt.title('将哈密顿回路画成环的图')

plt.subplot(133)
points_bin1 = ['GA', 'GD', 'DA']
points_bin2 = ['CB', 'FE']

pos_bin = {n: (1, 5-2*i) for i, n in enumerate(points_bin1)}
pos_bin.update({n: (3, 5-2*i) for i, n in enumerate(points_bin2)})

options_bin = {
        "font_size": 30,
        "node_size": 100,
        "node_color": "white",
        "edgecolors": "white",
        "edge_color": 'blue',
        "font_color": 'red',
        "linewidths": 5,
        "width": 5,
        'alpha':0.8,
        'with_labels':True
    }

nx.draw(G_bin, pos_bin, **options_bin)
plt.title('新的图G`是二部图')
  • 对偶图 > 设G是一个平面图,满足下列条件的图$G'$称为G的对偶图 > > + G的面$f$与$G'$的顶点$V'$ 一 一对应 > + 若G中的面$f_i$和$f_j$邻接于共同边界e,则在$G$'中有与e一一对应的边$e'$,其以$f_i$和$f_j$所对应的点$v_i'$和$v_j'$为两个端点 > + 若割边e处于$f$内,则在$G'$中$f$所对应的点v有一个自环$e’$与$e$一一对应

下面是几个对偶图的图:

对偶图

  • 对偶图的画法 > ①在图G中每个面内画一个顶点 > > ② 在这些新的顶点之间添加边,每条新的边恰好与G中一条边相交一次

下面是一个例子:

原图

第一步

第二步

我们看到对偶图中出现了一个自环, 然后回去看原图的桥,发现就是那一个仅有的桥造成了这个自环

(注: G的桥与G'的自环, G的自环与G'的桥 存在对应关系)

  • 图的平面性的应用: > 高速公路的设计 > 电路印刷板的设计中避免交叉

图的分类就是这么多了,继续学习下一章吧!