数据结构-第六章 图-笔记

目录

邻接矩阵的阶乘性质

例一:

例二:

图的存储

邻接矩阵法

邻接表法(完整版)

邻接表法(简化版)

十字链表法(只能存储有向图)

邻接多重表(只能存储无向图)

吉大版本的三元组表和十字链表

图的部分基本操作

在图中插入新结点

在图中删除结点

图的遍历

无向图的广度优先遍历(邻接矩阵)

无向图的广度优先遍历(邻接表)

无向图的广度优先遍历(含有多个连通分量的无向图)

有向图的广度优先遍历

无向图的深度优先遍历(邻接矩阵)

无向图的深度优先遍历(邻接表)

无向图的深度优先遍历(邻接表)(非递归版本)

无向图的深度优先遍历(含有多个连通分量的无向图)

有向图的深度优先遍历

图的应用

最小生成树(prim算法)

最小生成树(kruskal算法)

最小生成树的变形题

最短路径问题-BFS算法(单源,无权图)

最短路径问题-dijkstra算法(单源,带正权图)

dijkstra算法与prim算法的区别

最短路径问题-floyd算法(任意两点,带正、负权图)

几种最短路径算法的比较

有向无环图的简化

拓扑排序

用DFS实现拓扑排序

关键路径

“最大”路径长度 == “最短”时间?

事件vk的最早发生时间ve(k)、活动ai的最早开始时间e(i)

事件vk的最迟发生时间vl(k)、活动ai的最迟开始时间l(i)​编辑

时间余量

求关键路径

关键路径的几个特性 

习题-自由树的直径


邻接矩阵的阶乘性质

假设有图如下所示:

例一:

如图所示,A²[1][4]表示的是从A到D长度为2的路径数目,即用左矩阵的第一行与右矩阵的第4列作内积。

a_{1,2}为1表示A->B有1条长度为1的路径,a_{2,4}为1表示B->D有一条长度为1的路径,它们相乘为1,说明A->D有一条长度为2的路径。

完整结果如下:

例二:

左矩阵为A²,右矩阵为A,则A³[1][4] 表示的是从A到D的长度为3的路径的数目,即用左矩阵的第一行与右矩阵的第4列作内积。

A²[1][2]为0表示A->B没有长度为2的路径,A[2][4]为1表示B->D有一条长度为1的路径,它们相乘为0,说明当前这条路径不是一条A->D的长度为3的路径。

A²[1][3]为1表示A->C有一条长度为2的路径,A[3][4]为1表示C->D有一条长度为1的路径,它们相乘为1,说明A->D有一条长度为3的路径。

图的存储

邻接矩阵法

以下是较为常规的方法,不过在实际题目应用中如果所需用到的关于顶点的信息不多的话,不用这么麻烦来定义一个结构体graph,直接用一个二维数组int graph[][]来表示邻接矩阵即可。

#define maxsize 100
#define inf 999999
struct graph
{
    char node[maxsize];  //顶点表,记录每个点的名字,例如1号节点叫A,2号节点叫B
    int edge[maxsize][maxsize]; //邻接矩阵
    int nodenum, edgenum; //记录图的顶点数和边数
};

//构造邻接矩阵,传入图的信息及节点数量
graph* creat(vector<vector<int>>& vec, int node_num)
{
    graph* g = new graph;
    //初始化矩阵
    g->nodenum = node_num;
    for (int i = 0; i < maxsize; i++)
    {
        for (int j = 0; j < maxsize; j++)
        {
            //对角线上的值设为0,其他的设为无穷大
            if (i == j) g->edge[i][j] = 0;
            else g->edge[i][j] = inf;
        }
    }
    for (int i = 0; i < vec.size(); i++)
    {
        //一条a->b,权值为w的边
        int a = vec[i][0];
        int b = vec[i][1];
        int w = vec[i][2];
        g->edge[a][b] = w;
        g->edgenum++;
    }
    return g;
}

int main()
{   
    //每个一维的数组表示一条边的信息,依次是头结点、尾结点、权值
    vector<vector<int>> vec = { {1, 2, 5}, {2, 3, 4}, {3, 1, 8}, {3, 6, 9}, {4, 3, 5}, {4, 6, 6}, {5, 4, 5}, {6, 1, 3}, {6, 5, 1} };
    graph* g = creat(vec, 6);
    //打印邻接矩阵
    for (int i = 1; i < g->nodenum + 1; i++)
    {
        for (int j = 1; j < g->nodenum + 1; j++)
        {
            if (g->edge[i][j] == inf) cout << "i";
            else cout << g->edge[i][j];
            cout << " ";
        }cout << endl;
    }
    return 0;
}

打印结果如下(用 i 来表示无穷大)

 

邻接表法(完整版)

首先是完整的写法模板

#define maxsize 100
#define inf 999999
struct edgenode //边表结点
{
    int b;  //该结点的序号,通过该序号可以在邻接表adjlist[]中访问,得到该结点的信息
    int w;  //该条边的权值
    edgenode* next = nullptr; //指向下一个边表结点的指针
};
struct headnode //顶点表结点
{
    char data;   //存储该结点的信息,例如名字啥的
    edgenode* first = nullptr;    //指向第一个边表结点
};
struct graph
{
    headnode adjlist[maxsize];  //邻接表
    int nodenum, edgenum; //记录图的顶点数和边数
};

//向某个顶点中,用头插法插入新的边表结点
void insert_edgenode(headnode& hn, edgenode* en)
{
    en->next = hn.first;
    hn.first = en;
}

//构造邻接表,传入图的信息及节点数量
graph* creat(vector<vector<int>>& vec, int node_num)
{
    graph* g = new graph;
    //初始化邻接表
    g->nodenum = node_num;
    for (int i = 0; i < vec.size(); i++)
    {
        //一条a->b,权值为w的边
        int a = vec[i][0];
        int b = vec[i][1];
        int w = vec[i][2];
        //创建一个新的边表结点
        edgenode* node = new edgenode;
        node->b = b;
        node->w = w;
        insert_edgenode(g->adjlist[a], node);
    }
    return g;
}

int main()
{   
    //每个一维的数组表示一条边的信息,依次是头结点、尾结点、权值
    vector<vector<int>> vec = { {1, 2, 5}, {2, 3, 4}, {3, 1, 8}, {3, 6, 9}, {4, 3, 5}, {4, 6, 6}, {5, 4, 5}, {6, 1, 3}, {6, 5, 1} };
    graph* g = creat(vec, 6);
    //打印邻接表
    for (int i = 1; i < g->nodenum + 1; i++)
    {
        cout << i;
        edgenode* temp = g->adjlist[i].first;
        while (temp != nullptr)
        {
            cout << " -> " << temp->b << "," << temp->w;
            temp = temp->next;
        }cout << endl;
    }
    return 0;
}

打印结果如下:

 

邻接表法(简化版)

在实际运用、写算法题中构造邻接表一般不需要写的像上面一样复杂,我们可以发现上述代码中构造一个headnode的顶点结构体主要是为了存储节点中的信息,如果结点之中不需要存储信息,那么我们可以只定义一个边表结点的结构体,然后用一个数组 edgenode* adjlist[maxsize] 即可表示顶点表,这个数组当中的每个元素就是一个顶点,具体实现如下所示:

#define maxsize 100
#define inf 999999
struct edgenode //边表结点
{
    int b;  //该结点的序号,通过该序号可以在邻接表adjlist[]中访问,得到该结点的信息
    int w;  //该条边的权值
    edgenode* next = nullptr; //指向下一个边表结点的指针
};

edgenode* adjlist[maxsize]; //邻接表,其中每个元素就是一个顶点,并且这里和常规链表一样,让每个顶点充当哨兵结点可以方便操作

int main()
{
    for (int i = 1; i <= 3; i++)
    {
        adjlist[i] = (edgenode*)malloc(sizeof(edgenode));
        edgenode* tmp = (edgenode*)malloc(sizeof(edgenode));
        tmp->b = i;
        tmp->next = NULL;
        adjlist[i]->next = tmp;
    }
    for (int i = 1; i <= 3; i++)
    {
        edgenode* tmp = (edgenode*)malloc(sizeof(edgenode));
        tmp->b = i + 1;
        tmp->next = adjlist[i]->next;   //头插法插入新节点
        adjlist[i]->next = tmp;
    }
    for (int i = 1; i <= 3; i++)
    {
        printf("%d:", i);
        for (edgenode* it = adjlist[i]->next; it != NULL; it = it->next)
        {
            printf(" -> %d", it->b);
        }
        printf("\n");
    }
}

 打印结果如下:

                                

十字链表法(只能存储有向图)

十字链表法没用过,对我来说还是比较抽象的,自己动手画一遍会好点。

相比邻接表法的改进在于:顶点节点中添加了一个firstin域,连接以该顶点作为弧头的第一条弧(即入边);边表结点中添加了两个int类型的域来存储头结点和尾结点,并又添加了一个hlink域,连接弧头相同的下一条弧(即顶点的下一条入边) 。

【注】画图时要注意边表结点中的指针都是在边表结点当中互连的,不要连接到顶点表上去。

邻接多重表(只能存储无向图)

用邻接表存储无向图会将一条边重复存储,出现冗余的问题,当要删除一个结点时需要分别在两个结点的边表中遍历,效率较低。因此可以采用如下的邻接多重表:

