【动态规划】(一)动态规划理论及基础题目

news2024/11/26 2:40:51

动态规划理论及基础题目

  • 理论基础
  • 斐波那契数
  • 爬楼梯
  • 使用最小花费爬楼梯
  • 不同的路径
  • 不同的路径2
  • 整数拆分
  • 不同的二叉搜索树

理论基础

  • 动态规划,英文:Dynamic Programming,简称DP,如果某一问题有很多重叠子问题,使用动态规划是最有效的。

  • 所以动态规划中每一个状态一定是由上一个状态推导出来的,这一点就区分于贪心,贪心没有状态推导,而是从局部直接选最优的。

  • 动态规划是当前的结果取决于上一阶段的结果,因此存在一个递推公式

动态规划做题五部曲:

  1. 确定dp数组(dp table)以及下标的含义
  2. 确定递推公式
  3. dp数组如何初始化
  4. 确定遍历顺序
  5. 打印dp数组

如何debug:

  • 找问题的最好方式就是把dp数组打印出来,看看究竟是不是按照自己思路推导的!
  • 写代码之前一定要把状态转移在dp数组的上具体情况模拟一遍,心中有数,确定最后推出的是想要的结果。
  • 思考以下问题:
    • 是否举例推导状态转移公式?
    • 是否打印dp数组的日志?
    • dp数组打印结果与推导状态时一致?

斐波那契数

力扣链接
  斐波那契数,通常用 F(n) 表示,形成的序列称为 斐波那契数列 。该数列由 0 和 1 开始,后面的每一项数字都是前面两项数字的和。也就是: F(0) = 0,F(1) = 1 F(n) = F(n - 1) + F(n - 2),其中 n > 1 给你n ,请计算 F(n) 。

动规五部曲:

  • dp[i]的定义: 第 i 个数的斐波那契数值是dp[i]

  • 确定递推公式: F(n) = F(n - 1) + F(n - 2)

  • dp数组初始化: F(0) = 0,F(1) = 1

  • 确定遍历顺序: 从递归公式dp[i] = dp[i - 1] + dp[i - 2];中可以看出,dp[i]是依赖 dp[i - 1] 和 dp[i - 2],那么遍历的顺序一定是从前到后遍历。

程序实现:

int fib(int n) 
{
	if(n <= 1)
		return n;
	vector<int> dp(n + 1);
	dp[0] = 0;
	dp[1] = 1;
	for(int i = 2; i <= n; i++)
		dp[i] = dp[i-1] + dp[i-2];
	return dp[n];
}

爬楼梯

力扣链接

假设你正在爬楼梯。需要 n ( n > 0)阶你才能到达楼顶。每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?

示例 1:
输入: 2
输出: 2

示例 2:
输入: 3
输出: 3

思路:

  • 那么第一层楼梯再跨两步就到第三层 ,第二层楼梯再跨一步就到第三层。

  • 所以到第三层楼梯的状态可以由第二层楼梯 和 到第一层楼梯状态推导出来,那么就可以想到动态规划

  • 即要爬到第 i 层,可以由第 i - 1 层跨一步,或者 i - 2 层跨2步到达。

动态规划五部曲:

  • dp[i]的定义: 爬到第 i 层楼梯,有 dp[i] 种方法

  • 确定递推公式:

    • 首先是dp[i - 1],上i-1层楼梯,有dp[i - 1]种方法,那么再一步跳一个台阶不就是dp[i]了么。
    • 还有就是dp[i - 2],上i-2层楼梯,有dp[i - 2]种方法,那么再一步跳两个台阶不就是dp[i]了么
    • 那么dp[i]就是 dp[i - 1]与dp[i - 2]之和,即 dp[i] = dp[i-1] + dp[i-2]
  • dp数组初始化: dp[1] = 1; dp[2] = 2

  • 确定遍历顺序: 从递推公式dp[i] = dp[i - 1] + dp[i - 2]; 中可以看出,遍历顺序一定是从前向后遍历的。

  • 举例推导dp数组
    举例当n为5的时候,dp table(dp数组)应该是这样的


程序实现

int climbStairs(int n) 
{
    if (n <= 2)
        return n;
    vector<int> dp(n + 1);
    dp[1] = 1;
    dp[2] = 2;
    for (int i = 3; i <= n; i++)
        dp[i] = dp[i - 1] + dp[i - 2];
    return dp[n];
}

使用最小花费爬楼梯

力扣链接

给定一个整数数组 cost ,其中 cost[i] 是从楼梯第 i 个台阶向上爬需要支付的费用。一旦你支付此费用,即可选择向上爬一个或者两个台阶。

你可以选择从下标为 0 或下标为 1 的台阶开始爬楼梯,计算并返回达到楼梯顶部的最低花费

示例 1:
输入:cost = [10,15,20]
输出:15
解释:你将从下标为 1 的台阶开始。

  • 支付 15 ,向上爬两个台阶,到达楼梯顶部。
    总花费为 15 。

示例 2:
输入:cost = [1,100,1,1,1,100,1,1,100,1]
输出:6

动态规划五部曲:

  • dp[i]的定义: 到达第 i 台阶所花费的最少体力为 dp[i]

  • 确定递推公式: 可以有两个途径得到dp[i],一个是dp[i-1] 一个是dp[i-2]。

    • dp[i - 1] 跳到 dp[i] 需要花费 dp[i - 1] + cost[i - 1]。

    • dp[i - 2] 跳到 dp[i] 需要花费 dp[i - 2] + cost[i - 2]。

    • 一定是选最小的,所以

      dp[i] = min(dp[i - 1] + cost[i - 1], dp[i - 2] + cost[i - 2]);
      
  • dp数组初始化: 题意表明可以选择从下标为 0 或下标为 1 的台阶开始爬楼梯,因此 dp[0] = 0,dp[1] = 0

  • 确定遍历顺序: 因为是模拟台阶,而且 dp[i] 由 dp[i-1] dp[i-2]推出,所以是从前到后遍历cost数组就可以了

  • 举例推导dp数组
    拿示例2:cost = [1, 100, 1, 1, 1, 100, 1, 1, 100, 1] ,来模拟一下dp数组的状态变化,如下:


程序实现:

int minCostClimbingStairs(vector<int>& cost) 
{
	if(cost.size() <= 1)
		return 0;
	int size = cost.size();
	vector<int> dp(size + 1);		// 到达第 i 层的最小花费
	dp[0] = 0;
	dp[1] = 0;
	for(int i = 2; i <= size; i++)
		dp[i] = min(dp[i-1] + cost[i-1], dp[i-2] + cost[i-2]);
	// for(int num : dp)
	// 	cout << num << " ";
	// cout << endl;
	return dp[size];
}

不同的路径

力扣链接

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

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

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


示例 1:
输入:m = 3, n = 7
输出:28

示例 2:
输入:m = 2, n = 3
输出:3

思路:
  因为机器人只能向下或者向右移动一格,因此对于 nums[i][j] 只能由上方一格 nums[i-1][j] 或者 左边一格 nums[i][j-1] 移动得到。

动态规划五部曲:

  • dp[i][j]的定义: 从[0,0] 出发到 [i,j] 有 dp[i][j] 条不同的路径。

  • 确定递推公式: 向下移动一格的路径 + 向右移动一格的路径(类似爬楼梯)

    dp[i][j] = dp[i-1][j] + dp[i][j-1]
    
  • dp数组初始化:

    • 根据递推公式可以看,需要对第一行与第一列的数组初始化。

    • 第一列的 dp 数组均为1,因为机器人跳到第一列的任何格子的路径只有1条(一直向下),即 dp[i][0] = 1;

    • 第一列的 dp 数组均为1,因为机器人跳到第一行的任何格子的路径只有1条(一直向右),即 dp[0][j] = 1;

    • 其余格子均通过递推公式求得,因此初始化为任何值均可。

    • 因此为了简化程序,dp 数组全部初始化为 1 即可。

      vector<vector<int>> dp(m, vector<int>(n,1));
      
  • 确定遍历顺序: 根据递推公式,都是从其上方和左侧推导而来,因此从左到右,从上到下遍历即可。

  • 举例推导dp数组

