【Lintcode】437. Copy Books(配数学证明)

题目地址:

https://www.lintcode.com/problem/copy-books/description

给定一个数组 A A A,表示每本书有需要多少分钟誊抄完,再给定一个数 k k k表示抄书人的人数,规定每个人只能抄下标连续的书,并且每个人抄书的速度都是一样的。所有人可以同时抄,问怎样分配抄书的工作能够使得最慢的那个人抄完的时间最短。

法1:二分法。我们可以先考虑整个抄完的时间的可能的范围,再用二分答案的方法找出最少的时间是多少。直觉上来讲,给的时间越短,那么需要的人应该越多(这一点的证明并不难。首先,对于某个时间,由于抄书的方案数是有限的,有限集合一定存在最值,所以一定存在一种需要人数最少的分配抄书方案,让给定的时间变短,如果这个时间下那个方案仍然成立,则需要的人数不变;否则不成立的话,就要增加人手)。我们考虑最长可能的消耗时间。很显然所有书都给一个人抄耗时最长,耗时为数组所有数之和;而每本书都派一个人抄的情况下耗时最短,耗时为数组最大值。由此我们就有了二分答案的区间,设为 [ l , r ] [l,r] [l,r]。然后针对每个时间 m m m,我们求一下需要的人数,如果算出来需要的人数小于等于 k k k,说明还有可能加快抄写,于是在 [ l , m ] [l,m] [l,m]继续寻找答案;如果算出来需要的人数大于了 k k k,说明要求的时间太短了,完成不了,则在 ( m + 1 , r ] (m+1,r] (m+1,r]中寻找答案。

接下来的问题就是,给定一个时间,怎么计算需要的人数。这里可以采取贪心的做法。设给定的时间是 t t t,那我们先截取 A A A的最长前缀,使得这个前缀和是小于等于 t t t的,这个前缀的书就给第 1 1 1个人抄,接着再找尽量长的第二段,给第 2 2 2个人抄,以此类推。这样就能算出来需要多少人抄了。

剩余的问题是,上述贪心做法得到的人数,是否就是对于某个时间所需要的最小人数。数学归纳法。对 A A A的长度进行归纳。如果 A A A的长度为 1 1 1那显然正确。假设对于 A A A的长度小于 n n n的时候结论正确,考虑 A A A的长度等于 n n n的时候。设所需要的最小人数是 c c c,贪心法得到的人数是 x x x,则有 x ≥ c x\ge c xc。接着,设贪心方案最长前缀是 A [ 0 : s ] A[0:s] A[0:s],而最优方案分割出来的第一段是在 A [ 0 : t ] A[0:t] A[0:t],不妨设 t < s t<s t<s,由归纳假设, A [ s + 1 : ] A[s+1:] A[s+1:]这一段需要在最少人数就是真实的最少人数,由于最优解中 A [ t + 1 : ] A[t+1:] A[t+1:]这一段要比 A [ s + 1 : ] A[s+1:] A[s+1:]更长,所以需要的人也更多(或相等),所以有 c ≥ x c\ge x cx,所以 c = x c=x c=x,结论成立。

上面的”不妨设“的意思其实是,找到最优分割方案和贪心分割方案第一次不同的分割点,对于这个分割点进行分析。上面直接假设第一段的分割就是不同的了,所以可以那样”不妨设“。

代码如下:

public class Solution {
    /**
     * @param pages: an array of integers
     * @param k: An integer
     * @return: an integer
     */
    public int copyBooks(int[] pages, int k) {
        // write your code here
        // 判空
        if (pages == null || pages.length == 0) {
            return 0;
        }
        
        // 初始化二分答案的区间左右端点
        int l = 0, r = 0;
        for (int page : pages) {
            l = Math.max(l, page);
            r += page;
        }
        
        while (l < r) {
            int m = l + (r - l >> 1);
            if (check(pages, m, k)) {
                r = m;
            } else {
                l = m + 1;
            }
        }
        
        return l;
    }
    
    // 给定要求的耗时,判断k个人是否可以完工
    private boolean check(int[] pages, int time, int k) {
        // 记录所需要的人数
        int count = 1;
        int sum = 0;
        for (int i = 0; i < pages.length; i++) {
            sum += pages[i];
            // 如果sum大于了time,说明pages[i]必须得给下一个人抄,所以记count加一,并让i回退一格,让sum清零
            if (sum > time) {
                i--;
                count++;
                sum = 0;
            }
        }
        // 返回所需要的人数是否小于等于k
        return count <= k;
    }
}

时间复杂度 O ( l A log ⁡ ∑ i ( a i ) ) O(l_A\log\sum_i(a_i)) O(lAlogi(ai)) n n n为书的个数, a i a_i ai表示第 i i i本书所需抄写时间。

法2:动态规划。设 f [ i ] [ j ] f[i][j] f[i][j] i i i个人抄前 j j j本书需要的时间(这里 i i i j j j都从 1 1 1开始计数)。则当 j = 0 j=0 j=0时, f [ 0 ] [ j ] = 0 f[0][j]=0 f[0][j]=0,否则 f [ 0 ] [ j ] = ∞ f[0][j]=\infty f[0][j]=表示不可能。此外 f [ i ] [ 0 ] = 0 f[i][0]=0 f[i][0]=0。接着考虑递推关系。这里我们按照最后一个人抄后几本书来分类,分为最后一个人抄最后 1 1 1本书、 2 2 2本书这样分类,则有: f [ i ] [ j ] = min ⁡ 0 ≤ s ≤ j − 1 { max ⁡ { f [ i − 1 ] [ s ] , A [ j − 1 ] + A [ j − 2 ] + . . . + A [ s ] } } f[i][j]=\min_{0\le s\le j-1}\{\max\{f[i-1][s],A[j-1]+A[j-2]+...+A[s]\}\} f[i][j]=0sj1min{max{f[i1][s],A[j1]+A[j2]+...+A[s]}}代码如下:

import java.util.Arrays;

public class Solution {
    /**
     * @param pages: an array of integers
     * @param k:     An integer
     * @return: an integer
     */
    public int copyBooks(int[] pages, int k) {
        // write your code here
        // 由于一个人至少抄一本书,所以如果人数多于书的数量,则只需要书的数量这么多人就行了
        k = Math.min(k, pages.length);
        
        int[][] dp = new int[k + 1][pages.length + 1];
        Arrays.fill(dp[0], Integer.MAX_VALUE);
        dp[0][0] = 0;
        
        for (int i = 1; i <= k; i++) {
            for (int j = 1; j <= pages.length; j++) {
                dp[i][j] = Integer.MAX_VALUE;
                // lastTime从后向前累加抄书时间
                int lastTime = 0;
                for (int l = j - 1; l >= 0; l--) {
                    lastTime += pages[l];
                    if (l >= 1) {
                        dp[i][j] = Math.min(dp[i][j], Math.max(dp[i - 1][l], lastTime));
                    } else {
                        dp[i][j] = Math.min(dp[i][j], lastTime);
                    }
                }
            }
        }
        
        return dp[k][pages.length];
    }
}

时间复杂度 O ( k l A 2 ) O(kl_A^2) O(klA2),空间 O ( k l A ) O(kl_A) O(klA)

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值