[leetcode 雙周賽 11] 1231 分享巧克力


1231 Divide Chocolate 分享巧克力

問題描述

你有一大塊巧克力,它由一些甜度不完全相同的小塊組成。我們用數組 sweetness 來表示每一小塊的甜度。

你打算和 K 名朋友一起分享這塊巧克力,所以你需要將切割 K 次才能得到 K+1 塊,每一塊都由一些 **連續 **的小塊組成。

為了表現出你的慷慨,你將會吃掉 總甜度最小 的一塊,並將其余幾塊分給你的朋友們。

請找出一個最佳的切割策略,使得你所分得的巧克力 總甜度最大,並返回這個 最大總甜度

示例 1:

輸入: sweetness = [1,2,3,4,5,6,7,8,9], K = 5
輸出: 6
解釋: 你可以把巧克力分成 [1,2,3], [4,5], [6], [7], [8], [9]。

示例 2:

輸入: sweetness = [5,6,7,8,9,1,2,3,4], K = 8
輸出: 1
解釋: 只有一種辦法可以把巧克力分成 9 塊。

示例 3:

輸入: sweetness = [1,2,2,1,2,2,1,2,2], K = 2
輸出: 5
解釋: 你可以把巧克力分成 [1,2,2], [1,2,2], [1,2,2]。

提示:

  • 0 <= K < sweetness.length <= 10^4
  • 1 <= sweetness[i] <= 10^5

思路

  • 讀題
  1. 將數組分成K+1份, 每份獨立且包含是一連續子序列
  2. 計算每份元素之和, 使得最小那份是在所有可能分割方案中最大

貪心

假設必定有一最佳答案ans, 它的情況是:

  • 0 < ans <= sum(sweetness)/(K+1)
  • 每個分塊之和必定 >= ans
  • 以ans為界限分割的塊數量 >= (K+1)

先設ans=avg(sweetness, K+1), 期望最完美的情況下, 大家分到的一樣多, 最少的肯定也是所有方案中最多的
如果以此做界限切割不到K+1塊, 則縮減期望ans--
每次都假設當前期望ans是最佳切割方案的答案, 不斷-1逼近正確答案

  • 貪心切割
    每次累加之和大於等於閾值, 就認為這是最佳划分, 作為1塊
    最終判斷是否能切割到(K+1)塊

貪心+二分

最佳答案ans還有一個特性:

  • ans 是最佳答案
  • ans+1 不是答案
  • ans-1 是答案
    ans為界限, 大於它的都無法做出預期切割方案, 小於等於它的都可以做出切割方案, 其中等於它就是最佳切割方案

這樣就有一個二分的特性
我們可以在一個確定有正確答案的范圍內, 使用二分搜索, 不斷調整范圍區間, 直至找到正確答案
其中判斷正確答案的位置時:

  • [left, right] mid = (left + right) >> 1
  • checkout(mid)
  1. 可以通過 left = mid+1 向右逼近最佳
  2. 不可以通過 right = mid-1 向左逼近最佳
  • checkout 就是之前的貪心切割

代碼實現

貪心

/**
 * 超出時間限制 avg調整過慢
 */
class Solution {
    public int maximizeSweetness(int[] sweetness, int K) {
        // sum 該數組總和
        int sum = 0;
        for (int swt : sweetness) {
            sum += swt;
        }

        // avg 如果平均分給K+1個人
        int avg = sum / (K + 1);
        while (avg > 0) {
            // cur 當前分塊個數
            // curSum 每個分塊的大小
            int cur = 0, curSum = 0;
            for (int swt : sweetness) {
                curSum += swt;
                System.out.printf("cut:%d K:%d curSum:%d avg:%d\n", cur, K, curSum, avg);
                if (curSum >= avg) {
                    System.out.println();
                    // 從第0塊開始切 (cur++ > K or ++cur > (K+1))
                    if (cur++ >= K) {
                        return avg;
                    }
                    curSum = 0;
                }
            }
            // 以步長為1的"速度"向最佳答案靠近
            avg--;
        }

        return avg;
    }
}

貪心+二分

