数塔三角形问题的三种解法-(递归,递归hashmap优化,动态规划)

题描述:

设有一个三角形的数塔,顶点为根结点,每个结点有一个整数值。从顶点出发,可以向左走或向右走,求出从顶到底连起来的最短路径,如图所示:(对于节点中的数据可以自己定义)

《数塔三角形问题的三种解法-(递归,递归hashmap优化,动态规划)》

 解题思路:

一、先思考是否可以用递归解决,这道题明显可以使用dfs递归算出所有可能性,然后求出最短路径。

对于上图的递归而言,应从上往下走,2->3或2->4,3->6或者3->5等(即向左右子节点走)

  1. 本题最经典的做法就是将其当作一个二维数组,如a[0][0] = 2,a[1][0] = 3,a[1][1] = 4
  2. 接下来可以定义一个函数来指明节点a[i][j]的下一步,此处将其设为traverse(i,j)
  3. 对于每一个节点,在大方向上做两个操作,一是记录本节点的值,二是进入递归函数遍历他的左右子节点。
  4. 在不断向下递归的过程中,问题的规模在不断的缩小,最终到达最后一条边停止,也就是最下面一行。

 递归的思路较为简单明了,直接看代码分析复杂度:

public class 递归三角形 {
	//将二维数组后面的空缺补0,方便计算
        static int [][] triangle = {
           {2, 0, 0, 0},
           {3, 4, 0, 0},
           {6, 5, 7, 0},
           {4, 1, 8, 3}
	};
	public static int traverse(int i, int j) {
		int row = 4;//共有4行
		if(i >= row-1)//此处因为traverse函数从0开始
                {
			return 0;
		}
		int left = traverse(i+1,j)+triangle[i+1][j];//递归左子树节点
		int right = traverse(i+1,j+1)+triangle[i+1][j+1];//递归右子树节点
		return Math.min(left, right);//计算出两个子树的最短路径
	}
	public static void main(String[] args) throws Throwable{
		int sum = traverse(0,0)+triangle[0][0];
		System.out.println(sum);
	}
}

 递归一共就两个重难点:

1.注意递归函数的参数传递问题:很多人对于递归函数的参数不知道应该写几个,应该写些什么参数,不清楚自己需要哪些参数完成任务,个人的思路是:先写函数内容,再确定参数。首先明确,这个函数中我是否需要数组中的元素,是否要传递数组;然后是我的递归出口应该是什么,是否需要传递过去,最后考虑,递归过程中,我使用到了哪些东西,这样思考较为全面。例如本题中,使用到了二维数组的行与列,但对于二维数组本身,并未有要求,所以triangle数组可传可不传。

2.注意递归的出口问题:首先不要急于写代码,先思考清楚这个递归到哪里就会停止,是否会有多个出口条件,出口的参数类型是否唯一。本题的出口条件十分简单,就是到了最后一行直接返回。

traverse(i, j) = {
    traverse(i+1, j);    向节点i,j 下面的左节点走一步
    traverse(i+1, j+1);    向节点i,j 下面的右节点走一步
}

 而其复杂度是非常高的,因为他要不断地重复计算左右子节点,有重复的计算(本题中,对于节点 3 和 4 来说,如果节点 3 往右遍历, 节点 4 往左遍历,都到了节点 5,节点 5 往下遍历的话就会遍历两次,所以此时就会出现重复子问题),所以复杂度为O(2^n)。

二、思考是否可以用优化递归解决,基本上所有的递归问题都是可被优化的,优化又叫做”剪枝“,也叫做”备忘录法

显然可以使用最”强”数据结构HashMap来解决这个问题,不太了解HashMap的同学可以去百度搜一下,简单来说就是一个键值对的集合,特点就是存取速度奇快无比,我亲测,一道算法题用for循环跑下来需要98ms,39.12M,而用HashMap跑只需要3ms,42.05M。

import java.util.HashMap;