对我来说太抽象了。。不过吉大不考,先放过吧。 

吉大版本的三元组表和十字链表

参考《数据结构》P80 - P82。

图的部分基本操作

在图中插入新结点

不论是有向图还是无向图,都是与上图一样的操作。

邻接矩阵中插入新结点,只需要在顶点表中新开一个区域存放其信息,在邻接矩阵中新开一行和一列即可,然后再存放边。

 邻接表中插入新结点,只需要在顶点表中新开一个区域,然后依次插入边即可。

在图中删除结点

有向图与无向图的操作都一样。 

邻接矩阵中删除结点,如果要释放该结点所对应的空间的话,会导致出现大量的数据移动操作,复杂性较低。因此可以在邻接矩阵中将该结点所对应的行和列上的值都设为0/∞,同时在顶点表中添加一个bool类型的变量,用来表示某个顶点是否是空结点。

邻接表中删除结点,除了在顶点表中删除该结点所对应的所有边之外,还需要在其他顶点中遍历,删除连有该结点的其他信息。

图的遍历

无向图的广度优先遍历(邻接矩阵)

#define maxsize 100
#define inf 999999
struct graph
{
    char node[maxsize];  //顶点表,记录每个点的名字,例如1号节点叫A,2号节点叫B
    int edge[maxsize][maxsize]; //邻接矩阵
    int nodenum, edgenum = 0; //记录图的顶点数和边数
};

//构造邻接矩阵,传入图的信息及节点数量
graph* creat(vector<vector<int>>& vec, int node_num)
{
    graph* g = new graph;
    //初始化矩阵
    g->nodenum = node_num;
    for (int i = 0; i < maxsize; i++)
    {
        for (int j = 0; j < maxsize; j++)
        {
            //所有元素初始化为0
            g->edge[i][j] = 0;
        }
    }
    for (int i = 0; i < vec.size(); i++)
    {
        //一条两边结点为a和b的边
        int a = vec[i][0];
        int b = vec[i][1];
        g->edge[a][b] = 1;
        g->edge[b][a] = 1;
        g->edgenum++;
    }
    return g;
}

//在图g中,从顶点v开始进行广度优先遍历
void BFS(graph* g, int v, bool visited[])
{
    queue<int> que;     //辅助队列
    cout << v << " ";   //访问初始顶点v
    visited[v] = true;  //将顶点v标记已访问
    que.push(v);        //初始顶点v入队
    while (!que.empty())
    {
        int a = que.front();  //取队列头顶点a
        que.pop();
        for (int b = 1; b < g->nodenum + 1; b++)
        {
            if (g->edge[a][b] == 1) //如果a-b这条边存在
            {
                if (!visited[b])    //如果顶点b还未被访问过
                {
                    cout << b << " ";   //访问顶点b
                    visited[b] = true;  //将顶点b标记已访问
                    que.push(b);        //将顶点b入队   
                }
            }
        }
    }
}

int main()
{
    //每个一维的数组表示一条边的信息,依次是头结点、尾结点、权值
    vector<vector<int>> vec = { {1, 2, 0}, {1, 5, 0}, {2, 6, 0}, {6, 3, 0}, {6, 7, 0}, {3, 7, 0}, {3, 4, 0}, {4, 7, 0}, {4, 8, 0}, {7, 8, 0} };
    graph* g = creat(vec, 8);    //构造图

    bool visited[maxsize];    //辅助函数,用于判断某顶点是否被访问过

    for (int i = 1; i < g->nodenum + 1; i++)
    {
        visited[i] = false;
    }
    cout << "从顶点1出发得到的广度优先遍历:";
    BFS(g, 1, visited);    //从顶点1开始进行广度优先遍历
    cout << endl;

    for (int i = 1; i < g->nodenum + 1; i++)
    {
        visited[i] = false;
    }
    cout << "从顶点2出发得到的广度优先遍历:";
    BFS(g, 2, visited);    //从顶点2开始进行广度优先遍历
    cout << endl;

    for (int i = 1; i < g->nodenum + 1; i++)
    {
        visited[i] = false;
    }
    cout << "从顶点3出发得到的广度优先遍历:";
    BFS(g, 3, visited);    //从顶点3开始进行广度优先遍历
    return 0;
}

对于下图来说:

得到的打印结果如下所示:

【注】由于邻接矩阵的表示方式唯一,所得到的从顶点出发得到的广度优先遍历序列是唯一的,得到的广度优先生成树也是唯一的

无向图的广度优先遍历(邻接表)

#define maxsize 100
#define inf 999999
struct edgenode //边表结点
{
    int b;  //该结点的位置,通过该位置可以在邻接表adjlist[]中访问,得到该结点的信息
    int w;  //该条边的权值
    edgenode* next = nullptr; //指向下一个边表结点的指针
};
struct headnode //顶点表结点
{
    char data;   //存储该结点的信息,例如名字啥的
    edgenode* first = nullptr;    //指向第一个边表结点
};
struct graph
{
    headnode adjlist[maxsize];  //邻接表
    int nodenum, edgenum = 0; //记录图的顶点数和边数
};

//向某个顶点中,用头插法插入新的边表结点
void insert_edgenode(headnode& hn, edgenode* en)
{
    en->next = hn.first;
    hn.first = en;
}

//构造邻接表,传入图的信息及节点数量
graph* creat(vector<vector<int>>& vec, int node_num)
{
    graph* g = new graph;
    //初始化邻接表
    g->nodenum = node_num;
    for (int i = 0; i < vec.size(); i++)
    {
        //一条两边结点为a和b的边
        int a = vec[i][0];
        int b = vec[i][1];
        //创建一个新的边表结点
        edgenode* node = new edgenode;
        node->b = b;
        insert_edgenode(g->adjlist[a], node);

        node = new edgenode;
        node->b = a;
        insert_edgenode(g->adjlist[b], node);
    }
    return g;
}

//在图g中,从顶点v开始进行广度优先遍历
void BFS(graph* g, int v, bool visited[])
{
    queue<int> que;     //辅助队列
    cout << v << " ";   //访问初始顶点v
    visited[v] = true;  //将顶点v标记已访问
    que.push(v);        //初始顶点v入队
    while (!que.empty())
    {
        int a = que.front();  //取队列头顶点a
        que.pop();
        for (edgenode* it = g->adjlist[a].first; it != nullptr; it = it->next)
        {
            int b = it->b;
            if (!visited[b])    //如果顶点b还未被访问过
            {
                cout << b << " ";   //访问顶点b
                visited[b] = true;  //将顶点b标记已访问
                que.push(b);        //将顶点b入队   
            }
        }
    }
}

int main()
{
    //每个一维的数组表示一条边的信息,依次是头结点、尾结点、权值
    vector<vector<int>> vec = { {1, 2, 0}, {1, 5, 0}, {2, 6, 0}, {6, 3, 0}, {6, 7, 0}, {3, 7, 0}, {3, 4, 0}, {4, 7, 0}, {4, 8, 0}, {7, 8, 0}};
    graph* g = creat(vec, 8);
    //打印邻接矩阵
    for (int i = 1; i < g->nodenum + 1; i++)
    {
        cout << i;
        edgenode* temp = g->adjlist[i].first;
        while (temp != nullptr)
        {
            cout << " -> " << temp->b;
            temp = temp->next;
        }cout << endl;
    }cout << endl;

    bool visited[maxsize];    //辅助函数,用于判断某顶点是否被访问过

    for (int i = 1; i < g->nodenum + 1; i++)
    {
        visited[i] = false;
    }
    cout << "从顶点1出发得到的广度优先遍历:";
    BFS(g, 1, visited);    //从顶点1开始进行广度优先遍历
    cout << endl;

    for (int i = 1; i < g->nodenum + 1; i++)
    {
        visited[i] = false;
    }
    cout << "从顶点2出发得到的广度优先遍历:";
    BFS(g, 2, visited);    //从顶点2开始进行广度优先遍历
    cout << endl;

    for (int i = 1; i < g->nodenum + 1; i++)
    {
        visited[i] = false;
    }
    cout << "从顶点3出发得到的广度优先遍历:";
    BFS(g, 3, visited);    //从顶点3开始进行广度优先遍历

    return 0;
}

对于下图来说:

打印结果为:

 

【注】由于邻接表的表示方式不唯一,因此从某个顶点出发得到的广度优先遍历是不唯一的,得到的广度优先生成树也是不唯一的

无向图的广度优先遍历(含有多个连通分量的无向图)

上述BFS()函数的问题在于,若图中含有多个连通分量,那么仅凭一次BFS是无法遍历完所有顶点的,因此可以增加一个BFS_traverse()函数,每检测到visited数组中还有未被访问过的结点,就调用一次BFS函数,以邻接矩阵存储为例,代码如下所示:

#define maxsize 100
#define inf 999999
struct graph
{
    char node[maxsize];  //顶点表,记录每个点的名字,例如1号节点叫A,2号节点叫B
    int edge[maxsize][maxsize]; //邻接矩阵
    int nodenum, edgenum = 0; //记录图的顶点数和边数
};

