数据结构与算法--图的拓扑排序和关键路径

239 阅读14分钟

1、拓扑排序

设G = (V,E)是⼀一个具有n个顶点的有向图, V中的顶点序列列V1,V2,.....,Vn.若满⾜足从顶点Vi 到Vj有⼀一条路路径,则在顶点序列列Vi 必须在Vj 之前, 则我们称这样的顶点序列列成为拓拓扑序列列

所谓拓拓扑排序,其实就是对⼀一个有向图构造拓拓扑序列列的过程. 构造过程拓拓扑序列列会产⽣生2个结果:

  1. 如果此⽹网中的全部顶点被输出,则说明它不不存在环(回路路)的AOV⽹网;

  2. 如果输出的顶点数少了了,哪怕仅少了了⼀一个,也说明这个⽹网存在环(回路路),不不是AOV⽹网

算法基本思路路: 从AOV⽹网中选择⼀一个⼊入度为0的顶点输出,然后从删去此顶点,并删除以 此顶点为尾的弧. 继续重复此步骤,直到输出全部顶点或AOV⽹网中不不存在⼊入度为0的顶点 为⽌止.

在这个算法实现过程,我们需要借助⼀一个数据结构栈.来帮助我们解决避免每次查找时, 都要去遍历AOV图中的顶点表查找有没有⼊入度为0的顶点.

  1. 创建⼀一个栈(stack),⽤用来存储⼊入度in为0 的顶点序号;
  2. 遍历AOV图中顶点表,判断⼊入度为0的顶点全部⼊入栈;
#define OK        1
#define ERROR     0
#define TRUE      1
#define FALSE     0
#define MAXEDGE   20
#define MAXVEX    14
#define INFINITYC 65535

/* Status是函数的类型,其值是函数结果状态代码,如OK等 */
typedef int Status;
/* 邻接表结构****************** */
//边表结点
typedef struct EdgeNode {
    //邻接点域,存储该顶点对应的下标
    int adjvex;
    //用于存储权值,对于非网图可以不需要
    int weight;
    //链域,指向下一个邻接点
    struct EdgeNode *next;
}EdgeNode;

//顶点表结点
typedef struct VertexNode {
    //顶点入度
    int in;
    //顶点域,存储顶点信息
    int data;
    //边表头指针
    EdgeNode *firstedge;
}VertexNode, AdjList[MAXVEX];

//图结构
typedef struct {
    AdjList adjList;
    //图中当前顶点数和边数
    int numVertexes, numEdges;
}graphAdjList, *GraphAdjList;
/*3.拓扑排序. 若AOV网图无回路则输出拓扑排序的序列并且返回状态值1,若存在回路则返回状态值0*/
/*拓扑排序:解决的是一个工程能否顺序进行的问题!*/
Status TopologicalSort(GraphAdjList GL) {
    EdgeNode *e;
    int i, k, gettop;
    //用于栈指针下标
    int top = 0;
    //用于统计输出顶点的个数
    int count = 0;

    //建栈将入度为0的顶点入栈(目的:为了避免每次查找时都要遍历顶点表查找有没有入度为0的顶点)
    int *stack = (int *)malloc(GL->numVertexes * sizeof(int) );

    //1.遍历邻接表-顶点表,将入度in为0的顶点入栈
    /*参考图1> 此时stack栈中应该成为0,1,3.即V0,V1,V3的顶点入度为0*/
    for (i = 0; i < GL->numVertexes; i++) {
        //将入度为0的顶点入栈
        if (0 == GL->adjList[i].in) stack[++top] = i;
    }
    printf("top = %d\n", top);

    //2.循环栈结构(当栈中有元素则循环继续)
    while (top != 0) {
        //出栈
        gettop = stack[top--];
        printf("%d -> ", GL->adjList[gettop].data);

        //输出顶点,并计数
        count++;

        //遍历与栈顶相连接的弧
        for (e = GL->adjList[gettop].firstedge; e; e = e->next) {
            //获取与gettop连接的顶点
            k = e->adjvex;

            //1.将与gettop连接的顶点入度减1;
            //2.判断如果当前减1后为0,则入栈
            if (!(--GL->adjList[k].in) )
                //将k入栈到stack中,并且top加1;
                stack[++top] = k;
        }
    }

    /*思考:3 -> 1 -> 2 -> 6 -> 0 -> 4 -> 5 -> 8 -> 7 -> 12 -> 9 -> 10 ->13 -> 11
     这并不是唯一的拓扑排序结果.
     分析算法:将入度为0的顶点入栈的时间复杂度为O(n), 而之后的while 循环,每个顶点进一次栈,并且出一次栈. 入度减1, 则共执行了e次. 那么整个算法的时间复杂度为O(n+e)*/

    printf("\n");

    //判断是否把所有的顶点都输出. 则表示找到了拓扑排序;
    if (count < GL->numVertexes) return ERROR;
    else return OK;
}

swift

// 邻接表的节点
class Node {
    var adj_vex_index: Int // 弧头的下标,也就是被指向的下标
    var data: Int // 权重值
    var next: Node?
    init(adj_vex_index: Int, data: Int) {
        self.adj_vex_index = adj_vex_index
        self.data = data
    }
}

extension Node: CustomStringConvertible {
    var description: String {
        if next == nil {
            return "adj_vex_index:\(adj_vex_index),data:\(data)"
        }
        return "adj_vex_index:\(adj_vex_index),data:\(data),next->" + String(describing: next)
    }
}

// 顶点节点表
class VNode {
    var inDegree:Int // 入度
    var data: String // 顶点的值
    var firstedge: Node? // 顶点下一个是谁?
    init(inDegree:Int, data: String) {
        self.inDegree = inDegree
        self.data = data
    }
}

extension VNode: CustomStringConvertible {
    var description: String {
        return "inDegree:\(inDegree),data:\(data),firstedge:" + String(describing: firstedge)
    }
}
// 图的一些信息
class Graph {
    var adjlist = Array<VNode>() // 顶点表
    var arc_num = 0 // 边的个数
    var node_num = 0 // 节点个数
    /*3.拓扑排序. 若AOV网图无回路则输出拓扑排序的序列并且返回状态值1,若存在回路则返回状态值0*/
    /*拓扑排序:解决的是一个工程能否顺序进行的问题!*/
    func TopologicalSort() -> Bool {
        //用于统计输出顶点的个数
        var count = 0
        //建栈将入度为0的顶点入栈(目的:为了避免每次查找时都要遍历顶点表查找有没有入度为0的顶点)
        var stack = Array<Int>()
        //1.遍历邻接表-顶点表,将入度in为0的顶点入栈
        for i in 0 ..< node_num {
            if adjlist[i].inDegree == 0 {
                stack.append(i)
            }
        }
        //2.循环栈结构(当栈中有元素则循环继续)
        while stack.count > 0 {
            //出栈
            let top = stack.removeLast()
            print("\(adjlist[top].data) -> ", terminator: "")
            //输出顶点,并计数
            count += 1
            //遍历与栈顶相连接的弧
            var p = adjlist[top].firstedge
            while p != nil {
                //获取与gettop连接的顶点
                let k = p!.adj_vex_index
                //1.将与gettop连接的顶点入度减1;
                //2.判断如果当前减1后为0,则入栈
                adjlist[k].inDegree -= 1
                if adjlist[k].inDegree == 0 {
                    stack.append(k)
                }
                p = p?.next
            }
        }
        print("")
        if count < node_num {
            return false
        }
        return true
    }
    
    
}

extension Graph: CustomStringConvertible {
    var description: String {
        return "arc_num:\(arc_num),node_num:\(node_num),adjlist:" + String(describing: adjlist)
    }
}

2、关键路径

在⼀一个表示⼯工程的带权有向图中,⽤用顶点表示事件,⽤用有向边表示活动,⽤用边上的权值表示活动 的持续时间,这种有向图的边表表示活动的⽹网,我们称之为AOE ⽹网(Activity On Edge Network) 没有⼊入边的顶点称为始点或源点; 没有出边的顶点称为终点或汇点; 由于⼀一个⼯工程, 总有⼀一个开始,⼀一个结束.所以正常情况下,AOE⽹网只有⼀一个源点和⼀一个汇点.

  • 路路径上各个活动所持续的时间之和称为路路径⻓长度
  • 从源点到汇点具有最⼤大的路路径叫关键路路径
  • 在关键路路径上的活动叫关键活动
  • 事件最早发⽣生的时间etv (earliest time of vertex): 即顶点Vk 的最早发⽣生时间;
  • 事件最晚发⽣生时间ltv (latest time of vertex): 即顶点Vk 的最晚发⽣生时间,也就 是每个顶点对应的事件最晚需要开始的时间,超出此时间将会延误整个⼯工期;
  • 活动的最早开⼯工时间ete (earliest time of edge); 即弧Ak 的最早发⽣生时间;
  • 活动的最晚开⼯工时间 lte(latest time of edge); 即弧Ak 的最晚发⽣生时间,也就 是不不推迟⼯工期的最晚开⼯工时间.