public class 递归三角形hashmap优化 {
	static int [][] triangle = {
	    {2, 0, 0, 0},
            {3, 4, 0, 0},
            {6, 5, 7, 0},
            {4, 1, 8, 3}
	};
	static HashMap<String, Integer> map = new HashMap<String, Integer>();
	public static int traverse(int i, int j) {
		String key = i+""+j;
		if (map.get(key)!=null) {
			return map.get(key);
		}
		int line = 4;
		if (i >= 3) {
			return 0;
		}
		int left = traverse(i+1, j)+triangle[i+1][j];
		int right = traverse(i+1, j+1)+triangle[i+1][j+1];
		int res = Math.min(left, right);
		map.put(key, res);
		return res;
	}
        public static void main(String[] args) {
		int sum = traverse(0,0)+triangle[0][0];
		System.out.println(sum);
	}
}

代码与上面的重复度很高,只是加入了map的一些用法,将其存入了集合中。由于用hash表存储了节点的状态,所以其时间和空间的复杂度都是O(n)。

二、思考是否可以用动态规划解决,也就是自底向上的方法,或者叫”递推

《数塔三角形问题的三种解法-(递归,递归hashmap优化,动态规划)》

入手:明确自底向上的含义

1.题目中要求2到底层的最短路径,那就应该先求3和4到底层的路径互相比较出最短的,再加上2,就得出了最短路径

2.递推一次,先求3到底层的最短路径,应该先求出其左右子节点到底层的最短路径,即6和5到底层的路径互相比较出最短的,加上3,得出3到底层最短路径;再求4到底层的最短路径,应该先求出其左右子节点到底层的最短路径,即5和7到底层的路径互相比较出最短的,加上4,得出4到底层最短路径.

3.依次递推,注意结束位置是在倒数第二行,因为最后一行已经固定不动,且各自就已经是最短路径了。

以此来看,问题已经转换成先求倒数第二行的各自的最小路径,然后再求倒数第三行等等。

 

这里我们明确以下最优子结构,每一层节点到底部的最短路径依赖于它下层的左右节点的最短路径,求得的下层两个节点的最短路径对于依赖于它们的节点来说就是最优子结构,最优子结构对于子问题来说属于全局最优解,这样我们不必去求节点到最底层的所有路径了,只需要依赖于它的最优子结构即可推导出我们所要求的最优解,所以最优子结构有两层含义,一是它是子问题的全局最优解,依赖于它的上层问题只要根据已求得的最优子结构推导求解即可得全局最优解,二是它有缓存的含义,消除重叠子问题。

动态规划解题的重中之重:找到DP的状态和状态转移方程

dp[i,j] = Math.min(dp[i+1,j],dp[i+1,j+1]) + triangle[i,j]

一般来说,dp的状态转移方基本都是一维数组主要用来存储和缓冲的,下面来分析具体的代码和思路要点:

public class 动态规划 {
        //可以注意到这里的数组构成不太一样,后文会说
	static int [][] triangle= {
	        {2},
	        {3, 4},
	        {6, 5, 7},
	        {4, 1, 8, 3}
	};
	
	private static int f() {
		int line = 4;
		int dp[] = triangle[line-1];//line-1代表的是行数编号
                //两重循环后文会说
		for (int i = line-2; i >= 0; i--) {
			for (int j = 0; j < triangle[i].length; j++) {
				dp[j] = triangle[i][j]+Math.min(dp[j],dp[j+1]);
			}
		}
		return dp[0];
	}
	public static void main(String[] args) {
		int result = f();
		System.out.println(result);
	}
}
  1. 双重循环的思路:首先要知道行数的编号是如何编的,由上到下依次为0,1,2,3,共计4行数字。因为本题的dp方法是自底向上,而最后一行不需要参与dp,所以dp的数组只有line-1个位置,即只有0,1,2这3个位置。我们是从下往上一行行递推,所以外循环是指行数的变换,由line-2 -> 0;内循环是指一行中的两两互相比较(因为只有左右两个子节点),代码的第一个重点就是 j < triangle[i].length 如何理解,其中的triangle[i]指的是这一行的长度,就是行内比较,可以理解为i为x轴,j为y轴,这就引出了第二点。
  2. 数组的构建为何后面不补0了:还是因为 j < triangle[i].length。如果补0,则他会在每一行里多出若干个0,再两两比较得出0这个值,会导致程序错误

 

    原文作者:呼hu呼
    原文地址: https://blog.csdn.net/hutaizhang/article/details/105223408
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