有向无环图拓扑排序与关键路径 邻接表的出度,入度

数据结构 专栏收录该内容
12 篇文章 0 订阅

有向无环图 拓扑排序与关键路径

一:基本概念

1:有向无环图

一个无环的有向图称做有向无环图(Directed Acyclic Graph)。简称DAG 图。DAG 图是一类较有向树更一般的特殊有向图,
这里写图片描述

2:拓扑排序

对一个有向无环图(Directed Acyclic Graph简称DAG)G进行拓扑排序,是将G中所有顶点排成一个线性序列。简单的说,由某个集合上的一个偏序得到该集合上的一个全序,这个操作称之为拓扑排序。

3:关键路径

通俗的讲:关键路径是项目中最长的路径。任何关键路径上的元素的延迟(浮动)时间将直接影响项目的预期完成时间。

4:AOE网

顶点表示事件弧表示活动,弧上的权值表示活动持续的时间的有向图AOE(Activity On Edge Network)网。
(1) 活动(弧)开始的最早时间ee(i);
(2) 活动(弧)开始的最晚时间el(i) ;
(3) 事件(顶点)开始的最早时间ve(i);
(4) 事件(顶点)开始的最晚时间vl(i)。

5:关键活动

定义ee(i)=el(i)的活动叫关键活动。

二:拓扑排序

1:储存结构