//构造邻接矩阵,传入图的信息及节点数量
graph* creat(vector<vector<int>>& vec, int node_num)
{
    graph* g = new graph;
    //初始化矩阵
    g->nodenum = node_num;
    for (int i = 0; i < maxsize; i++)
    {
        for (int j = 0; j < maxsize; j++)
        {
            //所有元素初始化为0
            g->edge[i][j] = 0;
        }
    }
    for (int i = 0; i < vec.size(); i++)
    {
        //一条两边结点为a和b的边
        int a = vec[i][0];
        int b = vec[i][1];
        g->edge[a][b] = 1;
        g->edge[b][a] = 1;
        g->edgenum++;
    }
    return g;
}

//在图g中,从顶点v开始进行广度优先遍历
void BFS(graph* g, int v, bool visited[])
{
    queue<int> que;     //辅助队列
    cout << v << " ";   //访问初始顶点v
    visited[v] = true;  //将顶点v标记已访问
    que.push(v);        //初始顶点v入队
    while (!que.empty())
    {
        int a = que.front();  //取队列头顶点a
        que.pop();
        for (int b = 1; b < g->nodenum + 1; b++)
        {
            if (g->edge[a][b] == 1) //如果a-b这条边存在
            {
                if (!visited[b])    //如果顶点b还未被访问过
                {
                    cout << b << " ";   //访问顶点b
                    visited[b] = true;  //将顶点b标记已访问
                    que.push(b);        //将顶点b入队   
                }
            }
        }
    }
}

//对图进行广度优先遍历的总函数
void BFS_traverse(graph* g)
{
    bool visited[maxsize];    //辅助函数,用于判断某顶点是否被访问过
    for (int i = 1; i < g->nodenum + 1; i++)
    {
        visited[i] = false;
    }
    cout << "该图的广度遍历序列为:";
    for (int i = 1; i < g->nodenum + 1; i++)
    {
        //对每个连通分量调用一次BFS()函数
        if (!visited[i])
        {
            BFS(g, i, visited);    //从顶点i开始进行广度优先遍历
        }
    }
}


int main()
{
    //每个一维的数组表示一条边的信息,依次是头结点、尾结点、权值
    vector<vector<int>> vec = { {1, 2, 0}, {1, 5, 0}, {2, 6, 0}, {6, 3, 0}, {6, 7, 0}, {3, 7, 0}, {3, 4, 0}, {4, 7, 0}, {4, 8, 0}, {7, 8, 0}, {9, 10, 0}, {9, 11, 0}, {10, 11, 0} };
    graph* g = creat(vec, 11);    //构造图

    BFS_traverse(g);

    return 0;
}

对于下图来说:

                         

打印结果为:

【注】图中有几个连通分量就调用几次BFS函数

有向图的广度优先遍历

对于有向图来说,一次BFS不一定能遍历完所有顶点,例如下图所示:

若从顶点1出发,则一次BFS无法遍历完所有顶点。

若从顶点7或顶点8出发,只需一次BFS就可以遍历完所有顶点。

因此针对有向图,可以采用上述的“无向图的广度优先遍历(含有多个连通分量的无向图)”这种方法,即多次调用BFS。

无向图的深度优先遍历(邻接矩阵)

深度优先遍历与广度优先遍历相比,都只是换个DFS函数罢了

#define maxsize 100
#define inf 999999
struct graph
{
    char node[maxsize];  //顶点表,记录每个点的名字,例如1号节点叫A,2号节点叫B
    int edge[maxsize][maxsize]; //邻接矩阵
    int nodenum, edgenum = 0; //记录图的顶点数和边数
};

//构造邻接矩阵,传入图的信息及节点数量
graph* creat(vector<vector<int>>& vec, int node_num)
{
    graph* g = new graph;
    //初始化矩阵
    g->nodenum = node_num;
    for (int i = 0; i < maxsize; i++)
    {
        for (int j = 0; j < maxsize; j++)
        {
            //所有元素初始化为0
            g->edge[i][j] = 0;
        }
    }
    for (int i = 0; i < vec.size(); i++)
    {
        //一条两边结点为a和b的边
        int a = vec[i][0];
        int b = vec[i][1];
        g->edge[a][b] = 1;
        g->edge[b][a] = 1;
        g->edgenum++;
    }
    return g;
}
//在图g中,从顶点v开始进行深度度优先遍历
void DFS(graph* g, int v, bool visited[])
{
    cout << v << " ";   //访问初始顶点v
    visited[v] = true;  //将顶点v标记已访问
    for (int b = 1; b < g->nodenum + 1; b++)
    {
        int a = v;
        if (g->edge[a][b] == 1)    //如果a-b这条边存在
        {
            if (!visited[b])       //如果顶点b还未被访问过
            {
                DFS(g, b, visited);
            }
        }
    }
}

int main()
{
    //每个一维的数组表示一条边的信息,依次是头结点、尾结点、权值
    vector<vector<int>> vec = { {1, 2, 0}, {1, 5, 0}, {2, 6, 0}, {6, 3, 0}, {6, 7, 0}, {3, 7, 0}, {3, 4, 0}, {4, 7, 0}, {4, 8, 0}, {7, 8, 0} };
    graph* g = creat(vec, 8);    //构造图

    bool visited[maxsize];    //辅助函数,用于判断某顶点是否被访问过

    for (int i = 1; i < g->nodenum + 1; i++)
    {
        visited[i] = false;
    }
    cout << "从顶点1出发得到的深度优先遍历:";
    DFS(g, 1, visited);    //从顶点1开始进行深度优先遍历
    cout << endl;

    for (int i = 1; i < g->nodenum + 1; i++)
    {
        visited[i] = false;
    }
    cout << "从顶点2出发得到的深度优先遍历:";
    DFS(g, 2, visited);    //从顶点2开始进行深度优先遍历
    cout << endl;

    for (int i = 1; i < g->nodenum + 1; i++)
    {
        visited[i] = false;
    }
    cout << "从顶点3出发得到的深度优先遍历:";
    DFS(g, 3, visited);    //从顶点3开始进行深度优先遍历
    return 0;
}

对于下图来说:

 打印结果为:

【注】由于邻接矩阵的表示方式唯一,所得到的从顶点出发得到的深度优先遍历序列是唯一的,得到的深度优先生成树也是唯一的

无向图的深度优先遍历(邻接表)

同理将广度优先遍历的BFS函数改为DFS函数即可

#define maxsize 100
#define inf 999999
struct edgenode //边表结点
{
    int b;  //该结点的位置,通过该位置可以在邻接表adjlist[]中访问,得到该结点的信息
    int w;  //该条边的权值
    edgenode* next = nullptr; //指向下一个边表结点的指针
};
struct headnode //顶点表结点
{
    char data;   //存储该结点的信息,例如名字啥的
    edgenode* first = nullptr;    //指向第一个边表结点
};
struct graph
{
    headnode adjlist[maxsize];  //邻接表
    int nodenum, edgenum = 0; //记录图的顶点数和边数
};

//向某个顶点中,用头插法插入新的边表结点
void insert_edgenode(headnode& hn, edgenode* en)
{
    en->next = hn.first;
    hn.first = en;
}

//构造邻接表,传入图的信息及节点数量
graph* creat(vector<vector<int>>& vec, int node_num)
{
    graph* g = new graph;
    //初始化邻接表
    g->nodenum = node_num;
    for (int i = 0; i < vec.size(); i++)
    {
        //一条两边结点为a和b的边
        int a = vec[i][0];
        int b = vec[i][1];
        //创建一个新的边表结点
        edgenode* node = new edgenode;
        node->b = b;
        insert_edgenode(g->adjlist[a], node);

        node = new edgenode;
        node->b = a;
        insert_edgenode(g->adjlist[b], node);
    }
    return g;
}

//在图g中,从顶点v开始进行深度度优先遍历
void DFS(graph* g, int v, bool visited[])
{
    cout << v << " ";   //访问初始顶点v
    visited[v] = true;  //将顶点v标记已访问
    int a = v;
    for (edgenode* it = g->adjlist[a].first; it != nullptr; it = it->next)
    {
        int b = it->b;
        if (!visited[b])
        {
            DFS(g, b, visited);
        }
    }
}

int main()
{
    //每个一维的数组表示一条边的信息,依次是头结点、尾结点、权值
    vector<vector<int>> vec = { {1, 2, 0}, {1, 5, 0}, {2, 6, 0}, {6, 3, 0}, {6, 7, 0}, {3, 7, 0}, {3, 4, 0}, {4, 7, 0}, {4, 8, 0}, {7, 8, 0} };
    graph* g = creat(vec, 8);    //构造图

    bool visited[maxsize];    //辅助函数,用于判断某顶点是否被访问过

    for (int i = 1; i < g->nodenum + 1; i++)
    {
        visited[i] = false;
    }
    cout << "从顶点1出发得到的深度优先遍历:";
    DFS(g, 1, visited);    //从顶点1开始进行深度优先遍历
    cout << endl;

    for (int i = 1; i < g->nodenum + 1; i++)
    {
        visited[i] = false;
    }
    cout << "从顶点2出发得到的深度优先遍历:";
    DFS(g, 2, visited);    //从顶点2开始进行深度优先遍历
    cout << endl;

    for (int i = 1; i < g->nodenum + 1; i++)
    {
        visited[i] = false;
    }
    cout << "从顶点3出发得到的深度优先遍历:";
    DFS(g, 3, visited);    //从顶点3开始进行深度优先遍历
    return 0;
}

对于下图来说:

 打印结果为:

