Graph Introduction - HolmesJJ/Data-Structures-and-Algorithms GitHub Wiki

各种(无向)图

图Graph 超图Hypergraph 多重图Multigraph
结点Nodes(或顶点Vertices) 至少一个 至少一个 至少一个
边Edges(或弧Arcs) 1. 每条边edge连接图中两个(=2)结点nodes(有的结点nodes可能没有边edges相连)
2. 每条边edge都是唯一的
1. 每条边edge连接图中超过两个(>=2)结点nodes(有的结点nodes可能没有边edges相连)
2. 每条边edge都是唯一的
1. 每条边edge连接图中两个(=2)结点nodes(有的结点nodes可能没有边edges相连)
2. 图中两个(=2)结点nodes之间的边edge可能多于一条(>=1)

图Graph的定义 Graph G = <V, E>

  • V是结点nodes的集合
    • 至少一个|V| > 0
  • E是边edges的集合
    • E ⊆ {(v, w) : (v ∈ V), (w ∈ V)}
    • 定义一条从v到w的边 e = (v, w)
    • 每条边edge都是唯一的 For all e1, e2 ∈ E : e1 ≠ e2

图解

graph graph graph
graph graph graph

图Graph的术语

已连接Connected 未连接Disconnected 可到达Reachable / 不可到达Unreachable
图例 graph graph graph
两个组件Component
度Degree 半径Diameter
定义 相邻Adjacent边的数量 沿着最短路径的两个结点之间的最大距离
图例 graph graph
图的度Degree为相邻Adjacent边的最大数量,上图的度为3

特殊图

n:结点数

星Star 团Clique / 完全图complete graph 道Line(Path)
定义 一个中心结点,所有其它结点与与中心连接 一个图中两两不同的结点之间都恰有一条边相连
图例 graph graph graph
n - 1 n - 1 2
半径 2 1 n - 1
环Cycle 二分图Bipartite Graph
定义 结点分为两个集合,同一集合中的结点之间没有边
注意:环也是一个二分图
图例 graph graph
2
半径 n / 2 或 n / 2 -1 最大 n - 1

入门连接

图Graph, 深度优先遍历(DFS), 广度优先遍历(BFS)【数据结构和算法入门9】
邻接矩阵与邻接表

图解

邻接表AdjacencyList 邻接矩阵AdjacencyMatrix
graph graph

详解

图的存储结构(邻接矩阵、边数组、邻接表、十字链表、邻接多重表)
浅析图的邻接矩阵进行平方运算的含义
图的邻接矩阵和邻接表的比较

标准写法(邻接表)

邻接表中链表的结点AdjListNode

public class AdjListNode {

    // 图中每个结点在邻接表的下标
    private int index;
    // 每个结点之间的权重,无权图默认为0
    private int weight;

    public AdjListNode(int index) {
        this.index = index;
        this.weight = 0;
    }

    public AdjListNode(int index, int weight) {
        this.index = index;
        this.weight = weight;
    }

    public int getIndex() {
        return index;
    }

    public int getWeight() {
        return weight;
    }

    public void setWeight(int weight) {
        this.weight = weight;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (obj instanceof AdjListNode) {
            AdjListNode adjListNode = (AdjListNode)obj;
            return this.getIndex() == adjListNode.getIndex();
        } else {
            return false;
        }
    }
}

邻接表的结点

public class Node<T> {

    // 结点值
    private T data;
    // 邻接表中的头结点
    private ArrayList<AdjListNode> root;

    public Node(T data) {
        this.data = data;
        this.root = new ArrayList<>();
    }

    public void add(int index, int weight) {
        AdjListNode adjListNode = new AdjListNode(index, weight);
        // 若结点不存在,则添加结点
        if (!root.contains(adjListNode)) {
            root.add(adjListNode);
        }
        // 若结点已存在,则更新权重
        else {
            root.get(root.indexOf(adjListNode)).setWeight(weight);
        }
    }

    public void remove(int index) {
        // 要转型,否则remove的是下标为index的元素
        // override了AdjListNode的equals方法,确保index一样即代表AdjListNode一样
        root.remove(new AdjListNode(index));
    }

    public int size() {
        return root.size();
    }

    public ArrayList<AdjListNode> getRoot() {
        return root;
    }

    public T getData() {
        return data;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (obj instanceof Node<?>) {
            Node<?> node = (Node<?>)obj;
            return this.getData().equals(node.getData());
        } else {
            return false;
        }
    }
}

邻接表

public class AdjacencyList<T> {

    private ArrayList<Node<T>> nodes;

    // 结点v的数量
    public AdjacencyList(ArrayList<Node<T>> nodes) {
        this.nodes = nodes;
    }

    public int size() {
        return nodes.size();
    }

    // 在下标为i的结点v和在下标为j的结点w之间添加一条边,权重为w
    public void addEdge(int i, int j, int w) {
        nodes.get(i).add(j, w);
    }

