常用的算法 - 动态规划

常用的算法 - 动态规划

目录

1. 回顾
2. 简介
      2.1 动态规划.
3. 总结

1.回顾

      在上一篇文章中,总结了一些关于深度优先遍历(DFS)和广度优先遍历(BFS)的相关知识点。今天给大家总结一下动态规划的相关知识点。

2. 简介

动态规划

      基本属性
      题⽬分类
      解题思想
      巩固与加深:算法复杂度

2.1 动态规划

动态规划的定义

      ‣ ⼀种数学优化的⽅法,同时也是编程的⽅法。

重要属性

      ‣ 最优⼦结构 Optimal Substructure
             -状态转移⽅程 f(n)
      ‣ 重叠⼦问题 Overlapping Sub-problems

在这里插入图片描述
例子

      给定如下的有向图,求出从顶点A到C的zuiduanl最长路径,要求路径中的点只能出现一次。
在这里插入图片描述
解题思路

      按照题目要求可以看到,从A通往C有两条最长的路径,它们分别是 A -> D -> C以及 A -> B -> C,我们看其中一条就行。
      当我们用动态规划的思想去解决问题的时候,第一步就是要尝试把问题的规模减小,也就是找出子问题,然后看看能不能从求解子问题的方法中,找出通用的方法,也就是说一旦求得子问题的最优解,就能较为直观的获得最终的结果。
      就这道题目而言,求A到C的最长距离,让我们把问题规模减小,先看看从A到B的最⻓距离是多少,再看看B 到 C 的最⻓距离是多少。最后组合起来,应该就是A->C最长距离。
      由图可知,A到B的最⻓距离为A -> D -> C -> B;B 到 C 的最⻓距离为B -> A -> D -> C,所以A到C的最长距离应该为A -> D -> C -> B -> A -> D -> C,很显然,这并不满足题目的要求,也就是说对于这道题而言,并没有一个最佳的子结构,所以无法使用动态规划的思想去解答。

例题

       LeetCode 300. 最长递增子序列

题目:
给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。
子序列是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。

示例 1:
输入:nums = [10,9,2,5,3,7,101,18]
输出:4
解释:最长递增子序列是 [2,3,7,101],因此长度为 4 。

示例 2:
输入:nums = [0,1,0,3,2,3]
输出:4

示例 3:
输入:nums = [7,7,7,7,7,7,7]
输出:1

解题思路

      将问题规模减少,推导出状态转移⽅程式
      f(n)表示数组 nums[0, 1, 2, …, n-1] 中最⻓的⼦序列
      f(n − 1)表示数组 nums[0, 1, 2, … n-2] 中最⻓的⼦序列
      f(1)表示数组 nums[0] 的最⻓⼦序列

解决动态规划问题最难的两个地⽅:

‣ 如何定义 f(n)

      对于这道题⽽⾔,f(n)是以 nums[n-1] 结尾的最⻓的上升⼦序列的⻓度

‣ 如何通过f(1), f(2), . . . , f(n − 1)推导出f(n),即状态转移⽅程

      -拿nums[n-1]与⽐它⼩的每⼀个值 nums[i] 作⽐较,其中1 ≤ i < n,然后加 1 即可
      -因此状态转移⽅程为:f(n) = max{f(i)} + 1(1 ≤ i < n - 1,并且 nums[i-1] < nums[n-1])

代码如下

class LISRecursion {
	static int max;
	public int f(int[] nums, int n) {
		if (n <= 1) {
			return n;
 		}
		int result = 0, maxEndingHere = 1;
		for (int i = 1; i < n; i++) {
			result = f(nums, i);
			if (nums[i - 1] < nums[n - 1] && result + 1 > maxEndingHere) {
				maxEndingHere = result + 1;
			}
 		}
		if (max < maxEndingHere) {
			max = maxEndingHere;
		}
		return maxEndingHere;
 	}
	public int LIS(int[] nums) {
		max = 1;
 		f(nums, nums.length);
		return max;
	}
}

      ⾸先定义⼀个静态变量 max,⽤来保存最终的最⻓的上升⼦序列的⻓度。
      接下来看看如何实现状态转移⽅程,即f函数。
‣⾸先最基本的情况是:
      当数组的⻓度为 0 时,没有上升⼦序列,
      当数组⻓度为 1 时,最⻓的上升⼦序列⻓度是 1 。
‣maxEndingHere 变量的含义是:
      包含当前最后⼀个元素的情况下,最⻓的上升⼦序列⻓度。
‣从头遍历数组,
      递归求出以每个点为结尾的⼦数组中最⻓上升序列的⻓度。
