华为OD机试 - 坚果巧克力 - 二分查找(Python/JS/C/C++/Go 新系统 100分)

本题要求将n×m巧克力矩阵横切3刀、竖切3刀,划分为16块,最大化其中最小块的权值和。采用二分答案法结合二维前缀和与贪心竖切策略:先枚举所有合法横切位置,再对每个横切方案用贪心方式验证能否划分出4个纵向区域,使每块权值和≥mid;通过二分查找在[0,总和//16]区间内求得最大可行最小值。

在这里插入图片描述

华为OD机试 新系统 统一考试题库清单(持续收录中)以及考点说明(Python/JS/C/C++)。

专栏导读

本专栏收录于《华为OD机试真题(Python/JS/C/C++/Go)》。

刷的越多,抽中的概率越大,私信哪吒,备注华为OD,加入华为OD刷题交流群,每一题都有详细的答题思路、详细的代码注释、3个测试用例、为什么这道题采用XX算法、XX算法的适用场景,发现新题目,随时更新。

一、题目描述

有一块 n × m n \times m n×m( 4 ≤ n , m ≤ 50 4 \le n,m \le 50 4≤n,m≤50)的矩形巧克力,每个格子有一个正整数权值(权值 ≤ 1000 \le 1000 ≤1000),代表这一格的坚果数量。

必须沿着格子横切恰好 3 刀、竖切恰好 3 刀,将巧克力分成 4 × 4 = 16 4 \times 4 = 16 4×4=16 块,分给 16 个人。

切分难以保证每个人分到的一样多,因此你想照顾那个分得最少的人,请通过合理安排切分方式,使这个“分得最少的人”分到的坚果数量在所有切分方式中最多(即:求一种划分方式,最大化这 16 块中权值和最小的那一块的权值和,请输出这个最大化的最小权值和。)

二、输入描述

输入包含一行,格式为 n,m,[[a11,a12,...,a1m],[a21,...,a2m],...,[an1,...,anm]],其中 n n n 为行数, m m m 为列数,后续为 n n n 行 m m m 列的矩阵,表示每个格子的权值。

三、输出描述

输出一个整数,表示在最优切分方案下,16 块巧克力中权值和最小的那一块所能达到的最大权值和。

四、测试用例

测试用例1:

1、输入

4 7
2 1 1 1 2 1 1
2 2 2 2 2 2 2
1 2 3 4 5 6 7
7 6 5 4 3 2 1

2、输出

2

3、说明

横切位置固定为每两行之间,即三个横切位置分别在第 1、2、3 行之后。竖切位置选择在第 2、4、5 列之后。

四个纵向区域分别包含第 1–2 列、第 3–4 列、第 5 列、第 6–7 列。按照这组切法,16 块的最小权值和为 2,因此最大化后的结果至少为 2。枚举并验证所有切法后,无法获得更大的最小值,答案为 2。

测试用例2:

1、输入

5 6
1 1 1 5 5 5
1 1 1 1 1 1
6 6 6 6 6 6
6 6 6 6 6 6
6 6 6 6 6 6

2、输出

6

3、说明

横切位置选择在第 2、3、4 行之后,竖切位置选择在第 3、4、5 列之后。

第一块横向区域由前两行组成,其在四个纵向区域中的权值和均为 6;其余三个横向区域每行的元素均为 6,切分后的每块权值和也都不小于 6。因此答案至少为 6。验证更大的目标值不可行,最终输出 6。

五、解题思路

本题的核心是:将矩阵划分成 4 行、4 列的连续区域,共 16 块,最大化所有区域中最小的元素和。

直接枚举所有横切和竖切方案,组合数量较大,因此采用 二分答案 + 枚举横切位置 + 贪心竖切 的方法,降低计算量。

1、核心算法

二维前缀和: 预处理每一列从矩阵顶部到指定行的元素和。确定横切位置后,可以在 O(1) 时间内计算每个列在四个横向区域中的权值。

二分答案: 假设每一块的权值和至少为 x,判断是否存在符合条件的切分方案。若存在,则尝试更大的 x;否则尝试更小的 x。

枚举横切位置: 枚举三个递增的横切位置,将矩阵划分成四个非空的横向区域。

贪心竖切: 从左到右扫描每一列,分别累计四个横向区域的权值。当四个累计值都达到 x 时,就尽早完成一个纵向区域并清零累计值,继续扫描。若能形成至少四个满足条件的区域,则当前答案可行。

由于所有格子的权值均为正整数,提前完成一个区域不会减少后续可用的列数,因此这种贪心策略能够正确判断固定横切方案下的可行性。

2、数据结构与算法选择

二维数组: 保存巧克力矩阵。

二维前缀和数组: 快速计算每列在指定行区间内的权值和。

二分查找: 利用答案的单调性缩小搜索范围。若最小块的权值和至少为 x 时可行,那么所有不大于 x 的非负目标也可行。

贪心算法: 在固定横切方案下,尽早完成满足条件的纵向区域,以判断能否划分出四个合格区域。

设所有格子的权值总和为 S,答案不会超过 ⌊S/16⌋。因此,可以在区间 [0,⌊S/16⌋] 内二分答案。

六、Python算法源码


import re
import sys

# 提取输入中的全部整数,兼容空格和嵌套列表格式。
nums = list(map(int, re.findall(r"\d+", sys.stdin.read())))

if len(nums) < 2:
    raise SystemExit

n, m = nums[0], nums[1]
pref = [[0] * m for _ in range(n + 1)]
total = 0
idx = 2

# 建立逐列的行前缀和。
for r in range(n):
    for c in range(m):
        value = nums[idx]
        idx += 1
        pref[r + 1][c] = pref[r][c] + value
        total += value


def can(target):
    # 枚举三个横切位置,划分成四个非空横向区域。
    for i in range(1, n - 2):
        for j in range(i + 1, n - 1):
            for k in range(j + 1, n):
                s0 = s1 = s2 = s3 = 0
                parts = 0

                for c in range(m):
                    # 计算当前列对四个横向区域的权值贡献。
                    s0 += pref[i][c]
                    s1 += pref[j][c] - pref[i][c]
                    s2 += pref[k][c] - pref[j][c]
                    s3 += pref[n][c] - pref[k][c]

                    # 四个区域均达标,立即切出一个纵向区域。
                    if (s0 >= target and s1 >= target
                            and s2 >= target and s3 >= target):
                        parts += 1

                        if parts >= 4:
                            return True

                        s0 = s1 = s2 = s3 = 0

    return False


# 答案不超过总权值的 1/16,在此范围内二分。
low, high = 0, total // 16 + 1

while low + 1 < high:
    mid = (low + high) // 2

    if can(mid):
        low = mid
    else:
        high = mid

print(low)

七、JavaScript算法源码


const fs = require('fs');

// 提取所有整数,兼容空格、逗号和方括号格式。
const nums = (fs.readFileSync(0, 'utf8').match(/\d+/g) || [])
    .map(Number);

if (nums.length < 2) process.exit(0);

const n = nums[0];
const m = nums[1];
const pref = Array.from({ length: n + 1 }, () => Array(m).fill(0));

let total = 0;
let idx = 2;

// 建立逐列的行前缀和。
for (let r = 0; r < n; r++) {
    for (let c = 0; c < m; c++) {
        const value = nums[idx++];
        pref[r + 1][c] = pref[r][c] + value;
        total += value;
    }
}

function can(target) {
    // 枚举三个横切位置,得到四个非空横向区域。
    for (let i = 1; i <= n - 3; i++) {
        for (let j = i + 1; j <= n - 2; j++) {
            for (let k = j + 1; k <= n - 1; k++) {
                let s0 = 0, s1 = 0, s2 = 0, s3 = 0;
                let parts = 0;

                for (let c = 0; c < m; c++) {
                    // 使用前缀和计算当前列在四个区域中的权值。
                    s0 += pref[i][c];
                    s1 += pref[j][c] - pref[i][c];
                    s2 += pref[k][c] - pref[j][c];
                    s3 += pref[n][c] - pref[k][c];

                    // 四个区域同时达标,就尽早完成一个纵向区域。
                    if (s0 >= target && s1 >= target
                            && s2 >= target && s3 >= target) {
                        parts++;

                        if (parts >= 4) return true;

                        s0 = s1 = s2 = s3 = 0;
                    }
                }
            }
        }
    }

    return false;
}

// 使用总权值的 1/16 作为二分上界。
let low = 0;
let high = Math.floor(total / 16) + 1;

while (low + 1 < high) {
    const mid = Math.floor((low + high) / 2);

    if (can(mid)) {
        low = mid;
    } else {
        high = mid;
    }
}

console.log(low);

八、C算法源码


#include <stdio.h>

static int n, m;
static int pref[51][50];

/* 判断是否存在方案,使16块的权值和都至少为 target。 */
static int can(int target) {
    for (int i = 1; i <= n - 3; i++) {
        for (int j = i + 1; j <= n - 2; j++) {
            for (int k = j + 1; k <= n - 1; k++) {
                int s0 = 0, s1 = 0, s2 = 0, s3 = 0;
                int parts = 0;

                for (int c = 0; c < m; c++) {
                    // 利用前缀和,快速计算四个横向区域的列权值。
                    s0 += pref[i][c];
                    s1 += pref[j][c] - pref[i][c];
                    s2 += pref[k][c] - pref[j][c];
                    s3 += pref[n][c] - pref[k][c];

                    // 四个累计值均达标,贪心地完成一个纵向区域。
                    if (s0 >= target && s1 >= target
                            && s2 >= target && s3 >= target) {
                        parts++;

                        if (parts >= 4) return 1;

                        s0 = s1 = s2 = s3 = 0;
                    }
                }
            }
        }
    }

    return 0;
}

int main(void) {
    // 逐字符读取输入中的数字,兼容空格、逗号和方括号。
    int nums[2502], count = 0, ch;

    while ((ch = getchar()) != EOF) {
        if (ch >= '0' && ch <= '9') {
            int value = 0;

            do {
                value = value * 10 + ch - '0';
                ch = getchar();
            } while (ch != EOF && ch >= '0' && ch <= '9');

            nums[count++] = value;

            if (ch == EOF) break;
        }
    }

    if (count < 2) return 0;

    n = nums[0];
    m = nums[1];

    int idx = 2;
    int total = 0;

    // 建立逐列的行前缀和。
    for (int r = 0; r < n; r++) {
        for (int c = 0; c < m; c++) {
            int value = nums[idx++];
            pref[r + 1][c] = pref[r][c] + value;
            total += value;
        }
    }

    // 二分最大可行答案。
    int low = 0;
    int high = total / 16 + 1;

    while (low + 1 < high) {
        int mid = low + (high - low) / 2;

        if (can(mid)) {
            low = mid;
        } else {
            high = mid;
        }
    }

    printf("%d\n", low);
    return 0;
}

九、C++算法源码


#include <cctype>
#include <iostream>
#include <string>
#include <vector>
using namespace std;

static int n, m;
static int pref[51][50];

static bool can(int target) {
    // 枚举三个横切位置,形成四个非空横向区域。
    for (int i = 1; i <= n - 3; ++i) {
        for (int j = i + 1; j <= n - 2; ++j) {
            for (int k = j + 1; k <= n - 1; ++k) {
                int s0 = 0, s1 = 0, s2 = 0, s3 = 0;
                int parts = 0;

                for (int c = 0; c < m; ++c) {
                    // 前缀和使每个横向区域的列权值可以快速计算。
                    s0 += pref[i][c];
                    s1 += pref[j][c] - pref[i][c];
                    s2 += pref[k][c] - pref[j][c];
                    s3 += pref[n][c] - pref[k][c];

                    // 四个区域都达到目标,立即切出一个纵向区域。
                    if (s0 >= target && s1 >= target
                            && s2 >= target && s3 >= target) {
                        ++parts;

                        if (parts >= 4) return true;

                        s0 = s1 = s2 = s3 = 0;
                    }
                }
            }
        }
    }

    return false;
}

int main() {
    // 提取所有整数,兼容普通输入和嵌套列表输入。
    string input((istreambuf_iterator<char>(cin)),
                 istreambuf_iterator<char>());

    vector<int> nums;

    for (size_t i = 0; i < input.size();) {
        if (isdigit(static_cast<unsigned char>(input[i]))) {
            int value = 0;

            while (i < input.size()
                    && isdigit(static_cast<unsigned char>(input[i]))) {
                value = value * 10 + input[i] - '0';
                ++i;
            }

            nums.push_back(value);
        } else {
            ++i;
        }
    }

    if (nums.size() < 2) return 0;

    n = nums[0];
    m = nums[1];

    int idx = 2;
    int total = 0;

    // 建立逐列的行前缀和。
    for (int r = 0; r < n; ++r) {
        for (int c = 0; c < m; ++c) {
            int value = nums[idx++];
            pref[r + 1][c] = pref[r][c] + value;
            total += value;
        }
    }

    // 利用答案的单调性二分最大可行值。
    int low = 0;
    int high = total / 16 + 1;

    while (low + 1 < high) {
        int mid = low + (high - low) / 2;

        if (can(mid)) {
            low = mid;
        } else {
            high = mid;
        }
    }

    cout << low << '\n';
    return 0;
}

十、Go算法源码


package main

import (
    "fmt"
    "io"
    "os"
)

func can(target, n, m int, pref [][]int) bool {
    // 枚举三个横切位置,得到四个非空横向区域。
    for i := 1; i <= n-3; i++ {
        for j := i + 1; j <= n-2; j++ {
            for k := j + 1; k <= n-1; k++ {
                s0, s1, s2, s3, parts := 0, 0, 0, 0, 0

                for c := 0; c < m; c++ {
                    // 使用前缀和计算当前列在四个区域中的权值。
                    s0 += pref[i][c]
                    s1 += pref[j][c] - pref[i][c]
                    s2 += pref[k][c] - pref[j][c]
                    s3 += pref[n][c] - pref[k][c]

                    // 四个区域同时达标时,尽早完成一个纵向区域。
                    if s0 >= target && s1 >= target &&
                        s2 >= target && s3 >= target {
                        parts++

                        if parts >= 4 {
                            return true
                        }

                        s0, s1, s2, s3 = 0, 0, 0, 0
                    }
                }
            }
        }
    }

    return false
}

func main() {
    // 提取输入中的所有整数,兼容空格、逗号和方括号。
    data, _ := io.ReadAll(os.Stdin)
    nums := make([]int, 0, 2502)

    for i := 0; i < len(data); {
        if data[i] >= '0' && data[i] <= '9' {
            value := 0

            for i < len(data) &&
                data[i] >= '0' && data[i] <= '9' {
                value = value*10 + int(data[i]-'0')
                i++
            }

            nums = append(nums, value)
        } else {
            i++
        }
    }

    if len(nums) < 2 {
        return
    }

    n, m := nums[0], nums[1]
    pref := make([][]int, n+1)

    for r := range pref {
        pref[r] = make([]int, m)
    }

    idx := 2
    total := 0

    // 建立逐列的行前缀和。
    for r := 0; r < n; r++ {
        for c := 0; c < m; c++ {
            value := nums[idx]
            idx++
            pref[r+1][c] = pref[r][c] + value
            total += value
        }
    }

    // 最小块的权值和不超过总权值的 1/16,使用二分查找。
    low, high := 0, total/16+1

    for low+1 < high {
        mid := low + (high-low)/2

        if can(mid, n, m, pref) {
            low = mid
        } else {
            high = mid
        }
    }

    fmt.Println(low)
}

🏆下一篇:华为OD机试真题 - 简易内存池(Python/JS/C/C++/Go 新系统 200分)

🏆本文收录于,华为OD机试真题(Python/JS/C/C++/Go)

刷的越多,抽中的概率越大,私信哪吒,备注华为OD,加入华为OD刷题交流群,每一题都有详细的答题思路、详细的代码注释、3个测试用例、为什么这道题采用XX算法、XX算法的适用场景,发现新题目,随时更新。

在这里插入图片描述

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

哪 吒

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值