第六节:动态规划相关(不同路径、礼物最大价值、最长递增子序列)

一. 不同路径

1. 题目描述

 一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “Start” )。

 机器人每次只能 “向下或者向右” 移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish” )。

    问总共有多少条不同的路径?

    示例:

    leetcode:https://leetcode.cn/problems/unique-paths/description/

    难度:【中等】

 

2. 思路分析

    (1). 定义一个二维数组dp[i][j],表示从起点到网格的 (i, j) 点的不同路径数. 需要默认全部初始化为0

    (2). 初始化状态,将第一行和第一列中初始化为1

    (3). 确定状态转移方程

        dp[i][j] 等于上面方格 和 左面方格的和, 即 dp[i][j]=dp[i-1][j]+dp[i][j-1]

    (4). 返回最终结果

        dp[m-1][n-1]

 

3. 代码实操


/**
 * 求不同路径的总条数
 * @param m m行
 * @param n n列
 * @returns 返回到右下角的条数
 */
function uniquePaths(m: number, n: number): number {
	//1. 定义状态
	let dp: number[][] = [];
	//用0填充这个二维数组,否则undefined下面会报错
	for (let i = 0; i < m; i++) {
		let temp: number[] = [];
		for (let j = 0; j < n; j++) {
			temp.push(0);
		}
		dp.push(temp);
	}
	// console.log(dp);

	//2. 初始化状态
	for (let i = 0; i < m; i++) {
		dp[i][0] = 1; //第一列
	}
	for (let j = 0; j < n; j++) {
		dp[0][j] = 1; //第一行
	}

	//3.确定状态转移方程
	for (let i = 1; i < m; i++) {
		for (let j = 1; j < n; j++) {
			dp[i][j] = dp[i][j - 1] + dp[i - 1][j];
		}
	}

	//4. 返回最终结果
	return dp[m - 1][n - 1];
}

/**
 * 求不同路径的总条数--简洁代码
 * @param m m行
 * @param n n列
 * @returns 返回到右下角的条数
 */
function uniquePaths2(m: number, n: number): number {
	//1. 定义状态
	// let dp: number[][] = Array.from({ length: m }, () => new Array(n).fill(0));
	const dp: number[][] = new Array(m).fill(new Array(n).fill(0));

	//2. 初始化状态
	dp[0][0] = 1;

	//3.确定状态转移方程
	for (let i = 0; i < m; i++) {
		for (let j = 0; j < n; j++) {
			if (i == 0 || j == 0) {
				dp[i][j] = 1;
				continue; //直接进行下一次for循环
			}
			dp[i][j] = dp[i][j - 1] + dp[i - 1][j];
		}
	}

	//4. 返回最终结果
	return dp[m - 1][n - 1];
}

{
	//测试1
	console.log(uniquePaths(7, 3)); //28
	console.log(uniquePaths(3, 3)); //6
	//测试2
	console.log(uniquePaths2(7, 3)); //28
}

 

 

二. 礼物最大价值

1. 题目描述

     现有一个记作二维矩阵 frame 的珠宝架,其中 frame[i][j] 为该位置珠宝的价值。拿取珠宝的规则为:

    只能从架子的【左上角】开始拿珠宝,每次可以移动到【右侧】或【下侧】的相邻位置,到达珠宝架子的【右下角】时,停止拿取

    注意:珠宝的价值都是大于 0 的。除非这个架子上没有任何珠宝,比如 frame = [[0]]。

    示例:

    leetcode:https://leetcode.cn/problems/li-wu-de-zui-da-jie-zhi-lcof/description/

    难度:【中等】

 

2. 思路分析

(核心点:当前格子礼物的最大价值=当前格子礼物的价值 + Math.Max(上面格子最大最大价值,左面格子的最大价值))

(1).定义状态

    dp[i][j]表示当前格子礼物的最大价值

(2).初始化状态

     将第一行和第一列都初始化了

(3).确定状态转移方程

     dp[i][j] = frame[i][j] + Math.max(dp[i][j - 1], dp[i - 1][j]);

 (4). 返回最终结果

       dp[m-1][n-1]

 

3. 代码实操


/**
 * 求礼物的最大价值
 * @param frame frame[i][j] 为该位置珠宝的价值
 * @returns
 */
function jewelleryValue(frame: number[][]): number {
	//1. 定义状态
	let m = frame.length; //m行
	let n = frame[0].length; //n列
	let dp: number[][] = Array.from({ length: m }, () => new Array(n).fill(0)); //填充为0

	//2. 初始化状态
	dp[0][0] = frame[0][0];
	for (let i = 1; i < n; i++) {
		dp[0][i] = dp[0][i - 1] + frame[0][i]; //第一行
	}
	for (let j = 1; j < m; j++) {
		dp[j][0] = dp[j - 1][0] + frame[j][0]; //第一列
	}

	//3. 状态转移方程
	for (let i = 1; i < m; i++) {
		for (let j = 1; j < n; j++) {
			dp[i][j] = frame[i][j] + Math.max(dp[i][j - 1], dp[i - 1][j]);
		}
	}
	//4.返回最终结果
	return dp[m - 1][n - 1];
}

