这是一个**带约束的最大路径和问题**:在 $ m \times n $ 的整数矩阵中,从左上角 (0,0) 到右下角 (m-1,n-1),每次只能向右或向下移动,要找出 **k 条互不重叠的路径**(即每条路径经过的格子不能被其他路径使用),使得所有路径的总和最大。
---
### ❗ 问题分析
#### 关键点:
1. **路径唯一性**:每个单元格最多只能被一条路径使用。
2. **路径方向限制**:只能向右或向下 → 每条路径长度固定为 $ m + n - 1 $。
3. **k 条路径**:需要同时选出 k 条这样的路径,且它们之间无交集。
4. **目标最大化**:所有路径上的值之和最大。
这个问题是经典的 **“k-disjoint paths” 最大权路径问题**,属于 **NP-hard** 类问题,在一般图中是困难的。但由于我们有特殊的结构(DAG、仅右/下移动、网格图),可以考虑使用更高效的算法。
---
### ✅ 可行解法思路对比:
| 方法 | 是否适用 | 原因 |
|------|---------|------|
| 穷举法 | 不可行 | 路径数量指数级增长,枚举所有路径组合复杂度爆炸 |
| 分治法 | 不适用 | 子问题不独立,无法有效划分 |
| 回溯法 | 可行但效率低 | 可以尝试构造路径并回溯,但状态空间巨大 |
| 分支限界法 | 部分可行 | 可用于剪枝优化搜索,但仍难处理大规模数据 |
| **网络流建模(最小费用最大流)** | ✅ 推荐方法 | 将问题转化为 **带容量和费用的流问题**,利用拆点+费用流求解 |
---
## ✅ 正确解法:**最小费用最大流(Minimum Cost Maximum Flow)**
我们将此问题建模为一个 **最小费用最大流问题**(实际是最大收益,所以取负权转为最小费用):
### 🧩 建模方式:
1. **节点拆分(拆点法)**:
- 每个格子 $ (i,j) $ 拆成两个节点:入点 $ in(i,j) $ 和出点 $ out(i,j) $
- 从 $ in(i,j) $ 向 $ out(i,j) $ 连一条边:
- 容量为 1(表示该格子只能用一次)
- 费用为 $ -grid[i][j] $(因为我们要求最大和,费用流默认最小化费用,故取负)
2. **起点额外处理**:
- 起点 (0,0) 允许被多条路径共用?注意题目说“路径不重叠”,包括起点和终点也不能共享!
- 所以起点也只能用一次?但是我们要找 k 条路径!
⚠️ 注意矛盾:如果起点 (0,0) 必须被每条路径使用,而路径又不允许重叠,则当 $ k > 1 $ 时根本不可能存在多条路径!
但题目样例输入是:
```
3 3 2
1 2 3
4 5 6
7 8 9
```
说明确实允许 k=2 成立。
👉 因此必须明确:**是否允许起点和终点被多条路径共享?**
但从“路径不重叠”字面意义看,应是完全不共享任何单元格。
然而,在大多数类似题目的设定中(如洛谷 P2045 方格取数、NOI 2008 志愿者招募变形),**起点和终点允许被多次使用**,或者通过特殊设置让其容量为 k。
所以我们做出如下合理假设(否则 k>1 无解):
> 🔔 **允许起点 (0,0) 和终点 (m-1,n-1) 被最多 k 条路径共享,其余点只能使用一次。**
这是常见变体设定,否则问题无解。
---
### ✅ 模型修正(关键):
- 对于普通点 $ (i,j) $:
- 拆点后中间边容量 = 1,费用 = -grid[i][j]
- 对于起点 (0,0):
- 中间边容量 = k,费用 = -grid[0][0]
- 对于终点 (m-1,n-1):
- 若与起点不同,也设容量 = k,费用 = -grid[m-1][n-1]
- 如果相同(1x1 矩阵),则容量 = k 即可
此外,连接方向边(右/下):
- 从某个点的 `out` 节点连到它右边/下边点的 `in` 节点
- 容量无穷大(或 k),费用 0
最后建立超级源汇:
- 超级源 S → 起点的 `in(0,0)`,容量 = k,费用 = 0
- 终点的 `out(m-1,n-1)` → 超级汇 T,容量 = k,费用 = 0
然后跑 **最小费用最大流**,若最大流 == k,则说明找到了 k 条路径,总费用为 `-min_cost`
---
## ✅ C++ 实现(基于 SPFA 的费用流)
```cpp
#include <iostream>
#include <vector>
#include <queue>
#include <climits>
#include <algorithm>
using namespace std;
const int MAXN = 1000; // 最大节点数(根据 m*n*2 估算)
const int INF = INT_MAX;
struct Edge {
int to, cap, cost, rev;
Edge(int t, int ca, int co, int r) : to(t), cap(ca), cost(co), rev(r) {}
};
class MinCostMaxFlow {
public:
vector<vector<Edge>> graph;
vector<int> dist, potential, prevv, preve;
int n;
MinCostMaxFlow(int nodes) {
n = nodes;
graph.resize(n);
dist.resize(n);
potential.resize(n, 0);
prevv.resize(n);
preve.resize(n);
}
void addEdge(int from, int to, int cap, int cost) {
graph[from].emplace_back(to, cap, cost, graph[to].size());
graph[to].emplace_back(from, 0, -cost, graph[from].size() - 1);
}
bool spfa(int s, int t) {
fill(dist.begin(), dist.end(), INF);
queue<int> q;
vector<bool> inq(n, false);
dist[s] = 0;
q.push(s);
inq[s] = true;
while (!q.empty()) {
int u = q.front(); q.pop();
inq[u] = false;
for (int i = 0; i < graph[u].size(); ++i) {
Edge& e = graph[u][i];
if (e.cap > 0 && dist[e.to] > dist[u] + e.cost + potential[u] - potential[e.to]) {
dist[e.to] = dist[u] + e.cost + potential[u] - potential[e.to];
prevv[e.to] = u;
preve[e.to] = i;
if (!inq[e.to]) {
q.push(e.to);
inq[e.to] = true;
}
}
}
}
return dist[t] != INF;
}
pair<int, int> minCostMaxFlow(int s, int t) {
int totalFlow = 0, totalCost = 0;
fill(potential.begin(), potential.end(), 0);
while (spfa(s, t)) {
for (int i = 0; i < n; ++i) {
if (dist[i] != INF)
potential[i] += dist[i];
}
int f = INF;
for (int v = t; v != s; v = prevv[v]) {
int u = prevv[v];
int i = preve[v];
f = min(f, graph[u][i].cap);
}
for (int v = t; v != s; v = prevv[v]) {
int u = prevv[v];
int i = preve[v];
graph[u][i].cap -= f;
graph[v][graph[u][i].rev].cap += f;
}
totalFlow += f;
totalCost += f * potential[t]; // 注意这里用的是 potential[t]
}
return {totalFlow, totalCost};
}
};
int main() {
int m, n, k;
cin >> m >> n >> k;
vector<vector<int>> grid(m, vector<int>(n));
for (int i = 0; i < m; ++i) {
for (int j = 0; j < n; ++j) {
cin >> grid[i][j];
}
}
// 总节点数:每个格子拆成 in 和 out → 2*m*n,加上超级源S和汇T
int totalNodes = 2 * m * n + 2;
int S = totalNodes - 2, T = totalNodes - 1;
MinCostMaxFlow mcmf(totalNodes);
auto getIn = [&](int i, int j) { return (i * n + j) * 2; };
auto getOut = [&](int i, int j) { return (i * n + j) * 2 + 1; };
// 添加拆点边:in -> out
for (int i = 0; i < m; ++i) {
for (int j = 0; j < n; ++j) {
int cap = 1;
if ((i == 0 && j == 0) || (i == m-1 && j == n-1)) {
cap = k; // 起点终点可被k条路径使用
}
mcmf.addEdge(getIn(i, j), getOut(i, j), cap, -grid[i][j]); // 取负费用
}
}
// 添加移动边:out -> 相邻 in(右、下)
for (int i = 0; i < m; ++i) {
for (int j = 0; j < n; ++j) {
// 向右
if (j + 1 < n) {
mcmf.addEdge(getOut(i, j), getIn(i, j+1), k, 0);
}
// 向下
if (i + 1 < m) {
mcmf.addEdge(getOut(i, j), getIn(i+1, j), k, 0);
}
}
}
// 连接源点S到起点in(0,0),汇点out(m-1,n-1)到T
mcmf.addEdge(S, getIn(0, 0), k, 0);
mcmf.addEdge(getOut(m-1, n-1), T, k, 0);
auto result = mcmf.minCostMaxFlow(S, T);
int flow = result.first;
int minCost = result.second;
if (flow != k) {
// 无法找到k条不相交路径
cout << 0 << endl;
} else {
// 最大和 = -minCost
cout << (-minCost) << endl;
}
return 0;
}
```
---
### ✅ 解释
1. **拆点技术**:保证每个格子只被访问一次(除起点终点外)
2. **负权费用**:将“最大和”转换为“最小费用”
3. **SPFA + 势函数**:解决负权边下的最短路问题,支持连续增广
4. **容量控制**:起点/终点容量设为 k,其余为 1
5. **流量控制**:强制要求流满 k 单位才合法
---
### 📌 样例验证
输入:
```
3 3 2
1 2 3
4 5 6
7 8 9
```
路径1: (0,0)→(0,1)→(0,2)→(1,2)→(2,2): 和 = 1+2+3+6+9 = 21
路径2: (0,0)→(1,0)→(2,0)→(2,1)→(2,2): 和 = 1+4+7+8+9 = 29
但起点和终点重复了!所以必须允许它们共享。
最终两条路径覆盖:
- 路径1: 1,2,3,6,9
- 路径2: 1,4,7,8,9
中间点 (1,1)=5 未被使用,避免冲突
总和 = (1+2+3+6+9) + (4+7+8) + 9(重复计入两次)? ❌ 不行!
但实际上我们在模型中对起点和终点的点权只计算一次 per path,因为每条路径都经过这些点。
但在我们的建模中,`addEdge(in->out)` 是允许 k 次通过,每次都会扣除费用 `-grid[i][j]`,所以每条路径都会完整计数起点和终点的值。
因此两条路径会分别加一次 `1` 和 `9`,总和包含两次 `1` 和 `9`。
这符合逻辑吗?
👉 **不符合现实物理意义**:同一个数字不能被取两次。
但我们的问题描述是:“路径上的单元格不可重复”。
所以严格来说,**起点和终点也不能重复使用**!
这就导致:**k > 1 时无解!**
但样例显然期望输出一个大于单条路径的结果。
因此我们必须重新审视题目意图。
---
## 🔁 重新理解题目:可能是“k 条路径”但允许某些点共享?还是“可重复访问”?
查阅经典题目发现,本题极可能源自 **“数字梯形” 或 “方格取数” 类问题**,其中:
> ✅ **正确理解**:k 条路径可以从 (0,0) 出发到 (m-1,n-1),但:
> - 第一问:路径点不重合 → 本题情形
> - 但通常会有多个起点(如整行出发),而非单一入口
由于只有一个入口 (0,0),若要求 k 条路径完全不重合,则 **k 必须为 1**
除非放宽条件。
---
## ⚠️ 结论:题目可能存在歧义
但鉴于样例给出 k=2,并期待非平凡答案,合理的解释是:
> ❗ **题目中的“不重叠”是指内部点不重叠,起点和终点允许多次使用**
或者更进一步地:
> ✅ **标准做法是:起点/终点不限制使用次数,其他点最多一次**
这也是 OJ 中此类题的标准设定(例如:洛谷 P4013 数字梯形问题)
因此我们上述代码是符合这类标准模型的。
---
### ✅ 样例运行结果预测:
最大两条路径分别为:
- 路径1: 1 → 2 → 3 → 6 → 9 → sum=21
- 路径2: 1 → 4 → 7 → 8 → 9 → sum=29
但中间点 (1,1)=5 没有用,且 (0,0),(2,2) 共享
总和 = 21 + 29 - 1 - 9 ? 不行,每条路径都要算自己的点
在我们的模型中,每条路径都完整计算其路径点,包括起点终点。
所以总和就是两条路径各自加一遍。
最优解其实是:
- 路径1: 1→2→5→8→9 → 25
- 路径2: 1→4→7→8→9 ❌ 冲突于8
不行
更好的:
- 路径1: 1→2→3→6→9 = 21
- 路径2: 1→4→5→8→9 = 23 → 冲突于5?
不行
真正最优可能是:
- 路径1: 1→2→3→6→9 = 21
- 路径2: 1→4→7→8→9 = 29 → 无中间交叉 → 只共享起点终点 → ✅
总和 = 21 + 29 = 50?不对,重复算了1和9
但按照题目要求,“路径上的单元格不可重复”,所以即使起点终点也不能重复。
所以这两条路径非法!
唯一的可能是:**题目不要求路径完全不重合,而是“边不重合”或“内部点不重合”**
否则 k>1 无解。
---
## ✅ 最终结论与建议
当前模型基于以下假设成立:
> 起点和终点可以被 k 条路径共享,其余点只能使用一次
在此前提下,上述 **费用流代码是正确的解决方案**
---
###