【注】由于邻接表的表示方式不唯一,因此从某个顶点出发得到的深度优先遍历是不唯一的,得到的深度优先生成树也是不唯一的 

无向图的深度优先遍历(邻接表)(非递归版本)

在写题时遇到的,顺便记录一下。

其实和BFS差不多,只要把队列换成栈,就可以由BFS变为DFS了

#define maxsize 100
#define inf 999999
struct edgenode //边表结点
{
    int b;  //该结点的位置,通过该位置可以在邻接表adjlist[]中访问,得到该结点的信息
    int w;  //该条边的权值
    edgenode* next = nullptr; //指向下一个边表结点的指针
};
struct headnode //顶点表结点
{
    char data;   //存储该结点的信息,例如名字啥的
    edgenode* first = nullptr;    //指向第一个边表结点
};
struct graph
{
    headnode adjlist[maxsize];  //邻接表
    int nodenum, edgenum = 0; //记录图的顶点数和边数
};

//向某个顶点中,用头插法插入新的边表结点
void insert_edgenode(headnode& hn, edgenode* en)
{
    en->next = hn.first;
    hn.first = en;
}

//构造邻接表,传入图的信息及节点数量
graph* creat(vector<vector<int>>& vec, int node_num)
{
    graph* g = new graph;
    //初始化邻接表
    g->nodenum = node_num;
    for (int i = 0; i < vec.size(); i++)
    {
        //一条两边结点为a和b的边
        int a = vec[i][0];
        int b = vec[i][1];
        //创建一个新的边表结点
        edgenode* node = new edgenode;
        node->b = b;
        insert_edgenode(g->adjlist[a], node);

        node = new edgenode;
        node->b = a;
        insert_edgenode(g->adjlist[b], node);
    }
    return g;
}

//在图g中,从顶点v开始进行深度度优先遍历
void DFS(graph* g, int v, bool visited[])
{
    stack<int> sta;
    sta.push(v);
    visited[v] = true;
    while (!sta.empty())
    {
        int cur = sta.top();
        sta.pop();
        cout << cur << " ";   //访问初始顶点v
        for (edgenode* it = g->adjlist[cur].first; it != nullptr; it = it->next)
        {
            int b = it->b;
            if (!visited[b])
            {
                sta.push(b);
                visited[b] = true;
            }
        }
    }
}

int main()
{
    //每个一维的数组表示一条边的信息,依次是头结点、尾结点、权值
    vector<vector<int>> vec = { {1, 2, 0}, {1, 5, 0}, {2, 6, 0}, {6, 3, 0}, {6, 7, 0}, {3, 7, 0}, {3, 4, 0}, {4, 7, 0}, {4, 8, 0}, {7, 8, 0} };
    graph* g = creat(vec, 8);    //构造图

    bool visited[maxsize];    //辅助函数,用于判断某顶点是否被访问过

    for (int i = 1; i < g->nodenum + 1; i++)
    {
        visited[i] = false;
    }
    cout << "从顶点1出发得到的深度优先遍历:";
    DFS(g, 1, visited);    //从顶点1开始进行深度优先遍历
    cout << endl;

    for (int i = 1; i < g->nodenum + 1; i++)
    {
        visited[i] = false;
    }
    cout << "从顶点2出发得到的深度优先遍历:";
    DFS(g, 2, visited);    //从顶点2开始进行深度优先遍历
    cout << endl;

    for (int i = 1; i < g->nodenum + 1; i++)
    {
        visited[i] = false;
    }
    cout << "从顶点3出发得到的深度优先遍历:";
    DFS(g, 3, visited);    //从顶点3开始进行深度优先遍历
    return 0;
}

 对于下图来说:

 打印结果为:

无向图的深度优先遍历(含有多个连通分量的无向图)

与广度优先遍历类似,需要解决在多个连通分量下无法遍历完所有顶点的问题,以邻接矩阵存储为例的代码:

#define maxsize 100
#define inf 999999
struct graph
{
    char node[maxsize];  //顶点表,记录每个点的名字,例如1号节点叫A,2号节点叫B
    int edge[maxsize][maxsize]; //邻接矩阵
    int nodenum, edgenum = 0; //记录图的顶点数和边数
};

//构造邻接矩阵,传入图的信息及节点数量
graph* creat(vector<vector<int>>& vec, int node_num)
{
    graph* g = new graph;
    //初始化矩阵
    g->nodenum = node_num;
    for (int i = 0; i < maxsize; i++)
    {
        for (int j = 0; j < maxsize; j++)
        {
            //所有元素初始化为0
            g->edge[i][j] = 0;
        }
    }
    for (int i = 0; i < vec.size(); i++)
    {
        //一条两边结点为a和b的边
        int a = vec[i][0];
        int b = vec[i][1];
        g->edge[a][b] = 1;
        g->edge[b][a] = 1;
        g->edgenum++;
    }
    return g;
}
//在图g中,从顶点v开始进行深度度优先遍历
void DFS(graph* g, int v, bool visited[])
{
    cout << v << " ";   //访问初始顶点v
    visited[v] = true;  //将顶点v标记已访问
    for (int b = 1; b < g->nodenum + 1; b++)
    {
        int a = v;
        if (g->edge[a][b] == 1)    //如果a-b这条边存在
        {
            if (!visited[b])       //如果顶点b还未被访问过
            {
                DFS(g, b, visited);
            }
        }
    }
}

//对图进行深度优先遍历的总函数
void DFS_traverse(graph* g)
{
    bool visited[maxsize];    //辅助函数,用于判断某顶点是否被访问过
    for (int i = 1; i < g->nodenum + 1; i++)
    {
        visited[i] = false;
    }
    cout << "该图的深度遍历序列为:";
    for (int i = 1; i < g->nodenum + 1; i++)
    {
        //对每个连通分量调用一次DFS()函数
        if (!visited[i])
        {
            DFS(g, i, visited);    //从顶点i开始进行广度优先遍历
        }
    }
}

int main()
{
    //每个一维的数组表示一条边的信息,依次是头结点、尾结点、权值
    vector<vector<int>> vec = { {1, 2, 0}, {1, 5, 0}, {2, 6, 0}, {6, 3, 0}, {6, 7, 0}, {3, 7, 0}, {3, 4, 0}, {4, 7, 0}, {4, 8, 0}, {7, 8, 0}, {9, 10, 0}, {9, 11, 0}, {10, 11, 0} };
    graph* g = creat(vec, 11);    //构造图

    DFS_traverse(g);

    return 0;
}

对于下图来说:

                                

 打印结果为:

【注】在DFS_traverse函数中,图中有几个连通分量就调用几次BFS函数

有向图的深度优先遍历

对于有向图来说,一次DFS不一定能遍历完所有顶点,例如下图所示:

若从顶点1出发,则一次DFS无法遍历完所有顶点。

若从顶点7或顶点8出发,只需一次DFS就可以遍历完所有顶点。

因此针对有向图,可以采用上述的“无向图的深度优先遍历(含有多个连通分量的无向图)”这种方法。

图的应用

最小生成树(prim算法)

适用于稠密图,时间复杂度O(n²) (因为算法中有两个O(n)循环)

#define maxsize 100
#define inf 999999
struct graph
{
    char node[maxsize];  //顶点表,记录每个点的名字,例如1号节点叫A,2号节点叫B
    int edge[maxsize][maxsize]; //邻接矩阵
    int nodenum, edgenum = 0; //记录图的顶点数和边数
};

//构造邻接矩阵,传入图的信息及节点数量
graph* creat(vector<vector<int>>& vec, int node_num)
{
    graph* g = new graph;
    //初始化矩阵
    g->nodenum = node_num;
    for (int i = 0; i < maxsize; i++)
    {
        for (int j = 0; j < maxsize; j++)
        {
            //对角线上的值设为0,其他的设为无穷大
            if (i == j) g->edge[i][j] = 0;
            else g->edge[i][j] = inf;
        }
    }
    for (int i = 0; i < vec.size(); i++)
    {
        //一条两边结点为a和b的边
        int a = vec[i][0];
        int b = vec[i][1];
        int w = vec[i][2];
        g->edge[a][b] = w;
        g->edge[b][a] = w;
        g->edgenum++;
    }
    return g;
}

int prim(graph* g, int start)
{
    //记录生成树(注意是生成树,不是开始节点)到图中的顶点的最短距离
    int minlen[maxsize];
    //记录某个顶点是否已经被访问过,即是否已经加入了生成树中
    bool visited[maxsize];
    //将visited数组和minlen数组初始化
    for (int i = 1; i < g->nodenum + 1; i++)
    {
        visited[i] = false;
        minlen[i] = inf;        //初始化为无穷大
    }
    //先将start到自身的距离minlen[start]设置为0,以便于在后续的prim算法中start能被选中作为cur_index
    minlen[start] = 0;      //注意先别访问start,即先别将visited[start]设为true,访问start是后续的事

    int ans = 0;    //记录最小生成树的权值之和
    //开始 prim 算法
    //每次循环选中一个顶点,因此需循环 g->nodenum 次
    for (int i = 0; i < g->nodenum; i++)    //这里的i无实际意义,只用于循环
    {
        int cur_index = -1;
        int cur_minlen = inf;
        //找出到生成树的距离最短的点
        for (int j = 1; j < g->nodenum + 1; j++)
        {
            if (visited[j]) continue;   //已经访问过的顶点不用再考虑
            if (cur_minlen > minlen[j])
            {
                cur_index = j;
                cur_minlen = minlen[j];
            }
        }
        //将找到的顶点cur_index加入生成树中
        ans += cur_minlen;
        visited[cur_index] = true;
        if (i > 0) cout << " -- ";
        cout << cur_index;
        //因为向生成树中加入了一个新的顶点cur_index,所以现在要通过cur_index更新其他顶点到生成树的最短距离
        for (int j = 1; j < g->nodenum + 1; j++)
        {
            //如果顶点j已经被访问过,则不用考虑
            if (visited[j]) continue;
            //当顶点j和顶点cur_index之间不存在边,即g->edge[cur_index][j] == inf时,自然也满足下面这个式子
            minlen[j] = min(minlen[j], g->edge[cur_index][j]);
        }
    }
    return ans;
}

