题目4:项目管理

题目描述:公司共有 n 个项目和 m 个小组,每个项目要不没有归属,要不就由其中的一个小组负责。我们用 group[i] 代表第 i 个项目所属的小组,如果这个项目目前无人接手,那么 group[i] 就等于 -1。(项目和小组都是从零开始编号的)请你帮忙按要求安排这些项目的进度,并返回排序后的项目列表:

  1. 同一小组的项目,排序后在列表中彼此相邻。
  2. 项目之间存在一定的依赖关系,我们用一个列表 beforeItems 来表示,其中 beforeItems[i] 表示在进行第 i 个项目前(位于第 i 个项目左侧)应该完成的所有项目。

结果要求:
如果存在多个解决方案,只需要返回其中任意一个即可。
如果没有合适的解决方案,就请返回一个 空列表。

这题目乍一看就觉得挺难的,我们来理一理思路:

  • 返回排序后的列表,实际上每一个任务都花一个单位的时间,任务串行来做,不考虑时间因素 。
  • 同一个小组的项目排序后在列比中相邻:任务是以小组承包为单位打包进行调度的,这一个小组的所有项目开始之前,要消除其对于组外任务未进行任务的依赖。

Step1: 根据beforeItems建立邻接表itemsMap(Key:A, Set:A的依赖), 扫描group数组建立邻接表groupMap
Step2: 统计入度为0的项目存到数组inCount, 把所有入度为0的无归属项目先做完。
Step3: 循环搜索项目全部入度为0的小组,迭代处理该小组的所有项目,注意更新inCount数组。
思考:实际上,题目的参数beforeItems已经为我们建立好了一个邻接表,其结构是A-->list(_需要在A之前完成的项目列表)。如果用这样的邻接表结构,后面每当我们处理一个入度为0的节点(项目时),需删除其他项目对他的依赖,此时需要遍历整个beforeItems,需要遍历n遍。于是我们将itemMap存储成B-->list(list:需要在B之后开始的项目列表)
再思考:前面Step3"搜索项目全部入度为0的小组"实际上是可能不存在的,而这时却有可能存在解,此时我们是允许合理的组内依赖的,所以到此为止我才真正理解了这道题:如何管理组,识别组间依赖,管理组内依赖。
于是很难受地重新整理思路:

  • 建立节点的正邻接表、组和节点的入度表,节点入度只计算来自组内的,组入度对每个组计算一次,而不是每一个外部节点计算一次
  • 先对组进行拓扑排序并保存排序好的组索引
  • 对排序好的组,对每个组内的节点进行拓扑排序,并保存结果
    class Solution {
      class Graph {
          Map<Integer,List<Integer>> map;
          Map<Integer,List<Integer>> reverseMap;
          public Graph(int n) {
              map = new HashMap<>();
              reverseMap = new HashMap<>();
              for(int i=0;i<n;i++) {
                  map.put(i,new ArrayList<>());
                  reverseMap.put(i,new ArrayList<>());
              }
          }
          public Graph(List<Integer> list) {
              map = new HashMap<>();
              reverseMap = new HashMap<>();
              for(int i:list) {
                  map.put(i,new ArrayList<>());
                  reverseMap.put(i,new ArrayList<>());
              }
          }
          public void insert(int a,int b) {
              map.get(a).add(b);
              reverseMap.get(b).add(a);
          }
          public List<Integer> getTopu() {
              List<Integer> ans = new LinkedList<>();
              Map<Integer,Integer> rudu =new HashMap<>();
              for(Map.Entry<Integer,List<Integer>> entry : reverseMap.entrySet()) {
                  rudu.put(entry.getKey(),entry.getValue().size());
              }
              Queue<Integer> queue = new LinkedList<>();
              for(Map.Entry<Integer,Integer> entry : rudu.entrySet()) {
                  if(entry.getValue()==0) {
                      ans.add(entry.getKey());
                      queue.offer(entry.getKey());
                  }
              }
              while(!queue.isEmpty()) {
                  int tmp = queue.poll();
                  List<Integer> list = map.get(tmp);
                  for(int i:list) {
                      int du= rudu.get(i);
                      if(du==0) continue;
                      if(du==1) {
                          rudu.put(i,0);
                          ans.add(i);
                          queue.offer(i);
                      }else{
                          rudu.put(i,du-1);
                      }
                  }
              }
              return ans;
          }
      }
      public int[] sortItems(int n, int m, int[] group, List<List<Integer>> beforeItems) {
          //首先分组
          //建立一个图,组号组成的图
          int index = m;
          for(int i=0;i<n;i++) {
              if(group[i]==-1) {
                  group[i] = index++;
              }
          }
          Map<Integer,List<Integer>> mmm = new HashMap<>();
          for(int i=0;i<index;i++) {
              mmm.put(i,new ArrayList<>());
          }
          for(int i=0;i<n;i++) {
              mmm.get(group[i]).add(i);
          }
          Graph graph = new Graph(index);
          Graph[] graphs = new Graph[index];
          for(int i=0;i<index;i++) {
              graphs[i] = new Graph(mmm.get(i));
          }
          for(int i=0;i<n;i++) {
              int right = group[i];
              List<Integer> list = beforeItems.get(i);
              for(int j:list) {
                  int left = group[j];
                  if(left!=right) {
                      graph.insert(left,right);
                  }else{
                      graphs[left].insert(j,i);
                  }
              }
          }
          List<Integer> zuSort = graph.getTopu();
          if(zuSort.size()<index) return new int[]{};
          int[] ans = new int[n];
          int ind = 0;
          for(int i:zuSort) {
              List<Integer> tmp = graphs[i].getTopu();
              if(tmp.size()<mmm.get(i).size()) return new int[]{};
              for(int j:tmp) {
                  ans[ind++]=j;
              }
          }
          return ans;
      }
    }

