Leetcode5435. 并行课程 II

引入

本周的双周赛最后一题:5435. 并行课程 II,遇到了一个坑,原本以为是一道很简单的题,但是加上了某个条件后,解法完全不一样了。

题目是这样的:

给你一个整数 n 表示某所大学里课程的数目,编号为 1 到 n ,数组 dependencies 中, dependencies[i] = [xi, yi] 表示一个先修课的关系,也就是课程 xi 必须在课程 yi 之前上。同时你还有一个整数 k 。
在一个学期中,你 最多 可以同时上 k 门课,前提是这些课的先修课在之前的学期里已经上过了。
请你返回上完所有课最少需要多少个学期。题目保证一定存在一种上完所有课的方式。
示例 1:
在这里插入图片描述
输入:n = 4, dependencies = [[2,1],[3,1],[1,4]], k = 2
输出:3
解释:上图展示了题目输入的图。在第一个学期中,我们可以上课程 2 和课程 3 。然后第二个学期上课程 1 ,第三个学期上课程 4 。

这道题的坑点在于每个学期最多选k门课,我一开始使用的图的入度和出度来解答问题:

public class Solution {
    public int minNumberOfSemesters(int n, int[][] dependencies, int k) {
        //记录入度和出度的数组pre
        int[] pre=new int[n];
        //记录图
        List<List<Integer>> list=new ArrayList<>();
        for (int i=0;i<n;i++){
            list.add(new LinkedList<>());
        }

        for (int i=0;i<dependencies.length;i++){
            int a=dependencies[i][0]-1;
            int b=dependencies[i][1]-1;

            pre[b]++;//入度+1
            list.get(a).add(b);

        }

        int countTerm=0;
        Queue<Integer> queue=new LinkedList<>();
        for (int i=0;i<n;i++){
            if (pre[i]==0){
                //入度为0,本学期可以学习
                queue.add(i);
            }
        }

        while(!queue.isEmpty()){
            int size=queue.size()<k?queue.size():k;
            countTerm++;//学期+1
            for (int i=0;i<size;i++){
                int pos=queue.poll();
                System.out.println(countTerm+" "+(pos+1));
                List<Integer> curr=list.get(pos);

                for (int j=0;j<curr.size();j++){
                    pre[curr.get(j)]--;
					//入度为0,表示可以选择该门课了
                    if (pre[curr.get(j)]==0){
                        queue.add(curr.get(j));
                    }
                }
            }
        }
        return countTerm;
    }
}

看起来还好,但是在跑下面的用例的时候,我发现了错误:

9
[[4,8],[3,6],[6,8],[7,6],[4,2],[4,1],[4,7],[3,7],[5,2],[5,9],[3,4],[6,9],[5,7]]
2

输出:
6
实际答案5

我的代码执行顺序如下图所示:(每隔颜色代表一个学期)
在这里插入图片描述
而实际上,最优解应该是这样的:
在这里插入图片描述

也就是在第三学期选择1、7或者2、7,提前解锁了6,从而下一个学期能同时修1、6。

所以,当k小于目前队列queue的长度的时候,选择顺序很重要。

如果要弄好选择顺序,一般就是用回溯的方式了,不过我们这里既用了queue,再用一个回溯,感觉代码比较复杂。
一般能用回溯的题都能用动态规划来解决,那么这道题如何用动态规划呢?

如何建模?

遇到这道题,即使告诉你用动态规划来做,你也会一瞬间愣住。图的情况下,如何来做动态规划呢?

看了看题解,太过复杂了,因为需要状态压缩DP之类的,题解太少,不好理解。

这里我先留个坑,等以后题解多了再做动态规划的解法。

import java.util.*;

public class Solution {
    public static void main(String[] args) {
        int[] input = new int[]{};
        int[] output = new int[]{2, 3};
        System.out.println(new Solution());
    }

    public int get(int x, int i) {
        return (x >> i) & 1;
    }

    public int minNumberOfSemesters(int n, int[][] dependencies, int k) {
        int[] pre = new int[n];
        int[] post = new int[n];
        for (int[] e : dependencies) {
            int a = e[0] - 1;
            int b = e[1] - 1;
            pre[b] |= 1 << a;
            post[a] |= 1 << b;
        }

        int[] dp = new int[1 << n];
        dp[0] = 0;
        int inf = (int) 1e8;

        SubsetGenerator sg = new SubsetGenerator();
        for (int i = 1; i < 1 << n; i++) {
            boolean valid = true;
            int set = 0;
            dp[i] = inf;
            for (int j = 0; j < n; j++) {
                if (get(i, j) == 0) {
                    continue;
                }
                if ((pre[j] & i) != pre[j]) {
                    valid = false;
                }
                if ((post[j] & i) == 0) {
                    set |= 1 << j;
                }
            }
            if (!valid) {
                continue;
            }
            sg.reset(set);
            while (sg.hasNext()) {
                int next = sg.next();
                if (next != 0 && Integer.bitCount(next) <= k) {
                    dp[i] = Math.min(dp[i - next] + 1, dp[i]);
                }
            }
        }

        return dp[dp.length - 1];
    }
}

class SubsetGenerator {
    private int m;
    private int x;

    public void reset(int m) {
        this.m = m;
        this.x = m + 1;
    }

    public boolean hasNext() {
        return x != 0;
    }

    public int next() {
        return x = (x - 1) & m;
    }
}

其他方式:贪心

贪心的方式题目可以AC,但是还有一些题目没有包含用例是跑不通的。贪心的思想是:考虑优先去学最大出度的课程,最后才学最小出度的课程。

这种方式虽然不能说正确,但是比较好解决,只需要把代码改成PriorityQueue,或者增加一个出度的数组,然后每次从Queue中拿出所有的比较,选出k个即可。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章