这道题的最佳解法是使用小根堆的贪心方法,但其正确性并不是显而易见的。这里,我们结合具体场景来对贪心方法的正确性进行分析。
一个街区的情形
不需要分裂工人,直接让他去建造街区就好了。花费时间
两个街区的情形
我们必须先把当前的工人分裂为两个工人,然后让他们分别去建造街区。花费时间
N个街区的情形
如果我们继续按照这样的正向思维来分析,最后得到的可能就是一个DFS的方法。因为我们每一步都需要去抉择将几个工人进行分裂,而这一抉择的优劣并不是显然的,因此好的剪枝策略不易找到。
我们不妨换一个角度来理解,如果我们不是分裂工人,而是合并街区呢?上面两个街区的情形中,分裂工人的操作,实际上就等价于把这两个街区合并为了一个建造时间为
的新街区。
考虑对个街区进行合并。我们可以看到,选择任意两个街区
和
进行合并后,得到的“新”街区建造时间为:
Huffman Tree的启示
看到这里,不知道大家有没有想到经典的Huffman Tree?在Huffman Tree中,两个节点合并后得到的新节点为。为了让数值大的节点尽可能少参与到合并中,我们总是优先挑选两个最小的节点来进行合并。
本题中也是一样,为了让耗时长的街区尽可能少参与到合并中,我们总是优先挑选两个耗时最小的街区(这里的街区,可能是由之前操作合并得到的)进行合并。所以我们可以维护一个小根堆,每次取最上方两个街区进行合并,然后将合并得到的新街区重新加入堆中。
正确性的证明
如何严格证明这一贪心策略的正确性呢?我们可以参考Huffman Tree的证明过程,使用归纳法来进行证明。
问题重述
我们首先把对问题进行重述。根据前面的分析,将分裂工人转化为合并街区后,这一问题就可以重述为:
设有
,合并任意两个数
和
后得到一个新的数
,其中
且与
无关。那么,合并
次后最终得到的数的最小值是多少?
而我们解决这一问题的策略,即是上述的贪心策略:每次选取两个最小的数进行合并,直到只剩下一个数为止。
引理及证明
在证明贪心策略的正确性之前,我们首先需要证明一个引理。
引理 将合并过程表示为一棵节点的度为0或2的二叉树,其叶子节点为初始的数且至少有两个叶节点,父节点的值为,其中
为其两个子节点的值。假设
个数中最小的两个数为
,则在最优合并对应的二叉树中,
对应的叶节点一定具有最大的深度,且为兄弟节点。
引理的证明
假设存在一个叶节点,其深度
大于
的深度
,且
。假设
的深度为
的祖先节点为
,则
。考虑所有深度为
的节点的最大值
,可以得到
尝试交换和
。交换后
,
由可知,交换后二叉树的根节点
。因此,我们总可以通过若干次交换,使得
具有最大深度,并且根节点的值不大于原本根节点的值。
由于二叉树的节点度不能为1,所以深度最深的叶节点至少有两个。因此,我们可以再通过若干次交换,使得具有最大深度,并且根节点的值不大于原本根节点的值。由于
的深度不能超过
的深度,所以此时它们的深度一定相等。
若二叉树深度最深的一层只有两个叶节点,它们必定为和
且为兄弟。
假设在二叉树深度最深的一层还有其他叶节点,则由节点的度为0或2可知,至少还有两个叶节点,且满足
。若
不为兄弟节点,不妨假设
和
为兄弟节点,
为兄弟节点。则
和
的父节点
,
和
的父节点
。考虑倒数第二层的所有节点的最大值
,可以得到
若将和
交换,则
,从而
因此这一交换可以得到更优的合并方案。从而,和
一定为兄弟节点。
数学归纳
归纳基础 在时,使用贪心策略可以得到最优合并。
归纳步骤 假设时,使用贪心策略可以得到最优合并。我们需要证明:在
时,使用贪心策略可以得到最优合并。
归纳基础的证明 由前面对两个街区情形的分析可知,归纳基础成立。
归纳步骤的证明
- 不妨假设
是
个数中最小的两个数。根据贪心策略,我们将它们合并,得到一个新的数
。
- 将
和剩余
个数组合在一起,由归纳假设可知,使用贪心策略可以得到这
个数的最优合并,假设其对应的二叉树表示为T,根节点的值为
,则使用贪心策略合并
个数的代价也为
。
- 我们将T中
对应的叶节点拆分出两个新的叶节点
和
,可以得到一棵新的二叉树T',根节点的值为
。
- 假设存在一个比贪心策略得到的二叉树更优的合并二叉树S,则其根节点
,由引理可知,
和
一定具有最大深度,且为兄弟节点,因此我们可以把
和
合并得到
,这样就得到了一个具有
个叶节点的新二叉树S',其对应了
和剩余
个数的一种合并方式,且其根节点
。这与我们的归纳假设,也即T是
个数的最优合并,相矛盾。因此假设不成立,贪心策略得到合并
个数的方法是最优合并。
参考资料
参考代码(C++)
class Solution {
public:
int minBuildTime(vector<int>& blocks, int split) {
priority_queue<int,vector<int>,greater<>> pq;
for (int block : blocks)
pq.push(block);
while (pq.size() > 1){
int smallest = pq.top();
pq.pop();
int second_smallest = pq.top();
pq.pop();
pq.push(split + second_smallest);
}
return pq.top();
}
};