第六节:动态规划相关(不同路径、礼物最大价值、最长递增子序列)
一. 不同路径
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 : 原创博客请在转载时保留原文链接或在文章开头加上本人博客地址,否则保留追究法律责任的权利。