动态规划真的有那么抽象吗?(递推算法还是动态规划别傻傻分不清了) 以正确的姿势学习动态规划 (入门篇)

简介: 动态规划真的有那么抽象吗?(递推算法还是动态规划别傻傻分不清了) 以正确的姿势学习动态规划 (入门篇)

一. 从单纯的递推算法推开状态的一扇门

递推类的题目处理过程:


递推状态定义    (核心, 决定递推公式)  f[x]  : 符号表达式, 加上这个表达式子的定义

确定递推公式  (k i-----> k i+1)  确定f[x]  究竟依赖哪些 f[y]

初始化init  (递推初始状态)

循环或者递归程序实现

按照上述方式对于题目进行刨析:


70. 爬楼梯


假设你正在爬楼梯。需要 n 阶你才能到达楼顶。


每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?

  • 状态定义: dp[i] : 爬上 i 阶楼梯的方法数目
  • 状态转移方程: 分析依赖关系 ??    每一次可以爬上1 或者 2 个台阶, 所以 当你站上  i 阶级台阶的时候, 你的上一个状态 可以是站在 i - 1 阶台阶上, 或者是 i - 2 阶级台阶上的.  图解转移:

  • init : f0] = 1;    //只有一个方法, 最开始没爬,     f[1] = 1   从0层向上爬一层
class Solution {
public:
    int climbStairs(int n) {
        int f[n + 1];
        //init:
        f[0] = 1; f[1] = 1;
        for (int i = 2; i <= n; ++i) {
            f[i] = f[i - 1] + f[i - 2];
        }
        return f[n]; 
    }
};

二. 动态规划  和 递推算法 究竟是何关系, 动态规划为何是一种特殊的递推算法?

正如上图所示那样 : 动态规化一定是递推问题, 但是递推问题不一定是动态规划


动态规划是一种决策性的问题....    是在状态中做最优决策的一种特殊的递推算法....

此处先通过一道和上述递推问题及其类似的一道题目上手分析..  最后得出讨论

746. 使用最小花费爬楼梯


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


你可以选择从下标为 0 或下标为 1 的台阶开始爬楼梯。


请你计算并返回达到楼梯顶部的最低花费。


竟然动态规划是一种特殊的递推问题, 自然肯定是存在状态定义的, 状态定义是解决动态规划的核心, 状态定义定义的好, 会使得动态规划的状态转移方程简单许多...    (后序会一一解释)

定义状态: dp[i] : 爬上 i 阶台阶所需的最小花费

状态转移分析:  

initial :  dp[0] = 0;     dp[1] = 0;      起始楼梯, 没有花销:

class Solution {
public:
    int minCostClimbingStairs(vector<int>& cost) {
        int n = cost.size();
        int dp[n + 1];
        //init 
        dp[0] = 0; dp[1] = 0;
        for (int i = 2; i <= n; ++i) {
            dp[i] = min(dp[i - 1] + cost[i - 1], dp[i - 2] + cost[i - 2]);
            //决策
        }
        return dp[n];
    }
};

总结动态规划套路分析:

  • 状态定义:  dp[i] 符号表达式, 加上含义解释
  • 状态转移方程确定.  (状态决策过程)  决策最优
  • init: 初始化最初状态
  • 数学归纳法判断转移方程是否正确, (初学鸡肋, 高手有用, 检查)

三. 动态规划继续研究状态定义和状态转移,通过题目去看看定义和转移过程, 顺便引出状态树。。。。

剑指 Offer II 100. 三角形中最小路径之和


给定一个三角形 triangle ,找出自顶向下的最小路径和。


每一步只能移动到下一行中相邻的结点上。相邻的结点 在这里指的是 下标 与 上一层结点下标 相同或者等于 上一层结点下标 + 1 的两个结点。也就是说,如果正位于当前行的下标 i ,那么下一步可以移动到下一行的下标 i 或 i + 1 。