int main()
{
    //每个一维的数组表示一条边的信息,依次是头结点、尾结点、权值
    vector<vector<int>> vec = { {1, 2, 6}, {1, 3, 1}, {1, 4, 5}, {2, 3, 5}, {3, 4, 5}, {2, 5, 3}, {3, 5, 6}, {3, 6, 4}, {4, 6, 2}, {5, 6, 6} };
    graph* g = creat(vec, 6);
    //打印邻接矩阵
    for (int i = 1; i < g->nodenum + 1; i++)
    {
        for (int j = 1; j < g->nodenum + 1; j++)
        {
            if (g->edge[i][j] == inf) cout << "i";
            else cout << g->edge[i][j];
            cout << " ";
        }cout << endl;
    }cout << endl;
    cout << "最小生成树为:";
    int ans = prim(g, 1);
    cout << endl;
    cout << "最小生成树的权值为:" << ans;
    return 0;
}

对于下图来说:

 打印结果如下:

最小生成树(kruskal算法)

适用于稀疏图,时间复杂度O(eloge)(并查集的时间复杂度大概是O(loge),再加上外面的一层O(e)循环)

这个算法挺好理解的,简单讲就是在构建一个生成树的时候尽量选用那些权值最小的边来构建;由此可知如果要生成一个最大生成树,那就是把kruskal算法改成每次尽量选取权值最大的边即可。

#define maxsize 100
#define inf 999999
struct graph
{
    char node[maxsize];  //顶点表,记录每个点的名字,例如1号节点叫A,2号节点叫B
    int edge[maxsize][maxsize]; //邻接矩阵
    int nodenum = 0, edgenum = 0; //记录图的顶点数和边数
};

//构造邻接矩阵,传入图的信息及节点数量
graph* creat(vector<vector<int>>& vec, int node_num)
{
    graph* g = new graph;
    //初始化矩阵
    g->nodenum = node_num;
    for (int i = 0; i < maxsize; i++)
    {
        for (int j = 0; j < maxsize; j++)
        {
            //对角线上的值设为0,其他的设为无穷大
            if (i == j) g->edge[i][j] = 0;
            else g->edge[i][j] = inf;
        }
    }
    for (int i = 0; i < vec.size(); i++)
    {
        //一条两边结点为a和b的边
        int a = vec[i][0];
        int b = vec[i][1];
        int w = vec[i][2];
        g->edge[a][b] = w;
        g->edge[b][a] = w;
        g->edgenum++;
    }
    return g;
}

//并查集
struct Union_Find
{
    int father[maxsize];
    Union_Find(int n)
    {
        for (int i = 1; i < n + 1; i++)
        {
            father[i] = -1;
        }
    }
    int Find(int x)
    {
        if (father[x] < 0) return x;
        return father[x] = Find(father[x]);
    }
    void Union(int x, int y)
    {
        int fx = Find(x);
        int fy = Find(y);
        if (fx == fy) return;
        if (father[fx] < father[fy])
        {
            father[fy] = fx;
        }
        else
        {
            if (father[fx] == father[fy]) father[fy]--;
            father[fx] = fy;
        }
    }
};

//边的排序函数
void sort_edge(graph* g, int edge[][3])
{
    int k = 1;      //k记录已遍历到的edge数组的下标
    for (int i = 1; i < g->nodenum + 1; i++)
    {
        for (int j = i + 1; j < g->nodenum + 1; j++)
        {
            if (g->edge[i][j] == inf) continue; //如果i - j这条边不存在,跳过
            //分别存储两个节点和权值
            edge[k][0] = i;
            edge[k][1] = j;
            edge[k][2] = g->edge[i][j];
            k++;
        }
    }

    //冒泡排序
    for (int i = g->edgenum; i > 1; i--)
    {
        bool flag = false;
        for (int j = 1; j < i; j++)
        {
            if (edge[j][2] > edge[j + 1][2])
            {
                int tmp[3];
                //void * memcpy ( void * destination, const void * source, num )函数的用法:
                //将数组source复制num个字节到数组destination中
                memcpy(tmp, edge[j], sizeof(int) * 3);
                memcpy(edge[j], edge[j + 1], sizeof(int) * 3);
                memcpy(edge[j + 1], tmp, sizeof(int) * 3);
                flag = true;
            }
        }
        if (!flag) break;
    }

    cout << "排序后的边为:";
    for (int k = 1; k < g->edgenum + 1; k++)
    {
        cout << edge[k][0] << "--" << edge[k][1] << ":" << edge[k][2] << "  ";
    }cout << endl << endl;
}

int kruskal(graph* g)
{
    int edge[maxsize][3];   //用edge存储图的所有边
    sort_edge(g, edge);     //将所有边按权值从小到大排序
    Union_Find uf(g->nodenum);
    int T = g->nodenum;     //T表示连通分量的个数,一开始即为顶点个数
    int k = 1;              //edge数组中的元素下标
    int ans = 0;            //最小生成树的权值之和

    printf("最小生成树为:");
    //开始kruskal算法
    while (T > 1)           //直到连通分量个数为1,说明连成了一棵最小生成树
    {
        int a = edge[k][0];
        int b = edge[k][1];
        int w = edge[k][2];
        k++;
        if (uf.Find(a) == uf.Find(b))   //如果顶点a和顶点b属于同一个连通分量,跳过
        {
            continue;
        }
        uf.Union(a, b);     //连接顶点a和顶点b所在的两个连通分量
        printf("%d--%d:%d  ", a, b, w);
        ans += w;
        T--;                //连通分量个数减一
    }
    printf("\n\n");
    return ans;
}

int main()
{
    //每个一维的数组表示一条边的信息,依次是头结点、尾结点、权值
    vector<vector<int>> vec = { {1, 2, 6}, {1, 3, 1}, {1, 4, 5}, {2, 3, 5}, {3, 4, 4}, {2, 5, 3}, {3, 5, 6}, {3, 6, 4}, {4, 6, 2}, {5, 6, 6} };
    graph* g = creat(vec, 6);
    //打印邻接矩阵
    for (int i = 1; i < g->nodenum + 1; i++)
    {
        for (int j = 1; j < g->nodenum + 1; j++)
        {
            if (g->edge[i][j] == inf) cout << "i";
            else cout << g->edge[i][j];
            cout << " ";
        }cout << endl;
    }cout << endl;
    int ans = kruskal(g);
    cout << "最小生成树的权值为:" << ans << endl;
    return 0;
}

由于我对并查集比较熟悉,所以感觉kruskal算法比较好理解,代码也比较好写。

对下图来说:

结果为:

最小生成树的变形题

(21条消息) 最小生成树算法的相关变形题_作用太大了销夜的博客-CSDN博客

最短路径问题-BFS算法(单源,无权图)

就是在BFS的基础上加了两个数组,以邻接表的存储形式为例:

#define maxsize 100
#define inf 999999
struct edgenode //边表结点
{
    int b;  //该结点的位置,通过该位置可以在邻接表adjlist[]中访问,得到该结点的信息
    int w;  //该条边的权值
    edgenode* next = nullptr; //指向下一个边表结点的指针
};
struct headnode //顶点表结点
{
    char data;   //存储该结点的信息,例如名字啥的
    edgenode* first = nullptr;    //指向第一个边表结点
};
struct graph
{
    headnode adjlist[maxsize];  //邻接表
    int nodenum, edgenum = 0; //记录图的顶点数和边数
};

//向某个顶点中,用头插法插入新的边表结点
void insert_edgenode(headnode& hn, edgenode* en)
{
    en->next = hn.first;
    hn.first = en;
}

//构造邻接表,传入图的信息及节点数量
graph* creat(vector<vector<int>>& vec, int node_num)
{
    graph* g = new graph;
    //初始化邻接表
    g->nodenum = node_num;
    for (int i = 0; i < vec.size(); i++)
    {
        //一条两边结点为a和b的边
        int a = vec[i][0];
        int b = vec[i][1];
        //创建一个新的边表结点
        edgenode* node = new edgenode;
        node->b = b;
        insert_edgenode(g->adjlist[a], node);

        node = new edgenode;
        node->b = a;
        insert_edgenode(g->adjlist[b], node);
    }
    return g;
}