程序实现:

int uniquePaths(int m, int n) 
{
	vector<vector<int>> dp(m, vector<int>(n,1));
	// 其他块
	for(int i = 1; i < m; i++)
	{
		for(int j = 1; j < n; j++)
		{
			dp[i][j] = dp[i][j-1] + dp[i-1][j]; 
		}
	}		
	return dp[m-1][n-1];
}

不同的路径2

力扣链接

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

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

现在考虑网格中有障碍物。那么从左上角到右下角将会有多少条不同的路径?


网格中的障碍物和空位置分别用 1 和 0 来表示。

示例 1:


输入:obstacleGrid = [[0,0,0],[0,1,0],[0,0,0]]
输出:2

本题在上一题-不同的路径的不同点上进行优化

不同点:

  • 本题在递推时需要判断是否遇到障碍物,一旦遇到障碍物时,可走的路径为 0 (可省略,初始化就为0)

    if(obstacleGrid[i][j] == 0)
    	dp[i][j] = 0;			// 可省略 因为初始化就为 0 
    else
    	dp[i][j] = dp[i][j-1] + dp[i-1][j]; 
    
  • 在初始化时,第一行/列,在遇到障碍物前,可走路径为1,障碍物后的路径均为 0,不妨初始均为0,即包括第一行/列遇到障碍物后,同时包括递推时遇到障碍物。

    // 初始化程序优化
    vector<vector<int>> dp(m, vector<int>(n,0));
    
  • 初始化第一行 / 列 遇到障碍物前路径条数为 1

    // 第一列
    for(int i = 0; i < m; i++)
    {
    	if(obstacleGrid[i][0])
    		break;
    	else
    		dp[i][0] = 1;
    }
    // 第一行
    for(int  j = 0; j < n; j++)
    {
    	if(obstacleGrid[0][j])
    		break;
    	else
    		dp[0][j] = 1;
    }
    

程序实现:

int uniquePathsWithObstacles(vector<vector<int>>& obstacleGrid) 
{
	int m = obstacleGrid.size();
	int n = obstacleGrid[0].size();
	
	// 初始化程序优化
	vector<vector<int>> dp(m, vector<int>(n,0));
	// 第一列
	for(int i = 0; i < m; i++)
	{
		if(obstacleGrid[i][0])
			break;
		else
			dp[i][0] = 1;
	}
	// 第一行
	for(int  j = 0; j < n; j++)
	{
		if(obstacleGrid[0][j])
			break;
		else
			dp[0][j] = 1;
	}
		
	// 其他块
	for(int i = 1; i < m; i++)
	{
		for(int j = 1; j < n; j++)
		{
			if(obstacleGrid[i][j] == 0)
				dp[i][j] = 0;
			else
				dp[i][j] = dp[i][j-1] + dp[i-1][j]; 
		}
	}		
	return dp[m-1][n-1];
}

整数拆分

力扣链接

给定一个正整数 n,将其拆分为至少两个正整数的和,并使这些整数的乘积最大化。 返回你可以获得的最大乘积。

示例 1:
输入: 2
输出: 1

输入: 10
输出: 36
解释: 10 = 3 + 3 + 4, 3 × 3 × 4 = 36。

思路

  • 将n拆成2个数,i 和 n - i,乘积为 i * (n - i)

  • 将n拆成多个数(>2),其中一个数为 i,另外数的和为 n - i,乘积为 i * dp[n-i],其中 dp[j] 表示拆分数字 j,可以得到最大的乘积

  • 这里这个想法(dp的含义)其实不是特别好想

动态五部曲

  • dp[i]的定义:拆分数字 i,可得到的最大乘积

  • 递推公式:

    dp[i] = max(dp[i], max((i - j) * j, dp[i - j] * j));
    
  • dp数组初始化

    • 严格从 dp[i] 的定义来说,dp[0] dp[1] 就不应该初始化,也就是没有意义的数值
    • 这里只初始化 dp[2] = 1;
  • 确定遍历顺序: 通过递推公式看出,从前向后遍历,先有dp[i - j]再有dp[i],所以顺序为:

    for (int i = 3; i <= n ; i++) {
        for (int j = 1; j < i - 1; j++) {
            dp[i] = max(dp[i], max((i - j) * j, dp[i - j] * j));
        }
    }
    
  • 举例推导dp数组: 举例当n为10 的时候,dp数组里的数值,如下:


