【OD机试题+多种解法笔记】启动多任务排序

题目

一个应用启动时,会有多个初始化任务需要执行,并且任务之间有依赖关系,例如A任务依赖B任务,那么必须在B任务执行完成之后,才能开始执行A任务。

现在给出多条任务依赖关系的规则,请输入任务的顺序执行序列,规则采用贪婪策略,即一个任务如果没有依赖的任务,则立刻开始执行,如果同时有多个任务要执行,则根据任务名称字母顺序排序。

例如: B任务依赖A任务,C任务依赖A任务,D任务依赖B任务和C任务,同时,D任务还依赖E任务。那么执行任务的顺序由先到后是:A任务,E任务,B任务,C任务,D任务。这里A和E任务都是没有依赖的,立即执行。

输入描述

输入参数每个元素都表示任意两个任务之间的依赖关系,输入参数中符号”->”表示依赖方向。

例如A->B表示A依赖B,多个依赖之间用单个空格分割

输出描述

输出为排序后的启动任务列表,多个任务之间用单个空格分割

用例

输入输出
A->B  C->B

 B A C

B->A  C->A  D->B  D->C  D->E A E B C D

考点: 拓扑排序、图的遍历

思考

       任务之间有依赖关系,每个任务可能依赖多个任务执行,因此判断是拓扑排序问题。拓扑排序有两种方案,BFS和DFS。

      BFS方案会先构建一个入度表记录每个任务节点的入度数,然后将入度为0的节点筛选出来放入队列,遍历开始时要对队列按字母顺序排序,然后广度优先遍历它们。从队列出队当前任务节点,加入结果列表中,表示它已经从图中被删掉了(任务执行完了)。获取当前任务节点的邻接表,遍历前同样要对当前节点的邻接表按字母顺序排序,遍历邻接表更新每个节点的入度数-1,把入度数为0的节点加入队列。如此循环,最终的结果列表就是任务拓扑排序结果。

      DFS是通过递归后续遍历所有入度为0的节点的依赖节点获得逆序的拓扑排序结果,对结果再逆序获得正序的拓扑排序,但并不一定满足题意要求的并行任务按字母顺序排序。由于后续遍历是先把子节点加入结果列表,再到父节点,对于A:[B, C],结果列表中的顺序是BCA,对于A:[C,B],结果列表的顺序是CBA,因此每次dfs子节点时要先对子节点集合按字母顺序逆序排序再遍历。示例B->A  C->A  D->B  D->C  D->E 构造图{ A: [B,C], B:[D], C: [D], D:[], D: E:[D] },构造入度表indgrees = {A: 0, B: 1, C:1, D:2, E:0 }, A和E是入度为0的节点,因此逆序排序的初始化队列为[E,A],下面进行第一轮DFS遍历E,E包含一个子节点D,D没有子节点,将其标记为已访问并加入结果列表result = [D], 回溯到E,标记E为已访问并加入结果列表[D E];第二轮DFS遍历A,A的子节点们为[B,C],逆序得[C,B],取队首节点C继续DFS遍历C->[D], D在前面标记为访问过,回溯到C,标记C为已访问,加入结果列表[D E C],回溯到A,遍历B->[D],D在前面已经被标记访问过直接回溯B,标记B已访问,加入结果列表[D E C B],回溯到A,标记A已访问,加入结果列表[D E C B A],DFS遍历结束,对结果列表逆序得A B C E D,和题目给出的结果A E B C D不同,哪里错了?经过仔细排查,发现dfs在以某个根节点开始一次完整的后序遍历能维持一个逆序序列,如果先后以不同根节点开始了多轮遍历,这几轮的序列会发生交织!前面的推理过程进行了分别以E和A作为根节点的两轮后序遍历,E开始的后序遍历为D E,A开始后序遍历为 C B A,由于先E后A,所以A为根节点的后序序列会接在E的序列后面形成D C B A ,由于E和A都是入度为0的节点,因此它们比较特殊,应该单独从各自的子序列中拿出来组成一个新序列,这个新序列EA是作为最后访问的序列接在它们的子序列后面,即 D  CB  EA,处理完再对这个合并的序列逆序得到正确答案AEBCD。

解法一: 广度优先遍历

算法过程

1、初始化数据结构和变量,根据输入数据构建图edges和入度表indegrees, edges[u]表示任务u的邻接表,存放依赖于任务u的任务,indegrees[u]表示任务节点u的入度。定义全局结果列表result、队列queue;