反思一波:最开始建立思路写123步的时候太草率了,想简单了不够严谨。没有考虑到组间依赖普遍存在的问题,没能抓住这道题的核心点在哪里。
OK,fine 确实是一道很复杂的题,以后有必要重新再做一遍。

题目3:课程表ii

现在你总共有 n 门课需要选,记为 0 到 n-1。在选修某些课程之前需要一些先修课程。 例如,想要学习课程 0 ,你需要先完成课程 1 ,我们用一个匹配来表示他们: [0,1]
给定课程总量以及它们的先决条件,返回你为了学完所有课程所安排的学习顺序。
可能会有多个正确的顺序,你只要返回一种就可以了。如果不可能完成所有课程,返回一个空数组。
(就是在题目二的基础上存储课程列表,没有什么可说的,在Map/Set/Entry这遍历转换时候总是有编译错误,要靠提示才理得清,还是要多用HashMap 和 Set多熟悉呀)
代码如下,基本和前面的差不多。拓扑排序的题目要看清楚输入的有序对到底是谁依赖了谁,理清关系之后再决定如何存储邻接表。

import java.util.Map.*;
import java.util.Set.*;
class Solution {
    public int[] findOrder(int numCourses, int[][] prerequisites) {
        Map<Integer, HashSet<Integer>> map = new HashMap<>();
        int[] res = new int[numCourses];
        int i, j, cur, pos = 0;
        if(prerequisites.length==0){
            for(i = 0; i<numCourses; i++)
                res[i] = i;
            return res;
        }
        //step1建立邻接表
        for(i = 0; i<prerequisites.length; i++){
            if(map.containsKey(prerequisites[i][1])){
                map.get(prerequisites[i][1]).add(prerequisites[i][0]);
            } else{
                HashSet<Integer> set = new HashSet<>();
                set.add(prerequisites[i][0]);
                map.put(prerequisites[i][1], set);
            }
        }

        //统计入度
        int[] inCount = new int[numCourses]; 
        Set<Entry<Integer, HashSet<Integer>>> entrySet = map.entrySet();
        for(Entry<Integer, HashSet<Integer>> entry:entrySet){
            for(Integer to : entry.getValue()){
                inCount[to]++;
            }
        }

        //step2循环得到入度为0的节点入队,处理队中节点
        while(!map.isEmpty()){
            Queue<Integer> queue = new LinkedList<Integer>();
            //入度为0的节点进队
            for(i = 0; i<numCourses; i++){
                if(inCount[i]==0){
                    queue.offer(i);
                    inCount[i]--;
                }
            }
            if(queue.isEmpty() && !map.isEmpty()){
                return new int[0];
            }
            //处理入度为0节点
            while(!queue.isEmpty()){
                cur = queue.remove();
                res[pos++] = cur;
                if(map.containsKey(cur)){
                    for(Integer to : map.get(cur)){
                        inCount[to]--;
                        if(!map.containsKey(to)){
                            map.put(to, new HashSet<Integer>());
                        }
                    }
                    map.remove(cur);
                }
            }
        }//while
        return res;
    }
}

题目2:课程表

题目描述:你这个学期必须选修 numCourse 门课程,记为 0 到 numCourse-1 。
在选修某些课程之前需要一些先修课程。 例如,想要学习课程 0 ,你需要先完成课程 1 ,我们用一个匹配来表示他们:[0,1]。给定课程总量以及它们的先决条件,请你判断是否可能完成所有课程的学习?

本题思路:通过拓扑排序判断此课程安排图是否是有向无环图(DAG)

第一次做:邻接表存储图,普通拓扑排序。

用Map<Integer, HashSet<integer>>存储邻接表,解题思路就根据拓扑排序的基本思想具体分为三步:
Step1: 建立邻接表
Step2: 统计入度为0的节点
Step3: 处理入度为0的节点,循环至step2直到处理完所有节点
我这里是用队列来存储待处理的入度为0的节点,需要注意的是统计过的入度为0的节点,经过处理后要进行标记,防止其重复入队。我用一个dead数组显然是比较笨的方法,实际上用原来的入度标记数组inCount--(=-1)来标记处理过的节点就可以了。</integer>

