• 深度优先遍历


    --------------------siwuxie095

       

       

       

       

       

       

       

       

    深度优先遍历

       

       

    看如下实例:

       

       

       

       

    这张图的邻接表如下:

       

       

       

       

       

       

    关于深度优先遍历,要把握两点:

       

    1)所谓深度优先,就是从一个点开始,不停地向下试,直到

    试不下去为止。这个思路和树的深度优先是一致的

       

    2)图和树不一样的地方在于:树从根开始向下走, 一定有走

    不通的时候,而图则存在,不会走不通。因此,需要记录每一

    个点是否被遍历过(访问过)。如果被遍历过了,在下面的遍历

    中就不需要走了

       

       

       

       

       

    0 开始进行深度优先遍历为例(注意对照邻接表):

       

    首先遍历和 0 相连的第一个没有被遍历的顶点,即 1

       

    接着遍历和 1 相连的第一个没有被遍历的顶点,不存在。此路不通,退回到 0

       

    接着遍历和 0 相连的下一个没有被遍历的顶点,即 2

       

    接着遍历和 2 相连的第一个没有被遍历的顶点,不存在。此路不通,退回到 0

       

    接着遍历和 0 相连的下一个没有被遍历的顶点,即 5

       

    接着遍历和 5 相连的第一个没有被遍历的顶点,即 3。注意:0 已经被遍历过

       

    接着遍历和 3 相连的第一个没有被遍历的顶点,即 4

       

    接着遍历和 4 相连的第一个没有被遍历的顶点,即 6。注意:3 5 已经被遍历过

       

    其实到此为止,全部顶点已经遍历完毕

       

    接着遍历和 6 相连的第一个没有被遍历的顶点,不存在。此路不通,退回到 4

       

    接着遍历和 4 相连的下一个没有被遍历的顶点,不存在。此路不通,退回到 3

       

    接着遍历和 3 相连的下一个没有被遍历的顶点,不存在。此路不通,退回到 5

       

    接着遍历和 5 相连的下一个没有被遍历的顶点,不存在。此路不通,退回到 0

       

    接着遍历和 0 相连的下一个没有被遍历的顶点,不存在。至此,全部顶点遍历完毕

       

       

       

    通过深度优先遍历,就将这样一个连通图中的所有顶点都访问了一遍

       

       

       

       

       

       

       

    连通分量:求图中的连通分量的个数

       

       

    深度优先遍历的一个最为典型的应用,即 求图中的连通分量的个数

       

    看如下实例:

       

       

       

    如上,是一张图的三个部分,即 三个连通分量

       

    「连通分量和连通分量之间没有任何边相连」

       

    如果给出一张图,只需要整体遍历一遍这张图,

    即可求出这张图中的连通分量的个数

       

       

       

       

       

    程序 1

       

    SparseGraph.h:

       

    #ifndef SPARSEGRAPH_H

    #define SPARSEGRAPH_H

       

    #include <iostream>

    #include <vector>

    #include <cassert>

    using namespace std;

       

       

       

    // 稀疏图 - 邻接表

    class SparseGraph

    {

       

    private:

       

    int n, m; //n m 分别表示顶点数和边数

    bool directed; //directed表示是有向图还是无向图

    vector<vector<int>> g; //g[i]里存储的就是和顶点i相邻的所有顶点

       

    public:

       

    SparseGraph(int n, bool directed)

    {

    //初始化时,有n个顶点,0条边

    this->n = n;

    this->m = 0;

    this->directed = directed;

    //g[i]初始化为空的vector

    for (int i = 0; i < n; i++)

    {

    g.push_back(vector<int>());

    }

    }

       

       

    ~SparseGraph()

    {

       

    }

       

       

    int V(){ return n; }

    int E(){ return m; }

       

       

    //在顶点v和顶点w之间建立一条边

    void addEdge(int v, int w)

    {

       

    assert(v >= 0 && v < n);

    assert(w >= 0 && w < n);

       

    g[v].push_back(w);

    //1)顶点v不等于顶点w,即不是自环边

    //2)且不是有向图,即是无向图

    if (v != w && !directed)

    {

    g[w].push_back(v);

    }

       

    m++;

    }

       

       

    //hasEdge()判断顶点v和顶点w之间是否有边

    //hasEdge()的时间复杂度:O(n)

    bool hasEdge(int v, int w)

    {

       

    assert(v >= 0 && v < n);

    assert(w >= 0 && w < n);

       

    for (int i = 0; i < g[v].size(); i++)

    {

    if (g[v][i] == w)

    {

    return true;

    }

    }

       

    return false;

    }

       

       

    void show()

    {

       

    for (int i = 0; i < n; i++)

    {

    cout << "vertex " << i << ": ";

    for (int j = 0; j < g[i].size(); j++)

    {

    cout << g[i][j] << " ";

    }

    cout << endl;

    }

    }

       

       

       

    //相邻点迭代器(相邻,即 adjacent

    //

    //使用迭代器可以隐藏迭代的过程,按照一定的

    //顺序访问一个容器中的所有元素

    class adjIterator

    {

    private:

       

    SparseGraph &G; //图的引用,即要迭代的图

    int v; //顶点v

    int index; //相邻顶点的索引

       

    public:

       

    adjIterator(SparseGraph &graph, int v) : G(graph)

    {

    this->v = v;

    this->index = 0;

    }

       

       

    //要迭代的第一个元素

    int begin()

    {

    //因为有可能多次调用begin()

    //所以显式的将index设置为0

    index = 0;

    //如果g[v]size()不为0

    if (G.g[v].size())

    {

    return G.g[v][index];

    }

       

    return -1;

    }

       

       

    //要迭代的下一个元素

    int next()

    {

    index++;

    if (index < G.g[v].size())

    {

    return G.g[v][index];

    }

       

    return -1;

    }

       

       

    //判断迭代是否终止

    bool end()

    {

    return index >= G.g[v].size();

    }

    };

    };

       

       

    //事实上,平行边的问题,就是邻接表的一个缺点

    //

    //如果要在addEdge()中判断hasEdge(),因为hasEdge()O(n)的复

    //杂度,那么addEdge()也就变成O(n)的复杂度了

    //

    //由于在使用邻接表表示稀疏图时,取消平行边(即addEdge()

    //中加上hasEdge()),相应的成本比较高

    //

    //所以,通常情况下,在addEdge()函数中就先不管平行边的问题,

    //也就是允许有平行边。如果真的要让图中没有平行边,就在所有

    //边都添加进来之后,再进行一次综合的处理,将平行边删除掉

       

    #endif

       

       

       

    DenseGraph.h:

       

    #ifndef DENSEGRAPH_H

    #define DENSEGRAPH_H

       

    #include <iostream>

    #include <vector>

    #include <cassert>

    using namespace std;

       

       

       

    // 稠密图 - 邻接矩阵

    class DenseGraph

    {

       

    private:

       

    int n, m; //n m 分别表示顶点数和边数

    bool directed; //directed表示是有向图还是无向图

    vector<vector<bool>> g; //二维矩阵,存放布尔值,表示是否有边

       

    public:

       

    DenseGraph(int n, bool directed)

    {

    //初始化时,有n个顶点,0条边

    this->n = n;

    this->m = 0;

    this->directed = directed;

    //二维矩阵:nn列,全部初始化为false

    for (int i = 0; i < n; i++)

    {

    g.push_back(vector<bool>(n, false));

    }

    }

       

       

    ~DenseGraph()

    {

       

    }

       

       

    int V(){ return n; }

    int E(){ return m; }

       

       

    //在顶点v和顶点w之间建立一条边

    void addEdge(int v, int w)

    {

       

    assert(v >= 0 && v < n);

    assert(w >= 0 && w < n);

       

    //如果顶点v和顶点w之间已经存在一条边,

    //则直接返回,即排除了平行边

    if (hasEdge(v, w))

    {

    return;

    }

       

    g[v][w] = true;

    //如果是无向图,则g[w][v]处也设为true(无向图沿主对角线对称)

    if (!directed)

    {

    g[w][v] = true;

    }

       

    m++;

    }

       

       

    //hasEdge()判断顶点v和顶点w之间是否有边

    //hasEdge()的时间复杂度:O(1)

    bool hasEdge(int v, int w)

    {

    assert(v >= 0 && v < n);

    assert(w >= 0 && w < n);

    return g[v][w];

    }

       

       

    void show()

    {

       

    for (int i = 0; i < n; i++)

    {

    for (int j = 0; j < n; j++)

    {

    cout << g[i][j] << " ";

    }

    cout << endl;

    }

    }

       

       

       

    //相邻点迭代器(相邻,即 adjacent

    class adjIterator

    {

    private:

       

    DenseGraph &G; //图的引用,即要迭代的图

    int v; //顶点v

    int index; //相邻顶点的索引

       

    public:

       

    adjIterator(DenseGraph &graph, int v) : G(graph)

    {

    this->v = v;

    this->index = -1;

    }

       

       

    //要迭代的第一个元素

    int begin()

    {

    //找第一个为true的元素,即为要迭代的第一个元素

    index = -1;

    return next();

    }

       

       

    //要迭代的下一个元素

    int next()

    {

    for (index += 1; index < G.V(); index++)

    {

    if (G.g[v][index])

    {

    return index;

    }

    }

       

    return -1;

    }

       

       

    //判断迭代是否终止

    bool end()

    {

    return index >= G.V();

    }

    };

    };

       

       

    //addEdge()函数隐含着:当使用邻接矩阵表示稠密图时,已经

    //不自觉的将平行边给去掉了,即在添加边时,如果发现已经

    //存在该边,就不做任何操作,直接返回即可

    //

    //事实上,这也是使用邻接矩阵的一个优势可以非常方便的处理

    //平行边的问题

    //

    //另外,由于使用的是邻接矩阵,可以非常快速的用O(1)的方式,

    //来判断顶点v和顶点w之间是否有边

       

    #endif

       

       

       

    ReadGraph.h:

       

    #ifndef READGRAPH_H

    #define READGRAPH_H

       

    #include <iostream>

    #include <string>

    #include <fstream>

    #include <sstream>

    #include <cassert>

    using namespace std;

       

       

       

    //从文件中读取图的测试用例

    template <typename Graph>

    class ReadGraph

    {

       

    public:

       

    ReadGraph(Graph &graph, const string &filename)

    {

       

    ifstream file(filename);

    string line; //一行一行的读取

    int V, E;

       

    assert(file.is_open());

       

    //读取file中的第一行到line

    assert(getline(file, line));

    //将字符串line放在stringstream

    stringstream ss(line);

    //通过stringstream解析出整型变量:顶点数和边数

    ss >> V >> E;

       

    //确保文件里的顶点数和图的构造函数中传入的顶点数一致

    assert(V == graph.V());

       

    //读取file中的其它行

    for (int i = 0; i < E; i++)

    {

       

    assert(getline(file, line));

    stringstream ss(line);

       

    int a, b;

    ss >> a >> b;

    assert(a >= 0 && a < V);

    assert(b >= 0 && b < V);

    graph.addEdge(a, b);

    }

    }

       

    };

       

       

    #endif

       

       

       

    Component.h:

       

    #ifndef COMPONENT_H

    #define COMPONENT_H

       

    #include <iostream>

    #include <cassert>

    using namespace std;

       

       

       

    //通过深度优先遍历求图中的连通分量的个数(其中含有深度优先遍历的实现)

    template <typename Graph>

    class Component

    {

       

    private:

       

    Graph &G; //图的引用,即要进行深度优先遍历的图

    bool *visited; //每个顶点是否被访问过(是否被遍历过)

    int ccount; //连通分量的个数

    int *id; //同一个连通分量中的顶点,id相同,即表示相连

       

    //dfs()设置成私有函数

    void dfs(int v)

    {

    //将访问过的顶点置为true

    visited[v] = true;

    id[v] = ccount;

    //注意:声明迭代器时,前面还要加 typename,表明 adjIterator

    // Graph 中的类型,而不是成员变量

    typename Graph::adjIterator adj(G, v);

    for (int i = adj.begin(); !adj.end(); i = adj.next())

    {

    //如果没有被访问过,接着访问相邻的顶点(递归)

    if (!visited[i])

    {

    dfs(i);

    }

    }

    }

       

       

    public:

       

    Component(Graph &graph) : G(graph)

    {

       

    visited = new bool[G.V()];

    id = new int[G.V()];

    ccount = 0;

    for (int i = 0; i < G.V(); i++)

    {

    visited[i] = false;

    id[i] = -1;

    }

       

    //用深度优先遍历求图的连通分量的算法实现

    for (int i = 0; i < G.V(); i++)

    {

    //如果当前访问的顶点没有被访问过,就

    //对其进行深度优先遍历,并将和该顶点

    //相连的所有顶点都访问一遍,最后没有

    //被访问的顶点就一定在另外的连通分量

    //中,将 ccount 进行 ++ 即可

    if (!visited[i])

    {

    dfs(i);

    ccount++;

    }

    }

    }

       

       

    ~Component()

    {

    delete []visited;

    delete []id;

    }

       

       

    int count()

    {

    return ccount;

    }

       

       

    //判断顶点v和顶点w是否相连,即是否在同一连通分量中

    bool isConnected(int v, int w)

    {

    assert(v >= 0 && v < G.V());

    assert(w >= 0 && w < G.V());

    return id[v] == id[w];

    }

    };

       

       

    #endif

       

       

       

    main.cpp:

       

    #include "SparseGraph.h"

    #include "DenseGraph.h"

    #include "ReadGraph.h"

    #include "Component.h"

    #include <iostream>

    using namespace std;

       

       

       

    int main()

    {

       

    // TestG1.txt

    string filename1 = "testG1.txt";

    //稀疏图

    SparseGraph g1 = SparseGraph(13, false);

    ReadGraph<SparseGraph> readGraph1(g1, filename1);

    Component<SparseGraph> component1(g1);

    cout << "TestG1.txt, Component Count: " << component1.count() << endl;

       

    cout << endl;

       

       

    // TestG2.txt

    string filename2 = "testG2.txt";

    //稀疏图

    SparseGraph g2 = SparseGraph(7, false);

    ReadGraph<SparseGraph> readGraph2(g2, filename2);

    Component<SparseGraph> component2(g2);

    cout << "TestG2.txt, Component Count: " << component2.count() << endl;

       

    system("pause");

    return 0;

    }

       

       

    //1)图的深度优先遍历的复杂度:

    //

    //稀疏图 - 邻接表:O(V+E),通常情况下,E会比V大,所以也可以说是 O(E)

    //在邻接表的实现中,每一个顶点首先要访问,而每一个顶点的所有相邻顶点

    //就构成总共的边数,也就是说,将所有的边也都访问了一次,没有进行其

    //他多余的访问

    //

    //稠密图 - 邻接矩阵:O(V^2),原因在于,当想要获得一个顶点的所有相邻

    //顶点时,需要将图中的所有顶点都要扫一遍

    //

    //

    //所以,对于稀疏图而言,通常使用邻接表的表达方式,它的效率就会更好

    //

    //

    //

    //2)另外,除了这里的无向图之外,深度优先遍历算法对有向图依然有效

    //

    //

    //

    //3)深度优先遍历还能用来检测图中是否有环,不过在无向图中查看是否

    //有环,通常意义不大,但对于有向图来说,查看图中是否有环,就是非常有

    //意义的一件事情

       

       

    运行一览:

       

       

       

       

    其中,testG1.txt 和 testG2.txt 的内容如下:

       

       

       

       

    两个 txt 文件都可以分成两个部分:

       

    1)第一行:两个数字分别代表顶点数和边数

       

    2)其它行:每一行的两个数字表示一条边

       

       

       

       

       

       

       

    寻路:获得两点间的一条路径

       

       

    在进行深度优先遍历的过程中,也形成了一条一条的路径

       

       

       

    如果想获得两点间的一条路径,就可以在深度优先遍历的过

    程中找到。当然,这里并不能保证是最短路径

       

       

       

    显然,需要在遍历的同时,对路径进行存储,具体做法:

       

    遍历到某顶点的同时,存储一下是从哪个顶点遍历到了当前

    顶点,即 存储当前顶点的上一个顶点

       

       

       

    如果从 0 开始进行深度优先遍历,则遍历顺序为:

       

    0125346

       

    通过遍历顺序,就可以轻而易举地倒推 0 到任意一个顶点

    的具体路径

       

       

       

       

       

    程序 2:(在程序 1 的基础上用 Path.h 替换 Component.h,

    同时修改 main.cpp 即可)

       

    Path.h:

       

    #ifndef PATH_H

    #define PATH_H

       

    #include <vector>

    #include <stack>

    #include <iostream>

    #include <cassert>

    using namespace std;

       

       

       

    //通过深度优先遍历寻路

    template <typename Graph>

    class Path

    {

       

    private:

       

    Graph &G; //图的引用,即要进行寻路的图

    int s; //从顶点 s 到任意其它顶点的路径,s source

    bool *visited; //每个顶点是否被访问过(是否被遍历过)

    int *from; //每访问一个顶点,就存储一下是从哪个顶点遍历到了当前顶点

       

    void dfs(int v)

    {

       

    visited[v] = true;

       

    //注意:声明迭代器时,前面还要加 typename,表明 adjIterator

    // Graph 中的类型,而不是成员变量

    typename Graph::adjIterator adj(G, v);

    for (int i = adj.begin(); !adj.end(); i = adj.next())

    {

    if (!visited[i])

    {

    from[i] = v;

    dfs(i);

    }

    }

    }

       

       

    public:

       

    Path(Graph &graph, int s) :G(graph)

    {

       

    // 算法初始化

    assert(s >= 0 && s < G.V());

       

    visited = new bool[G.V()];

    from = new int[G.V()];

    for (int i = 0; i < G.V(); i++)

    {

    visited[i] = false;

    from[i] = -1;

    }

    this->s = s;

       

    // 寻路算法

    dfs(s);

    }

       

       

    ~Path()

    {

    delete []visited;

    delete []from;

    }

       

       

    //从顶点s到顶点w是否有路:如果visited[w]true

    //表明从顶点s通过DFS访问到了顶点w,即有路

    bool hasPath(int w)

    {

    assert(w >= 0 && w < G.V());

    return visited[w];

    }

       

       

    //找到从顶点s到顶点w的路径:通过from数组从顶点w倒推回去,

    //并存储在栈中,最后再从栈中转存到向量中

    void path(int w, vector<int> &vec)

    {

    stack<int> s;

    int p = w;

    //直到倒推到源顶点,它的from值为-1,即 from[s] = -1

    while (p != -1)

    {

    s.push(p);

    p = from[p];

    }

       

    //为了安全起见,先将向量vector清空

    vec.clear();

    //只要栈不为空,就将栈顶元素放入向量中,并出栈

    while (!s.empty())

    {

    vec.push_back(s.top());

    s.pop();

    }

    }

       

       

    //打印从顶点s到顶点w的路径

    void showPath(int w)

    {

       

    vector<int> vec;

    path(w, vec);

    for (int i = 0; i < vec.size(); i++)

    {

    cout << vec[i];

    if (i == vec.size() - 1)

    {

    cout << endl;

    }

    else

    {

    cout << " -> ";

    }

    }

    }

    };

       

       

    #endif

       

       

       

    main.cpp:

       

    #include "SparseGraph.h"

    #include "DenseGraph.h"

    #include "ReadGraph.h"

    #include "Path.h"

    #include <iostream>

    using namespace std;

       

       

       

    int main()

    {

       

    string filename = "testG2.txt";

    //稀疏图

    SparseGraph g = SparseGraph(7, false);

    ReadGraph<SparseGraph> readGraph(g, filename);

    g.show();

    cout << endl;

       

    Path<SparseGraph> dfs(g, 0);

    cout << "DFS : ";

    dfs.showPath(6);

       

    system("pause");

    return 0;

    }

       

       

    运行一览:

       

       

       

       

    其中,testG2.txt 的内容同程序 1

       

       

       

       

       

       

       

       

       

    【made by siwuxie095】

  • 相关阅读:
    Project Euler 389 Platonic Dice (概率)
    单纯形(相关题目)
    关于C++中的内存泄露
    莫比乌斯反演与积性函数求和筛法中的一些细节
    清华集训2015-Day 2
    bzoj3456-城市规划
    多项式运算的一些技术
    bzoj2302-Problem c
    bzoj4300-绝世好题
    bzoj4726-Sabota?
  • 原文地址:https://www.cnblogs.com/siwuxie095/p/7119542.html
Copyright © 2020-2023  润新知