(1):采用邻接表作为存储结构(无向图
邻接表详细知识见下:
https://blog.csdn.net/weixin_39956356/article/details/80514672

注意:权值一定要在建好链表后赋值

这里写图片描述
相关代码

//创建有向图的邻接表
void CreatDLGraph(DLGraph &G)
{
    for (int i = 0; i < G.vexnum; i++) {
        printf("Please enter %d data:", i + 1);
        scanf(" %c", &G.vertices[i].data);                                      //输入顶点值
        G.vertices[i].firstarc = NULL;                                          //弧表头指针置为空  
    }

    VertexType v1, v2;                                                          //输入的两条弧(A B C···)
    EdgeType w;
    int i, j;                                                                   //获取(v1,v2)各自在图中位置      
    printf("Please input the two data of arc,for example: A C\n");
    for (int k = 0; k < G.arcnum; k++) {
        printf("The %d arc: ", k + 1);
        scanf(" %c", &v1);                                                      //输入第一个顶点
        getchar();                                                              //把输入的空格读走
        v2 = getchar();                                                         //输入弧的第二个顶点
        scanf("%d", &w);                                                        //输入权值
        i = LocateVex(G, v1);                                                   //获取弧的第一个节点位置
        j = LocateVex(G, v2);                                                   //获取弧的第二个节点位置



        /*******************************************************
        **  1:无向图相互的,所以下面的两段代码就i,j互换
        **  2:这里要清楚怎么接上去的
        新结点的next后面接的是之前的一串上(可以这么想,新的节点接在原来的左弧)
        不断把上次firtarc赋值给新节点的next,新的p又接在firtarc
        ********************************************************/
        ArxNode *p = (ArxNode *)malloc(sizeof(ArxNode));                        //分配弧结点空间
        p->adjvex = j;
        p->nextarc = G.vertices[i].firstarc;                                    //i是弧尾,且出发的弧是一链表,说明这里的p是
        G.vertices[i].firstarc = p;

        //注意:这里必须在邻接表创建后才能赋值w,不然G.vertices[i].firstarc是个空指针-error
        G.vertices[i].firstarc->weight = w;                                     //某活动持续的时间

        //p = (ArxNode *)malloc(sizeof(ArxNode));                                   //重新分配弧结点空间(上面的消逝了)
        //p->adjvex = i;
        //p->nextarc = G.vertices[j].firstarc;                                  //将结点i链接到j的单链表中
        //G.vertices[j].firstarc = p;
    }
}

(2):邻接表求出度——这个很简单,邻接本来就是一个节点所有出发的弧
这里写图片描述

相关代码

//求出度
void FindOutDegree(DLGraph &G, int *indegree)
{
    ArxNode *p;                                                                 //切记:这里创建一个临时p,我们不可以把之前邻接表结构改变了,就像给你U盘拷贝东西,你只能复制而不是剪切!你不可以改变之前的结构,不然之后邻接表是个空的
    int num;
    for (int i = 0; i < G.vexnum; i++)                                          
    {
        num = 0;
        p = G.vertices[i].firstarc;                                             //先指向一个结点
        while (p != NULL) {
            p = p->nextarc;                                                     //指向下一条弧
            num++;                                                              //出度加一
        }
        indegree[i] = num;
    }
}

(3):邻接表求入度——这个对邻接表而言是不可以直接求出,需要一次遍历才能求出一个节点的入度
这里写图片描述

相关代码

//求入度
void FindInDegree(DLGraph &G, int *indegree)
{
    ArxNode *p;                                                                 //切记:这里创建一个临时p,我们不可以把之前邻接表结构改变了,就像给你U盘拷贝东西,你只能复制而不是剪切!你不可以改变之前的结构,不然之后邻接表是个空的
    int num;
    for (int i = 0; i < G.vexnum; i++)                                          //求所有节点的入度
    {
        num = 0;
        for(int j = 0; j < G.vexnum; j++)                                       //一个结点的入度需要遍历所有结点才可以知道
        { 
            p = G.vertices[j].firstarc;                                         //先指向一个结点
            while (p != NULL) {
                if (p->adjvex == i)                                             //从另外一个结点出发的链域,发现与该次所求结点一致,入读加一,break
                {
                    num++;
                    break;
                }
                p = p->nextarc;                                                 //指向下一条弧
            }
        }
        indegree[i] = num;
        //printf("%d\t", indegree[i]);
    }
}

2:拓扑排序

(1):这里涉及两个栈(S,T),事件开始的最早时间ve[i]
S—-拓扑有序栈
T—-逆拓扑有序栈
初始值全为0ve[i] = 0; 最早发生时间
这里写图片描述
这里写图片描述
(2):入度为0,全部入栈S
这里写图片描述
(3):S出栈(直到S为空),T入栈
(4):ve[k] (终点)值更新 popDataIndex(起点)–>k(终点)
这里写图片描述
(5):关于栈的基础函数

stack.h

#ifndef _STACK_H
#define _STACK_H
#include <stdlib.h>

#define STACK_INIT_SIZE 8                                           //储存空间初始量
#define STACKINCREMENT  5                                           //储存空间分配增量

#define ERROR       0
#define OK           1
#define TRUE         2
#define OVERFLOW    -2

typedef int Status;
typedef char SElemType;

typedef struct 
{
    SElemType *base;                                                //栈底
    SElemType *top;                                                 //栈顶
    int        stacksize;                                           //每次的新元素个数
}SqStack;

Status InitStack(SqStack &s);                                       //构造一个空栈操作              
Status Push(SqStack &s, SElemType e);                               //栈的插入操作 Push
SElemType Pop(SqStack &s);                                          //取栈顶元素 Pop
SElemType GetTop(SqStack &s);                                       //获取栈顶元素
SElemType StackLength(SqStack &s);                                  //获取栈当前元素个数
Status StackEmpty(SqStack &s);
#endif // !_STACK_H

stack.cpp

#include "stdafx.h"
#include "stack.h"

//构造一个空栈操作
Status InitStack(SqStack &s)
{
    s.base = (SElemType *)malloc(STACK_INIT_SIZE * sizeof(SElemType));
    if (!s.base)
        return OVERFLOW;
    memset(s.base, 0, STACK_INIT_SIZE * sizeof(SElemType));                 //初始化每个都弄为0
    s.top = s.base;
    s.stacksize = STACK_INIT_SIZE;
    return OK;
}


//栈的插入操作 Push
Status Push(SqStack &s, SElemType e)
{
    if ((s.top - s.base) >= s.stacksize)
    {
        s.base = (SElemType *)realloc(s.base, (STACK_INIT_SIZE + STACKINCREMENT) * sizeof(SElemType));
        if (!s.base)
            return OVERFLOW;
        s.top = s.base + s.stacksize;
        s.stacksize += STACKINCREMENT;
    }
    *s.top++ = e;
    return OK;
}


//取栈顶元素 Pop
SElemType Pop(SqStack &s)
{
    if (s.base == s.top)
        return ERROR;
    return *--s.top;
}


//获取栈顶元素
SElemType GetTop(SqStack &s)
{
    if (s.base == s.top)
        return ERROR;
    return *(s.top - 1);
}


//获取栈当前元素个数
SElemType StackLength(SqStack &s)
{
    return (s.top - s.base) / sizeof(SElemType);
}

//判栈空
Status StackEmpty(SqStack &s)
{
    if (s.base == s.top)
        return 1;
    return 0;
}

相关代码

int *ve;
//拓扑排序
int TopologicalOrder(DLGraph &G, SqStack &S, SqStack &T)
{
    int *indegree = (int *)malloc(G.vexnum * sizeof(int));
    ve = (int *)malloc(G.vexnum * sizeof(int));
    FindInDegree(G, indegree);                                                  //求出每个节点的入度

    int count = 0;                                                              //累计访问的结点个数
    InitStack(S);                                                               //拓扑有序栈
    for (int i = 0; i < G.vexnum; i++) {
        if (!indegree[i])
            Push(S, G.vertices[i].data);                                        //入度为0,全部入栈S
        ve[i] = 0;                                                              //最早发生时间
    }

    InitStack(T);                                                               //逆拓扑有序栈
    while(!StackEmpty(S)){
        char popData = Pop(S);                                                  //出栈元素
        int popDataIndex = LocateVex(G, popData);                               //出栈元素的位置
        Push(T, popData);                                                       //出栈元素马上入栈T
        count++;                                                                //访问节点数+1

        ArxNode *p = G.vertices[popDataIndex].firstarc;
        for (; p; p = p->nextarc) {
            int k = p->adjvex;
            if (--indegree[k] == 0)                                             //减1,入度为0,入栈S
                Push(S, G.vertices[k].data);
            if (ve[popDataIndex] + p->weight > ve[k])                           //更新k最早发生时间
                ve[k] = ve[popDataIndex] + p->weight;
        }
    }

    printf("\n\nve[]:\n");
    for(int i = 0; i < G.vexnum; i++)
        printf("%d\t", ve[i]);

    if (count < G.vexnum)                                                       //该有向网有环
        return ERROR;
    return OK;
}

三:关键路径

1:vl[i]初始值是关键路径长度(最长时间),也就是T栈顶元素对应的ve[ ]

这里写图片描述

2:逆序求vl[ ],T出栈(直至为0),下面用一个图将清楚

这里写图片描述
这里写图片描述

3:求所有的关键路径并输出,关键路径长度,注意怎么计算ee(正着) 与 el (反着)的

4:ee = el,关键路径

这里写图片描述
这里写图片描述

相关代码

//求关键路径及长度
int CriticalPath(DLGraph &G)
{
    SqStack S;
    SqStack T;
    int *vl = (int *)malloc(G.vexnum * sizeof(int));

    if (!TopologicalOrder(G, S, T)){                                            //有环结束,不需要继续了
        printf("The direction graph contains rings, Can't output critical path, Test end!!!\n");
        return ERROR;
    }
    else
    {
        ArxNode *p;
        int k;
        for (int i = 0; i < G.vexnum; i++)
            vl[i] = ve[LocateVex(G, GetTop(T))];                                //获取栈T的栈顶元素,找到其位置,把ve[]赋值给vl[]的所有元素(就是把最大值赋给所有的vl[])
        while (!StackEmpty(T)) {
            char popData = Pop(T);                                              //出栈元素
            int popDataIndex = LocateVex(G, popData);                           //出栈元素的位置
            p = G.vertices[popDataIndex].firstarc;
            for (; p; p = p->nextarc) {
                 k = p->adjvex;
                if (vl[k] - p->weight < vl[popDataIndex]) {                     //倒序求,已知k(弧头)求popDataIndex(弧尾),弧尾-弧头<弧头,替换
                    vl[popDataIndex] = vl[k] - p->weight;
                }
            }
        }

        printf("\n\nvl[]:\n");                                                  //输出vl[]
        for(int i = 0; i < G.vexnum; i++)
            printf("%d\t", vl[i]);

        /*******************************************************************
        **  这里我举个例子,比如A->B
        **  在第一次循环中,i是A  k是B
                           el是'vl[B]'- 权值 ---如果等于--ee = ve[i],关键路径
        ********************************************************************/
        int ee, el;
        printf("\n\ncritical path  weight\tee    el    是否为关键路径");
        for (int i = 0; i < G.vexnum; i++)                                      //求所有的关键路径
            for (p = G.vertices[i].firstarc; p; p = p->nextarc) {               //不防从第一个点开始,比如A
                k = p->adjvex;
                ee = ve[i];     el = vl[k] - p->weight;
                if (ee == el) {
                    printf("\n%c --> %c weight: %d\tee: %d el:%d   关键路径", G.vertices[i].data, G.vertices[k].data, p->weight, ee, el);
                }
            }
        printf("\n\nThe length of critical path is: %d", vl[G.vexnum-1]);           //关键路径长度
    }
    return OK;
}

三:主函数与输出

(1):主函数

#include "stdafx.h"
#include "Topological.h"
#include "stack.h"

int main()
{
    DLGraph G;
    printf("Please enter vexnum and arcnum: ");
    scanf("%d %d", &G.vexnum, &G.arcnum);                                               //输入结点数,弧数

    CreatDLGraph(G);                                                                    //创建有向图的邻接表
    printf("\n无向图的邻接表输出:\n");
    PrintDLGraph(G);                                                                    //输出有向图的邻接表

    CriticalPath(G);                                                                    //输出关键路径与长度

    return 0;
}

(2):输出
这里写图片描述

四:感谢与源代码(VS2017)

(1):感谢以下文章对我的帮助
https://www.cnblogs.com/acmtime/p/6106148.html
(2):源代码
链接: https://pan.baidu.com/s/1-n70wJc42KfQvtHOsJn-DQ 密码: zjbg

  • 4
    点赞
  • 4
    评论
  • 14
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

©️2021 CSDN 皮肤主题: 编程工作室 设计师:CSDN官方博客 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值