import java.util.Map.*;
import java.util.Set.*;
class Solution {
    public boolean canFinish(int numCourses, int[][] prerequisites) {
        Map<Integer, HashSet<Integer>> map = new HashMap<>();
        int[] dead = new int[numCourses];
        int i, j, cur;
        //step1建立邻接表
        for(i = 0; i<prerequisites.length; i++){
            if(map.containsKey(prerequisites[i][0])){
                map.get(prerequisites[i][0]).add(prerequisites[i][1]);
            } else{
                HashSet<Integer> set = new HashSet<>();
                set.add(prerequisites[i][1]);
                map.put(prerequisites[i][0], set);
            }
        }
        //step2循环得到入度为0的节点入队,处理队中节点
        while(!map.isEmpty()){
            Queue<Integer> queue = new LinkedList<Integer>();
            //统计入度
            int[] inCount = new int[numCourses]; 
            Set<Entry<Integer, HashSet<Integer>>> entrySet = map.entrySet();
            for(Entry<Integer, HashSet<Integer>> entry:entrySet){
                for(Integer to : entry.getValue()){
                    inCount[to]++;
                }
            }
            //入度为0的节点进队
            for(i = 0; i<numCourses; i++){
                if(inCount[i]==0 && dead[i]==0)
                    queue.offer(i);
            }
            if(queue.isEmpty() && !map.isEmpty())
                return false;
            //处理入度为0节点
            while(!queue.isEmpty()){
                cur = queue.remove();
                map.remove(cur);
                dead[cur]=1;
            }
        }//while
        return true;
    }
}

通过需要223ms,观察发现统计入度的循环其实不用放在大的while循环里面,入度其实可以在每一次while循环处理的时候进行更新,这样优化了算法的时间复杂度。
优化后提交,通过需要21ms。

题目1:节点间通路

第一次做:普通递归(超时)

没有搜索算法思想在里面,就是一个普通递归,因为直接写出来递归会超时,就加了一个path记录,用在if条件判断处,用来减少递归次数,然而依然超时。

class Solution {
    boolean res = false;
    public boolean findWhetherExistsPath(int n, int[][] graph, int start, int target){
        int[][] path = new int[n][n];
        for(int i = 0; i<graph.length; i++){
            path[graph[i][0]][graph[i][1]] = 1;
        }
        return checkPath(path, start, target);
    }

    public boolean checkPath(int[][] path, int begin, int target){
        if(path[begin][target]==1){
            return true;
        }
        for(int i = 0; i<target; i++){
            if((path[begin][i]==1||checkPath(path, begin, i))&&(path[i][target]==1||checkPath(path, i, target))){//减少递归次数
                path[begin][target] = 1;
                return true;
            }
        }
        return false;
    }
}

第二次做:数组邻接矩阵+DFS思想

为什么超时呢?可能是这种分治思想还是递归次数太多,那么用深度优先的思路还是递归的方式来做一下吧。

class Solution {
    //普通递归思路改造成深度优先搜索
    boolean res = false;
    public boolean findWhetherExistsPath(int n, int[][] graph, int start, int target){
        int[][] path = new int[n][n];
        for(int i = 0; i<graph.length; i++){
            path[graph[i][0]][graph[i][1]] = 1;
        }
        return checkPath(path, start, start, target);
    }

    public boolean checkPath(int[][] path, int start, int begin, int target){
        if(path[begin][target]==1){
            return true;
        }
        for(int i = 0; i<target; i++){
            if(path[begin][i] == 1){//减少递归次数
                path[start][i] = 1;
                if(path[i][target]==1){
                    return true;
                }
                else if(checkPath(path, i, target)){
                    path[i][target] = 1;
                    return true;
                }
            }
        }
        return false;
    }
}

结果,在最后一条数据n=100000的时候超出内存限制

第三次做:Map实现邻接表+DFS

为什么会在最后一条数据n=100000的时候超出内存限制?我认为原因是不能够使用邻接矩阵吧,有太多的冗余,采用邻接表结构会节省很多内存,虽然搜索时间复杂度会*k倍。

class Solution {
    //邻接矩阵改成邻接表,用Map来实现
    boolean res = false;
    public boolean findWhetherExistsPath(int n, int[][] graph, int start, int target){
        Map<Integer, HashSet<Integer>> map = new HashMap<>();
        // graph[i][0]--指向-->graph[i][1]
        for(int i = 0; i<graph.length; i++){
            int from = graph[i][0];
            int to = graph[i][1];
            if(map.containsKey(from)){
                map.get(from).add(to);
            } else {
                HashSet<Integer> set = new HashSet<>();
                set.add(to);
                map.put(from, set);
            }
        }
        return searchBFS(map, start, target);
    }

    private boolean searchDFS(Map<Integer, HashSet<Integer>> map, int start, int target){
        if(start==target)
            return  true;
        HashSet<Integer> set = map.get(start);
        if(set==null || set.isEmpty()){
            return false;
        }
        for(Integer to:set){
            if(searchDFS(map, to, target))
                return true;
        }
        return false;
    }
}