图解思路:

  • init :     dp[0][0] = cost[0][0];
  • 转移方程  : dp[i][j] = min(dp[i - 1][j] + cost[i][j], dp[i - 1][j - 1] + cost[i][j]);
class Solution {
public:
    int minimumTotal(vector<vector<int>>& cost) {
        int n = cost.size();
        int dp[n][n];
        //init
        dp[0][0] = cost[0][0];
        for (int i = 1; i < n; ++i) {       //行
            for (int j = 0; j <= i; ++j) {
                if (j == 0) dp[i][j] = dp[i - 1][j] + cost[i][j];
                else if (j == i) dp[i][j] = dp[i - 1][j - 1]  + cost[i][j];
                else {
                    dp[i][j] = min(dp[i - 1][j], dp[i - 1][j - 1] ) + cost[i][j]; 
                    //状态抉择
                }             
            }
        }
        int ans = 0x3f3f3f3f;
        for (int i = 0; i < n; ++i) {   
            ans = min(ans, dp[n - 1][i]);       //从最下面一层中抉择结果
        }
        return ans;
    }
};

状态从新定义:  看看状态定义好的优势所在:


dp[i][j] : 从最下面一层到 (i, j) 点的 最短路劲和


init : dp[n - 1][i] : 最下面一层的所有点


转移方程:   dp[i][j] = min(dp[i + 1][j], dp[i + 1][j + 1]) + cost[i][j];


上述定义优势总结: 逆向的定义,  正向走和逆向走其实路劲长度是一致的, 但是排除了特殊情况的判断了          

class Solution {
public:
    int minimumTotal(vector<vector<int>>& cost) {
        int n = cost.size();
        int dp[n][n];
        //init
        for (int i = 0; i < n; ++i) {
            dp[n - 1][i] = cost[n - 1][i];
        }
        for (int i = n - 2; i >= 0; --i) {
            for (int j = 0; j <= i; ++j) {
                dp[i][j] = min(dp[i + 1][j], dp[i + 1][j + 1]) + cost[i][j];
            }
        }
        return dp[0][0];
    }
};

三. 再来几道状态定义的练习

198. 打家劫舍

你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。


给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。


状态定义:  dp[i] : 偷窃前面  i  间屋子且 没有偷相邻物资可以偷窃的最大金额

上述状态定义正确吗????????      是否存在信息不足?????      还有一个限制,  相邻的物资不可以偷.    所以偷 第 i 间和 没偷 第 i 间是两个状态。。。。 (状态定义核心关键2: 包含所有状态)    

dp[i][j]  (j = 0 或者1 ):  dp[i][0] :  代表不偷 第 i 间屋子可以获取的最大金额,  dp[i][1] : 表示偷窃  第 i 间屋子可以获取的最大金额

状态转移:   dp[i][0] = max(dp[i - 1][1], dp[i - 1][0]);    不偷第 i 间屋子 就可以偷第 i - 1间屋子,  (注意:是可以)              

dp[i][1] = dp[i - 1][0] + nums[i - 1]    偷 第 i 间屋子的最大金额, 只能从 dp[i - 1][0] 转移过来, 因为偷了  第  i 间屋子, 则 第  i - 1 间屋子一定是没有被偷的状态  (不能同时偷连续的房子,会报警)

class Solution {
public:
    int rob(vector<int>& nums) {
        //状态定义:  还是思考最重要的问题??  状态如何定义?????
        //  dp[i] ????  足够吗??  我们尝试一下定义: 前i家物资  最后第i 家偷盗的最大偷盗金额??
        //  好像不够呀:  因为 这个转移可能是从 前面的  i - 1 不偷转移过来. 也可以是 i - 1偷转移过来. 这样一分析:  中间还需要保存 不偷的结果呀:
        //所以这样一想还是需要二维的定义:
        //dp[i][0] dp[i][1]  就是前面的i个房间  第 i 个房间 不偷和偷的最大偷盗金额
        int n = nums.size();
        int dp[n + 1][2];
        dp[0][0] = dp[0][1] = 0;            //没有房间一定是0结果 init
        for (int i = 1; i <= n; ++i) {
            dp[i][0] = max(dp[i - 1][1], dp[i - 1][0]);
            dp[i][1] = dp[i - 1][0] + nums[i - 1]; //只能是前一个房子不偷
        } 
        return max(dp[n][0], dp[n][1]);
    }
};

