动态规划应用之一

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


有一个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][n], j=y[m][n]) {
			m = i;
			n = j;
			System.out.print("[" + m + "," + n + "]->");
		}
		System.out.println("[" + (rowCount-1) + "," + (lineCount-1) + "]");
	}
}

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




代码整体上其实很简单,只要理解了实现过程的思想,那么就能容易的写出上述代码。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值