class Solution {
    public int maximizeSweetness(int[] sweetness, int K) {
        // ans 返回答案
        // left 二分左值
        // right 二分右值 大小為1e4*1e5
        int ans = 0, left = 0, right = (int) 1e9 + 50;

        // 最佳甜度必定在[left, right]區間內
        while (left <= right) {
            int mid = (left + right) >> 1;
            // 檢測: 以mid為界限, 大於它的都不可以, 小於等於則可以
            if (check(sweetness, K + 1, mid)) {
                // 最佳mid值在后半段 [mid+1, right]
                left = mid + 1;
                ans = Math.max(mid, ans);
            } else {
                // 最佳mid值在前半段 [left, mid-1]
                right = mid - 1;
            }
        }

        return ans;
    }

    private boolean check(int[] arr, int len, int threshold) {
        // cur 在分塊總和滿足閾值threshold的情況下 可切塊數量 --> k+1
        // sum 單分塊之和
        int cur = 0, sum = 0;

        for (int a : arr) {
            sum += a;
            // 該連續塊之和符合閾值 予以分塊
            if (sum >= threshold) {
                cur++;
                sum = 0;
            }
        }

        // 如果在閾值threshold下 可以分成k+1塊 該切割策略符合題意
        return cur >= len;
    }
}
  • 改進: 縮小二分查找范圍
/**
 * 同上相同的思路
 * 不過使用二叉搜索的方式 並且進行了'剪枝'(最終結果必定不超過平均值, 縮小了二叉搜索范圍)
 */
class Solution {
    public int maximizeSweetness(int[] sweetness, int K) {
        int sum = 0;
        for (int swt : sweetness) {
            sum += swt;
        }

        // right 采用平均值
        int left = 0, right = sum / (K + 1), ans = 0;

        while (left <= right) {
            int mid = (left + right) >> 1, cur = 0, curSum = 0;
            System.out.printf("[left:%d mid:%d right:%d]\n", left, mid, right);
            for (int swt : sweetness) {
                curSum += swt;
                System.out.printf("[cut:%d K:%d] [curSum:%d mid:%d]\n", cur, K, curSum, mid);
                if (curSum >= mid) {
                    if (cur++ >= K) {
                        break;
                    }
                    curSum = 0;
                }
            }

            if (cur > K) {
                ans = Math.max(ans, mid);
                left = mid + 1;
            } else {
                right = mid - 1;
            }
        }

        return ans;
    }
}
input:
[1,2,3,4,5,6,7,8,9]
5
output:
[left:0 mid:3 right:7]
[cut:0 K:5] [curSum:1 mid:3]
[cut:0 K:5] [curSum:3 mid:3]
[cut:1 K:5] [curSum:3 mid:3]
[cut:2 K:5] [curSum:4 mid:3]
[cut:3 K:5] [curSum:5 mid:3]
[cut:4 K:5] [curSum:6 mid:3]
[cut:5 K:5] [curSum:7 mid:3]
[left:4 mid:5 right:7]
[cut:0 K:5] [curSum:1 mid:5]
[cut:0 K:5] [curSum:3 mid:5]
[cut:0 K:5] [curSum:6 mid:5]
[cut:1 K:5] [curSum:4 mid:5]
[cut:1 K:5] [curSum:9 mid:5]
[cut:2 K:5] [curSum:6 mid:5]
[cut:3 K:5] [curSum:7 mid:5]
[cut:4 K:5] [curSum:8 mid:5]
[cut:5 K:5] [curSum:9 mid:5]
[left:6 mid:6 right:7]
[cut:0 K:5] [curSum:1 mid:6]
[cut:0 K:5] [curSum:3 mid:6]
[cut:0 K:5] [curSum:6 mid:6]
[cut:1 K:5] [curSum:4 mid:6]
[cut:1 K:5] [curSum:9 mid:6]
[cut:2 K:5] [curSum:6 mid:6]
[cut:3 K:5] [curSum:7 mid:6]
[cut:4 K:5] [curSum:8 mid:6]
[cut:5 K:5] [curSum:9 mid:6]
[left:7 mid:7 right:7]
[cut:0 K:5] [curSum:1 mid:7]
[cut:0 K:5] [curSum:3 mid:7]
[cut:0 K:5] [curSum:6 mid:7]
[cut:0 K:5] [curSum:10 mid:7]
[cut:1 K:5] [curSum:5 mid:7]
[cut:1 K:5] [curSum:11 mid:7]
[cut:2 K:5] [curSum:7 mid:7]
[cut:3 K:5] [curSum:8 mid:7]
[cut:4 K:5] [curSum:9 mid:7]

參考資源

第 11 場雙周賽 全國排名
【算法實況】旅游期間打一場雙周賽 - 力扣雙周賽 - LeetCode Biweekly 11


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM