使用动态编程在矩阵中遍历的最大成本

时间:2015-09-30 21:26:32

标签: java algorithm dynamic-programming

假设我在Java中使用m x n矩阵。

我想找到从第一列到最后一列的最大遍历成本。每个值代表产生的成本。我可以在矩阵上向上,向下和向右行进。每个单元只能访问一次。允许从列的顶部单元格转换到相同的底部,反之亦然。

为简单起见,请考虑以下矩阵:

2 3 17
4 1 -1
5 0 14

如果我想找到最高费用,我的回答是46(2→5→4→1→3→0→14→17)。

我尝试使用动态方法使用以下递归关系解决此问题:

maxCost(of destination node) = max{ maxCost(at neighbouring node 1), maxCost(at neighbouring node 2), maxCost(at neighbouring node 3) } + cost(of destination node)

在这种情况下,它将类似于:

maxCost(17) = max{ maxCost(3), maxCost(-1), maxCost(14) } + 17;

因为,每个单元格只允许访问一次,我知道我需要维护一个相应的m x n isVisited矩阵。但是,我无法弄清楚如何维护isVisited矩阵。当计算maxCost(3)时,将修改矩阵;但是对于maxCost(-1)和maxCost(14),我会要求它的初始状态(会丢失)。

我的方法是否适用于此问题?另外,我无法弄清楚我的功能应该如何。 (这是我第一次尝试动态编程)。

5 个答案:

答案 0 :(得分:5)

这是一个很好的,有点棘手的问题。对于DP解决方案,我们必须以符合principle of optimality的方式对其进行说明。

这要求我们定义一个“状态”,以便可以根据一个n路决策来编写问题,这个决策将我们带到一个新的状态,而这个状态反过来又是同一个问题的一个新的,更小的版本。

状态的合适选择是遍历的当前位置加上有符号整数f,表示当前列中的未遍历(我称之为“空闲”)行的位置和数量。我们可以把它写成三元组[i,j,f]。

f的值告诉我们是否可以向上和/或向下移动。 (除非我们在正确的列中,否则总是可以向右移动,并且永远不可能向左移动。)如果f为负,则在当前位置“上方”有f个自由行,这可能包围到矩阵底部。如果是肯定的,则下面有f个免费行。请注意,f = m-1和f = 1-m意味着相同的事情:除了当前位置之外,所有行都是空闲的。为简单起见,我们将使用f == m-1来表示该情况。

单个整数f是我们描述自由空间所需要的全部因为我们只能以1的步长遍历,而我们永远不会向左移动。因此,同一列中不能有不连续的自由空间组。

