LeetCode 1335 工作计划的最低难度

本题是一道经典的“极小极大化”问题,又结合了动态规划的考察,难度集中于数据建模过程而非编码阶段。

原题链接如下: LeetCode 1335 工作计划的最低难度

题目实际上要求把 n 项工作分成 d 份,保证每份至少又一项工作,而且每天工作的难度就是每份划分中的最大值,而总难度为最大值的和,最后要保证这个总难度最小。

实际有 5 个示例输入:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
Input 1:
jobDifficulty = [6,5,4,3,2,1]
d = 2

Input 2:
jobDifficulty = [9,9,9]
d = 4

Input 3:
obDifficulty = [1,1,1]
d = 3

Input 4:
jobDifficulty = [7,1,7,1,7,1]
d = 3

Input 5:
jobDifficulty = [11,111,22,222,33,333,44,444]
d = 6

每个输入的数据都满足如下规则:

  • jobDifficulty 的长度区间为 $[1, 300]$
  • jobDifficulty 的每一项取值区间为 $[0, 1000]$
  • d 的取值区间为 $[1, 10]$

相应的示例输出为:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
Output 1:
7

Output 2:
-1

Output 3:
3

Output 4:
15

Output 5:
843

不难看出,而如果最后一天需要完成 k 项工作,那么之前的 d - 1 天里面就要完成 n - k 项工作,和最后的结果需要的答案结构类似,是一个规模更小的子问题,可以套用动态规划的解题思路。 不妨假设 $f(i,j)$ 代表使用 $i + 1$ 天完成 $jobDifficulty[0]$ 到 $jobDifficulty[j]$ 的所有工作的最小总难度,那么就有如下的递推公式:

提示
此处使用 $i + 1$ 而不是 $i$ 是为了编码方便,毕竟数组是从 0 开始计数的,同时 $jobDifficulty[0..j]$ 区间实际有 $j + 1$ 项工作

$$ f(i,j)=\min_{k=i}^{j}{{f(i-1,k-1)+\max_{p=k}^{j}{jobDifficulty[p]}}} $$

公式中需要注意的地方如下:

  • i 必定不大于 j
  • k 从 i 开始,是为了保证子问题求解时至少每天完成 1 项工作
  • 以上公式前提是 i 大于 0 时成立,i 为边界值 0 时为 $f(0,j)=\max_{p=0}^{j}{jobDifficulty[p]}$

可以注意到对于 $f(i,j)$,如果在 $[0..j)$ 的区间坐标中有最大的坐标 x 使得 jobDifficulty[x] 大于 jobDifficulty[j],而且那么我们在枚举 k 的时候有如下可能:

  • 如果 $k < x$ 则区间 $jobDifficulty[k..j]$ 上的最大值是区间 jobDifficulty[k..x] 的最大值,所以 $f(i,j)=f(i,x)$
  • 如果 $k \ge x$ 则区间 $jobDifficulty[k..j]$ 上的最大值就是 jobDifficulty[j],所以 $f(i,j)=jobDifficulty[j]+\min_{p=x}^{j-1}{{f(i-1,p)}}$

第一种情况只需要知道 x 的值即可,那么只要使用单调栈即可,即维护一个存储工作下标(逐渐递增)的单调栈,保证从栈底到栈顶的下标对应的工作难度递减。 第二种情况较为复杂,为了方便对于多个 $f(i,j)$ 求解最小值的计算,单调栈的保存的数据可以更加复杂,即维护一个存储二元组 (idx, val) 的单调栈:

  • 保证从栈底到栈顶的 idx 递增
  • 保证从栈底到栈顶的 idx 对应的 jobDifficulty[idx] 递减
  • 保证 val 就是各个 $f(i - 1,j)$ 的最小值

这样在不断枚举 j 的过程中就可以不断求解和记录 $\min_{p=x}^{j-1}{{f(i-1,p)}}$ 的各项值。

在区间 $jobDifficulty[k..j]$ 上求最大值不会增加额外的时间复杂度,具体思路是:如果我们从小到大枚举 k 的值,区间会不断减小,求最大值的操作不便实现。不如从大到小枚举 k 使得区间从 0 开始增长,比较新的 jobDifficulty[k] 就能得到新区间对应的最大值。

显然,类似背包问题中对空间复杂度的优化,本题的递推公式中 $f(i)$ 只和 $f(i-1)$ 相关,可以使用一维数组优化缓存数据,只需要倒序遍历 j 即可。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
impl Solution {
    pub fn min_difficulty(job_difficulty: Vec<i32>, d: i32) -> i32 {
        let n = job_difficulty.len();
        let d = d as usize;
        if d > n {
            return -1;
        }
        let mut dp = vec![0; n];
        let mut mx = 0;
        for i in 0..n {
            mx = mx.max(job_difficulty[i]);
            dp[i] = mx;
        }
        for i in 1..d {
            for j in (i..n).rev() {
                let mut mx = 0;
                dp[j] = 0x3f3f3f;
                for k in (i..=j).rev() {
                    mx = mx.max(job_difficulty[k]);
                    dp[j] = dp[j].min(dp[k - 1] + mx);
                }
            }
        }
        dp[n - 1]
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
use std::collections::VecDeque;

impl Solution {
    pub fn min_difficulty(job_difficulty: Vec<i32>, d: i32) -> i32 {
        let n = job_difficulty.len();
        let d = d as usize;
        if d > n {
            return -1;
        }
        let mut dp = vec![vec![0; n]; d];
        let mut mx = 0;
        for i in 0..n {
            mx = mx.max(job_difficulty[i]);
            dp[0][i] = mx;
        }
        for i in 1..d {
            let mut stack = VecDeque::new();
            for j in i..n {
                let mut mn = dp[i - 1][j - 1];
                while let Some((idx, val)) = stack.back() {
                    if job_difficulty[*idx] >= job_difficulty[j] {
                        break;
                    } else {
                        mn = mn.min(*val);
                        stack.pop_back();
                    }
                }
                dp[i][j] = mn + job_difficulty[j];
                if let Some((idx, _)) = stack.back() {
                    dp[i][j] = dp[i][j].min(dp[i][*idx]);
                }
                stack.push_back((j, mn));
            }
        }
        dp[d - 1][n - 1]
    }
}