力扣日记1494

1. 题目

LeetCode 1494. 并行课程 II

1.1 题意

严格按照选课先修顺序选课,每个学期选课数有上限,求选完所有课程的最短学期数

1.2 分析

这道题的数据量很小,而且作为困难题,开始考虑是不是用暴力递归。

1 <= n <= 15

事实上,我在写的时候大意了没有闪,把他看成一个拓扑排序,觉得依次把入度为0的课程选择即可。

第60个测试样例直接错。意识到每个学期选择的入度为0的课程是有影响的。那么递归直接忽略,先考虑可否贪心。

能不能每次贪心选择入度为0,但出度尽可能大的节点呢?这样可以使得剩下的图的边数减少的更多。
思考一下,设想一个极限场景:有一条关键路径上有很多课程,但是都只受限于一个出度为1,入度为0的课程,这样这条路径会被“饿死”,被选择到的时间很晚,这并不是最优的方法。(下图举例,下图是可以在5个学期被选完的,关键取决于第一个学期的选择)
举个栗子
排除了贪心就考虑动态规划,

第一个问题,怎么存信息?最多只有15个课程,可以使用状态压缩的方法,利用n位one-hot编码(int变量即可)来表示每个课程是否已经被修过。

第二个问题,怎么递推?由状态信息(存的是该情况下已经修过的课程和没有修过的课程),查找此情况下入度为0的节点,如果节点数小于等于k则选上所有课程,节点数大于k需要做一个 C N k C_{N}^{k} CNk 从入度为0的节点中选取k个节点,并且把所有的情况都遍历一遍,这里可以使用dfs去做。为了防止出现重复状态可以使用set做容器

1.3 我的解法

class Solution {
public:
    void dfs(int vis, int ind, int k, int chooseNum, vector<int> &node,set<int> &st){
        // 细节第一个递归边界在第二个递归边界前
        // 不然会缺少情况
        if(chooseNum == k){
            // 选到了足够的课程
            st.insert(vis);
            return;
        }
        // 所有情况都已经遍历
        if(ind >= node.size()){
            return;
        }

        // not choose
        // 不选择课程
        dfs(vis, ind+1, k, chooseNum, node, st);
        // choose
        // 选择该课程
        // 这里注意位运算优先级顺序 都打上括号
        if( ( vis | (1<<node[ind]) ) != vis)
            // 简单的剪枝
            // 即该课程未被选过
            dfs(vis|(1<<node[ind]), ind+1, k, chooseNum+1, node, st);
    }
    int minNumberOfSemesters(int n, vector<vector<int>>& relations, int k) {
        // 状态压缩
        // 这里实际可以使用两个set
        vector<set<int> > state;
        // 初始化
        state.emplace_back(set<int>{0} );
        int res = 0;
        int m = relations.size();
        while(1){
            res++; // 统计学期数
            set<int> st;
            for(auto it = state.back().begin(); it != state.back().end(); it++){
                // 每一次要把前一个学期所有的情况都做一遍计算
                int vis = *it;
                // get in degree
                // 计算入度
                vector<int> inDegree(n+1, 0);
                for(int i=0; i<m; i++){
                    // not vis
                    // 位运算细节,表示没有修该课程
                    if((vis & (1 << relations[i][0]) ) == 0 ){
                        inDegree[relations[i][1]]++;
                    }
                }
                // 查找入度为0 并且没有访问的节点
                // find node: inDegree = 0
                vector<int> node;
                for(int i=1;i<=n;i++){
                    if(inDegree[i] == 0 && ( ( vis & (1<<i) ) == 0) ){
                        // not vis
                        node.emplace_back(i);
                    }
                }
                // 如果可选课程不足k,全选
                if(node.size()<=k){
                    for(int i:node){
                        vis |= (1<<i);
                    }
                    st.insert(vis);
                }else{
                    // choose node 
                    // combine and arrange all cases
                    // 可选课程较多,做排列组合,遍历所有情况
                    dfs(vis, 0, k, 0, node, st);
                }
            }
            if(st.find( (1<<(n+1)) - 2 ) != st.end()){
                // 判断有没有全部修读的情况
                // 有可以直接结束,(这个感觉有点像bfs
                // (1<<(n+1)) - 2 注意运算优先级
                // 我使用的是从第1位到第n位来表示是否访问过该节点
                // 所有节点都访问的情况应该是 第1~n位为1 其余位为0
                // (1<<(n+1)) - 1 是第0~n位全为1
                // 再减1 第0位也是0了
                break;
            }
            else{
                state.emplace_back(st);
            }
        }
        return res;
    }
};

1.4 学习题解反思

时间复杂度分析:
最外层循环最多n次(一学期修一门课程)
set一层的循环,每次的状态最多是 C n k C_{n}^{k} Cnk(所有课程都可以直接选)
内层找入度、节点的,最多次是边的数目: n * (n-1) / 2
然后node中做dfs的部分上界为2^n(两种情况,深度为node的大小)
最后乘再一起 n 3 ∗ 2 n ∗ C n k n^3*2^n*C_{n}^{k} n32nCnk 这其实是个不确切的上界
空间复杂度分析:
最坏情况下n次循环,然后每次最多装 C n k C_{n}^{k} Cnk个状态
(其实这里可以把前面的n去掉,因为每次都之和前一个学期状态有关,用两个set循环替也是可以)

题解学习:g 没学
题解的方法很强,又是学到了的一天。
题解把所有状态都枚举,然后把每个状态对应需要学习的先修课提前计算好(这是题解的时间复杂度低于我的时间复杂度的原因,不用每次去求状态对应要学的先修课程,但是是牺牲空间的前提,不过我的解法也可以提前将每个状态可以学的课程,是用空间换时间的代价)
另外他在可修课程数大于k需要做一个 C N k C_{N}^{k} CNk 的情况使用了一个二进制子集枚举的方法。

1.5 bug日记

1.5.1 思路想错

1.5.2 位运算优先级

2. 后记

每日困难题坐牢 人菜瘾大多练
仅分享自己的想法,有意见和指点非常感谢

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值