#define OK        1
#define ERROR     0
#define TRUE      1
#define FALSE     0

#define MAXEDGE   30
#define MAXVEX    30
#define INFINITYC 65535

typedef int Status;    /* Status是函数的类型,其值是函数结果状态代码,如OK等 */
/* 邻接表结构****************** */
//边表结点
typedef struct EdgeNode {
    //邻接点域,存储该顶点对应的下标
    int adjvex;
    //用于存储权值,对于非网图可以不需要
    int weight;
    //链域,指向下一个邻接点
    struct EdgeNode *next;
}EdgeNode;

//顶点表结点
typedef struct VertexNode {
    //顶点入度
    int in;
    //顶点域,存储顶点信息
    int data;
    //边表头指针
    EdgeNode *firstedge;
}VertexNode, AdjList[MAXVEX];

typedef struct {
    AdjList adjList;
    //图中当前顶点数和边数
    int numVertexes, numEdges;
}graphAdjList, *GraphAdjList;
/* 关于AOE网图的存储代码段-End! */
int *etv, *ltv; /* 事件最早发生时间和最迟发生时间数组,全局变量 */
int *stack2;   /* 用于存储拓扑序列的栈 */
int top2;       /* 用于stack2的指针*/

//拓扑排序
Status TopologicalSort(GraphAdjList GL)
{
    //若GL无回路,则输出拓扑排序序列且返回状态OK, 否则返回状态ERROR;
    EdgeNode *e;
    int i, k, gettop;
    //栈指针下标;
    int top = 0;
    //用于统计输出的顶点个数.作为拓扑排序是否存在回路的判断依据;
    int count = 0;
    //建栈,将入度in = 0的顶点入栈;
    int *stack = (int *)malloc(GL->numVertexes * sizeof(int));

    //遍历顶点表上入度in = 0 入栈
    for (i = 0; i < GL->numVertexes; i++) {
        //printf("%d %d\n",i,GL->adjList[i].in);
        if (0 == GL->adjList[i].in) {
            stack[++top] = i;
        }
    }

    //* stack2 的栈指针下标
    top2 = 0;
    //* 初始化拓扑序列栈
    stack2 = (int *)malloc(sizeof(int) * GL->numVertexes);
    //* 事件最早发生时间数组
    etv = (int *)malloc(sizeof(GL->numVertexes * sizeof(int)));
    //* 初始化etv 数组
    for (i = 0; i < GL->numVertexes; i++) {
        //初始化
        etv[i] = 0;
    }

    printf("TopologicSort:\t");
    while (top != 0) {
        gettop = stack[top--];
        printf("%d -> ", GL->adjList[gettop].data);
        count++;

        //将弹出的顶点序号压入拓扑排序的栈中;
        stack2[++top2] = gettop;

        //例如gettop为V0 ,那么与V0相连接的结点就有etv[1] = 3; etv[2] = 4;
        //例如gettop为V1 ,那么与V1连接的结点就有etv[4]= 3+6=9; etv[3] = 8;
        //例如gettop为V2 ,那么与V2连接的结点就有etv[5]= 4+7=11; etv[3] = 12;
        //例如gettop为V3 ,那么与V3连接的结点就有etv[4]= 12+3=15;
        for (e = GL->adjList[gettop].firstedge; e; e = e->next) {
            k = e->adjvex;

            //将i顶点连接的邻接顶点入度减1,如果入度减一后为0,则入栈
            if (!(--GL->adjList[k].in)) stack[++top] = k;

            //求各顶点事件的最早发生的时间etv值
            //printf("etv[gettop]+e->weight = %d\n",etv[gettop]+e->weight);
            //printf("etv[%d] = %d\n",k,etv[k]);
            if ((etv[gettop] + e->weight) > etv[k]) {
                etv[k] = etv[gettop] + e->weight;
            }
        }
    }
    printf("\n");

    //打印etv(事件最早发生时间数组)
//    for (i = 0; i < GL->numVertexes; i++) {
//        printf("etv[%d] = %d\n",i,etv[i]);
//    }
//    printf("\n");

    if (count < GL->numVertexes) return ERROR;
    else return OK;
    return OK;
}

