二分最小块权值,用非负矩阵上的横向与纵向贪心判定目标是否可行。
OJ: luogu
题目 ID: P3017
难度:提高+/省选-
标签:二分答案贪心矩阵USACO
日期: 2026-07-16 17:48
题意
给定一个
要求最大化所有
思路
先看一个可以直接验证想法的朴素解:
/**
* Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* rainboy的学习导航网站: https://idx.roj.ac.cn
* create_at: 2026-07-16 17:48
* update_at: 2026-07-22 21:04
*/
// brute.cpp:小数据暴力解,枚举所有横切位置,再枚举每条横带内部的竖切位置。
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 12;
int R, C, A, B;
int a[MAXN][MAXN];
int row_cut[MAXN]; // row_cut[k] 表示第 k 条横带的结束行
int ans;
int rect_sum(int r1, int r2, int c1, int c2) {
int sum = 0;
for (int i = r1; i <= r2; i++) {
for (int j = c1; j <= c2; j++) {
sum += a[i][j];
}
}
return sum;
}
// 对固定的一条横带,枚举 B-1 个竖切位置,求这条横带最优的最小块权值。
int best_strip_value(int r1, int r2) {
vector<int> cuts;
int best = 0;
int limit = 1 << (C - 1);
for (int mask = 0; mask < limit; mask++) {
cuts.clear();
for (int j = 1; j < C; j++) {
if ((mask & (1 << (j - 1))) != 0) {
cuts.push_back(j);
}
}
if ((int)cuts.size() != B - 1) {
continue;
}
int last = 1;
int worst = 1000000000;
for (int i = 0; i < (int)cuts.size(); i++) {
int now = cuts[i];
worst = min(worst, rect_sum(r1, r2, last, now));
last = now + 1;
}
worst = min(worst, rect_sum(r1, r2, last, C));
best = max(best, worst);
}
return best;
}
void evaluate_current_rows() {
int last_row = 1;
int worst = 1000000000;
for (int i = 1; i <= A; i++) {
int now_row = row_cut[i];
worst = min(worst, best_strip_value(last_row, now_row));
last_row = now_row + 1;
}
ans = max(ans, worst);
}
// 递归枚举每条横带的结束行。
void dfs_rows(int dep, int start_row) {
if (dep == A) {
row_cut[dep] = R;
evaluate_current_rows();
return;
}
int max_end = R - (A - dep);
for (int end_row = start_row; end_row <= max_end; end_row++) {
row_cut[dep] = end_row;
dfs_rows(dep + 1, end_row + 1);
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> R >> C >> A >> B;
for (int i = 1; i <= R; i++) {
for (int j = 1; j <= C; j++) {
cin >> a[i][j];
}
}
ans = 0;
dfs_rows(1, 1);
cout << ans << '\n';
return 0;
}这个暴力枚举所有横切方案,再对每条横带枚举所有竖切位置,直接算出当前切法下最小块权值。它能帮助确认题意和辅助小数据对拍,但组合数量太大,不能处理
这题的目标是“最大化最小值”,可以二分答案。设当前尝试的最小块权值为 target,我们只需要判断能否切出 target。
判定的关键在于矩阵元素非负。
从上到下扫描行,维护当前横带中每一列的累计权值 col_sum[j]。每加入一行后,从左到右累计列和;一旦当前累计值达到 target,就立刻切出一个竖块并把累计值清零。如果当前横带能切出至少 col_sum,继续寻找下一条横带。
为什么可以“立刻切”?因为所有数都是非负的。横向上,一个块越早达到 target 就越早结束,只会给后面的块留下更多列。纵向上,一条横带越早完成,就越能给后面的横带留下更多行。因此这个贪心判定不会错过可行方案。
若 check(target) 可以完成至少 target 可行,二分时继续增大答案;否则减小答案。
代码
/**
* Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* rainboy的学习导航网站: https://idx.roj.ac.cn
* create_at: 2026-07-16 17:48
* update_at: 2026-07-22 21:04
*/
#include <bits/stdc++.h>
using namespace std;
const int MAXR = 505;
const int MAXC = 505;
int R, C, A, B;
int a[MAXR][MAXC];
int col_sum[MAXC]; // 当前横带中每一列的权值和
long long total_sum;
bool check(int target) {
memset(col_sum, 0, sizeof(col_sum));
int strips = 0;
for (int i = 1; i <= R; i++) {
for (int j = 1; j <= C; j++) {
col_sum[j] += a[i][j];
}
int pieces = 0;
int cur = 0;
for (int j = 1; j <= C; j++) {
cur += col_sum[j];
if (cur >= target) {
pieces++;
cur = 0;
}
}
// 当前横带已经能切出 B 块,越早结束越不影响后面。
if (pieces >= B) {
strips++;
memset(col_sum, 0, sizeof(col_sum));
}
}
return strips >= A;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> R >> C >> A >> B;
for (int i = 1; i <= R; i++) {
for (int j = 1; j <= C; j++) {
cin >> a[i][j];
total_sum += a[i][j];
}
}
int left = 0;
int right = (int)(total_sum / (A * B)) + 1;
while (left + 1 < right) {
int mid = (left + right) / 2;
if (check(mid)) {
left = mid;
}
else {
right = mid;
}
}
cout << left << '\n';
return 0;
}复杂度
设矩阵总权值为
空间复杂度为
总结
本题的核心不是枚举切线,而是把“最小块权值最大”转成二分判定。非负矩阵保证了横带内从左到右贪心、横带之间从上到下贪心都是安全的。