‣判断⼀下
      如果该数⽐⽬前最后⼀个数要⼩,如果该数⽐⽬前最后⼀个数要⼩,就能构成⼀个新的上升⼦序列。
      这个新的⼦序列有可能成为最终的答案。
‣最后返回以当前数结尾的上升⼦序列的最⻓⻓度。

时间复杂度分析

‣ 迭代法
‣ 公式法

-当n = 1时,递归直接返回1,执⾏时间为O(1)
      T(1) = O(1)
-当n = 2时,内部调⽤了⼀次递归求解T(1)
      T(2) = T(1)
-当n = 3时,T(3) = T(1) + T(2)

以此类推
-T(n − 1) = T(1) + T(2) + . . . + T(n − 2)
-T(n) = T(1) + T(2) + . . . + T(n − 1)
-T(n − 1) = T(1) + T(2) + . . . + T(n − 2)
-T(n) = T(1) + T(2) + . . . + T(n − 1)
T(n) = 2 × T(n − 1)≠T(n) = a ⋅ T( n b \frac{n}{b} bn) + f(n)
      O(n) = O(2n)

记忆法代码如下

class LISMemoization {
	static int max;
	static HashMap<Integer, Integer> cache;
	public int f(int[] nums, int n) {
		if (cache.containsKey(n)) {
		return cache.get(n);
		}
		if (n <= 1) {
			return n;
		}
		int result = 0, maxEndingHere = 1;
		for (int i = 1; i < n; i++) {
			...
		}
		if (max < maxEndingHere) {
 			max = maxEndingHere;
		}
 		cache.put(n, maxEndingHere);
		return maxEndingHere;
 	}
}

分析

‣⾸先,定义⼀个哈希表 cache,⽤来保存我们的计算结果。
‣每次调⽤递归函数的时候,
      判断⼀下对于这个输⼊,我们是否已经计算过了,
      也就是 cache ⾥是否已经保留了这个值,
      是的话,⽴即返回,如果不是,再继续递归调⽤。
‣在返回当前结果前,保存到 cache ⾥,
      这样下次遇到了同样的输⼊时,就不⽤再浪费时间计算了。

时间复杂度分析

O(f(n)) = O(n) + O(n2) = O(n2)<O(2n)

      对于这种将问题规模不断减少的做法,我们把它称为⾃顶向下的⽅法。由于递归的存在,程序运⾏时对堆栈的消耗以及处理很慢,在实际⼯作中并不推荐。

⾃底向上
代码如下

class LISDP {
	public int LIS(int[] nums, int n) {
		int[] cache = new int[n];
		int i, j, max = 0;
		for (i = 0; i < n; i++) cache[i] = 1;
		for (i = 0; i < n; i++) {
			for (j = 0; j < i; j++) {
				if (nums[j] < nums[i] && cache[i] < cache[j] + 1) {
						cache[i] = cache[j] + 1;
				}
			}
			max = Math.max(max, cache[i]);
		}
		return max;
	}
}

分析

      ‣这次我们⽤⼀个⼀维数组 cache 来存储计算过的结果。
      ‣初始化 cache ⾥的每个元素的值为 1,表示以每个元素作为结尾的最⻓⼦序列的⻓度初始化为 1。
      ‣⾃底向上地求解每个⼦问题的最优解。
      ‣拿遍历中遇到的每个元素 nums[j] 与 nums[i] ⽐较,如果发现 nums[j] < nums[i],说明 nums[i] 有机会构成上升序列,如果新的上升序列⽐之前计算过的还要⻓,更新⼀下,保存到 cache 数组⾥。
      ‣⽤当前计算好的⻓度与全局的最⼤值进⾏⽐较。
      ‣最后得出最⻓的上升序列的⻓度。

时间复杂度分析

      这是⼀个双重循环
      i = 0 时,内循环执⾏ 0 次
      i = 1 时,内循环执⾏ 1次
      i = n - 1 时,内循环执⾏ n - 1 次
      O(1 + 2 + . . . + n − 1) = O( n ∗ ( n − 1 ) 2 \frac{n*(n-1)}{2} 2n(n1)) = O(n2)

动态规划解题难点

      ‣ 应当采⽤什么样的数据结构来保存什么样的计算结果
      ‣ 如何利⽤保存下来的计算结果推导出状态转移方程

3. 总结

      本文章是小朱近日刷算法题,看视频之后做的一些笔记和总结,希望大家有所收获,不足之处,请指正,如果对你有帮助可以点赞加收藏。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小朱不猪

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值