53. 最大子数组和剑指 Offer II 091. 粉刷房子


假如有一排房子,共 n 个,每个房子可以被粉刷成红色、蓝色或者绿色这三种颜色中的一种,你需要粉刷所有的房子并且使其相邻的两个房子颜色不能相同。


当然,因为市场上不同颜色油漆的价格不同,所以房子粉刷成不同颜色的花费成本也是不同的。每个房子粉刷成不同颜色的花费是以一个 n x 3 的正整数矩阵 costs 来表示的。


例如,costs[0][0] 表示第 0 号房子粉刷成红色的成本花费;costs[1][2] 表示第 1 号房子粉刷成绿色的花费,以此类推。


请计算出粉刷完所有房子最少的花费成本。


状态定义:   和上一题一样, 核心关键, 状态定义需要包含所有情况, 染不同的颜色就是不同的状态,  所以需要一个二维数组, 第二维度标识颜色


dp[i][j]  (j = 0, 1, 2)  :  含义, 前  i  间屋子, 最后第 i 间 屋子 染成  j  颜色所需要的最小花费


状态转移:    核心关键,  最后 的  i  染的颜色   i - 1 不可以染,  所以   dp[i][j] 依赖的是前面的dp[i - 1][k]   ( k != j)

上述还是比较抽象,  接下来看代码就好懂了:

class Solution {
public:
    int minCost(vector<vector<int>>& costs) {
        //定义状态: dp[i][j] : 前 i 个房子 最后一个粉刷 j 颜色的最小花费
        //依赖关系:   dp[i][j]----> dp[i - 1][k]   k != j 
        int n = costs.size();
        int dp[n + 1][3];
        for (int i = 0; i < 3; ++i) {               //init没有一栋屋子染色
            dp[0][i] = 0;
        } 
        for (int i = 1; i <= n; ++i) {              //枚举屋子进行状态决策
            dp[i][0] = min(dp[i - 1][1] + costs[i - 1][1],
                         dp[i - 1][2] + costs[i - 1][2]);
            dp[i][1] = min(dp[i - 1][0] + costs[i - 1][0], 
                         dp[i - 1][2] + costs[i - 1][2]);
            dp[i][2] = min(dp[i - 1][1] + costs[i - 1][1], 
                          dp[i - 1][0] + costs[i - 1][0]);
        }
        return min(dp[n][0], min(dp[n][1], dp[n][2]));
    }
};

53. 最大子数组和  


输入一个整型数组,数组中的一个或连续多个整数组成一个子数组。求所有子数组的和的最大值。


要求时间复杂度为O(n)。


状态定义: dp[i] :  到 下标 i 为止的子数组的和的最大值


状态转移:  要么连续  要么断开从新开始.....   (连续子数组最大,  只要连续子数组还没成为负数, 就还有可以继续连续,  因为未变成负数之前都可以为后序连续数组变大提供贡献, 除非 < 0 就可以断开了, 要上你只能使得整体变小     品一品这句话的意思, )


依赖状态:  dp[i - 1];  


状态转移方程:  dp[i] = max(dp[i - 1] + nums[i], nums[i]);

class Solution { 
public:
    int maxSubArray(vector<int>& nums) {
        int n = nums.size();
        int dp[n];
        dp[0] = nums[0];//init                          
        int ans = dp[0];
        for (int i = 1; i < n; ++i) {
            dp[i] = max(dp[i - 1] + nums[i], nums[i]);//状态抉择, 连续或者断开
            ans = max(ans, dp[i]);
        }
        return ans;
    }
};

