
专栏导读
本专栏收录于《华为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算法的适用场景,发现新题目,随时更新。

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

被折叠的 条评论
为什么被折叠?