//求关键路径, GL为有向网,则输出G的各项关键活动;
void CriticalPath(GraphAdjList GL)
{
    EdgeNode *e;
    int i, gettop, k, j;

    //声明活动最早发生时间和最迟发生时间变量;
    int ete, lte;

    //求得拓扑序列,计算etv数组以及stack2的值
    TopologicalSort(GL);

    //打印etv数组(事件最早发生时间)
    printf("etv:\n");
    for (i = 0; i < GL->numVertexes; i++) {
        printf("etv[%d] = %d \n", i, etv[i]);
    }
    printf("\n");

    //事件最晚发生时间数组
    ltv = (int *)malloc(sizeof(int) * GL->numVertexes);

    //初始化ltv数组
    for (i = 0; i < GL->numVertexes; i++) {
        //初始化ltv数组. 赋值etv最后一个事件的值
        ltv[i] = etv[GL->numVertexes - 1];
        //printf("ltv[%d] = %d\n",i,ltv[i]);
    }

    //计算ltv(事件最晚发生时间) 出栈求ltv
    while (top2 != 0) {
        //出栈(栈顶元素)
        gettop = stack2[top2--];

        //找到与栈顶元素连接的顶点; 例如V0是与V1和V2连接
        for (e = GL->adjList[gettop].firstedge; e; e = e->next) {
            //获取与gettop 相连接的顶点
            k = e->adjvex;
            //计算min(ltv[k]-e->weight,ltv[gettop])
            if (ltv[k] - e->weight < ltv[gettop]) {
                //更新ltv 数组
                ltv[gettop] = ltv[k] - e->weight;
            }
        }
    }

    //打印ltv 数组
    printf("ltv:\n");
    for (i = 0; i < GL->numVertexes; i++) {
        printf("ltv[%d] = %d \n", i, ltv[i]);
    }

    printf("\n");
    //求解ete,lte 并且判断lte与ete 是否相等.相等则是关键活动;
    //2层循环(遍历顶点表,边表)
    for (j = 0; j < GL->numVertexes; j++) {
        for (e = GL->adjList[j].firstedge; e; e = e->next) {
            //获取与j连接的顶点;
            k = e->adjvex;
            //ete 就是表示活动 <Vk, Vj> 的最早开工时间, 是针对这条弧来说的.而这条弧的弧尾顶点Vk 的事件发生了, 它才可以发生. 因此ete = etv[k];
            ete = etv[j];
            //lte 表示活动<Vk, Vj> 的最晚开工时间, 但此活动再晚也不能等Vj 事件发生才开始,而是必须在Vj 事件之前发生. 所以lte = ltv[j] - len<Vk, Vj>.
            lte = ltv[k] - e->weight;
            //如果ete == lte 则输出j,k以及权值;
            if (ete == lte) {
                printf("<%d-%d> length:%d\n", GL->adjList[j].data, GL->adjList[k].data, e->weight);
            }
        }
    }
}

swift

// 邻接表的节点
class Node {
    var adj_vex_index: Int // 弧头的下标,也就是被指向的下标
    var data: Int // 权重值
    var next: Node?
    init(adj_vex_index: Int, data: Int) {
        self.adj_vex_index = adj_vex_index
        self.data = data
    }
}

extension Node: CustomStringConvertible {
    var description: String {
        if next == nil {
            return "adj_vex_index:\(adj_vex_index),data:\(data)"
        }
        return "adj_vex_index:\(adj_vex_index),data:\(data),next->" + String(describing: next)
    }
}

// 顶点节点表
class VNode {
    var inDegree:Int // 入度
    var data: String // 顶点的值
    var firstedge: Node? // 顶点下一个是谁?
    init(inDegree:Int, data: String) {
        self.inDegree = inDegree
        self.data = data
    }
}

extension VNode: CustomStringConvertible {
    var description: String {
        return "inDegree:\(inDegree),data:\(data),firstedge:" + String(describing: firstedge)
    }
}
// 图的一些信息
class Graph {
    var adjlist = Array<VNode>() // 顶点表
    var arc_num = 0 // 边的个数
    var node_num = 0 // 节点个数
    /* 关于AOE网图的存储代码段-End! */
    var etv = Array<Int>(), ltv = Array<Int>() /* 事件最早发生时间和最迟发生时间数组,全局变量 */
    var stack2 = Array<Int>() /* 用于存储拓扑序列的栈 */
    
