Leetcode 1199 建造街区的最短时间(贪心算法及证明)

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Leetcode 1199 建造街区的最短时间(贪心算法及证明)相关的知识,希望对你有一定的参考价值。

参考技术A

题目链接

这道题的最佳解法是使用小根堆的贪心方法,但其正确性并不是显而易见的。这里,我们结合具体场景来对贪心方法的正确性进行分析。

不需要分裂工人,直接让他去建造街区就好了。花费时间

我们必须先把当前的工人分裂为两个工人,然后让他们分别去建造街区。花费时间

如果我们继续按照这样的正向思维来分析,最后得到的可能就是一个DFS的方法。因为我们每一步都需要去抉择将几个工人进行分裂,而这一抉择的优劣并不是显然的,因此好的剪枝策略不易找到。

我们不妨换一个角度来理解,如果我们不是分裂工人,而是合并街区呢?上面两个街区的情形中,分裂工人的操作,实际上就等价于把这两个街区合并为了一个建造时间为

的新街区。

考虑对 个街区进行合并。我们可以看到,选择任意两个街区 和 进行合并后,得到的“新”街区建造时间为:

看到这里,不知道大家有没有想到经典的Huffman Tree?在Huffman Tree中,两个节点合并后得到的新节点为 。为了让数值大的节点尽可能少参与到合并中,我们总是优先挑选两个最小的节点来进行合并。

本题中也是一样,为了让耗时长的街区尽可能少参与到合并中,我们总是优先挑选两个耗时最小的街区(这里的街区,可能是由之前操作合并得到的)进行合并。所以我们可以维护一个小根堆,每次取最上方两个街区进行合并,然后将合并得到的新街区重新加入堆中。

如何严格证明这一贪心策略的正确性呢?我们可以参考Huffman Tree的证明过程,使用归纳法来进行证明。

我们首先把对问题进行重述。根据前面的分析,将分裂工人转化为合并街区后,这一问题就可以重述为:

而我们解决这一问题的策略,即是上述的贪心策略:每次选取两个最小的数进行合并,直到只剩下一个数为止。

在证明贪心策略的正确性之前,我们首先需要证明一个引理。

引理 将合并过程表示为一棵节点的度为0或2的二叉树,其叶子节点为初始的数且至少有两个叶节点,父节点的值为 ,其中 为其两个子节点的值。假设 个数中最小的两个数为 ,则在最优合并对应的二叉树中, 对应的叶节点一定具有最大的深度,且为兄弟节点。

引理的证明

假设存在一个叶节点 ,其深度 大于 的深度 ,且 。假设 的深度为 的祖先节点为 ,则 。考虑所有深度为 的节点的最大值 ,可以得到

尝试交换 和 。交换后 ,

由 可知,交换后二叉树的根节点 。因此,我们总可以通过若干次交换,使得 具有最大深度,并且根节点的值不大于原本根节点的值。
由于二叉树的节点度不能为1,所以深度最深的叶节点至少有两个。因此,我们可以再通过若干次交换,使得 具有最大深度,并且根节点的值不大于原本根节点的值。由于 的深度不能超过 的深度,所以此时它们的深度一定相等。

若二叉树深度最深的一层只有两个叶节点,它们必定为 和 且为兄弟。
假设在二叉树深度最深的一层还有其他叶节点,则由节点的度为0或2可知,至少还有两个叶节点 ,且满足 。若 不为兄弟节点,不妨假设 和 为兄弟节点, 为兄弟节点。则 和 的父节点 , 和 的父节点 。考虑倒数第二层的所有节点的最大值 ,可以得到

若将 和 交换,则 ,从而

因此这一交换可以得到更优的合并方案。从而, 和 一定为兄弟节点。

归纳基础 在 时,使用贪心策略可以得到最优合并。
归纳步骤 假设 时,使用贪心策略可以得到最优合并。我们需要证明:在 时,使用贪心策略可以得到最优合并。

归纳基础的证明 由前面对两个街区情形的分析可知,归纳基础成立。
归纳步骤的证明

LeetCode 1723. 完成所有工作的最短时间 Find Minimum Time to Finish All Jobs(Java)

1723. 完成所有工作的最短时间 Find Minimum Time to Finish All Jobs(Hard)

##DFS##

采用回溯、剪枝

每项工作都有可能分给不同的工人,因此采用回溯法枚举所有可能的情况,并更新可能的最短的最大工作时间ans,对于不可能比ans小的最大工作时间的情况,直接剪枝

时间复杂度 O ( m n ) O(m^n) O(mn),m为工人数,n为任务数

class Solution {

    int ans = Integer.MAX_VALUE;

    public int minimumTimeRequired(int[] jobs, int k) {
        int[] times = new int[k];
        // 排序比不排序要更快
        Arrays.sort(jobs);
        dfs(jobs, times, 0, k);
        return ans;
    }

    // times数组记录每个工人当前的工作时间,cnt记录遍历到第cnt项工作,k为工人数量
    public void dfs(int[] jobs, int[] times, int cnt, int k) {
        // 遍历完所有工作的情况
        if (cnt == jobs.length) {
            int max = Arrays.stream(times).max().getAsInt();
            if (max < ans) ans = max;
            return;
        }

        for (int i = 0; i < k; i ++) {
            //当前情况下,不可能比ans更小,剪枝
            if (jobs[cnt] + times[i] > ans) {
                continue;
            }
            
            // 第i个工人执行第cnt项工作
            times[i] += jobs[cnt];
            dfs(jobs, times, cnt + 1, k);
            // 恢复现场
            times[i] -= jobs[cnt];
            // 等于0时,已经出现了最小答案,直接返回
            if (times[i] == 0) break;
        }
    }
}

以上是关于Leetcode 1199 建造街区的最短时间(贪心算法及证明)的主要内容,如果未能解决你的问题,请参考以下文章

LeetCode1723. 完成所有工作的最短时间 Java回溯+剪枝(详解)

javascript 这是一个用Vue.js建造的古腾堡街区

LeetCode 1723 完成所有工作的最短时间[二分法 回溯] HERODING的LeetCode之路

LeetCode 1723. 完成所有工作的最短时间 Find Minimum Time to Finish All Jobs(Java)

LeetCode 2335. 装满杯子需要的最短总时长

LeetCode 2335. 装满杯子需要的最短总时长