leetcode
leetcode 1351 ~ 1400
并行课程 II

并行课程 II

难度:

标签:

题目描述

给你一个整数 n 表示某所大学里课程的数目,编号为 1 到 n ,数组 relations 中, relations[i] = [xi, yi]  表示一个先修课的关系,也就是课程 xi 必须在课程 yi 之前上。同时你还有一个整数 k 。

在一个学期中,你 最多 可以同时上 k 门课,前提是这些课的先修课在之前的学期里已经上过了。

请你返回上完所有课最少需要多少个学期。题目保证一定存在一种上完所有课的方式。

 

示例 1:

输入:n = 4, relations = [[2,1],[3,1],[1,4]], k = 2
输出:3 
解释:上图展示了题目输入的图。在第一个学期中,我们可以上课程 2 和课程 3 。然后第二个学期上课程 1 ,第三个学期上课程 4 。

示例 2:

输入:n = 5, relations = [[2,1],[3,1],[4,1],[1,5]], k = 2
输出:4 
解释:上图展示了题目输入的图。一个最优方案是:第一学期上课程 2 和 3,第二学期上课程 4 ,第三学期上课程 1 ,第四学期上课程 5 。

示例 3:

输入:n = 11, relations = [], k = 2
输出:6

 

提示:

  • 1 <= n <= 15
  • 1 <= k <= n
  • 0 <= relations.length <= n * (n-1) / 2
  • relations[i].length == 2
  • 1 <= xi, yi <= n
  • xi != yi
  • 所有先修关系都是不同的,也就是说 relations[i] != relations[j] 。
  • 题目输入的图是个有向无环图。

代码结果

运行时间: 67 ms, 内存: 16.2 MB


/*
 * 题目思路:
 * 1. 使用拓扑排序来计算课程的先修关系。
 * 2. 对于每个学期,选择最多k门可以上的课程,确保所有先修课都已完成。
 * 3. 使用动态规划记录当前已完成课程的状态,并计算最少学期数。
 * 4. 使用Java Stream简化操作和代码。
 */
import java.util.*;
import java.util.stream.*;

public class Solution {
    public int minNumberOfSemesters(int n, int[][] relations, int k) {
        int[] prerequisites = new int[n];
        Arrays.stream(relations).forEach(relation -> prerequisites[relation[1] - 1] |= (1 << (relation[0] - 1)));

        int[] dp = new int[1 << n];
        Arrays.fill(dp, n + 1);
        dp[0] = 0;

        IntStream.range(0, (1 << n)).forEach(state -> {
            int availableCourses = IntStream.range(0, n)
                    .filter(i -> (state & (1 << i)) == 0 && (state & prerequisites[i]) == prerequisites[i])
                    .map(i -> (1 << i))
                    .reduce(0, (a, b) -> a | b);

            for (int sub = availableCourses; sub > 0; sub = (sub - 1) & availableCourses) {
                if (Integer.bitCount(sub) <= k) {
                    dp[state | sub] = Math.min(dp[state | sub], dp[state] + 1);
                }
            }
        });

        return dp[(1 << n) - 1];
    }
}

解释

方法:

该题解采用了状态压缩动态规划的思路。首先用一个整数表示每门课程的先修课状态,然后枚举所有可能的已上课程状态,对于每个状态,如果能找到一个不超过k门可上的先修课程组合,就可以推出这个状态的最少学期数。状态转移方程为f[i] = min(f[i], f[i^j] + 1),其中i是当前状态,j是当前状态的一个不超过k门课的子集。最终答案为f[-1],即全部课程都上完的状态所需的最少学期数。在此基础上,题解还用随机化算法进行了预处理,多次随机打乱课程顺序,计算需要的最少学期数,取最小值作为答案,以尽量逼近最优解。

时间复杂度:

O(2^n * n^k)

空间复杂度:

O(2^n)

代码细节讲解

🦆
在使用状态压缩动态规划时,你是如何确定使用整数来表示每门课程的先修课状态的?
在状态压缩动态规划中,使用整数来表示状态是为了利用位运算的高效性,简化状态的存储与转移。每个位代表一门课程是否已经完成,例如,一个整数中的第i位为1则表示第i门课程已完成。这样,课程的先修条件可以通过整数的位与操作来检查,若课程A的先修课为B和C,则只需检查表示状态的整数中B和C对应的位是否为1。这种方法能有效地处理状态的转移和检查,使得算法在处理大量数据时仍保持高效。
🦆
题解中提到的随机化算法为什么会进行80次尝试?这个数字是如何确定的,它与算法的准确性和效率有什么关系?
随机化算法中进行80次尝试是一个经验性选择,旨在在算法的准确性和效率之间找到一个平衡点。通过多次随机打乱课程顺序并计算每种情况下的最少学期数,可以增加找到接近最优解的机会。选择80次是基于算法的执行时间和结果稳定性的考虑;较多的尝试次数通常能提高结果的准确性,但同时也会增加算法的运行时间。这个数字可能根据实际问题的规模和复杂度进行调整,以达到最佳的性能。
🦆
在动态规划中,状态转移方程`f[i] = min(f[i], f[i^j] + 1)`里的`i^j`操作是什么意思?它如何帮助确定状态转移?
在状态转移方程中,`i^j`表示的是位异或操作,用来从当前状态i中移除已经选择上课的子集j。具体来说,如果i表示当前的课程状态(哪些课已完成),而j是在当前状态下可以选择上的课程集合的子集,那么`i^j`将会生成一个新的状态,表示在上完j集合中的课程后剩余的课程状态。这种操作可以帮助算法从当前状态转移到新的状态,且每次状态转移都对应于一个学期的课程安排。通过这种方式,动态规划算法可以逐步构建出完成所有课程所需的最小学期数。

相关问题