[USACO11MAR] Brownie Slicing G

GitHub跳转原题关系图返回列表

二分最小块权值,用非负矩阵上的横向与纵向贪心判定目标是否可行。

OJ: luogu

题目 ID: P3017

难度:提高+/省选-

标签:二分答案贪心矩阵USACO

日期: 2026-07-16 17:48

题意

给定一个 R×CR\times C 的非负矩阵。先横切成 AA 条连续横带,每条横带再各自竖切成 BB 个连续块。其它牛会拿走较大的块,Bessie 最后只能得到权值最小的那一块。

要求最大化所有 A×BA\times B 个块中的最小权值。

思路

先看一个可以直接验证想法的朴素解:

cpp
/**
 * 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;
}

这个暴力枚举所有横切方案,再对每条横带枚举所有竖切位置,直接算出当前切法下最小块权值。它能帮助确认题意和辅助小数据对拍,但组合数量太大,不能处理 R,C500R,C\leqslant 500

这题的目标是“最大化最小值”,可以二分答案。设当前尝试的最小块权值为 target,我们只需要判断能否切出 A×BA\times B 个块,并且每一块权值都至少为 target

判定的关键在于矩阵元素非负。

从上到下扫描行,维护当前横带中每一列的累计权值 col_sum[j]。每加入一行后,从左到右累计列和;一旦当前累计值达到 target,就立刻切出一个竖块并把累计值清零。如果当前横带能切出至少 BB 个竖块,就立刻结束这条横带,清空 col_sum,继续寻找下一条横带。

为什么可以“立刻切”?因为所有数都是非负的。横向上,一个块越早达到 target 就越早结束,只会给后面的块留下更多列。纵向上,一条横带越早完成,就越能给后面的横带留下更多行。因此这个贪心判定不会错过可行方案。

check(target) 可以完成至少 AA 条横带,说明 target 可行,二分时继续增大答案;否则减小答案。

代码

cpp
/**
 * 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;
}

复杂度

设矩阵总权值为 SS。一次判定需要扫描整个矩阵,时间复杂度为 O(RC)O(RC);二分次数为 O(logS)O(\log S),总时间复杂度为 O(RClogS)O(RC\log S)

空间复杂度为 O(RC+C)O(RC+C),其中矩阵占 O(RC)O(RC),判定时的列和数组占 O(C)O(C)

总结

本题的核心不是枚举切线,而是把“最小块权值最大”转成二分判定。非负矩阵保证了横带内从左到右贪心、横带之间从上到下贪心都是安全的。