<think>我们正在讨论“传纸条”问题的动态规划解法,该问题通常涉及在一个网格中从左上角到右下角传递纸条,要求找出两条不相交的路径使得获得的数字总和最大。根据引用[1]和[2],我们使用四维动态规划数组dp[x1][y1][x2][y2],其中(x1,y1)表示第一条路径的位置,(x2,y2)表示第二条路径的位置,并且由于两条路径是同步进行的(即步数相同),所以有x1+y1=x2+y2。这样可以减少状态数量。
在引用[2]中提到了状态转移的四种情况(两个纸条各有两种移动方向:上或左,组合起来四种情况),并给出了状态转移方程的核心部分。
现在,我们需要用C++实现这个传纸条问题的动态规划解法。
步骤:
1. 定义网格的大小(m行n列)和网格的值(假设用一个二维数组grid存储)。
2. 定义四维dp数组,维度为[m][n][m][n](注意:实际实现中可能因为空间太大而需要优化,但这里先按四维实现)。
3. 初始化:dp[0][0][0][0] = grid[0][0](因为两条路径都从(0,0)开始,但注意实际上两条路径不能重合,所以除了起点外,其他点不能重合。而起点是同一个点,我们这里先赋值为起点的值,然后在后续状态转移中注意跳过重合点?但题目要求路径不相交,所以我们需要避免在两个纸条走到同一个格子(除了起点和终点?实际上题目要求不相交,也就是说不能经过同一个格子,包括起点和终点?这里需要澄清)。通常,题目允许起点和终点重合,但中间路径不能相交。然而,在本题中,两条路径都是从(0,0)到(m-1,n-1),所以起点和终点都是同一个点。为了避免重复计算,我们可以在状态转移中排除走到同一个格子的情况(除了起点和终点)。但实际上,引用[1]中提到了两个人同步,所以步数相同,即x1+y1=x2+y2,因此我们可以用k=x1+y1=x2+y2作为步数,然后遍历x1,x2,则y1=k-x1, y2=k-x2。
为了避免四维数组空间过大(当m,n较大时,四维数组可能达到50^4=6250000,这是可以接受的,如果m,n<=50),但我们可以使用滚动数组优化。不过,我们先实现基础版本。
4. 状态转移方程:
对于每个步数k(从0到m+n-2),遍历x1(从0到min(k, m-1)),那么y1=k-x1(要求y1在[0,n-1]内),同样遍历x2(从0到min(k, m-1)),那么y2=k-x2(同样要求y2在[0,n-1]内)。
如果两条路径走到同一个格子(即x1==x2且y1==y2),那么我们不能重复计算该格子的数值(除了起点和终点?)。但是,题目要求路径不能相交,所以除了起点和终点,中间不能走到同一个格子。所以,在状态转移时,我们需要注意:
- 当k=0时,起点,我们直接赋值grid[0][0](两条路径都在这里,但是题目允许起点,所以这里我们算一次)。
- 当k>0且k<m+n-2(即中间过程)时,如果两条路径走到同一个格子,那么这个状态是非法的,我们不应该考虑(或者跳过,或者将其设为负无穷表示非法)。
- 当k=m+n-2(终点)时,两条路径都到达终点,所以终点格子只能算一次?但实际上,题目要求两条路径,那么终点格子会被两条路径都走,所以应该算两次?但是题目要求的是纸条传递,同一个格子上的数字只能被一个纸条获取。所以,两条路径除了起点和终点外,不能有交点。因此,在中间过程中,如果两条路径走到同一个格子,那么该状态无效。但是终点呢?终点我们允许两条路径都到达,但由于终点只有一个数字,所以实际上只能被计算一次。然而,在两条路径的终点状态,我们只能加一次终点的值。但是,我们两条路径从不同的方向到达终点,那么实际上我们需要避免重复加?所以,我们可以在终点状态时,如果两条路径到达同一个点,则只加一次。但是,起点也是同一个点,我们也是算了一次。
因此,我们可以在状态转移中这样处理:当(x1,y1)和(x2,y2)是同一个点时,该状态的值为grid[x1][y1]加上转移前的值(因为起点我们就是这么做的),而在中间过程中,我们不应该让两条路径走到同一个点(因为起点之后就不能再走到同一个点了)。所以,我们需要在状态转移中避免两条路径走到同一个点(除了起点)。
然而,在起点之后的每一步,我们都要保证两条路径不会走到同一个点。所以,在状态转移时,如果两条路径在同一个点(除了起点),我们就不更新这个状态(或者设为非法状态)。
但是,注意起点我们已经在初始化时设置了,那么对于k>=1的状态,我们遍历到的状态如果x1==x2且y1==y2,那么我们就跳过这个状态(即不进行状态转移,或者将该状态的值设为一个很小的数,这样不会影响最大值)。
5. 状态转移方程:
对于当前状态dp[x1][y1][x2][y2](其中k=x1+y1=x2+y2),它可以由以下四个状态转移而来:
a. 第一条路径从上方来,第二条路径从上方来:dp[x1-1][y1][x2-1][y2]
b. 第一条路径从上方来,第二条路径从左方来:dp[x1-1][y1][x2][y2-1]
c. 第一条路径从左方来,第二条路径从上方来:dp[x1][y1-1][x2-1][y2]
d. 第一条路径从左方来,第二条路径从左方来:dp[x1][y1-1][x2][y2-1]
然后,我们在这四个状态中取最大值,加上当前两个格子的数值。但是,要注意:
- 如果两个格子是同一个(即(x1,y1)==(x2,y2)),那么只能加一次grid[x1][y1](因为同一个格子的数字只能被取一次)。
- 否则,加上grid[x1][y1] + grid[x2][y2]。
因此,状态转移方程可以写为:
if (x1 == x2 && y1 == y2)
dp[x1][y1][x2][y2] = max(tmp1, tmp2, tmp3, tmp4) + grid[x1][y1];
else
dp[x1][y1][x2][y2] = max(tmp1, tmp2, tmp3, tmp4) + grid[x1][y1] + grid[x2][y2];
但是,注意:在起点(0,0)处,我们已经初始化了,所以不需要转移。在k>=1时,我们进行转移。另外,在遍历时,要确保x1,y1,x2,y2在网格范围内。
6. 边界条件:在k=0时,只有dp[0][0][0][0]是有效的,值为grid[0][0](因为起点重合)。
7. 最终结果:dp[m-1][n-1][m-1][n-1],表示两条路径都到达终点的最大值(注意终点重合,所以我们也只能加一次终点的值)。
但是,注意:题目要求两条路径不能相交,我们的状态转移中通过跳过中间重合点(即k>=1时,如果两条路径重合则跳过)来避免。但是,我们在状态转移方程中已经处理了重合的情况(即当重合时只加一次值),这样会不会导致中间出现重合点?实际上,我们并没有跳过中间重合点的状态,而是直接计算了重合点的状态(但在重合点时只加一次值)。但是题目要求不能相交,所以重合点是不允许出现的(除了起点和终点)。因此,我们需要确保在中间过程中两条路径不能走到同一个点。所以,在状态转移时,如果k>=1且k<=m+n-3(即中间过程)时,我们不允许两条路径重合(即跳过重合状态)。但是,在起点(k=0)和终点(k=m+n-2)我们允许重合。
所以,在循环中,我们可以这样:
for k from 1 to m+n-2:
for x1 from max(0,k-n+1) to min(k, m-1):
y1 = k - x1;
for x2 from max(0,k-n+1) to min(k, m-1):
y2 = k - x2;
if (x1==x2 && y1==y2) {
// 如果是中间过程,跳过(因为起点已经初始化了,所以在k>=1时,重合点都是不允许的,除了终点?)
// 但终点是k=m+n-2,所以这里需要区分终点和中间过程。
// 实际上,当k=m+n-2时,只有终点一个状态,即x1=x2=m-1, y1=y2=n-1,这个点我们允许重合。
// 所以,在中间过程中(k>=1且k<=m+n-3),遇到重合点我们就跳过。
// 如果是终点,我们允许重合,并且只加一次值。
if (k != m+n-2)
continue;
}
// 否则,进行状态转移
但是,这样处理会使得在终点时,我们只加一次值。但是,在起点我们已经在初始化中处理了。
然而,还有一种方法是:在状态转移过程中,我们允许重合点,但是在计算时,重合点只加一次值。然后,在中间过程中,如果两条路径重合了,那么这个状态的值可能是非法的(因为题目不允许中间重合),所以我们需要避免使用这个状态去更新后续状态?但是,我们的状态转移方程已经包括了这些状态,所以我们需要在状态转移时禁止中间重合点状态被更新(即不更新,或者设为负无穷,这样不会影响最大值)。因此,我们可以这样:
if (x1==x2 && y1==y2) {
if (k !=0 && k != m+n-2) {
// 中间过程重合,非法,跳过(即不更新)
// 或者我们可以设置dp[x1][y1][x2][y2]为一个很小的数,但这样可能会被后面的状态转移用到,所以最好不更新(保留初始值,初始值可以设为负无穷)
continue;
}
}
所以,我们可以在初始化dp数组时,全部初始化为一个很小的数(比如-1e9),然后起点dp[0][0][0][0]=grid[0][0]。然后,在循环中,如果遇到中间过程重合,则跳过(不进行状态转移,这样该状态的值还是负无穷,就不会影响后续状态)。
8. 代码实现步骤:
- 定义m,n
- 定义网格grid
- 定义dp数组:vector<vector<vector<vector<int>>>> dp(m, vector<vector<vector<int>>>(n, vector<vector<int>>(m, vector<int>(n, -1e9))));
- 初始化:dp[0][0][0][0] = grid[0][0];
- 枚举k从1到m+n-2(因为k=0已经初始化)
for x1 in [0, min(k, m-1)]:
y1 = k - x1;
if (y1>=n) continue; // 或者x1的范围控制好:x1从max(0, k-n+1)到min(k, m-1)
for x2 in [0, min(k, m-1)]:
y2 = k - x2;
if (y2>=n) continue; // 同上,控制范围避免越界
// 检查是否中间重合点(除了起点和终点)
if (x1==x2 && y1==y2 && k!=m+n-2) {
// 中间重合点,跳过
continue;
}
// 状态转移:四种前驱状态
int val = 0;
// 如果当前两个点重合,那么只加一次grid[x1][y1](这个重合只可能发生在终点,因为中间重合点我们已经跳过了)
if (x1==x2 && y1==y2) {
val = grid[x1][y1];
} else {
val = grid[x1][y1] + grid[x2][y2];
}
int tmp = -1e9;
// 情况1:第一条路径从上方来(x1-1>=0),第二条路径从上方来(x2-1>=0)
if (x1-1>=0 && x2-1>=0) {
tmp = max(tmp, dp[x1-1][y1][x2-1][y2]);
}
// 情况2:第一条路径从上方来(x1-1>=0),第二条路径从左方来(y2-1>=0)
if (x1-1>=0 && y2-1>=0) {
tmp = max(tmp, dp[x1-1][y1][x2][y2-1]);
}
// 情况3:第一条路径从左方来(y1-1>=0),第二条路径从上方来(x2-1>=0)
if (y1-1>=0 && x2-1>=0) {
tmp = max(tmp, dp[x1][y1-1][x2-1][y2]);
}
// 情况4:第一条路径从左方来(y1-1>=0),第二条路径从左方来(y2-1>=0)
if (y1-1>=0 && y2-1>=0) {
tmp = max(tmp, dp[x1][y1-1][x2][y2-1]);
}
if (tmp != -1e9) {
dp[x1][y1][x2][y2] = max(dp[x1][y1][x2][y2], tmp + val); // 这里可能dp[x1][y1][x2][y2]已经初始化过(比如从另一种方式到达),所以取max?但是实际上每个状态只被计算一次?不,因为可能有多种方式到达同一个状态(同一个x1,y1,x2,y2),所以我们需要取最大值。由于我们初始化为负无穷,所以第一次遇到就会更新,后面再遇到就取最大值。
}
// 如果tmp还是-1e9,说明没有前驱状态,那么当前状态不可达,我们就保持负无穷
- 最终结果:dp[m-1][n-1][m-1][n-1](注意终点重合,但我们在状态转移中已经处理了,终点重合只加一次grid[m-1][n-1])
但是,这里有一个问题:两条路径的行走顺序?实际上,我们枚举的k是步数,每一步两条路径都走一步,所以是同步的。
9. 空间优化:四维数组空间复杂度为O(m*n*m*n),在m,n=50时,50^4=6250000,这是可以接受的(在C++中大约6250000个int,25MB左右)。但如果m,n更大,就需要优化。我们可以用滚动数组,因为k只依赖于k-1,所以我们可以用两个三维数组(去掉一维,因为k=x1+y1,所以用三维:dp[k][x1][x2])来保存状态。但这里我们先实现四维。
代码实现(注意:这里假设网格grid已经存储了数值,且非负,所以不可达状态用负无穷不会影响最大值)
注意:在状态转移时,我们可能会遇到同一个状态被多次更新(因为四种前驱状态),所以我们要取最大值。但因为我们初始化为负无穷,所以每次遇到更大的值就更新。
下面给出C++代码:
注意:我们假设网格grid的大小为m行n列。
</think>以下是使用C++实现"传纸条"问题(动态规划解法)的完整代码和详细说明。该问题要求在一个网格中找到两条从左上角(0,0)到右下角(m-1,n-1)的路径,使路径上的数字和最大,且两条路径不能相交(起点和终点除外)。
```cpp
#include <iostream>
#include <vector>
#include <algorithm>
#include <climits>
using namespace std;
int main() {
// 网格尺寸和初始化(示例数据)
int m = 3, n = 3;
vector<vector<int>> grid = {
{0, 3, 9},
{2, 8, 5},
{5, 7, 0}
};
// 四维DP数组初始化
vector<vector<vector<vector<int>>>> dp(
m, vector<vector<vector<int>>>(
n, vector<vector<int>>(
m, vector<int>(n, INT_MIN)
)
)
);
// 起点初始化
dp[0][0][0][0] = grid[0][0];
// 动态规划:k为步数(x+y)
for (int k = 1; k <= m + n - 2; ++k) {
for (int x1 = max(0, k - n + 1); x1 <= min(k, m - 1); ++x1) {
int y1 = k - x1;
if (y1 >= n) continue;
for (int x2 = max(0, k - n + 1); x2 <= min(k, m - 1); ++x2) {
int y2 = k - x2;
if (y2 >= n) continue;
// 跳过中间重合点(除终点外)
if (x1 == x2 && y1 == y2 && k != m + n - 2) continue;
// 计算当前格子价值
int current_val = (x1 == x2 && y1 == y2)
? grid[x1][y1]
: grid[x1][y1] + grid[x2][y2];
// 四种状态转移
int max_prev = INT_MIN;
if (x1 > 0 && x2 > 0)
max_prev = max(max_prev, dp[x1-1][y1][x2-1][y2]); // 上+上
if (x1 > 0 && y2 > 0)
max_prev = max(max_prev, dp[x1-1][y1][x2][y2-1]); // 上+左
if (y1 > 0 && x2 > 0)
max_prev = max(max_prev, dp[x1][y1-1][x2-1][y2]); // 左+上
if (y1 > 0 && y2 > 0)
max_prev = max(max_prev, dp[x1][y1-1][x2][y2-1]); // 左+左
// 更新当前状态
if (max_prev != INT_MIN) {
dp[x1][y1][x2][y2] = max(dp[x1][y1][x2][y2], max_prev + current_val);
}
}
}
}
// 输出结果
cout << "最大价值: " << dp[m-1][n-1][m-1][n-1] << endl;
return 0;
}
```
### 关键实现细节
1. **状态定义**
使用四维DP数组$dp[x1][y1][x2][y2]$,表示两条路径分别到达$(x1,y1)$和$(x2,y2)$时的最大价值[^1]。
2. **同步移动优化**
利用$k = x1 + y1 = x2 + y2$确保两条路径同步移动,减少无效状态[^1]。
3. **重合点处理**
- 起点($k=0$): 强制重合,值为$grid[0][0]$
- 中间点($1 \leq k \leq m+n-3$): 禁止重合(`continue`跳过)
- 终点($k=m+n-2$): 允许重合,值取$grid[m-1][n-1]$[^2]
4. **状态转移方程**
四种前驱状态取最大值:
$$
\small
dp[x1][y1][x2][y2] = \max\begin{cases}
dp[x1-1][y1][x2-1][y2] \\
dp[x1-1][y1][x2][y2-1] \\
dp[x1][y1-1][x2-1][y2] \\
dp[x1][y1-1][x2][y2-1]
\end{cases} +
\begin{cases}
grid[x1][y1] & \text{重合时} \\
grid[x1][y1] + grid[x2][y2] & \text{不重合时}
\end{cases}
$$
### 复杂度分析
- **时间复杂度**:$O(m^2n^2)$
四重循环($k$、$x1$、$x2$各$O(\max(m,n))$,总$O(m^2n^2)$)
- **空间复杂度**:$O(m^2n^2)$
四维DP数组开销
### 优化方向
1. **滚动数组**
利用$k$只依赖$k-1$的特性,可将空间降至$O(m^2)$[^2]
2. **对称性剪枝**
利用路径对称性$x1 \leq x2$减少一半状态
3. **记忆化搜索**
避免无效状态计算,提高实际效率