152. 乘积最大子数组


给你一个整数数组 nums ,请你找出数组中乘积最大的非空连续子数组(该子数组中至少包含一个数字),并返回该子数组所对应的乘积。


测试用例的答案是一个 32-位 整数。


子数组 是数组的连续子序列。


状态定义: dp[i][0] 和 dp[i][1] : 分别保存前i 个元组的子数组最小和最大乘积


状态转移方程分析:  因为可能存在一个非常小的负数 * 一个负数让整个连续的子数组乘积变得非常大, 所以我们在整个过程中需要保存最大的连续子数组乘积 同时也需要保存最小的连续子数组的乘积, 所以搞出来一个二维数组, 一维保存最小乘积, 另外一位保存最大乘积


依赖状态: dp[i][0] 和 dp[i][1]  都是依赖: dp[i - 1][0]   dp[i - 1][1]   nums[i - 1]的


接下来的最大值和最小值从:  dp[i - 1][0] * nums[i - 1] , dp[i - 1][1] * nums[i - 1]  以及 nums[i - 1] 三者中选择,             nums[i - 1] 代表断开前面的乘积, 从新开始

class Solution {
    //至于这个题目:  首先还是状态定义:  
    //首先: 这个题目:  我们首先分析: 存在  一个负数  *   负变的非常大, 所以一定保存两个状态:
    //最小值状态  +  最大值状态:  
    //dp[i][0]  和 dp[i][1] 定义:  前面的i个元素的子数组的最小最大乘积
public:
    int maxProduct(vector<int>& nums) {
        int n = nums.size();
        int dp[n + 1][2];
        dp[0][0] = 1, dp[0][1] = 1;         //init
        int ans = INT_MIN;
        for (int i = 1; i <= n; ++i) {
            dp[i][0] = min(dp[i - 1][1] * nums[i - 1], dp[i - 1][0] * nums[i - 1]);
            dp[i][0] = min(dp[i][0], nums[i - 1]);  //代表断开
            dp[i][1] = max(dp[i - 1][1] * nums[i - 1], dp[i - 1][0] * nums[i - 1]);
            dp[i][1] = max(dp[i][1], nums[i - 1]);  //代表断开
            ans = max(dp[i][1], ans);               //ans可能是在中间产生
        }
        return ans;                            //最后返回这个最大值就是
    }
};

四. 总结:

本章通过区分递推算法 和 动态规划 切入,  引出动态规划的本质和  递推算法不同, 动态规划本质上是一种决策性算法,  是需要做决策获取最优解的算法


然后是抓住动态规划算法的核心, 状态定义, 明确状态定义, 时刻认清状态的含义, 进而通过题目含义 结合状态定义  得出  状态转移方程  (做决策)    然后注意init 和  边界.


状态定义一定包含所有状态, 题目中 或多或少 都会暗示状态的定义, 相邻屋子不可以偷, 就有偷或不偷两个状态,  相邻屋子染色不同, 就有屋子染什么颜色的不同状态


下一章 继续 动态规划算法的优化 , 主要是空间优化, 空间压缩的技巧 +  三种经典背包问题的处理,加刷题


