引入
本周的双周赛最后一题: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个即可。