现在DP“决定”是一个四方选择:

  1. 站在当前广场:仅在最后一栏有效。
  2. 向上移动:仅在上面有空闲空间时才有效。
  3. 下移:仅在下方有空位时才有效。
  4. 向右移动:除最后一栏外有效。
  5. 设,C(t)是DP中的最大代价函数,其中t是三元组[i,j,f]。然后我们可以实现的最大成本是在做出上面的最佳决策1到4之后,来自矩阵的成本A [i,j]加到其余遍历的成本上。最佳决策只是产生最高成本的决策!

    所有这些使得C成为所有元素都是有条件的集合的最大值。

    C[i,j,f] = max { A[i,j] if j==n-1, // the "stand pat" case
                   { A[i,j]+C[i,j+1,m-1] if j<n-1  // move right
                   { A[i,j]+C[i+1,j,f-1] if f>0    // move down
                   { A[i,j]+C[i-1,j,2-m] if f==m-1 // first move in col is up
                   { A[i,j]+C[i-1,j,f+1] if f<0    // other moves up
    

    有时单词比代数更清晰。 “失败”案件将是......

      

    从位置[i,j]到目标(右列)的一个潜在最大路径成本是矩阵值A [i,j]加上通过向下移动到位置[i + 1,j]可获得的最大成本。但是只有在那里有空闲空间(f> 0)时我们才能向下移动。向下移动后,其中少了一个(f-1)。

    这解释了递归表达式为C [i + 1,j,f-1]的原因。其他案例只是其中的变种。

    另请注意,上面隐含了“基本案例”。在f = 0和j = n-1的所有状态中,你都拥有它们。递归必须停止。

    要获得最终答案,您必须考虑所有有效起始位置的最大值,这是第一列元素,并且列中的所有其他元素都是空的:max C[i,0,m-1]表示i = 0..m- 1。

    由于您未能成功找到DP,因此这是一个表格构建代码,以显示它的工作原理。 DP中的依赖关系需要谨慎选择评估顺序。当然f参数可以是负数,并且行参数包装。我在调整f和i的2个函数中处理了这些。储存量为O(平方公尺):

    import java.util.Arrays;
    
    public class MaxPath {
      public static void main(String[] args) {
        int[][] a = {
          {2, 3, 17},
          {4, 1, -1},
          {5, 0, 14}
        };
        System.out.println(new Dp(a).cost());
      }
    }
    
    class Dp {
      final int[][] a, c;
      final int m, n;
    
      Dp(int[][] a) {
        this.a = a;
        this.m = a.length;
        this.n = a[0].length;
        this.c = new int[2 * m - 2][m];
      }
    
      int cost() {
        Arrays.fill(c[fx(m - 1)], 0);
        for (int j = n - 1; j >= 0; j--) {
          // f = 0
          for (int i = 0; i < m; i++) {
            c[fx(0)][i] = a[i][j] + c[fx(m - 1)][i];
          }
          for (int f = 1; f < m - 1; f++) {
            for (int i = 0; i < m; i++) {
              c[fx(-f)][i] = max(c[fx(0)][i], a[i][j] + c[fx(1 - f)][ix(i - 1)]);
              c[fx(+f)][i] = max(c[fx(0)][i], a[i][j] + c[fx(f - 1)][ix(i + 1)]);
            }
          }
          // f = m-1
          for (int i = 0; i < m; i++) {
            c[fx(m - 1)][i] = max(c[fx(0)][i], 
                a[i][j] + c[fx(m - 2)][ix(i + 1)], 
                a[i][j] + c[fx(2 - m)][ix(i - 1)]);
          }
          System.out.println("j=" + j + ": " + Arrays.deepToString(c));
        }
        return max(c[fx(m - 1)]);
      }
      // Functions to account for negative f and wrapping of i indices of c.
      int ix(int i) { return (i + m) % m; }
      int fx(int f) { return f + m - 2; }
      static int max(int ... x) { return Arrays.stream(x).max().getAsInt(); }
    }
    

    这是输出。如果您了解DP,则可以看到它构建从列j = 2到j = 0的最佳路径。矩阵由f = -1,0,1,2和i = 0,1,2索引。

    j=2: [[31, 16, 14], [17, -1, 14], [17, 13, 31], [31, 30, 31]]
    j=1: [[34, 35, 31], [34, 31, 31], [34, 32, 34], [35, 35, 35]]
    j=0: [[42, 41, 44], [37, 39, 40], [41, 44, 42], [46, 46, 46]]
    46
    

    结果显示(j = 0,列f = m-1 = 2)所有元素,如果第一列与起点一样好。

答案 1 :(得分:4)

这是一个艰难的。请注意,由于您的路径无法重复访问过的单元格,因此您可能的路径将具有类似蛇的行为,例如:

enter image description here

我们的想法是在f[j][i]中存储以单元格(j, i)结尾的最大路径长度。让我们说现在我们想要从f[j][i-1]过渡到f[j'][i]。然后,我们可以选择直接从单元格(j, i)转到单元格(j', i),也可以通过环绕顶部/边框边缘从单元格(j, i)转到单元格(j', i) 。因此,f[j][i]的更新可以计算为:

enter image description here

,其中

enter image description here

这里a是给定的数组。

现在的问题是如何有效地计算sum(a[j..j'][i],否则运行时将是O(m^3n)。您可以通过为tmp_sum使用临时变量sum(a[j..j'][i])来解决此问题,并在递增j时递增。{1}}。然后算法的runitme将是O(m^2 n)

以下是一个示例实现:

package stackoverflow;

public class Solver {

    int m, n;
    int[][] a, f;

    public Solver(int[][] a) {
        this.m = a.length;
        this.n = a[0].length;
        this.a = a;
    }

    void solve(int row) {
        f = new int[m][n];
        for (int i = 0; i < m; ++i)
            for (int j = 0; j < n; ++j)
                f[i][j] = Integer.MIN_VALUE;

        for (int i = 0; i < n; ++i) {
            int sum = 0;
            for (int j = 0; j < m; ++j)
                sum += a[j][i];
            for (int j1 = 0; j1 < m; ++j1) {
                int tmp_sum = 0;
                boolean first = true;
                for (int j2 = j1; j2 != j1 || first; j2 = (j2+1)%m) {
                    if (first)
                        first = false;
                    tmp_sum += a[j2][i];
                    int best_sum = Math.max(tmp_sum, sum - tmp_sum +a[j1][i]+a[j2][i]);
                    if (j1 == j2)
                        best_sum = a[j1][i];
                    int prev = 0;
                    if (i > 0)
                        prev = f[j1][i-1];
                    f[j2][i] = Math.max(f[j2][i], best_sum + prev);
                }
            }
        }

        System.out.println(f[row][n-1]);
    }

    public static void main(String[] args) {
        new Solver(new int[][]{{2, 3, 17}, {4, 1, -1}, {5, 0, 14}}).solve(0); //46
        new Solver(new int[][]{{1, 1}, {-1, -1}}).solve(0); //2
    }
}

答案 2 :(得分:2)

感谢大家的贡献。

我使用recursive使用system stack技术提出了一个解决方案。我认为我的解决方案相对容易理解。

这是我的代码:

import java.util.Scanner;

public class MatrixTraversal {

    static int[][] cost;
    static int m, n, maxCost = 0;

    public static void solve(int currRow, int currCol, int[][] isVisited, int currCost) {

        int upperRow, lowerRow, rightCol;
        isVisited[currRow][currCol] = 1;

        currCost += cost[currRow][currCol];             //total cost upto current position

        if( currCol == (n - 1)                          //if we have reached the last column in matrix
            && maxCost < currCost )                     //and present cost is greater than previous maximum cost
            maxCost = currCost;

        upperRow = ((currRow - 1) + m) % m;             //upper row value taking care of teleportation
        lowerRow = (currRow + 1) % m;                   //lower row value taking care of teleportation
        rightCol = currCol + 1;                         //right column value

        if( isVisited[upperRow][currCol] == 0 )     //if upper cell has not been visited
            solve(upperRow, currCol, isVisited, currCost);

        if( isVisited[lowerRow][currCol] == 0 )     //if lower cell has not been visited
            solve(lowerRow, currCol, isVisited, currCost);

        if( rightCol != n &&                            //if we are not at the last column of the matrix
            isVisited[currRow][rightCol] == 0 )     //and the right cell has not been visited
            solve(currRow, rightCol, isVisited, currCost);

        isVisited[currRow][currCol] = 0;

    }

    public static void main(String[] args) {

        int[][] isVisited;
        int i, j;

        Scanner sc = new Scanner(System.in);

        System.out.print("Enter the no.of rows(m): ");
        m = sc.nextInt();

        System.out.print("Enter the no.of columns(n): ");
        n = sc.nextInt();

        cost = new int[m][n];
        isVisited = new int[m][n];

        System.out.println("Enter the cost matrix:");
        for(i = 0; i < m; i++)
            for(j = 0; j < n; j++)
                cost[i][j] = sc.nextInt();              //generating the cost matrix

        for(i = 0; i < m; i++)
            solve(i, 0, isVisited, 0);                  //finding maximum traversal cost starting from each cell in 1st column 

        System.out.println(maxCost);

    }

}

但是,我不确定这是否是计算解决方案的最佳和最快方式。

请告诉我您的看法。我会相应地接受这个答案。

答案 3 :(得分:1)

一种可能的优化是,我们只需要为具有负数的列或长度小于m的非负列的序列计算不同的选项(除了完整的总和),由具有负数的列包围。我们需要一列和一个(概念)矩阵来计算这类列的序列的最大值;当前列的矩阵,可转换为每个出口点的最大列数。每个矩阵表示y处的输入和y'处的退出的最大总和以及恰好位于入口点之前的先前最大值(每个都有两种可能性,具体取决于路径方向)。矩阵沿对角线(意为sum entry...exit = sum exit...entry)对称反射,直到添加每个入口点的各个先前最大值。

在示例中添加一个带负数的附加列,我们可以看到如何应用累积总和:

2  3  17  -3
4  1  -1  15
5  0  14  -2

(我们暂时忽略前两个非负列,稍后再添加15个。)

Third column:

 y' 0  1  2
y 
0   17 30 31
1   30 -1 30
2   31 30 14

对于第四列矩阵,每个入口点需要与前一列中相同出口点的最大值组合。例如,输入点0添加了max(17,30,31)

 y' 0  1  2
y 
0   -3 12 10  + max(17,30,31)
1   12 15 13  + max(30,-1,30)
2   10 13 -2  + max(31,30,14)

       =

    28 43 41
    42 45 43
    41 44 29

我们可以看到最终的最大值有(进入,退出)(1,1)和解决方案:

15 + (0,1) or (2,1) + (1,1)

答案 4 :(得分:1)

让我们看看这里的动态编程答案与答案中的蛮力方法有什么不同,以及我们如何调整你的答案。举个简单的例子,

a = {{17, -3}
    ,{-1, 15}}

蛮力将遍历并比较所有路径:

17,-3
17,-3,15
17,-1,15
17,-1,15,-3

-1,15
-1,15,-3
-1,17,-3
-1,17,-3,15

动态编程解决方案利用了列之间的选择点,因为只有一种可能性 - 向右移动。在列之间的每次移动中,动态编程解决方案使用max函数应用修剪方法,该方法将搜索限制为已证实的成本高于其他路径的路径。

Gene提供的递归解决方案中的上下选择导致在svs解决方案的循环中发现类似的遍历,这意味着将修剪同一列中的进入和退出之间的选择。再看看我们的例子:

a = {{17, -3}
    ,{-1, 15}}

f(-1) -> max(15,15 - 3)
      -> 17 -> max(-3,-3 + 15) 

f(17) -> max(-3,-3 + 15)
      -> -1 -> max(15,15 - 3) 

无需检查完整路径总和-1,15,-3或同时检查17 - 1 + 1517 - 1 + 15 - 3,因为在每种情况下我们都知道哪个结尾会更大,这要归功于{{ 1}}功能:max

矩阵阵列解决方案与递归略有不同,但效果相似。我们只关注列{,17 - 1 + 15之间的移动,这只能在一个地方发生,我们选择仅将添加到目前为止最好的总和为j to j + 1计算j。看一下这个例子:

j + 1

a = {{17, -3} ,{-1, 15}} 时间内计算j = 0列出口点的最佳总和矩阵:

O(m^2)

现在,对于17 16 ,我们只计算沿着列j = 1的列j = 1可实现的最佳路径,并且在列j = 1上有退出点,记住要将这些路径的入口点添加到以前的最佳位置(意思是从列到左边的数字,用*表示:

best exit at -3 = max(-3 + 17*, 15 - 3 + 16*) = 28
best exit at 15 = max(15 + 16*, -3 + 15 + 17*) = 31

现在要调整你的版本,考虑如何改变它,以便递归在每一步中选择从后续调用中返回的最大总和。