第六節:動態規劃相關(不同路徑、禮物最大價值、最長遞增子序列)

一. 不同路徑

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 : 原創博客請在轉載時保留原文鏈接或在文章開頭加上本人博客地址,否則保留追究法律責任的權利。
 
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章