int dist[maxsize];  //记录某个顶点到源点的最短路径
int path[maxsize];  //顶点在这条最短路径上的前驱顶点
//在图g中,求顶点v到其他顶点的最短路径
void BFS_min_distance(graph* g, int v, bool visited[])
{
    for (int i = 1; i < g->nodenum + 1; i++)
    {
        dist[i] = inf;  //初始距离都设为无穷大
        path[i] = -1;   //初始前驱都设为-1
    }
    queue<int> que;     //辅助队列
    dist[v] = 0;        //源点v到自身的距离初始化为0
    visited[v] = true;  //将顶点v标记已访问
    que.push(v);        //初始顶点v入队
    while (!que.empty())
    {
        int a = que.front();  //取队列头顶点a
        que.pop();
        for (edgenode* it = g->adjlist[a].first; it != nullptr; it = it->next)
        {
            int b = it->b;
            if (!visited[b])    //如果顶点b还未被访问过
            {
                dist[b] = dist[a] + 1;
                path[b] = a;
                visited[b] = true;  //将顶点b标记已访问
                que.push(b);        //将顶点b入队   
            }
        }
    }
}

int main()
{
    //每个一维的数组表示一条边的信息,依次是头结点、尾结点、权值
    vector<vector<int>> vec = { {1, 2, 0}, {1, 5, 0}, {2, 6, 0}, {6, 3, 0}, {6, 7, 0}, {3, 7, 0}, {3, 4, 0}, {4, 7, 0}, {4, 8, 0}, {7, 8, 0} };
    graph* g = creat(vec, 8);
    //打印邻接矩阵
    for (int i = 1; i < g->nodenum + 1; i++)
    {
        cout << i;
        edgenode* temp = g->adjlist[i].first;
        while (temp != nullptr)
        {
            cout << " -> " << temp->b;
            temp = temp->next;
        }cout << endl;
    }cout << endl;

    bool visited[maxsize];    //辅助函数,用于判断某顶点是否被访问过

    for (int i = 1; i < g->nodenum + 1; i++)
    {
        visited[i] = false;
    }
    BFS_min_distance(g, 2, visited);
    cout << "2到8的最短路径为:";
    int i = 8;
    while (i != -1)
    {
        cout << i << " <- ";
        i = path[i];
    }cout << endl;
    cout << "其长度为:" << dist[8];
    return 0;
}

对于下图来说: 

 打印结果为:

最短路径问题-dijkstra算法(单源,带正权图)

dijkstra算法与prim算法差不多,有非常多相似的地方,下面代码我尽量与上面的prim算法写成一个模板:

#define maxsize 100
#define inf 999999
struct graph
{
    char node[maxsize];  //顶点表,记录每个点的名字,例如1号节点叫A,2号节点叫B
    int edge[maxsize][maxsize]; //邻接矩阵
    int nodenum, edgenum; //记录图的顶点数和边数
};

//构造邻接矩阵,传入图的信息及节点数量
graph* creat(vector<vector<int>>& vec, int node_num)
{
    graph* g = new graph;
    //初始化矩阵
    g->nodenum = node_num;
    for (int i = 0; i < maxsize; i++)
    {
        for (int j = 0; j < maxsize; j++)
        {
            //对角线上的值设为0,其他的设为无穷大
            if (i == j) g->edge[i][j] = 0;
            else g->edge[i][j] = inf;
        }
    }
    for (int i = 0; i < vec.size(); i++)
    {
        //一条a->b,权值为w的边
        int a = vec[i][0];
        int b = vec[i][1];
        int w = vec[i][2];
        g->edge[a][b] = w;
        g->edgenum++;
    }
    return g;
}

int minlen[maxsize];    //记录源点到每个顶点的最短距离
bool visited[maxsize];  //记录每个顶点是否已经确定了最短路径
int path[maxsize];      //记录每个顶点在这条最短路径上的前驱顶点
//dijkstra算法求源点start到其他顶点的最短距离
void dijkstra(graph* g, int start)
{
    for (int i = 1; i < g->nodenum + 1; i++)
    {
        visited[i] = false;
        minlen[i] = inf;    //初始化为无穷大
        path[i] = -1;
    }
    //先将start到自身的距离minlen[start]设置为0,以便于在后续的dijkstra算法中start能被选中作为cur_index
    minlen[start] = 0;      //注意先别访问start,即先别将visited[start]设为true,访问start是后续的事

    //开始dijkstra算法
    //每次循环选中一个顶点,因此需循环 g->nodenum 次
    for (int i = 0; i < g->nodenum; i++)    //i无实际意义,只用于循环
    {
        int cur_index = -1;
        int cur_minlen = inf;
        //找出源点到其距离最短的点
        for (int j = 1; j < g->nodenum + 1; j++)
        {
            if (visited[j]) continue;   //已经访问过的顶点不再考虑
            if (cur_minlen > minlen[j])
            {
                cur_minlen = minlen[j];
                cur_index = j;
            }
        }
        visited[cur_index] = true;
        //在新确定的顶点的基础上,若源点start通过顶点cur_index到其他顶点的距离更短,则更新
        for (int j = 1; j < g->nodenum + 1; j++)
        {
            //如果顶点j已经被访问过,则不用考虑
            if (visited[j]) continue;
            //minlen[cur_index]是源点start到顶点cur_index的距离,g->edge[cur_index][j]是顶点cur_index到顶点j的距离
            //所以minlen[cur_index] + g->edge[cur_index][j]就是源点start到顶点j的距离
            if (minlen[j] > minlen[cur_index] + g->edge[cur_index][j])
            {
                //当顶点j和顶点cur_index之间不存在边,即g->edge[cur_index][j] == inf时,自然也满足下面这个式子
                minlen[j] = minlen[cur_index] + g->edge[cur_index][j];
                path[j] = cur_index;    //更新了顶点j所在的最短路径,所以更新其前驱
            }
        }
    }
}

int main()
{
    //每个一维的数组表示一条边的信息,依次是头结点、尾结点、权值
    vector<vector<int>> vec = { {1, 2, 10}, {1, 5, 5}, {2, 5, 2}, {5, 2, 3}, {2, 3, 1}, {5, 3, 9}, {5, 4, 2}, {4, 1, 7}, {3, 4, 4}, {4, 3, 6} };
    graph* g = creat(vec, 5);
    //打印邻接矩阵
    for (int i = 1; i < g->nodenum + 1; i++)
    {
        for (int j = 1; j < g->nodenum + 1; j++)
        {
            if (g->edge[i][j] == inf) cout << "i";
            else cout << g->edge[i][j];
            cout << " ";
        }cout << endl;
    }cout << endl;
    dijkstra(g, 1);
    cout << "minlen数组:";
    for (int i = 1; i < g->nodenum + 1; i++)
    {
        cout << minlen[i] << " ";
    }cout << endl;
    cout << "path数组:";
    for (int i = 1; i < g->nodenum + 1; i++)
    {
        cout << path[i] << " ";
    }cout << endl;

    cout << "1到3的最短路径为:";
    int i = 3;
    while (i != -1)
    {
        cout << i << " <- ";
        i = path[i];
    }cout << "1" << endl;
    cout << "其长度为:" << minlen[3] << endl;
    return 0;
}

对于下图来说:

 打印结果为:

dijkstra算法与prim算法的区别

这两种算法十分相似

①dijkstra算法由于需要记录最短路径,因此比prim算法多出一个查找前驱顶点的path数组

②在prim算法中,对于已经访问过的顶点,其minlen数组的值只在未访问之前有用,而在访问之后是多少已经不重要了;

而在dijkstra算法中由于我们就是要求顶点到源点的最短路径,因此各个顶点的minlen数组的值必须时时更新。

贪心策略是这两种算法最显著的区别

prim算法每次贪心时是取最小生成树该顶点的最短距离,即

            minlen[j] = min(minlen[j], g->edge[cur_index][j]);

而dijkstra算法每次贪心时是取源点该顶点的最短距离,即

            if (minlen[j] > minlen[cur_index] + g->edge[cur_index][j])
            {
                minlen[j] = minlen[cur_index] + g->edge[cur_index][j];
                path[j] = cur_index;    //更新了顶点j所在的最短路径,所以更新其前驱
            }

最短路径问题-floyd算法(任意两点,带正、负权图)

floyd算法算是最好写的一个了,顺便简单实现了一下路径的打印。

注意floyd与上述两种最短路径算法有个区别是path数组是二维的。这也很好理解,因为BFS算法和dijkstra算法都是单源最短路径算法,只要一个一维的数组即可表示各个顶点到源点的最短路径上的前驱结点;而floyd算法是计算任意顶点间的最短路径,因此需要用到二维数组,才能够表示任意两个顶点间的最短路径需要经过哪个顶点。

同理,在floyd算法中,存储最短路径长度的数组也是二维的。

#define maxsize 100
#define inf 999999
struct graph
{
    char node[maxsize];  //顶点表,记录每个点的名字,例如1号节点叫A,2号节点叫B
    int edge[maxsize][maxsize]; //邻接矩阵
    int nodenum, edgenum; //记录图的顶点数和边数
};

//构造邻接矩阵,传入图的信息及节点数量
graph* creat(vector<vector<int>>& vec, int node_num)
{
    graph* g = new graph;
    //初始化矩阵
    g->nodenum = node_num;
    for (int i = 0; i < maxsize; i++)
    {
        for (int j = 0; j < maxsize; j++)
        {
            //对角线上的值设为0,其他的设为无穷大
            if (i == j) g->edge[i][j] = 0;
            else g->edge[i][j] = inf;
        }
    }
    for (int i = 0; i < vec.size(); i++)
    {
        //一条a->b,权值为w的边
        int a = vec[i][0];
        int b = vec[i][1];
        int w = vec[i][2];
        g->edge[a][b] = w;
        g->edgenum++;
    }
    return g;
}

