您的位置:首页 > 其它

动态规划应用之一

2016-04-06 08:19 267 查看
笔者在前不久做了腾讯的在线笔试题,面试的移动客户端开发实习生岗位,其中有这样的一个题目:

有一个M行N列的矩阵,其中部分格子里面有一些有价值的物品。现在你从左上角出发,每次只能向右或者向下走,走到右下角的时候,你能获取的物品的总价值最大有多少?

输入数据:第一行有两个数字M N,表示这个矩阵有M行N列。然后从第二行开始,有M行整数,每行都有N个非负整数,表示这一格的物品的价值

输出数据:可以获取的最大的物品总价值

数据范围:0<M, N<=1000,矩阵中的数字不会超过1000

示例

输入:

4 5

0 0 8 0 0

0 0 0 9 0

0 7 0 0 0

0 0 6 0 0

输出:

17

看到这个题,最初的想法是,找出矩阵的最大值,然后以这个值为界将矩阵分成两个矩阵(左上矩阵:这个最大值为左上矩阵的右下角元素;右下矩阵:这个最大值为右下矩阵的左上角元素)。后来一细想,这样做是有问题的,因为这样就直接抛弃了左下和右上的矩阵,就丢失了很多信息。后来想到用动态规划的方法去解决就清晰多了。

明确一下,矩阵有M行和N列,因为数组序号是从0开始的,因此这里行号和列号我也从0开始,那么最后一行就是(M-1)行,最后一列就是(N-1)列。

动态规划有两大性质:最优子结构和重叠子问题。利用动态规划的思想对此问题进行简单的分析。

假设整体最优解路线必定经过格子[x][y](x,y在矩阵范围内),则整体的最优解必定是([0][0]->[x][y])和([x][y]->[M-1][N-1])这两个矩阵最优解的和,这就是最优子结构;如果基于这个思想去编程实现的话,我们需要对(x,y)在整个矩阵范围内进行扫描,并找到整体最优解的最大值。然而,这样实现就会显现出重叠子问题重复求解的问题。比如,对于([0][0]->[x][y]),他的最优解必定是【([0][0]->[x-1][y])的最优解加上(x,y)上的价值】与【([0][0]->[x][y-1])的最优解加上(x,y)上的价值】的较大者,而([0][0]->[x-1][y])的最优解或者([0][0]->[x][y-1])的最优解,我们可能会多次重复求解。

另一种思考方式是,

我们从某一端开始考虑,比如从[0][0]开始,他的下一步必定是[0][1]或者[1][0]之一的格子,因此,那么问题就变成了([0][0]->[0][1])和([0][1]->[M-1][N-1])的最优解或者([0][0]->[1][0])和([1][0]->[M-1][N-1])的最优解的较大者,那么这个和就是整体的最优解。然后对所有的格子进行这样的考虑。在实现过程中,我们并不知道后面那一部分的最优解,只是计算前面那一部分的最优解,这个计算是一步一步扩大的。需要明确的是,第一行的元素只能是从[0][0]一直往右走,而第一列的元素只能是从[0][0]往下走。



如图所示,matrix矩阵是输入的价值矩阵,result, x, y矩阵是计算过程中定义的矩阵。result[i][j]保存从[0][0]到[i][j]的最优解,x[i][j]和y[i][j]分别保存着(i, j)的最优下一步的行列标号。结合上图和下面的代码。

上面的介绍是从[0][0]开始考虑的,下面的代码实际是从[M-1][N-1]开始考虑的,具体如下:

package com.wl.test;

import java.util.Scanner;

public class Main {

public static void main(String[] args) {

Scanner scanner = new Scanner(System.in);

int max;

String line = "";
String[] parts;

int lineCount = 0, rowCount = 0;
int[][] matrix, result;
int[][] x, y;
int i, j;

while(scanner.hasNextLine()) {

line = scanner.nextLine();
parts = line.split(" ");
rowCount = Integer.parseInt(parts[0]);
lineCount = Integer.parseInt(parts[1]);

matrix = new int[rowCount][lineCount];
result = new int[rowCount][lineCount];
x = new int[rowCount][lineCount];
y = new int[rowCount][lineCount];

for(i=0; i<rowCount; i++) {
line = scanner.nextLine();
parts = line.split(" ");
for(j=0; j<lineCount; j++) {
matrix[i][j] = Integer.parseInt(parts[j]);
}
}

result[rowCount-1][lineCount-1] = 0;
for(i=rowCount-2; i>=0; i--) {
result[i][lineCount-1] = matrix[i][lineCount-1] + result[i+1][lineCount-1];
x[i][lineCount-1] = i+1;
y[i][lineCount-1] = lineCount-1;
}
for(j=lineCount-2; j>=0; j--) {
result[rowCount-1][j] = matrix[rowCount-1][j] + result[rowCount-1][j+1];
x[rowCount-1][j] = rowCount-1;
y[rowCount-1][j] = j+1;
}

for(i=rowCount-2; i>=0; i--) {
for(j=lineCount-2; j>=0; j--) {
if(result[i+1][j] > result[i][j+1]) {
max = result[i+1][j];
x[i][j] = i+1;
y[i][j] = j;
} else {
max = result[i][j+1];
x[i][j] = i;
y[i][j] = j+1;
}
result[i][j] = matrix[i][j] + max;
}
}

x[rowCount-1][lineCount-1] = rowCount-1;
y[rowCount-1][lineCount-1] = lineCount-1;

System.out.println(result[0][0]);
printArray(result, rowCount, lineCount);
printArray(x, rowCount, lineCount);
printArray(y, rowCount, lineCount);
printRoute(x, y, rowCount, lineCount);
}

scanner.close();
}

private static void printArray(int[][] array, int m, int n) {
for(int i=0; i<m; i++) {
for(int j=0; j<n; j++) {
System.out.print(array[i][j] + "\t");
}
System.out.println();
}
System.out.println();
}

private static void printRoute(int[][] x, int[][] y, int rowCount, int lineCount) {
for(int i=0, j=0, m=0, n=0; i!=(rowCount-1)||j!=(lineCount-1); i=x[m]
, j=y[m]
) {
m = i;
n = j;
System.out.print("[" + m + "," + n + "]->");
}
System.out.println("[" + (rowCount-1) + "," + (lineCount-1) + "]");
}
}

上面代码中,第一个for循环用于处理输入,紧接着的两个for循环用于处理最有一行和最后一列的result值,因为那些格子上只能往一个方向走,向右或者向下,没有其他的方向。接下来的嵌套for循环用于扫描所有的格子,直到第一个为止。x和y矩阵记录了最优路径。result矩阵保存了最优解的值,他的另一个作用就是消除了重叠子问题,他保存了子问题的值,因此只需要一次运算。



代码整体上其实很简单,只要理解了实现过程的思想,那么就能容易的写出上述代码。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息