程序实现:

int integerBreak(int n) 
{
	// dp[i] : i 拆分后得到的最大乘积数
	vector<int> dp(n+1);
	// dp[0] dp[1] 无意义
	dp[0] = dp[1] = 0;	
	dp[2] = 1;
	for(int i = 3; i <= n; i++)
	{
		// 对 i 进行拆分 j 和 i-j
		for(int j = 1; j < i; j++)		
		{
			dp[i] = max(dp[i], max((i - j) * j, dp[i - j] * j));
		}
	}
	return dp[n];
}

不同的二叉搜索树

力扣链接

给定一个整数 n,求以 1 … n 为节点组成的二叉搜索树有多少种?

示例:

突破口:

思路,画图找规律


当 n = 3 时,可以分为以下几种情况:

  • 当 1 为头结点的时候,其右子树有两个节点,和 n 为 2 的时候两棵树的布局是一样的啊!
  • 当 3 为头结点的时候,其左子树有两个节点,和 n 为 2 的时候两棵树的布局也一样
  • 当 2 为头结点的时候,其左右子树都只有一个节点,和 n 为 1 的时候只有一棵树的布局也是一样的!

致此,发现了重叠子问题,也就是可以通过dp[1] 和 dp[2] 来推导出来dp[3]的某种方式。

dp[3],就是 元素1为头结点搜索树的数量 + 元素2为头结点搜索树的数量 + 元素3为头结点搜索树的数量

元素1为头结点搜索树的数量 = 右子树有2个元素的搜索树数量 * 左子树有0个元素的搜索树数量

元素2为头结点搜索树的数量 = 右子树有1个元素的搜索树数量 * 左子树有1个元素的搜索树数量

元素3为头结点搜索树的数量 = 右子树有0个元素的搜索树数量 * 左子树有2个元素的搜索树数量

有2个元素的搜索树数量就是dp[2]。

有1个元素的搜索树数量就是dp[1]。

有0个元素的搜索树数量就是dp[0]。

所以dp[3] = dp[2] * dp[0] + dp[1] * dp[1] + dp[0] * dp[2]

如图所示:


此时我们已经找到递推关系了,那么可以用动规五部曲再系统分析一遍。

  • dp[i]含义: 1 到 i 为节点组成的二叉搜索树的个数为dp[i]。

  • 确定递推公式: dp[i] += dp[ 以j为头结点左子树节点数量 ] * dp[ 以j为头结点右子树节点数量 ],j相当于是头结点的元素,从1遍历到i为止。所以递推公式:

    dp[i] += dp[j - 1] * dp[i - j]; 
    

    j-1 为 j 为头结点左子树节点数量,i-j 为以 j 为头结点右子树节点数量

  • dp数组初始化:

    dp[0] = 1;	dp[1] = 1;
    
  • 确定遍历顺序: 通过递推公式看出,节点 i 依靠 i 之前节点数的状态,所以为顺序遍历。

    for(int i = 2; i <= n; i++)
    {
    	for(int j = 1; j <= i; j++)
    	{
    		dp[i] += dp[j-1] * dp[i-j];
    	}
    }
    
  • 举例推导dp数组: n为5时候的dp数组状态如图:


程序实现:

int numTrees(int n) 
{
	// dp[i] : 输入 i 能形成的最多二叉搜索树的个数
	vector<int> dp(n+1);
	dp[0] = 1;
	dp[1] = 1;
	for(int i = 2; i <= n; i++)
	{
		for(int j = 1; j <= i; j++)
		{
			dp[i] += dp[j-1] * dp[i-j];
		}
	}
	
	for(int num: dp)
		cout << num << " ";
	
	return dp[n];
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2128907.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

告别存储烦恼,MyDiskTest全方位检测解决方案

科技改变生活&#xff0c;但质量决定科技的生命力——这句话在数字化时代尤为贴切。近年来&#xff0c;随着科技的飞速发展&#xff0c;U盘、SD卡、CF卡等移动存储设备已成为我们日常工作和生活中不可或缺的工具。它们便携、高效&#xff0c;能够快速存储和传输大量数据。然而&…

【详解】数据库E-R图——医院计算机管理系统

题目 某医院病房计算机管理中需要如下信息&#xff1a; 科室&#xff1a;科室名&#xff0c;科室地址&#xff0c;科室电话&#xff0c;医生姓名 病房&#xff1a;病房号&#xff0c;床位号&#xff0c;所属科室名 医生&#xff1a;工作证号&#xff0c;姓名&#xff0c;性别&a…

GPT撰写开题报告教程——课题确定及文献调研

撰写开题报告是一项复杂而重要的任务&#xff0c;需要涵盖从主题选择到文献综述、研究方法等多个环节。借助AI&#xff0c;如ChatGPT&#xff0c;可以显著提高这一过程的效率以及内容的质量。本文将详细探讨如何一步步利用ChatGPT撰写开题报告。 一、开题报告内容 一个清晰的…

基于R语言的统计分析基础:使用ggplot2包进行绘图

安装ggplot2包并查看官方文档 ggplot2是一个基于图形语法的R包&#xff0c;它允许用户通过声明式方式指定数据、美学映射和图形元素来灵活创建复杂且美观的可视化图表。 ggplot2包官方教学文档&#xff1a;ggplot2官方文档 在R语言中安装ggplot2有两种方法&#xff1a; 安装整…

【SQL】百题计划:SQL判断条件OR的使用。

【SQL】百题计划-20240912 Select name, population, area from World where area>3000000 or population > 25000000;

替换传统数据处理平台,TDengine 与华风数据达成合作

在全球能源转型的大背景下&#xff0c;新能源产业正迎来前所未有的发展机遇。随着国家对可再生能源的政策支持和市场需求的不断增长&#xff0c;风电、光伏和储能等新能源项目如雨后春笋般蓬勃发展。然而&#xff0c;随之而来的数据处理与管理挑战也日益凸显。面对海量的设备运…

YOLO-v8:对yolov8网络的改进教程(以GAM注意力模块为例)

本文将介绍如何在YOLOv8网络中进行模块化修改。 通过将改进的核心模块添加到项目中&#xff0c;即可直接运行各种 YOLOv8-xxx.yaml 网络配置文件&#xff0c;支持乐高式创新扩展。无论是进行网络结构的调整还是增加新的功能模块&#xff0c;用户只需一键运行&#xff0c;轻松实…

净赚百亿背后,海尔智家的机遇与隐忧

广撒网、出海忙&#xff0c;海尔智家如何熬过存量周期&#xff1f; 转载&#xff1a;科技新知 原创 作者丨田箫 编辑丨赛柯 冰箱、空调、洗衣机不好卖了&#xff0c;已成为不争的事实。 在购房热情降温、收入预期低迷的双重打击下&#xff0c;白电品牌正艰难求生。然而&#x…

通过ASCII码打印HelloWorld(花式打印HelloWorld)

/*** 通过ASCII码打印HelloWorld*/ public class Main {public static void main(String[] args) {String target "HelloWorld";String fi "";for (int i 0; i < target.length(); i) {for (int x 0; x < 127; x) {char c (char) x;String d f…

怎么利用短信接口发送文字短信

在当今这个快节奏的数字时代&#xff0c;即时通讯已成为人们日常生活和工作中不可或缺的一部分。而短信接口&#xff08;SMS Interface&#xff09;&#xff0c;作为传统与现代通讯技术结合的典范&#xff0c;凭借其高效、稳定、广泛覆盖的特性&#xff0c;在众多领域发挥着不可…

K8s中HPA自动扩缩容及hml

1.HPA&#xff1a;基于cpu的利用率来动态实现pod数量的自动伸缩&#xff0c;创建的方法一种是yaml文件&#xff0c;一种是命令行&#xff08;运用比较少&#xff09;&#xff1b;在yaml文件中必须要有资源控制&#xff08;cpu&#xff09;的字段才能生效的。 必要条件&#xf…

linux内核驱动:ptp内核phc框架

目录 一、介绍二、PHC驱动文件三、主要数据结构四、初始化和调用流程五、总结 一、介绍 本文基于linux内核5.10.xxx总结ptp1588精确时间协议实现过程中&#xff0c;内核部分的8A34002实现的phc(PTP hardware clock)驱动支持&#xff1b; ptp的系统框架 .红圈部分为本笔记总结的…

RK3568 初识

RK3565是福州本土集成电路设计企业的产品&#xff0c;售价在200RMB左右&#xff0c;润和DAYU200完成基于RK3568的鸿蒙适配&#xff0c;官方售价高达2000RMB 瑞芯微电子有限公司&#xff08;Rockchips Electronics CO., Ltd&#xff09;: 规模&#xff1a;2000人市值&#xff…

CSS实现前端布局更巧妙的方案!在 flex 布局中通过使用 margin 实现水平垂直居中以及其他常见的前端布局

在前端开发中&#xff0c;实现水平垂直居中一直是个热门话题。随着 CSS Flexbox 布局的普及&#xff0c;开发者们开始更多地使用 justify-content 和 align-items 这两个属性来解决这个问题。 然而&#xff0c;还有一种更加简洁、灵活的方式——使用 margin: auto; 来实现居中以…

【北京迅为】《STM32MP157开发板使用手册》- 第二十三章 Cortex-M4 开发环境搭建

iTOP-STM32MP157开发板采用ST推出的双核cortex-A7单核cortex-M4异构处理器&#xff0c;既可用Linux、又可以用于STM32单片机开发。开发板采用核心板底板结构&#xff0c;主频650M、1G内存、8G存储&#xff0c;核心板采用工业级板对板连接器&#xff0c;高可靠&#xff0c;牢固耐…

香港科技大学工学院2025/2026年度硕士研究生(MSc)项目招生宣讲会

&#x1f514;香港科技大学工学院2025/2026年度硕士研究生&#xff08;MSc&#xff09;项目招生宣讲会 &#x1f559;时间&#xff1a;2024年9月24日&#xff08;星期二&#xff09;14:30 &#x1f3e0;地点&#xff1a;香港中文大学&#xff08;深圳&#xff09;图书馆培训室…

【时序分析】作业汇编

一、基础知识 时间序列分析就是对一个时间序列进行建模&#xff0c;扣除各种趋势项&#xff08;线性趋势、余弦趋势、有色噪声ARIMA&#xff09;&#xff0c;得到一个白噪声序列&#xff1b;换言之&#xff0c;我们要提取其中的有用信息&#xff08;非白噪声序列&#xff09;&…

Linux 之 RPM [Red - Hat Package Manager]【包管理】

命令符 -i&#xff08;install&#xff09;&#xff1a;安装软件包。--test&#xff1a;测试安装&#xff0c;并不实际安装&#xff0c;只是检查依赖关系等是否满足安装条件。--nodeps&#xff1a;忽略依赖关系进行安装。不过这种方式可能导致软件因缺少依赖而无法正常运行&am…

【论文阅读】Face2Diffusion for Fast and Editable Face Personalization

code&#xff1a;mapooon/Face2Diffusion: [CVPR 2024] Face2Diffusion for Fast and Editable Face Personalization https://arxiv.org/abs/2403.05094 (github.com) 论文 介绍 面部个性化旨在将从图像中获取的特定面部插入到预先训练的文本到图像扩散模型中。然而&#…

linux服务器日常运维开机关机关服务命令

Linux开机关机命令 Linux服务器开机和关机命令 在Linux系统中&#xff0c;开机和关机通常涉及到几个命令&#xff1a; 开机&#xff1a; reboot - 重新启动正在运行的系统。 shutdown -r now - 立即重启系统。 关机&#xff1a; poweroff - 关闭系统并关闭电源。 shutdo…