int minlen[maxsize][maxsize];    //A[i][j]表示顶点i到顶点j的最短路径长度
int path[maxsize][maxsize]; //path[i][j]表示顶点i到顶点j的最短路径经过顶点path[i][j]
void floyd(graph* g)
{
    for (int i = 1; i < g->nodenum + 1; i++)
    {
        for (int j = 1; j < g->nodenum + 1; j++)
        {
            minlen[i][j] = g->edge[i][j];
            path[i][j] = -1;
        }
    }
    //开始floyd算法
    for (int k = 1; k < g->nodenum + 1; k++)
    {
        for (int i = 1; i < g->nodenum + 1; i++)
        {
            for (int j = 1; j < g->nodenum + 1; j++)
            {
                if (minlen[i][j] > minlen[i][k] + minlen[k][j])
                {
                    minlen[i][j] = minlen[i][k] + minlen[k][j];
                    path[i][j] = k;
                }
            }
        }
    }
}

//递归打印i到j的最短路径
void print_minlen(int i, int j)
{
    if (path[i][j] != -1)
    {
        print_minlen(i, path[i][j]);
        cout << "--" << path[i][j] << "--";
        print_minlen(path[i][j], j);
    }
}

int main()
{
    //每个一维的数组表示一条边的信息,依次是头结点、尾结点、权值
    vector<vector<int>> vec = { {1, 3, 1}, {1, 5, 10}, {2, 5, 5}, {2, 4, 1}, {3, 2, 1}, {3, 5, 7}, {4, 5, 1} };
    graph* g = creat(vec, 5);
    //打印邻接矩阵
    for (int i = 1; i < g->nodenum + 1; i++)
    {
        for (int j = 1; j < g->nodenum + 1; j++)
        {
            if (g->edge[i][j] == inf) cout << "i";
            else cout << g->edge[i][j];
            cout << " ";
        }cout << endl;
    }cout << endl;

    floyd(g);

    cout << "minlen数组:" << endl;
    for (int i = 1; i < g->nodenum + 1; i++)
    {
        for (int j = 1; j < g->nodenum + 1; j++)
        {
            if (minlen[i][j] == inf) cout << "i";
            else cout << minlen[i][j];
            cout << " ";
        }cout << endl;
    }cout << endl;

    cout << "path数组:" << endl;
    for (int i = 1; i < g->nodenum + 1; i++)
    {
        for (int j = 1; j < g->nodenum + 1; j++)
        {
            if (path[i][j] < 0) cout << path[i][j] << " ";
            else cout << path[i][j] << "  ";
        }cout << endl;
    }cout << endl;

    cout << "1到5的最短路径:1--";
    print_minlen(1, 5);
    cout << "--5" << endl;
    cout << "其长度为:" << minlen[1][5] << endl;
    return 0;
}

对于下图:

                        

 打印结果为:

几种最短路径算法的比较

注意floyd算法只是无法解决负权回路,正权回路是可以的

有向无环图的简化

如下图

可合并一些相同的子式,化为:

拓扑排序

代码还是比较好写的

#define maxsize 100
#define inf 999999
struct edgenode //边表结点
{
    int b;  //该结点的位置,通过该位置可以在邻接表adjlist[]中访问,得到该结点的信息
    int w;  //该条边的权值
    edgenode* next = nullptr; //指向下一个边表结点的指针
};
struct headnode //顶点表结点
{
    char data;   //存储该结点的信息,例如名字啥的
    edgenode* first = nullptr;    //指向第一个边表结点
};
struct graph
{
    headnode adjlist[maxsize];  //邻接表
    int nodenum, edgenum; //记录图的顶点数和边数
};

//向某个顶点中,用头插法插入新的边表结点
void insert_edgenode(headnode& hn, edgenode* en)
{
    en->next = hn.first;
    hn.first = en;
}

//构造邻接表,传入图的信息及节点数量
graph* creat(vector<vector<int>>& vec, int node_num)
{
    graph* g = new graph;
    //初始化邻接表
    g->nodenum = node_num;
    for (int i = 0; i < vec.size(); i++)
    {
        //一条a->b,权值为w的边
        int a = vec[i][0];
        int b = vec[i][1];
        int w = vec[i][2];
        //创建一个新的边表结点
        edgenode* node = new edgenode;
        node->b = b;
        node->w = w;
        insert_edgenode(g->adjlist[a], node);
    }
    return g;
}

int ans[maxsize];   //记录拓扑排序结果
bool topologicalsort(graph* g)
{
    //初始化ans数组
    for (int i = 1; i < g->nodenum + 1; i++) ans[i] = -1;
    int indegree[maxsize] = { 0 };  //记录每个点的入度
    //初始化indegree数组
    for (int i = 1; i < g->nodenum + 1; i++)
    {
        for (edgenode* it = g->adjlist[i].first; it != nullptr; it = it->next)
        {
            indegree[it->b]++;
        }
    }
    int sta[maxsize];   //栈,用于存储入度为0的点(用队列实现也可以)
    int top = -1;
    //初始化栈,将所有入度为0的点入栈
    for (int i = 1; i < g->nodenum + 1; i++)
    {
        if (indegree[i] == 0)
        {
            top++;
            sta[top] = i;
        }
    }
    int count = 0;      //记录输出的顶点个数

    //开始拓扑排序算法
    while (top != -1)
    {
        int cur = sta[top]; //取出栈顶顶点,即一个入度为0的顶点
        top--;
        ans[count + 1] = cur;   //将顶点cur放入输出数组中,这里下标从1开始只是为了方便
        count++;
        //将顶点cur所指向的所有顶点的入度都减一,相当于从逻辑上删去了cur这个点和由它所发出的边
        for (edgenode* it = g->adjlist[cur].first; it != nullptr; it = it->next)
        {
            indegree[it->b]--;
            if (indegree[it->b] == 0)   //如果这个顶点的入度变为0了,就入栈
            {
                top++;
                sta[top] = it->b;
            }
        }
    }
    //如果count的值小于顶点个数,说明没有输出所有点,有向图中有环,拓扑排序失败
    if (count < g->nodenum) return false;  
    return true;    //拓扑排序成功
}

int main()
{
    //每个一维的数组表示一条边的信息,依次是头结点、尾结点、权值
    vector<vector<int>> vec = { {1, 2, 0}, {2, 4, 0}, {4, 5, 0}, {3, 4, 0}, {3, 5, 0} };
    graph* g = creat(vec, 5);
    //打印邻接表
    for (int i = 1; i < g->nodenum + 1; i++)
    {
        cout << i;
        edgenode* temp = g->adjlist[i].first;
        while (temp != nullptr)
        {
            cout << " -> " << temp->b << "," << temp->w;
            temp = temp->next;
        }cout << endl;
    }cout << endl;

    if (topologicalsort(g) == true)
    {
        cout << "拓扑排序成功,结果为:";
        for (int i = 1; i < g->nodenum + 1; i++)
        {
            cout << ans[i] << " ";
        }cout << endl;
    }
    else cout << "拓扑排序失败,图中有环" << endl;

    return 0;
}

对于下图来说:

                        

 打印结果为:

对于下图来说:

                              

 打印结果为:

用DFS实现拓扑排序

一开始去网上找别人的代码,后来再看了一下王道所给的代码,发现一开始想复杂了,用DFS实现拓扑排序主要是基于DFS的这样一个特点:一个顶点的访问比它的前驱先结束,比它的后继晚结束。因此可以加入一个时间的概念,记录每个顶点的访问结束时间,那么将最终得到的时间序列降序输出就是所需要的拓扑排序,因为访问结束时间越晚,说明这个顶点在拓扑排序中排越前面。 这个代码只需在DFS上加入关于时间的变量即可。

当然,有个更加更加简单的方法,就是利用栈的特性把访问结束的顶点存入栈中,最后依次取出栈中元素即可。(如果是一访问结束就输出,那么就能得到逆拓扑排序)

#define maxsize 100
#define inf 999999
struct edgenode //边表结点
{
    int b;  //该结点的位置,通过该位置可以在邻接表adjlist[]中访问,得到该结点的信息
    int w;  //该条边的权值
    edgenode* next = nullptr; //指向下一个边表结点的指针
};
struct headnode //顶点表结点
{
    char data;   //存储该结点的信息,例如名字啥的
    edgenode* first = nullptr;    //指向第一个边表结点
};
struct graph
{
    headnode adjlist[maxsize];  //邻接表
    int nodenum, edgenum = 0; //记录图的顶点数和边数
};

//向某个顶点中,用头插法插入新的边表结点
void insert_edgenode(headnode& hn, edgenode* en)
{
    en->next = hn.first;
    hn.first = en;
}
//构造邻接表,传入图的信息及节点数量
graph* creat(vector<vector<int>>& vec, int node_num)
{
    graph* g = new graph;
    //初始化邻接表
    g->nodenum = node_num;
    for (int i = 0; i < vec.size(); i++)
    {
        //一条a->b,权值为w的边
        int a = vec[i][0];
        int b = vec[i][1];
        int w = vec[i][2];
        //创建一个新的边表结点
        edgenode* node = new edgenode;
        node->b = b;
        node->w = w;
        insert_edgenode(g->adjlist[a], node);
    }
    return g;
}