2、根据入度表和图筛选入度为0的节点放入队列queue中,对队列按字母顺序排序;

3、开始广度优先遍历,出队当前节点,加入结果列表,并取当前节点邻接表,按字母顺序排序邻接表,遍历邻接表更新节点入度-1,若节点入度为0则加入队列;

4、循环3,直至队列为空退出循环,result即最终结果。

参考代码

function toplogicalBFS(line) {
    const arr = line.split(' ').map(item => item.split('->'));
    const indegrees = {};
    const edges = {};

    for (let [u, v] of arr) {
        if (!edges[v]) { // 图单向映射就行了
            edges[v] = [];
        }
        edges[v].push(u);
        if (indegrees[u] === undefined) {
            indegrees[u] = 0;
        }
        if (indegrees[v] === undefined) {
            indegrees[v] = 0;
        }
        indegrees[u]++;
    }

    const result = [];
    const queue = Object.keys(indegrees).filter(k => indegrees[k] === 0);
    queue.sort(); // 字母排序
    while(queue.length) {
        let u = queue.shift();
        result.push(u);
        let list = edges[u]||[];
        list.sort(); // 字母排序
        for (let v of list ) {
            indegrees[v]--;
            if (indegrees[v] === 0) {
                queue.push(v);
            }
        }      
    }

    return result.join(' ');    
}


let inputs = [
`A->B C->B`,
`A->B`,
`D->B B->E C->B B->A`,     // A E B C D
`B->A C->A D->B D->C D->E` // A E B C D
];

inputs.forEach(line => {
    console.log(toplogicalBFS(line));
});

解法二:深度优先遍历

算法过程

1、初始化数据结构和变量,根据输入数据构建图edges, edges[u]表示任务u的邻接表,存放依赖于任务u的任务,遍历图edges得到入度表并筛选出入度为0的任务集合startTasks且按字母顺序逆序排序,定义全局结果列表result和节点访问状态备忘录visited;

2、循环遍历启动任务并对每一个启动任务开启一轮dfs递归遍历,每轮循环会产生一个逆序序列,用临时变量数组作为dfs函数第二个引用参数传入,当前节点是第一个参数;

3、dfs当前任务节点,判断是否已被访问,若是则return,否则先标记为已访问,再获取邻接表并进行逆序排序,遍历邻接表对每个节点继续dfs,在dfs函数最后位置把当前任务push到当前轮的序列参数数组中;

4、当启动任务的循环中dfs流程执行完成时,当前轮的逆序序列数组完成填充,pop最后一个元素,即移除数组中的当前启动任务节点,余下的是排除当前启动任务的逆序序列,将其合并到结果列表中;

5、最后结果列表中是所有轮dfs遍历启动任务的后驱节点们构成的逆序序列,再把当前启动任务列表startTasks追加到结果列表中的尾部,对整个结果列表进行反转后按空格连接成一个字符串即得到整个任务的拓扑排序结果字符串。

参考代码

function topologicalDFS(line) {
   const tasks = line.trim().split(' ').map(item => item.split('->'));
   const edges = {};
   const inDegrees = {};
    
   for (let [cur, dep] of tasks) {
	   if (!edges[dep]) { // 图节点单向映射就行了
           edges[dep] = [];
	   }       
	   edges[dep].push(cur);
       inDegrees[cur] = (inDegrees[cur] || 0) + 1;
       if (!inDegrees[dep]) {
           inDegrees[dep] = 0;
       }
   }

   const startTasks = Object.keys(inDegrees)
       .filter(task => inDegrees[task] === 0)
       .sort((a,b) => b.localeCompare(a)); // 逆序排序启动任务

   const visited = new Set();
   let result = [];

   const dfs = function(task, arr) {
       if (visited.has(task)) return;
       visited.add(task);
       
       const nextTasks = edges[task]||[];
       nextTasks.sort((a,b) => b.localeCompare(a)); // 逆序排序
       for (let next of nextTasks) {
          dfs(next, arr);
       }
       arr.push(task);
   };

   for (const task of startTasks) {
      let arr = [];
      dfs(task, arr);
      arr.pop(); // 移除移动任务
      result.push(...arr);
   }
    result.push(...startTasks); // 追加启动任务
    return result.reverse().join(' ');
}

let inputs = [
`A->B C->B`,
`A->B`,
`D->B B->E C->B B->A`,
`B->A C->A D->B D->C D->E`     // A E B C D
];

inputs.forEach(line => {
    console.log(topologicalDFS(line));
});

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值