📌  相关文章
📜  两个人从矩阵的左上角到右下角的最大点数(1)

📅  最后修改于: 2023-12-03 15:06:14.084000             🧑  作者: Mango

两个人从矩阵的左上角到右下角的最大点数

介绍

给定一个$m*n$的矩阵,矩阵中的每个元素代表当前位置的点数。两个人分别从左上角出发,同时向右下角移动,并且只能向右或向下移动。两人所经过的所有点数之和即为这两人的总点数,在每个时刻两人必须位于同一个位置,且不能经过同一个点超过一次。请编写一个函数,求出两人的总点数之和的最大值,并返回路径。

举个例子,例如下方的矩阵:

1 2 3
4 5 6
7 8 9

其中,如果两人的路径分别为:(0,0) -> (1,1) -> (2,2)(0,0) -> (1,0) -> (2,1) -> (2,2),则两人的总点数之和为 $26$。

这个问题可以通过动态规划的方法来解决,本质上是一个类似于背包问题的应用。

算法

具体而言,假设矩阵为$A$,我们定义一个三维数组$dp[i][j][k]$,其中$i$表示第一个人所在位置,$j$表示第二个人所在位置,$k$表示两个人总共走过的点数,即$dp[i][j][k]$表示两个人分别从$(0,0)$出发,走了$k$步,第一个人所在位置为$i$,第二个人所在位置为$j$的总点数之和的最大值。

那么我们可以考虑状态转移方程:

$$ dp[i][j][k] = \max\left{ \begin{aligned} & dp[i-1][j-1][k-1] + A_{i,j} &\quad i, j, k > 0 ; \text{且} ; i \neq j \neq k \ & dp[i-1][j][k-1] + A_{i,j} &\quad j, k > 0 ; \text{且} ; i = j \neq k \ & dp[i][j-1][k-1] + A_{i,j} &\quad i, k > 0 ; \text{且} ; i \neq j = k \ & dp[i-1][j][k] + A_{i,j} &\quad j, k > 0 ; \text{且} ; i = k < j \ & dp[i][j-1][k] + A_{i,j} &\quad i, k > 0 ; \text{且} ; i < j = k \ & -\infty &\quad \text{otherwise} \ \end{aligned} \right. $$

其中对于前四个情况,我们分别讨论两个人分别走了一步以及两个人分别都走了一步同一个点的情况,对于后两种情况则分别讨论其中一个人没有走过下一个点的情况。

最终的答案即为$dp[m-1][n-1][2(m+n)-2]$,因为两个人最多走$m+n-2$步,每步至少可以得到一个点数,因此两个人最多可以得到$2(m+n)-2$个点数。

参考代码

以下是Python实现代码的片段,返回值为最大点数之和以及两个人的路径:

def two_persons_max_points(matrix) -> Tuple[int, List[Tuple[int, int]]]:
    m, n = len(matrix), len(matrix[0])    
    dp = [[[-1e9] * (2 * (m + n) - 2) for _ in range(n)] for _ in range(m)]
    path = [[[[0, 0] for _ in range(2 * (m + n) - 2)] for _ in range(n)] for _ in range(m)] 
    dp[0][0][0] = matrix[0][0]
    for k in range(1, 2 * (m + n) - 2):
        for i in range(max(0, k-n+1), min(m, k+1)):
            for j in range(max(0, k-n+1), min(m, k+1)):
                if i == j == k == 0: continue
                if i == j == k != 0: continue
                if i == k and j == k: continue
                if i == j: # 仅有一个人走
                    if dp[i-1][j][k-1] + matrix[i][j] > dp[i][j][k]:
                        dp[i][j][k] = dp[i-1][j][k-1] + matrix[i][j]
                        path[i][j][k] = [i-1, j, k-1]
                    if dp[i][j-1][k-1] + matrix[i][j] > dp[i][j][k]:
                        dp[i][j][k] = dp[i][j-1][k-1] + matrix[i][j]
                        path[i][j][k] = [i, j-1, k-1]
                elif j == k: # 只有j走
                    if dp[i-1][j][k] + matrix[i][j] > dp[i][j][k]:
                        dp[i][j][k] = dp[i-1][j][k] + matrix[i][j]
                        path[i][j][k] = [i-1, j, k]
                    if dp[i][j-1][k] + matrix[i][j] > dp[i][j][k]:
                        dp[i][j][k] = dp[i][j-1][k] + matrix[i][j]
                        path[i][j][k] = [i, j-1, k]
                elif i == k: # 只有i走
                    if dp[i-1][j][k] + matrix[i][j] > dp[i][j][k]:
                        dp[i][j][k] = dp[i-1][j][k] + matrix[i][j]
                        path[i][j][k] = [i-1, j, k]
                    if dp[i][j-1][k] + matrix[i][j] > dp[i][j][k]:
                        dp[i][j][k] = dp[i][j-1][k] + matrix[i][j]
                        path[i][j][k] = [i, j-1, k]
                elif i < j: # j先走
                    if dp[i][j-1][k-1] + matrix[i][j] > dp[i][j][k]:
                        dp[i][j][k] = dp[i][j-1][k-1] + matrix[i][j]
                        path[i][j][k] = [i, j-1, k-1]
                    if dp[i-1][j][k] + matrix[i][j] > dp[i][j][k]:
                        dp[i][j][k] = dp[i-1][j][k] + matrix[i][j]
                        path[i][j][k] = [i-1, j, k]
                else: # i先走
                    if dp[i-1][j][k-1] + matrix[i][j] > dp[i][j][k]:
                        dp[i][j][k] = dp[i-1][j][k-1] + matrix[i][j]
                        path[i][j][k] = [i-1, j, k-1]
                    if dp[i][j-1][k] + matrix[i][j] > dp[i][j][k]:
                        dp[i][j][k] = dp[i][j-1][k] + matrix[i][j]
                        path[i][j][k] = [i, j-1, k]
    points = dp[-1][-1][-1]
    p1, p2 = [(m-1, n-1)], [(m-1, n-1)]
    i, j, k = m-1, n-1, 2 * (m + n) - 3
    while i + j > 0:
        qi, qj, qk = path[i][j][k]
        if (i == qi and j == qj + 1) or (i == qi + 1 and j == qj): # 向右或者向下走
            if k > 0: # 不是第一步
                if i > qi and j > qj: # 两人处于不同位置
                    if i != k and j != k and qi != k and qj != k:
                        p1.append((qi, qj))
                        p2.append((i, j))
                else: # 同一位置,只用加一次
                    if k not in [i, j]: 
                        p1.append((qi, qj))
                        p2.append((i, j))
            k -= 1 # 步数减一
        i, j = qi, qj # 前进到上一个位置
    assert p1[-1] == (0, 0) and p2[-1] == (0, 0)
    return points, list(reversed(p1)) + p2[1:]

其中我们使用了Python的多维列表来表示三维数组$dp$和路径$pat$,算法时间复杂度为$O(m^3)$,其中$m = \max{m, n}$。

总结

这个问题是一个比较有趣的动态规划应用,和许多其他动态规划问题一样,最关键也是最难的地方在于如何定义状态和状态转移方程。状态定义应该清晰、直观,并且不应该包括无效状态,转移方程则应该覆盖所有合法情况。

此外,在动态规划的过程中我们还需要记录路径,本例中的方案比较简单:我们能够通过转移方程求出当前状态的最优方案(即当前位置的最优得分来自于哪一个位置),然后将对应的位置加入路径即可。具体实现中需要注意边界情况和列表下标的范围。

最后,这个问题给出的时间复杂度是$O(m^3)$,其中$m = \max{m, n}$,但是我们可能有更优化的算法,例如可以利用滚动数组来减少空间复杂度,并且可以使用类似于图搜索的算法来避免枚举所有状态,如果读者有这方面的思路,欢迎留言讨论。