int alltime = 0;   //全局变量的时间戳,用于记录当前时间
struct node_time    //记录顶点的时间信息
{
    int name = 0;   //因为后续需要排序,所以标记一下各个顶点的名字/序号
    int end = 0;    //记录访问结束的时间
};
node_time end_time[maxsize];  //记录每个顶点的访问结束时间
//在图g中,从顶点v开始进行深度度优先遍历
void DFS_topologicalsort(graph* g, int v, bool visited[])
{
    visited[v] = true;  //将顶点v标记已访问
    int a = v;
    for (edgenode* it = g->adjlist[a].first; it != nullptr; it = it->next)
    {
        int b = it->b;
        if (!visited[b])
        {
            DFS_topologicalsort(g, b, visited);
        }
    }
    alltime++;  //当前时间加一
    end_time[v].end = alltime;    //记录顶点v的访问结束时间
}

//对图进行深度优先遍历的总函数
void DFS_traverse_topologicalsort(graph* g)
{
    bool visited[maxsize];    //辅助函数,用于判断某顶点是否被访问过
    for (int i = 1; i < g->nodenum + 1; i++)
    {
        end_time[i].name = i;
        visited[i] = false;
    }
    for (int i = 1; i < g->nodenum + 1; i++)
    {
        //对每个连通分量调用一次DFS()__traverse_topologicalsort函数
        if (!visited[i])
        {
            DFS_topologicalsort(g, i, visited);    //从顶点i开始进行广度优先遍历
        }
    }
}

int main()
{
    //每个一维的数组表示一条边的信息,依次是头结点、尾结点、权值
    vector<vector<int>> vec = { {1, 2, 0}, {2, 4, 0}, {4, 5, 0}, {3, 4, 0}, {3, 5, 0} };
    graph* g = creat(vec, 5);    //构造图


    //打印邻接表
    for (int i = 1; i < g->nodenum + 1; i++)
    {
        cout << i;
        edgenode* temp = g->adjlist[i].first;
        while (temp != nullptr)
        {
            cout << " -> " << temp->b << "," << temp->w;
            temp = temp->next;
        }cout << endl;
    }cout << endl;

    DFS_traverse_topologicalsort(g);
    cout << endl;

    cout << "各个顶点的完成时间为:";
    for (int i = 1; i < g->nodenum + 1; i++)
    {
        cout << end_time[i].end << " ";
    }cout << endl;
    //将顶点按照访问结束时间降序排序,晚完成的顶点在拓扑排序中排在前面
    bool flag = true;
    while (flag)
    {
        flag = true;
        for (int i = 1; i < g->nodenum; i++)
        {
            if (end_time[i].end < end_time[i + 1].end)
            {
                flag = false;
                int temp = end_time[i].name;
                end_time[i].name = end_time[i + 1].name;
                end_time[i + 1].name = temp;

                temp = end_time[i].end;
                end_time[i].end = end_time[i + 1].end;
                end_time[i + 1].end = temp;
            }
        }
    }
    cout << "拓扑排序的结果为:";
    for (int i = 1; i < g->nodenum + 1; i++)
    {
        cout << end_time[i].name << " ";
    }

    return 0;
}

对于下图:

                                

打印结果为:

 这个图的拓扑排序从1或者3开始都行,但因为DFS是从顶点1开始遍历一遍,发现没有访问顶点3后,又接着从顶点3遍历一遍,所以这里3的结束时间晚于1,自然就排在了1的前面

关键路径

这部分内容忘得差不多了,得好好记笔记研究总结一下。

“最大”路径长度 == “最短”时间?

这里有个一开始令我不解的概念,为什么“最大”(带权)路径长度的路径,反而是完成整个“最短”时间? 例如在上图中,从开始到结束有两条路径:V1->V3->V4 和 V1->V2->V3->V4,前者的路径长度是4,后者的路径长度是6,因此最大路径长度是后者,最短时间就是6。

首先我们处理AOE网的目的是为了完成整个工程,而其中一些活动是可以并列进行的。上述的第一条路径虽然长度比第二条的短,但是第一条路径到达结束的时间只有4,这么点时间怎么能够完成“洗番茄”、“切番茄”、“炒菜”3件事呢?这说明时间为4的话根本不够用,而如果走第二条路径就能恰好完成所有活动,因此最短时间是6,而这也是从V1到V4的最大路径长度。

综上所述,只有路径长度最大,才能恰好完成所有活动,而且所用的时间就是最短时间。

事件vk的最早发生时间ve(k)、活动ai的最早开始时间e(i)

【注】e为earliest,v可以记为vertex(顶点)

与完成工程的最短时间类似 ,这里的最早发生、开始时间其实就是源点到该顶点的最长路径长度,因为只有路径最长才能恰好完成前面的所有活动和事件,而只有完成前面的所有活动和事件,该活动(事件)才能尽早开始。由于顶点所代表的事件是没有执行时间的,所以事件开始的时候活动也就开始了,因此ve(k) = e(i)。

计算方法:

  

事件vk的最迟发生时间vl(k)、活动ai的最迟开始时间l(i)

 【注】l为latest,v可以记为vertex(顶点)

事件的最迟发生时间与后继活动的执行时间和后继事件的最迟发生时间有关。因为一个顶点可能后面会连有多个顶点和多条边,因此需要考虑多点。

例如在上图中源点V1的后继活动有

                打鸡蛋:执行时间2分钟、洗番茄:执行时间1分钟,

后继事件有

                可以炒了:最迟第4分钟发生、可以切了:最迟第1分钟发生。

如果在第2分钟时才发生事件V1,那么虽然可以在第4分钟到达V3,但是事件V2的最迟发生时间早就已经过去了,这显然是不行的,因此事件V1的最迟发生时间只能是0。

而活动的最迟开始时间只需考虑一个后继事件即可,因为一条边只能指向一个顶点嘛。例如上述图中,活动炒菜的后继事件结束的最迟开始时间是6,而由于炒菜需要2分钟,因此为了在第6分钟赶到V4,炒菜这个活动的最迟开始时间是4。

计算方法:

时间余量

由上面所学,就引出了时间余量这个概念,然后就有了求关键路径的方法。

例如上图中,打鸡蛋这个活动既可以从第0分钟就开始,也可以从第2分钟再开始,它的时间余量是2,并且这说明打鸡蛋不是一个关键活动,因此关键路径不会经过这个活动。

而该图的关键路径V1->V2->V3->V4中每个活动的时间余量都是0。

求关键路径

首先写出图的拓扑排序,然后执行下图操作:

先事件,再活动,后余量,“是活鱼”。

关键路径的几个特性 

习题-自由树的直径

问题:

自由树(即无环连通图)T的直径是树中所有顶点之间最短路径的最大值,试设计一个时间复杂度尽可能低的算法求T的直径。

思路:

两次bfs,时间复杂度只有O(n) 

定理证明:【证明】树上问题-树的直径 - RioTian - 博客园 (cnblogs.com)

代码如下:

#define maxsize 100

int maxlen = 0;     //记录一次bfs中的最长路径
//从初始节点b开始进行bfs,返回距离v最长的一个端点
int bfs(int graph[][7], int n, int v)
{
    int que[maxsize];
    int front = 0, rear = 0;
    que[rear++] = v;   rear %= maxsize;
    int visited[maxsize] = { 0 };
    visited[v] = 1;
    maxlen = -1;
    int ans = 0;    //记录距离v最远的点,即从最后一层中选择一个节点即可
    while (front != rear)
    {
        int m = (rear - front + maxsize) % maxsize;
        maxlen++;
        for (int k = 0; k < m; k++)
        {
            int cur = que[front++];     front %= maxsize;
            ans = cur;
            for (int i = 1; i <= n; i++)
            {
                if (graph[cur][i] && !visited[i])
                {
                    que[rear++] = i;   rear %= maxsize;
                    visited[i] = 1;
                }
            }
        }
    }
    return ans;
}

int main()
{
    int graph[7][7] = { 0 };
    int n = 6;
    graph[1][2] = 1;    graph[2][1] = 1;
    graph[1][3] = 1;    graph[3][1] = 1;
    graph[2][5] = 1;    graph[5][2] = 1;
    graph[2][4] = 1;    graph[4][2] = 1;
    graph[4][6] = 1;    graph[6][4] = 1;
    for (int i = 1; i <= n; i++)
    {
        for (int j = 1; j <= n; j++)
        {
            printf("%d ", graph[i][j]);
        }printf("\n");
    }printf("\n");

    int s = bfs(graph, n, 1);
    printf("从顶点 1 开始,第一次bfs找到的最远顶点:%d,距离: %d\n\n", s, maxlen);
    int t = bfs(graph, n, s);
    printf("从顶点 %d 开始,第二次bfs找到的最远顶点:%d,距离: %d\n\n", s, t, maxlen);
    int diameter = maxlen;

    printf("顶点 %d 和顶点 %d 之间的最短路径就是直径,长度为 %d\n", s, t, diameter);
    return 0;
}

对于下图来说:

                                

 输出结果为:

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值