相关文章
|
6天前
|
负载均衡 算法
架构学习:7种负载均衡算法策略
四层负载均衡包括数据链路层、网络层和应用层负载均衡。数据链路层通过修改MAC地址转发帧;网络层通过改变IP地址实现数据包转发;应用层有多种策略,如轮循、权重轮循、随机、权重随机、一致性哈希、响应速度和最少连接数均衡,确保请求合理分配到服务器,提升性能与稳定性。
66 11
架构学习:7种负载均衡算法策略
|
1天前
|
算法 Java C++
【潜意识Java】蓝桥杯算法有关的动态规划求解背包问题
本文介绍了经典的0/1背包问题及其动态规划解法。
25 5
|
2月前
|
存储 算法 安全
2024重生之回溯数据结构与算法系列学习之串(12)【无论是王道考研人还是IKUN都能包会的;不然别给我家鸽鸽丟脸好嘛?】
数据结构与算法系列学习之串的定义和基本操作、串的储存结构、基本操作的实现、朴素模式匹配算法、KMP算法等代码举例及图解说明;【含常见的报错问题及其对应的解决方法】你个小黑子;这都学不会;能不能不要给我家鸽鸽丢脸啊~除了会黑我家鸽鸽还会干嘛?!!!
2024重生之回溯数据结构与算法系列学习之串(12)【无论是王道考研人还是IKUN都能包会的;不然别给我家鸽鸽丟脸好嘛?】
|
2月前
|
机器学习/深度学习 人工智能 自然语言处理
【EMNLP2024】基于多轮课程学习的大语言模型蒸馏算法 TAPIR
阿里云人工智能平台 PAI 与复旦大学王鹏教授团队合作,在自然语言处理顶级会议 EMNLP 2024 上发表论文《Distilling Instruction-following Abilities of Large Language Models with Task-aware Curriculum Planning》。
|
2月前
|
算法 Python
在Python编程中,分治法、贪心算法和动态规划是三种重要的算法。分治法通过将大问题分解为小问题,递归解决后合并结果
在Python编程中,分治法、贪心算法和动态规划是三种重要的算法。分治法通过将大问题分解为小问题,递归解决后合并结果;贪心算法在每一步选择局部最优解,追求全局最优;动态规划通过保存子问题的解,避免重复计算,确保全局最优。这三种算法各具特色,适用于不同类型的问题,合理选择能显著提升编程效率。
71 2
|
2月前
|
算法 安全 搜索推荐
2024重生之回溯数据结构与算法系列学习之单双链表精题详解(9)【无论是王道考研人还是IKUN都能包会的;不然别给我家鸽鸽丢脸好嘛?】
数据结构王道第2.3章之IKUN和I原达人之数据结构与算法系列学习x单双链表精题详解、数据结构、C++、排序算法、java、动态规划你个小黑子;这都学不会;能不能不要给我家鸽鸽丢脸啊~除了会黑我家鸽鸽还会干嘛?!!!
|
2月前
|
算法 安全 NoSQL
2024重生之回溯数据结构与算法系列学习之栈和队列精题汇总(10)【无论是王道考研人还是IKUN都能包会的;不然别给我家鸽鸽丢脸好嘛?】
数据结构王道第3章之IKUN和I原达人之数据结构与算法系列学习栈与队列精题详解、数据结构、C++、排序算法、java、动态规划你个小黑子;这都学不会;能不能不要给我家鸽鸽丢脸啊~除了会黑我家鸽鸽还会干嘛?!!!
|
2月前
|
机器学习/深度学习 算法 Python
机器学习入门:理解并实现K-近邻算法
机器学习入门:理解并实现K-近邻算法
46 0
|
4天前
|
算法 数据安全/隐私保护 计算机视觉
基于Retinex算法的图像去雾matlab仿真
本项目展示了基于Retinex算法的图像去雾技术。完整程序运行效果无水印,使用Matlab2022a开发。核心代码包含详细中文注释和操作步骤视频。Retinex理论由Edwin Land提出,旨在分离图像的光照和反射分量,增强图像对比度、颜色和细节,尤其在雾天条件下表现优异,有效解决图像去雾问题。
|
4天前
|
算法 数据可视化 安全
基于DWA优化算法的机器人路径规划matlab仿真
本项目基于DWA优化算法实现机器人路径规划的MATLAB仿真,适用于动态环境下的自主导航。使用MATLAB2022A版本运行,展示路径规划和预测结果。核心代码通过散点图和轨迹图可视化路径点及预测路径。DWA算法通过定义速度空间、采样候选动作并评估其优劣(目标方向性、障碍物距离、速度一致性),实时调整机器人运动参数,确保安全避障并接近目标。

热门文章

最新文章