按行扫描的四状态 DP:左端点先减后增、右端点先增后减,连通约束化为矩形前驱区域,用区域最大值把转移压到 O(N·K·M²)。
OJ: roj
题目 ID: 3139
难度:提高+/省选-
标签:
创建: 2026-10-01 19:57
更新: 2026-10-10 11:30
形式化题目
给定
- 占据连续的若干行,每行取一段连续的列
,且 ; - 相邻两行的区间必须相交(连通,中间没有空缺);
- 全部左端点
先递减后递增(存在一个谷点),全部右端点 先递增后递减(存在一个峰点),两段都允许非严格单调,退化为纯单调也合法。
样例中
| 行\列 | 1 | 2 | 3 |
|---|---|---|---|
| 1 | 10 | 20 | 30 |
| 2 | 40 | 2 | 3 |
最优方案选第 1 行全部三格与第 2 行第 1 列,权值和
正解
思路
朴素想法是枚举连通块的上下边界,再枚举每一行的左右端点,组合数是指数级的;直接套「枚举每行区间 + 检查单调性」的搜索同样不可行。需要把「端点序列的形状」压缩进状态里。
关键观察:每个端点序列最多只有一个拐点——左端点先降后升、右端点先升后降,各允许在任意一行至多翻转一次方向。于是逐行扫描时,每个端点只需记住 1 位方向信息:
:左端点仍在递减段(还没到谷), :已过谷转为递增; :右端点仍在递增段(还没到峰), :已过峰转为递减。
拐弯只能发生一次,所以
转移时枚举本行
| 当前 |
允许的上一层状态 | 前驱 |
含义 |
|---|---|---|---|
| 两端都未拐弯,持续张开 | |||
| 左过谷、右仍递增 | |||
| 左仍递减、右过峰 | |||
| 任意状态 | 两端都已拐弯,持续收缩 |
转移方程为
若对每个状态直接枚举
最后回溯方案:逐行冻结 DP 表,从最优终点出发,按同一套「方向 + 相交」约束反查一个等值前驱;找不到前驱即说明本行是连通块首行(多个等值前驱时从大到小扫描,保证输出确定性)。
代码
/**
* 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-10-10 11:30
* update_at: 2026-10-10 11:30
*/
// 油田(Oil Deposits,恰好 k 格):按行 DP + 四种单调转移的区域最大值 + 回溯方案。
#include <cstdio>
#include <vector>
#include <algorithm>
typedef long long ll;
const ll NEG = -1000000000LL; // 「不可行」哨兵
ll n, m, k;
std::vector<std::vector<ll> > pre; // 行前缀和
// (目标状态层, 可用的上一层状态层, rect_max 的区域种类)
// 状态层 s = fl*2+fr:fl=1 左端点已过谷,fr=1 右端点已过峰
const int COMBOS_DST[4] = {0, 2, 1, 3};
const int COMBOS_KIND[4] = {0, 1, 2, 3};
const int SRC_A[4] = {0, 0, 0, 0};
const int SRC_B[4] = {0, 2, 1, 1};
const int SRC_C[4] = {0, 0, 0, 2};
const int SRC_D[4] = {0, 0, 0, 3};
// 对上一层权值表做区域最大值查询,结果表下标 [l][r]
std::vector<ll> rect_max(std::vector<ll>& a, ll kind) {
ll mm = m;
std::vector<ll> res(mm * mm, NEG);
if (kind == 3) { // 行内右后缀 max(q ≥ r)+ 列向前缀 max(p ≤ l)
std::vector<ll> t2 = a;
for (ll p = 0; p < mm; p++) {
ll b = p * mm, cur = NEG;
for (ll q = mm - 1; q >= 0; q--) {
if (t2[b + q] > cur) cur = t2[b + q];
t2[b + q] = cur;
}
}
for (ll p = 1; p < mm; p++) {
for (ll q = 0; q < mm; q++) {
ll u = t2[(p - 1) * mm + q];
if (u > t2[p * mm + q]) t2[p * mm + q] = u;
}
}
return t2;
}
if (kind == 1) { // 先列向并(p ≤ l),再行内从 q=l 起右扫(q ∈ [l, r])
std::vector<ll> e = a;
for (ll p = 1; p < mm; p++) {
for (ll q = 0; q < mm; q++) {
ll u = e[(p - 1) * mm + q];
if (u > e[p * mm + q]) e[p * mm + q] = u;
}
}
for (ll l = 0; l < mm; l++) {
ll b = l * mm, cur = NEG;
for (ll q = l; q < mm; q++) {
if (e[b + q] > cur) cur = e[b + q];
res[b + q] = cur;
}
}
return res;
}
if (kind == 2) { // 先行内右后缀(q ≥ r),再列内从 p=r 往上扫(p ∈ [l, r])
std::vector<ll> g = a;
for (ll p = 0; p < mm; p++) {
ll b = p * mm, cur = NEG;
for (ll q = mm - 1; q >= 0; q--) {
if (g[b + q] > cur) cur = g[b + q];
g[b + q] = cur;
}
}
for (ll r = 0; r < mm; r++) {
ll cur = NEG;
for (ll p = r; p >= 0; p--) {
if (g[p * mm + r] > cur) cur = g[p * mm + r];
res[p * mm + r] = cur;
}
}
return res;
}
// kind 0:方形 [l, r]×[l, r],行 max + 列 max 后按边长递推
std::vector<ll> rp = a;
for (ll p = 0; p < mm; p++) {
ll b = p * mm;
for (ll q = 1; q < mm; q++) {
if (rp[b + q - 1] > rp[b + q]) rp[b + q] = rp[b + q - 1];
}
}
std::vector<ll> cp(mm * mm, NEG);
for (ll l = 0; l < mm; l++) {
ll cur = NEG;
for (ll p = l; p < mm; p++) {
if (a[p * mm + l] > cur) cur = a[p * mm + l];
cp[p * mm + l] = cur;
}
}
for (ll d = 0; d < mm; d++) { // d = r - l,小边长先算
for (ll l = 0; l + d < mm; l++) {
ll r = l + d;
ll v = rp[l * mm + r];
if (cp[r * mm + l] > v) v = cp[r * mm + l];
if (d) {
ll u = res[(l + 1) * mm + r];
if (u > v) v = u;
}
res[l * mm + r] = v;
}
}
return res;
}
struct Pick {
ll row, l, r;
};
int main() {
if (scanf("%lld %lld %lld", &n, &m, &k) != 3) return 0; // 空输入安全返回
std::vector<std::vector<ll> > w(n, std::vector<ll>(m, 0));
for (ll i = 0; i < n; i++) {
for (ll j = 0; j < m; j++) {
scanf("%lld", &w[i][j]);
}
}
if (k == 0) { // 空连通块
printf("Oil : 0\n");
return 0;
}
pre.assign(n, std::vector<ll>(m + 1, 0));
for (ll i = 0; i < n; i++) {
for (ll c = 0; c < m; c++) {
pre[i][c + 1] = pre[i][c] + w[i][c];
}
}
ll W = m * m;
ll k1 = k + 1;
std::vector<ll> slots, sizes;
for (ll l = 0; l < m; l++) {
for (ll r = l; r < m; r++) {
slots.push_back(l * m + r);
sizes.push_back(r - l + 1);
}
}
ll S = (ll)slots.size();
// 四层 DP 表,下标 [s][j*W + slot]
std::vector<std::vector<ll> > F(4, std::vector<ll>(k1 * W, NEG));
// 每行结束后冻结一张表,供最后回溯方案
std::vector<ll> hist((size_t)n * 4 * k1 * W, 0);
ll best = NEG;
ll best_row = 0, best_state = 0, best_slot = 0;
std::vector<ll> A(W, 0), R;
std::vector<ll> seg_row(S, 0);
for (ll i = 0; i < n; i++) {
std::vector<std::vector<ll> > newF(4, std::vector<ll>(k1 * W, NEG));
for (ll t = 0; t < S; t++) {
ll l = slots[t] / m, r = slots[t] % m;
seg_row[t] = pre[i][r + 1] - pre[i][l];
}
ll jtop = i * m;
if (jtop > k - 1) jtop = k - 1;
for (ll ci = 0; ci < 4; ci++) {
ll dst_s = COMBOS_DST[ci], kind = COMBOS_KIND[ci];
std::vector<ll> dst = newF[dst_s];
for (ll jp = 1; jp <= jtop; jp++) {
ll base = jp * W;
for (ll t = 0; t < W; t++) A[t] = F[SRC_A[ci]][base + t];
if (ci == 1) { // (1,0) ← (0,0)/(1,0)
for (ll t = 0; t < W; t++) {
ll x = F[SRC_B[ci]][base + t];
if (x > A[t]) A[t] = x;
}
} else if (ci == 2) { // (0,1) ← (0,0)/(0,1)
for (ll t = 0; t < W; t++) {
ll x = F[SRC_B[ci]][base + t];
if (x > A[t]) A[t] = x;
}
} else if (ci == 3) { // (1,1) ← 任意
for (ll s = 1; s < 4; s++) {
for (ll t = 0; t < W; t++) {
ll x = F[s][base + t];
if (x > A[t]) A[t] = x;
}
}
}
ll mx = A[0];
for (ll t = 1; t < W; t++) {
if (A[t] > mx) mx = A[t];
}
if (mx <= NEG / 2) continue; // 该层源状态全不可达
R = rect_max(A, kind);
for (ll t = 0; t < S; t++) {
ll jn = jp + sizes[t];
if (jn > k) continue;
ll v = R[slots[t]] + seg_row[t];
if (v <= NEG / 2) continue;
if (v > newF[dst_s][jn * W + slots[t]]) {
newF[dst_s][jn * W + slots[t]] = v;
}
}
}
}
// 本行作为连通块首行:单调状态回到 (0,0)
for (ll t = 0; t < S; t++) {
if (sizes[t] <= k) {
ll slot = sizes[t] * W + slots[t];
if (seg_row[t] > newF[0][slot]) newF[0][slot] = seg_row[t];
}
}
// 连通块可以在任意一行结束:记录恰 k 格的历史最优
ll base_k = k * W;
for (ll s = 0; s < 4; s++) {
for (ll t = 0; t < S; t++) {
ll v = newF[s][base_k + slots[t]];
if (v > best) {
best = v;
best_row = i;
best_state = s;
best_slot = slots[t];
}
}
}
for (ll s = 0; s < 4; s++) {
for (ll t = 0; t < k1 * W; t++) {
hist[((size_t)i * 4 + s) * k1 * W + t] = newF[s][t];
}
}
F.swap(newF);
}
std::vector<Pick> picked;
ll end_row = best_row, s = best_state, slot = best_slot;
ll l = slot / m, r = slot % m;
ll j = k;
while (true) {
Pick pk;
pk.row = end_row;
pk.l = l;
pk.r = r;
picked.push_back(pk);
ll sz = r - l + 1;
ll jp = j - sz;
ll fl = s >> 1, fr = s & 1;
ll want = hist[((size_t)end_row * 4 + s) * k1 * W + j * W + slot]
- (pre[end_row][r + 1] - pre[end_row][l]);
bool found = false;
ll ns = 0, nl = 0, nr = 0;
if (end_row > 0) {
for (ll s0 = 0; s0 < 4; s0++) {
if ((s0 >> 1) > fl || (s0 & 1) > fr) continue; // 拐过的弯不能回退
for (ll p = m - 1; p >= 0; p--) {
if (fl == 0 && p < l) continue;
if (fl == 1 && p > l) continue;
for (ll q = m - 1; q >= 0; q--) {
if (fr == 0 && q > r) continue;
if (fr == 1 && q < r) continue;
if (q < l || p > r || p > q) continue;
ll hv = hist[((size_t)(end_row - 1) * 4 + s0) * k1 * W + jp * W + p * m + q];
if (hv == want) {
found = true;
ns = s0;
nl = p;
nr = q;
break;
}
}
// 与 Python 的 `if nxt: break` 一致:可能由更早的 s0 置位
if (found) break;
}
}
}
if (!found) break; // 没有前驱行:本行即连通块首行
s = ns;
l = nl;
r = nr;
slot = l * m + r;
j = jp;
end_row = end_row - 1;
}
std::reverse(picked.begin(), picked.end());
printf("Oil : %lld\n", best);
for (size_t t = 0; t < picked.size(); t++) {
for (ll c = picked[t].l; c <= picked[t].r; c++) {
printf("%lld %lld\n", picked[t].row + 1, c + 1);
}
}
return 0;
}#!/usr/bin/env python3
# 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-10-01 20:42
# update_at: 2026-10-01 20:42
import sys
from array import array
NEG = -10**9 # 「不可行」哨兵:比任何合法权值和都小一个数量级
def rect_max(a: list[int], kind: int) -> list[int]:
"""对上一层 (p, q) 权值表做区域最大值查询,结果表下标 [l][r] 即当前行取 [l, r] 的最优前驱值。
四种 kind 对应四种单调转移允许的前驱区域(p=上一行左端点, q=上一行右端点):
kind 0:方形 p, q ∈ [l, r](两端点都还在「先减/先增」段);
kind 1:p ≤ l 且 q ∈ [l, r](左端点已过谷, 右端点仍在递增段);
kind 2:p ∈ [l, r] 且 q ≥ r(左端点仍在递减段, 右端点已过峰);
kind 3:p ≤ l 且 q ≥ r(两端点都已拐弯, 持续收缩)。
"""
m = int(len(a) ** 0.5)
if kind == 3: # 两次单向扫描:行内右后缀 max(q ≥ r)+ 列向前缀 max(p ≤ l)
res = a[:]
for p in range(m):
b, t = p * m, NEG
for q in range(m - 1, -1, -1): # 行内从右往左并
if res[b + q] > t:
t = res[b + q]
res[b + q] = t
for p in range(1, m):
for q in range(m):
u = res[(p - 1) * m + q]
if u > res[p * m + q]:
res[p * m + q] = u
return res
if kind == 1: # 先列向并(p ≤ l),再行内从 q=l 起右扫(q ∈ [l, r])
e = a[:]
for p in range(1, m):
for q in range(m):
u = e[(p - 1) * m + q]
if u > e[p * m + q]:
e[p * m + q] = u
res = [NEG] * (m * m)
for l in range(m):
b, t = l * m, NEG
for q in range(l, m):
if e[b + q] > t:
t = e[b + q]
res[b + q] = t
return res
if kind == 2: # 先行内右后缀(q ≥ r),再列内从 p=r 往上扫(p ∈ [l, r])
g = a[:]
for p in range(m):
b, t = p * m, NEG
for q in range(m - 1, -1, -1):
if g[b + q] > t:
t = g[b + q]
g[b + q] = t
res = [NEG] * (m * m)
for r in range(m):
t = NEG
for p in range(r, -1, -1):
if g[p * m + r] > t:
t = g[p * m + r]
res[p * m + r] = t
return res
# kind 0:方形 [l, r]×[l, r]。行 max + 列 max 后按边长递推:
# 方形 [l, r] = 方形 [l+1, r] ∪ 第 l 行 ∪ 第 l 列
rp = a[:]
for p in range(m):
b = p * m
for q in range(1, m):
u = rp[b + q - 1]
if u > rp[b + q]:
rp[b + q] = u
cp = [NEG] * (m * m) # 列向只从 p=l 起并:p<l 的槽是合法状态,不能混进方形
for l in range(m):
t = NEG
for p in range(l, m):
x = a[p * m + l]
if x > t:
t = x
cp[p * m + l] = t
res = [NEG] * (m * m)
for d in range(m): # d = r - l,小边长先算
for l in range(m - d):
r = l + d
v = rp[l * m + r] # 第 l 行 q ∈ [l, r](q<l 的槽 p>q 恒为哨兵)
c = cp[r * m + l] # 第 l 列 p ∈ [l, r]
if c > v:
v = c
if d:
u = res[(l + 1) * m + r]
if u > v:
v = u
res[l * m + r] = v
return res
# (目标状态层, 可用的上一层状态层, rect_max 的区域种类)
# 状态层 s = fl*2+fr:fl=1 表示左端点已过谷(开始递增),fr=1 表示右端点已过峰(开始递减)
COMBOS = (
(0, (0,), 0), # (0,0) ← (0,0) 两端都未拐弯
(2, (0, 2), 1), # (1,0) ← (0,0)/(1,0) 左过谷、右仍递增
(1, (0, 1), 2), # (0,1) ← (0,0)/(0,1) 左仍递减、右过峰
(3, (0, 1, 2, 3), 3), # (1,1) ← 任意 两端都已拐弯,持续收缩
)
def solve() -> None:
data = iter(map(int, sys.stdin.buffer.read().split()))
n, m, k = next(data), next(data), next(data)
w = [[next(data) for _ in range(m)] for _ in range(n)]
if k == 0: # 空连通块
print("Oil : 0")
return
pre = [[0] * (m + 1) for _ in range(n)] # 行前缀和:区间 [l, r] 和 = pre[i][r+1] - pre[i][l]
for i in range(n):
for c, x in enumerate(w[i]):
pre[i][c + 1] = pre[i][c] + x
segs = [(l, r) for l in range(m) for r in range(l, m)] # 合法列区间,扁平槽位 W = l*m + r
slots = [l * m + r for l, r in segs]
sizes = [r - l + 1 for l, r in segs]
m2, k1 = m * m, k + 1
# 四层 DP 表,下标 [s][j*m2 + W]:已用 j 格、当前行取该区间、单调状态 s 的最大权值和
F = [[NEG] * (k1 * m2) for _ in range(4)]
hist: list[tuple] = [] # 每行结束后冻结一张表,供最后回溯方案
best, best_state = NEG, (0, 0, 0)
for i in range(n):
newF = [[NEG] * (k1 * m2) for _ in range(4)]
seg_row = [pre[i][r + 1] - pre[i][l] for l, r in segs] # 本行每个区间的权值和
jtop = min(k - 1, i * m) # 上一层最多 i*m 格;且 j + size ≤ k ⇒ j ≤ k-1
for dst_s, srcs, kind in COMBOS:
dst = newF[dst_s]
for jp in range(1, jtop + 1):
base = jp * m2
A = F[srcs[0]][base:base + m2] # 可用源层逐槽取 max 合成一张表
for s in srcs[1:]:
B = F[s]
for t in range(m2):
x = B[base + t]
if x > A[t]:
A[t] = x
if max(A) <= NEG // 2: # 该层源状态全不可达
continue
R = rect_max(A, kind)
for t, W in enumerate(slots):
jn = jp + sizes[t]
if jn > k:
continue
v = R[W] + seg_row[t]
if v <= NEG // 2:
continue
slot = jn * m2 + W
if v > dst[slot]:
dst[slot] = v
# 本行作为连通块首行:单调状态回到 (0,0),之前各行均为 0 格
for t, W in enumerate(slots):
if sizes[t] <= k:
slot = sizes[t] * m2 + W
if seg_row[t] > newF[0][slot]:
newF[0][slot] = seg_row[t]
# 连通块可以在任意一行结束:记录「恰 k 格」的历史最优
base_k = k * m2
for s in range(4):
layer = newF[s]
for W in slots:
v = layer[base_k + W]
if v > best:
best, best_state = v, (i, s, W)
hist.append(tuple(array("i", layer) for layer in newF))
F = newF
# 回溯:从最优终点逐行往回找一个取到该值的前驱,找不到即说明本行是首行
end_row, s, W = best_state
l, r = divmod(W, m)
j = k
picked: list[tuple[int, int, int]] = []
while True:
picked.append((end_row, l, r))
size = r - l + 1
jp = j - size
fl, fr = s >> 1, s & 1
want = hist[end_row][s][j * m2 + W] - (pre[end_row][r + 1] - pre[end_row][l])
nxt = None
if end_row > 0:
prev = hist[end_row - 1]
for s0 in range(4):
if (s0 >> 1) > fl or (s0 & 1) > fr: # 拐过的弯不能回退
continue
for p in range(m - 1, -1, -1): # 多个等优前驱时从大到小扫描
if (fl == 0 and p < l) or (fl == 1 and p > l): # 左端点单调方向
continue
for q in range(m - 1, -1, -1):
if (fr == 0 and q > r) or (fr == 1 and q < r): # 右端点单调方向
continue
if q < l or p > r or p > q: # 相邻行相交(连通)且前驱区间合法
continue
if prev[s0][jp * m2 + p * m + q] == want:
nxt = (s0, p, q)
break
if nxt:
break
if nxt is None: # 没有前驱行:本行即连通块首行
break
s, l, r = nxt
W = l * m + r
j, end_row = jp, end_row - 1
picked.reverse() # 输出按行号、列号升序
out = [f"Oil : {best}"]
out += [f"{ri + 1} {c + 1}" for ri, l, r in picked for c in range(l, r + 1)]
print("\n".join(out))
if __name__ == "__main__":
solve()复杂度
- 时间:每个(行、
层、状态组合)做一次 区域预处理与 查询,共 ; 、 时约为 量级。 - 空间:两层工作表各
个整数,逐行存档用于回溯共 ,约 12 MB。
总结
- 每个端点序列只有一个拐点:把「是否已拐弯」压成 2 bit 方向状态,四种组合即覆盖全部合法轮廓。
- 单调方向与相邻行相交两类约束叠加后,前驱集合恰好是矩形/方形,转移转化为区域最大值查询;一维前后缀与边长递推把单层预处理压到
。 维度(恰好 格)按本行宽度平移对齐;连通块的起点用「重新起步」状态、终点对所有行取最大处理。