//测试
console.log(
	jewelleryValue([
		[1, 3, 1],
		[1, 5, 1],
		[4, 2, 1],
	])
);

 

 

三. 最长递增子序列

一. 题目描述

   给你一个整数数组 nums ,找到其中最长严格递增子序列的“长度”。

   子序列 是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。

   例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列(不是递增的!!)

    leetCode:https://leetcode.cn/problems/longest-increasing-subsequence/description/

    难度:【中等】

 

二. 思路分析-动态规划

    1. 定义状态:设dp[i]表示以第i个元素结尾的最长上升子序列的长度。

    2. 初始状态:对于每个i,dp[i]的初始值为1,因为每个元素本身也可以作为一个长度为1的上升子序列。

    3. 确定状态转移方程:

     (1).对于每个i,我们需要找到在[0, i-1]范围内比nums[i]小的元素,以这些元素结尾的最长上升子序列中最长的那个子序列的长度。

     (2).然后将其加1即可得到以nums[i]结尾的最长上升子序列的长度。

     (3).状态转移方程为:

        dp[i] = Math.max(dp[j] + 1, dp[i]);  其中j < i且nums[j] <nums[i]。

     (4). 返回最终结果

        A.可以直接求数组最大值

        B.或者用个max变量进行存放

 

三. 代码实操-动态规划


/**
 * 写法1
 * @param nums
 * @returns
 */
function lengthOfLIS(nums: number[]): number {
	//1. 定义状态  2. 初始化状态
	let n = nums.length;
	let dp: number[] = new Array(n).fill(1);

	//3.确定状态转移方程
	for (let i = 1; i < n; i++) {
		for (let j = 0; j <= i - 1; j++) {
			if (nums[j] < nums[i]) {
				dp[i] = Math.max(dp[j] + 1, dp[i]); //核心
			}
		}
	}

	//4.返回最终结果
	return Math.max(...dp);
}

/**
 * 写法2--提出来最大值
 * @param nums
 * @returns
 */
function lengthOfLIS2(nums: number[]): number {
	//1. 定义状态  2. 初始化状态
	let n = nums.length;
	let dp: number[] = new Array(n).fill(1);

	//3.确定状态转移方程
	let max = dp[0];
	for (let i = 1; i < n; i++) {
		for (let j = 0; j <= i - 1; j++) {
			if (nums[j] < nums[i]) {
				dp[i] = Math.max(dp[j] + 1, dp[i]); //核心
				max = Math.max(dp[i], max);
			}
		}
	}

	//4.返回最终结果
	return max;
}

 

四. 思路分析-贪心+二分

 1. 维护一个数组tails,用于记录扫描到的元素应该存放的位置。

 2. 扫描原数组中的每个元素num,和tails数组中最后一个元素的进行比较。

   (1).如果该num更小,则用该num覆盖tails中的最后一个元素

   (2).如果该num更大,则将该num插入到tails的尾部

 3. tails数组的长度,就是最长递增子序列的长度

补充-为什么这么神奇刚好是数组的长度呢?(了解)

     ◼ 情况一:如果是逆序的时候,一定会一直在一个上面加加加

     ◼ 情况二:一旦出现了比前面的最小值的值大的,那么就一定会增加一个新的数列,说明在上升的过程

     ◼ 情况三:如果之后出现一个比前面数列小的,那么就需要重新计算序列:

 

五. 代码实操-动态规划


/**
 * 求最长递增子序列的长度
 * @param nums 数组序列
 * @returns 返回最长递增子序列的长度
 */
function lengthOfLIS(nums: number[]): number {
	//1.定义变量
	let n = nums.length;
	let tails: number[] = [];

	//2.遍历nums数组
	for (let i = 0; i < n; i++) {
		const current = nums[i];
		//3. 进行二分查找
		let left = 0;
		let right = tails.length - 1;
		while (left <= right) {
			let mid = Math.floor((left + right) / 2); //下整
			if (current <= tails[mid]) {
				right = mid - 1;
			} else {
				left = mid + 1;
			}
		}

		//4. 判断是否找到元素
		if (left == tails.length) {
			//表示没有找到
			tails.push(current);
		} else {
			tails[left] = current;
		}
	}

	return tails.length;
}
//测试
{
	console.log(lengthOfLIS([10, 9, 2, 4, 3, 7, 101, 18])); //4
	console.log(lengthOfLIS([7, 7, 7, 7, 7, 7, 7])); //1
}

 

 

 

 

 

 

 

!

  • 作       者 : Yaopengfei(姚鹏飞)
  • 博客地址 : http://www.cnblogs.com/yaopengfei/
  • 声     明1 : 如有错误,欢迎讨论,请勿谩骂^_^。
  • 声     明2 : 原创博客请在转载时保留原文链接或在文章开头加上本人博客地址,否则保留追究法律责任的权利。
 
posted @ 2024-03-12 08:46  Yaopengfei  阅读(7)  评论(1编辑  收藏  举报