LeetCode/完成任务的最少工作时间段

一个工作时间段可以连续工作sessiontime个小时
给你任务列表task,task[i]表示第i项任务花费时间
求完成全部工作所需最小时间段(可以按任意顺序完成任务)

1. 回溯法

回溯时按任务下标推进,边界条件为任务下标等于任务长度
同时要记录回溯几个状态,
分别是当前任务下标、已用时间段、各时间段已消耗时间
选择分为在已开辟时间段内分配任务(对所有已开辟时间段做选择)和新开辟时间段

class Solution {
    vector <int> tasks, times; //times表示第i个时间段任务消耗了多少时间
    int ans = 20; //当前已找到的最小时间段
    int w, n; //w表示时间段时间上限,n表示最大时间段数
    void dfs(int u, int k){//u为当前任务下标,k为已开辟时间段
        if(k >= ans) return; //此时搜索未结束已经是不可能解,剪枝,后面只存储更小的时间段值
        if(u == n){ //任务已经递归完,边界条件
            ans = k;
            return;
        }
        //尝试使用前面的时间段放任务进去
        for(int i = 0;i < k; i++){  
            if(times[i] + tasks[u] <= w){
                times[i] += tasks[u]; //尝试分配
                dfs(u + 1, k);//递归下一个任务
                times[i] -= tasks[u];//取消选择
            }
        }
        //使用新的时间段,并直接分配任务,每个任务都有机会分配到新的时间段
        times[k] = tasks[u];
        dfs(u + 1, k + 1);//查看下一个任务,新增时间段
        times[k] = 0;//取消选择
    }
public:
    int minSessions(vector<int>& tasks, int w){
        times.resize(20);
        sort(tasks.begin(), tasks.end(), greater<int>());
        this->tasks = tasks;
        this->w = w; //变为全局
        this->n = tasks.size();//变为全局
        dfs(0, 0); //初始从任务0开始,开辟时间段为0
        return ans;
    }
};

2. 状态压缩动态规划

其实就是回溯法的正向递推
从递推过程来看,实现的是一个单向的尝试分配的过程,并记录每个状态的最优值,
在对一个新任务进行分配的时候,已经将旧任务的所有分配状态计算完,同时状态的最优值满足无后效性

class Solution {
public:
    int minSessions(vector<int>& tasks, int sessionTime) {
        int n = tasks.size();
        vector<pair<int, int>> dp(1 << n, {INT_MAX, INT_MAX});//动态规划数组,dp[i].first表示已使用工作段、dp[i].second表示最后一个工作段已使用时间
        dp[0] = {1, 0};//初始未完成任何任务时,已使用工作段为1,第一个工作段使用时间为0
        
        auto add = [&](const pair<int, int>& s, int x) -> pair<int, int> {//在当前工作段进行添加任务
            if (s.second + x <= sessionTime) {
                return {s.first, s.second + x};
            }
            return {s.first + 1, x};
        };
        
        for (int mask = 1; mask < (1 << n); ++mask) {//遍历所有状态
            for (int i = 0; i < n; ++i) {//遍历所有任务
                if (mask & (1 << i)) {//如果该状态下该任务已经完成,从该任务未完成的最优状态进行转移
                    dp[mask] = min(dp[mask], add(dp[mask ^ (1 << i)], tasks[i]));//存储当前状态最优值, mask ^ (1 << i)即该任务未完成的状态
                }
            }
        }
        return dp[(1 << n) - 1].first; 
    }
};

3. 枚举子集的动态规划

对于动态规划,从2可以知道,我们不仅要知道当前状态所用的时间段数,还要知道当前时间段用了多少时间,才能进行下一步转移
所以2中的动态规划用了两个值来进行存储
其实可以单独用时间段数量进行存储,状态转移的时候,遍历满足条件的任务子集,当前状态就是由上一个状态和任务子集转移而来

class Solution {
public:
    int minSessions(vector<int>& tasks, int sessionTime) {
        int n = tasks.size();
        vector<int> valid(1 << n);//用于判断任务子集是否符合要求
        for (int mask = 1; mask < (1 << n); ++mask) {//遍历所有任务子集
            int needTime = 0;//判断当前任务子集需要的时间
            for (int i = 0; i < n; ++i) {//
                if (mask & (1 << i))  //判断子集第i位是否为1
                    needTime += tasks[i];
            }
            if (needTime <= sessionTime) 
                valid[mask] = true;
        }

        vector<int> dp(1 << n, INT_MAX / 2);//初始化dp数组
        dp[0] = 0; //不做任何任务时,消耗时间段为0
        for (int mask = 1; mask < (1 << n); ++mask) {//遍历所有状态
            for (int subset = mask; subset; subset = (subset - 1) & mask) {//子集初始为当前状态,从大到小遍历所有子集
                if (valid[subset]) {//子集存在
                    dp[mask] = min(dp[mask], dp[mask ^ subset] + 1); //由状态mask^sub转移(其实就是去掉了子集的状态)
                }
            }
        }
        return dp[(1 << n) - 1];
    }
};


posted @ 2023-05-19 13:11  失控D大白兔  阅读(95)  评论(0编辑  收藏  举报