【算法】Parallel Courses II 并行课程 II

文章目录

  • Parallel Courses II 并行课程 II
    • 问题描述:
    • 分析
    • 代码

Parallel Courses II 并行课程 II

问题描述:

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

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

k,n范围[1,15],releastions 范围[0,n*n/2],xi!=yi

分析

这个问题是很久之前写过的,也是比较有意思的一类问题。现在的思路比之前要清晰。
一般来说,对于课程,常见的处理就是拓扑排序。而在这里,就不一定合适。因为有限制,一个学期semester中,最多可以同时上k门课,也就是说在一个学期中上课数量是1~k。
而且每个学期能选择的课程也是会受到上一个学期的已学课程限制。即必须在本学期之前完成该课程的所有前置课程,才可以学习这个课程。
一般看到这里,都会想到使用拓扑。但是正如上面说的,每个学期选课的限制,选择不同的课程会影响到最终的学期数。
因此简单的使用拓扑是不行的。
而最简单的思路就是暴力,一共最多有15个课程,而课程的前置课程数量可能是0~15。
可以通过位运算,用1表示已经完成的课程,那么,原始的课程编号1~ 15,在位运算中就对应0~14位,那么全部完成的状态就是 e n d s t a t e = ( 1 < < n ) − 1 endstate =(1<endstate=(1<<n)1,即0~14全是1,这个状态也记为全集 U U U.
为了方便处理,对前置课程进行预处理,用pre[i]表示课程i的前置课程集合。
如果想到达最终的状态endstate,那么就必须到达前一个状态prestate ,而这样一个最接近endstate的状态,就需要枚举了。
关键在于枚举的范围,首先prestate一定是endstate的子集,其次因为存在限制,要保证prestate中1的数量cnt1 c n t 1 + k < = c n t 2 cnt1+k<=cnt2 cnt1+k<=cnt2
,同时还要保证prestate是合法的,即不能出现有依赖的课程同时出现在prestate的未结课程中。这是一个暴力递归的思路。在此思路上可以转换成为递推

另一个递归思路,角度稍有不同,是以dfs(i)表示 要完成未完成的课程集合i,需要花费的最少学期。也就是说 集合i 的补集 A = ∁ U i A= \complement_Ui A=Ui,A表示已经完成的课程集合
对i的每一位进行检查,从而筛选出当前学期可以选择的课程。用candidate表示当前学期可以选择的课程,如果i的第j位为1,那么 p r e [ j ] ∣ A = = A pre[j]|A==A pre[j]A==A ,就说明课程j的前置课程已经都完成了,课程j可以加入candidate, c a n d i d a t e ∣ = ( 1 < < j ) candidate|=(1<candidate=(1<<j)
拿到本学期可以选择的课程集合candidate,接下来就是第二个限制k。
如果candidate的课程数量即1的数量<=k,就说明本学期可以把candidate的课程全部完成,那么下学期就剩余的课程为 ( i ⊕ c a n d i d a t e ) (i\oplus candidate) (icandidate).
如果candidate的课程数量大于k,那么就必须进行枚举,即从candidate集合中枚举子集,子集的条件则是子集中必须有k个课程。当一个子集为sub时,那么说明当前学期可以完成sub的课程,下一学期开始就需要完成的课程为 ( i ⊕ c a n d i d a t e ) (i\oplus candidate) (icandidate).
在枚举子集过程中,会有许多结果 d f s ( i ⊕ s u b ) dfs(i\oplus sub) dfs(isub),为了达到最小,故选择 r e s = m i n ( d f s ( i ⊕ s u b ) ) + 1 res= min(dfs(i\oplus sub))+1 res=min(dfs(isub))+1.
A = ∁ U i , / / A 是 i 的补集 D F S ( i ) = min ⁡ D F S ( i ⊕ s u b ) + 1 , c n t O n e ( s u b ) ∈ [ 1 , k ] , A N D , s u b ⊂ c a n d i d a t e c a n d i d a t e 为当前可选课程的集合, c n t O n e 为计算集合中元素的数量 A= \complement_Ui ,//A 是i的补集 \\ DFS(i) = \min{DFS(i\oplus sub)}+1 , cntOne(sub)\in[1,k] ,AND, sub\subset candidate\\ candidate为当前可选课程的集合,cntOne为计算集合中元素的数量 A=Ui,//Ai的补集DFS(i)=minDFS(isub)+1,cntOne(sub)[1,k],AND,subcandidatecandidate为当前可选课程的集合,cntOne为计算集合中元素的数量

在dfs的过程中,必然会发生重复的计算,所以加入memo进行记忆化搜索。

代码

class Solution {
    private int[] pre, memo;
    private int _k, U,INF;

    public int minNumberOfSemesters(int n, int[][] relations, int k) {
        _k = k;
        INF= Integer.MAX_VALUE>>1;
        pre = new int[n];
        for (int[] r : relations){
            pre[r[1] - 1] |= 1 << (r[0] - 1);
        } 
        U = (1 << n) - 1; // 全集
        memo = new int[1 << n];
        Arrays.fill(memo, INF); // INF 表示没有计算过
        return dfs(U);
    }

    private int dfs(int i) {
        if (i == 0) return 0; // 空集
        if (memo[i] != INF) return memo[i]; // 之前算过了
        int candidate = 0, A = U ^ i; //A 是 i 的补集
        for (int j = 0; j < pre.length; j++){
            if ((i >> j & 1) > 0 && (pre[j] | A) == A){
                // pre[j]前置课程已经完成,j可以学
                candidate |= 1 << j;
            }
        } 
        if (Integer.bitCount(candidate) <= _k){
            // 如果小于 k,本学期可以完成
            return memo[i] = dfs(i ^ candidate) + 1;
        }  
        int res = INF;
        // 枚举 candidate 的子集 sub
        for (int sub = candidate; sub > 0; sub = (sub - 1) & candidate){            
            if (Integer.bitCount(sub) == _k){//在sub中挑选k个课程
                res = Math.min(res, dfs(i ^ sub) + 1);
            }
        } 
        return memo[i] = res;
    }
} 

时间复杂度 O(指数级)

空间复杂度: O(指数级)

转换自底向上的递推。从1开始枚举直到U,f[i]表示完成状态i的课程最少需要的学期。

A = ∁ U i , / / A 是 i 的补集 F [ i ] = min ⁡ F ( i ⊕ s u b ) + 1 , c n t O n e ( s u b ) ∈ [ 1 , k ] , A N D , s u b ⊂ c a n d i d a t e c a n d i d a t e 为当前可选课程的集合, c n t O n e 为计算集合中元素的数量 A= \complement_Ui ,//A 是i的补集 \\ F[i] = \min{F(i\oplus sub)}+1 , cntOne(sub)\in[1,k] ,AND, sub\subset candidate\\ candidate为当前可选课程的集合,cntOne为计算集合中元素的数量 A=Ui,//Ai的补集F[i]=minF(isub)+1,cntOne(sub)[1,k],AND,subcandidatecandidate为当前可选课程的集合,cntOne为计算集合中元素的数量

class Solution {
    public int minNumberOfSemesters(int n, int[][] relations, int k) {         
        int INF= Integer.MAX_VALUE>>1,U = (1 << n) - 1; // 全集
        int[] pre = new int[n],f = new int[U+1];;
        for (int[] r : relations){
            pre[r[1] - 1] |= 1 << (r[0] - 1);
        } 
        for(int i = 1;i<=U;i++){
            int candidate = 0, A = U ^ i; //A 是 i 的补集
            for (int j = 0; j < pre.length; j++){
                if ((i >> j & 1) > 0 && (pre[j] | A) == A){
                    // pre[j]前置课程已经完成,j可以学
                    candidate |= 1 << j;
                }
            } 
            if (Integer.bitCount(candidate) <= k){
                // 如果小于 k,本学期可以完成
                f[i] = f[i ^ candidate] + 1;
                continue;
            }  
            f[i] = INF;
            // 枚举 candidate 的子集 sub
            for (int sub = candidate; sub > 0; sub = (sub - 1) & candidate){            
                if (Integer.bitCount(sub) == k){//在sub中挑选k个课程
                    f[i] = Math.min(f[i], f[i^sub] + 1);
                }
            } 
        }
        return f[U];
    }
} 

代码的思路,来源于灵神大佬,值得学习参考。

预备知识需要知道如何枚举子集,以及位运算相关。

Tag

Graph Bit Manipulation Dynamic Programming Bitmask

你可能感兴趣的:(数据结构与算法,算法,动态规划)