    /*3.拓扑排序. 若AOV网图无回路则输出拓扑排序的序列并且返回状态值1,若存在回路则返回状态值0*/
    /*拓扑排序:解决的是一个工程能否顺序进行的问题!*/
    func TopologicalSort() -> Bool {
        //用于统计输出顶点的个数
        var count = 0
        //建栈将入度为0的顶点入栈(目的:为了避免每次查找时都要遍历顶点表查找有没有入度为0的顶点)
        var stack = Array<Int>()
        //1.遍历邻接表-顶点表,将入度in为0的顶点入栈
        for i in 0 ..< node_num {
            if adjlist[i].inDegree == 0 {
                stack.append(i)
            }
        }
        
        //* 初始化拓扑序列栈
        stack2.removeAll()
        //* 事件最早发生时间数组
        etv.removeAll()
        //* 初始化etv 数组
        for _ in 0 ..< node_num {
            //初始化
            etv.append(0)
        }
        //2.循环栈结构(当栈中有元素则循环继续)
        while stack.count > 0 {
            //出栈
            let top = stack.removeLast()
            print("\(adjlist[top].data) -> ", terminator: "")
            //将弹出的顶点序号压入拓扑排序的栈中;
            stack2.append(top)
            //输出顶点,并计数
            count += 1
            //遍历与栈顶相连接的弧
            var p = adjlist[top].firstedge
            while p != nil {
                //获取与gettop连接的顶点
                let k = p!.adj_vex_index
                //1.将与gettop连接的顶点入度减1;
                //2.判断如果当前减1后为0,则入栈
                adjlist[k].inDegree -= 1
                if adjlist[k].inDegree == 0 {
                    stack.append(k)
                }
                //求各顶点事件的最早发生的时间etv值
                //printf("etv[gettop]+e->weight = %d\n",etv[gettop]+e->weight);
                //printf("etv[%d] = %d\n",k,etv[k]);
                if (etv[top] + p!.data) > etv[k] {
                    etv[k] = etv[top] + p!.data;
                }
                p = p!.next
            }
        }
        print("")
        if count < node_num {
            return false
        }
        return true
    }
    
    //求关键路径, GL为有向网,则输出G的各项关键活动;
    func CriticalPath() -> Void {
        //求得拓扑序列,计算etv数组以及stack2的值
        let _ = TopologicalSort()
        print(etv)
        //事件最晚发生时间数组
        var ltv = Array<Int>()
        //初始化ltv数组
        for _ in 0 ..< node_num {
            //初始化ltv数组. 赋值etv最后一个事件的值
            ltv.append(etv[node_num - 1])
        }
        //计算ltv(事件最晚发生时间) 出栈求ltv
        while stack2.count > 0 {
            //出栈(栈顶元素)
            let top = stack2.removeLast()
            //找到与栈顶元素连接的顶点; 例如V0是与V1和V2连接
            var p = adjlist[top].firstedge
            while p != nil {
                //获取与gettop 相连接的顶点
                let k = p!.adj_vex_index
                //计算min(ltv[k]-e->weight,ltv[top])
                if ltv[k] - p!.data < ltv[top] {
                    //更新ltv 数组
                    ltv[top] = ltv[k] - p!.data
                }
                p = p!.next
            }
        }
        //打印ltv 数组
        print(ltv)
        //求解ete,lte 并且判断lte与ete 是否相等.相等则是关键活动;
        //2层循环(遍历顶点表,边表)
        for j in 0 ..< node_num {
            var p = adjlist[j].firstedge
            while p != nil {
                //获取与j连接的顶点;
                let k = p!.adj_vex_index
                //ete 就是表示活动 <Vk, Vj> 的最早开工时间, 是针对这条弧来说的.而这条弧的弧尾顶点Vk 的事件发生了, 它才可以发生. 因此ete = etv[k];
                let ete = etv[j];
                //lte 表示活动<Vk, Vj> 的最晚开工时间, 但此活动再晚也不能等Vj 事件发生才开始,而是必须在Vj 事件之前发生. 所以lte = ltv[j] - len<Vk, Vj>.
                let lte = ltv[k] - p!.data
                //如果ete == lte 则输出j,k以及权值;
                if (ete == lte) {
                    print("<\(adjlist[j].data)-\(adjlist[k].data)> length:\(p!.data)")
                }
                p = p!.next
            }
        }
    }
}

extension Graph: CustomStringConvertible {
    var description: String {
        return "arc_num:\(arc_num),node_num:\(node_num),adjlist:" + String(describing: adjlist)
    }
}