    // 在下标为i的结点v和在下标为j的结点w之间移除一条边
    public void removeEdge(int i, int j) {
        nodes.get(i).remove(j);
    }

    public ArrayList<Node<T>> getNodes() {
        return nodes;
    }

    public void print() {
        for (Node<T> node : nodes) {
            System.out.println("这是顶点: " + node.getData() + " 的链表: ");
            for (int j = 0; j < node.size() - 1; j++) {
                System.out.print(nodes.get(node.getRoot().get(j).getIndex()).getData() + " -> ");
            }
            if (node.size() > 0) {
                System.out.print(nodes.get(node.getRoot().get(node.size() - 1).getIndex()).getData());
            }
            System.out.println();
        }
    }
}

标准写法(邻接矩阵)

public class AdjacencyMatrix {

    // 邻接矩阵
    private int[][] adjMatrix;

    // n代表结点数
    public AdjacencyMatrix(int n) {
        adjMatrix = new int[n][n];
    }

    // 在结点v添加一条v与w相连的边
    // 注意:这里只是代表v指向w的边,若是无向图,就需要再调用addEdge(w, v)
    public void addEdge(int v, int w) {
        adjMatrix[v][w] = 1;
    }

    // 在结点v移除一条v与w相连的边
    // 注意:这里只是代表v指向w的边,若是无向图,就需要再调用removeEdge(w, v)
    public void removeEdge(int v, int w) {
        adjMatrix[v][w] = 0;
    }
}

邻接表和邻接矩阵的比较

邻接表AdjacencyList 邻接矩阵AdjacencyMatrix
空间复杂度 O(V + E) O(V^2)
环的空间复杂度 O(V) O(V^2)
团的空间复杂度 O(V + E) = O(V^2) O(V^2)
查询较快 找到v的所有相邻结点
枚举所有相邻结点
结点v和结点w是否相邻
查询较慢 结点v和结点w是否相邻 找到v的所有相邻结点
枚举所有相邻结点
  • 若图中边e的数目远远小于结点v^2,则称作稀疏Sparse图,此时用邻接表表示比用邻接矩阵表示节省空间
  • 若图中边e的数目接近于结点v^2,对于无向图接近于v*(v-1),称作稠密Dense图,考虑到邻接表中要附加链域,采用邻接矩阵表示法为宜

要点

  • 定理:令G是一个有推广的邻接矩阵A的p阶标定图,则A^n的i行j列元素(aij)^n等于由vi到vj的长度为n的通道的数目
    • 通俗描述:若邻接矩阵为A,则A^n中的A[i][j]表示的是从i出发走到点j走n步,有多少种走法
      • 另B = A^2,B[c, d] >= 1 当且仅当 对于一些x,A[c, x] == A[x, d],代表两个结点(c和d代表同一个结点)和x之间有边相连
    • 推论:设A为简单图G的邻接矩阵
      • 则A2的元素(aii)^2是vi的度数,A3的元素(aii)^2是含vi的三角形的数目的两倍
      • 若G是连通的,对于i ≠ j,vi与vj之间的距离是使A^n的(aij)^n ≠ 0的最小整数n
  • 无向图的邻接矩阵和邻接表都要注意,每次添加或移除结点时,需要考虑两个方向,e = (v, w)和e = (w, v)都要操作
  • 把一个题目转换成图,最重要是确定结点,边,边的方向,边权分别指代什么

适用场景

着色问题

算法:根据四色定理(Four color theorem),求出地图的所有着色方案

魔方

2阶魔方

算法题-二阶魔方还原
高效的计算机解魔方算法:二阶段算法

结点
魔方的一个状态
8个块的排列,每个块都有3个面暴露在外面
如果两个状态仅相差一步,则它们用边相连
转动90度 或 转动180度
结点的数量 = 8! ꞏ 3^8 = 264,539,520
魔方是对称的,例如魔方当前状态为A,魔方可能有另一个状态B,
然而换个角度看魔方A就是魔方B,不需要拧动,这就出现了重复
有8 * 3 = 24种状态重复
结点的数量 = 8! * 3^8 / 24 = 7! * 3^7 = 11,022,480
  • 只能转90度:半径 = 14,即最多14步可以复原魔方
  • 转90度 或 转动180度:半径 = 11,即最多11步可以复原魔方

3阶魔方

  • 结点的数量 ≈ 43 quintillion = 4300 亿亿
  • 半径 = 20,即最多20步可以复原魔方

n阶魔方

  • 半径 = Θ(n^2 / logn)

网页排名PageRank

思想:n阶邻接矩阵 = PageRank
对PageRank算法的简单理解

Leetcode

3 * 3的板

graph
结点
棋盘的一个状态
九个滑块的排列
如果两个状态仅相差一步,则它们用边相连
结点的数量 = 9! = 362,880 边的数量 < 4*9! < 1,451,520

最终组成的图的最大的度是4,每个滑块的滑动方式最多只有4种,处于边缘的滑块则少于4种

⚠️ **GitHub.com Fallback** ⚠️