前两天偶然跟同事复盘,发现之前的功能业务里面的算法写的实在是不堪入目。整个算法结构超过50行...全是if,else🤣。主打的就是一个特殊问题定制讲解,不过这也暴漏了多个问题。
实际上,在很多开发任务中,涉及到功能模块时,因为每个人工作经验的丰富度不同,其写出来的模块所能承载的业务场景数量就不同。但毋庸置疑的是,动态规划的思想一定程度上,是可以让写的功能模块更稳定且优雅~
所以针对此情况,今天特地自己给自己写一篇关于动态规划的思想。
一、简单概括
动规,简而言之就是一种以大问题拆分小问题,小问题以贪心思想拿到当前最优解,存储结果,进而与其他小问题进行比较,直至拿到整个小问题中最优解然后退出。
怎么理解这句话呢?比如,在同一个时刻,小李媳妇让他去送孩子上学,但上司打电话要求小李立刻修复一个影响项目上线BUG,然后此时地上的热水壶又被孩子chuang倒烫到了小李的脚0.o
好了,在整个场景下,让我们动态规划一下,如果你是小李,你要怎么处理该场景?
当然,这个例子不太恰当,毕竟每个人对于每个事情的看待情况不一样,不过这个例子仅仅是一个抽象层面的举例,不必太较真哈~
我们来分析,整体问题就是处理当前影响自己原本计划的事情,这里的问题①送孩子,问题②修复bug,问题③被热水烫伤。
如果拆分成问题结构为,①,①②,①②③。
结合当下社会主流价值观来看,如果仅有问题①,那么就去送孩子就可以了。如果问题是①②,那么肯定是小李修复bug,孩子交给他媳妇去送。如果问题是①②③,那啥也别说,先把脚用冷水冲一下,然后去修复bug,孩子交给他媳妇去送。
这个例子实则也就是在讲,当整个问题出现在面前时,尝试分析局部问题中出现该种情况要如何处理,最后在拿到基于当前问题的最优解即可。
二、具象化举例
这里我们以两道题来做例子吧。先来一道简单的题 牛客网<BM64 最小花费爬楼梯>
题目链接:最小花费爬楼梯_牛客题霸_牛客网
题目要求:给定一个整数数组 cost ,其中 cost[i] 是从楼梯第i \i 个台阶向上爬需要支付的费用,下标从0开始。一旦你支付此费用,即可选择向上爬一个或者两个台阶。你可以选择从下标为 0 或下标为 1 的台阶开始爬楼梯。请你计算并返回达到楼梯顶部的最低花费。
简而言之,就是给你一个数组,你可以从下表为0,1开始爬,每次可以爬一个or两个,但不管爬一个还是爬两个,你需要付费当前下表内的元素价格。直到跃出数组元素个数才算结束。
分析一下这道题不难得出,首先,每一步的子问题在于,我这一个台阶,可以由上一个台阶+1得到,也可以由上上一个台阶+1得到。那我怎么确定我要选哪一个呢?
很简单,看一下如果是由上一个台阶+1得到,我们可以看看(上一个台阶)到达花费的费用加(上一个台阶)所属(数组下标的钱)对比(上上一个台阶)到达花费的费用加(上上一个台阶所属数组下下标的钱)。哪个小,我们就选哪个
具体实现:
int minCostClimbingStairs(vector<int>& cost) {
vector<int> dp(cost.size()+1,0);//存储每层台阶的结果
for(int i = 2;i<=cost.size();i++) {
dp[i] = min(cost[i-1]+dp[i-1],cost[i-2]+dp[i-2]);
}//循环遍历获得从第2阶台阶开始的每一阶最优结果。
return *(dp.end()-1);
}
这里就有大佬说了,你举得这个例子这么简单,忽悠人是吧?
好好好,既然大佬这么说了,那我们稍微再加一点点难度。
题目要求:给定两个字符串str1和str2,输出两个字符串的最长公共子序列。如果最长公共子序列为空,则返回"-1"。目前给出的数据,仅仅会存在一个最长的公共子序列。数据范围:0 \le |str1|,|str2| \le 20000≤∣str1∣,∣str2∣≤2000。空间复杂度 O(n^2)O(n2) ,时间复杂度 O(n^2)O(n2)
简而言之,就是给定两个字符串,要求拿出里面相同的最长的子序列。(子序列不同于字串,他是可以跨字符获取的)
分析一下:在这里,假设有str1,str2。那么我们要做的,就是将两个字符串以二维数组形式进行划分,将相同的子序列个数所在坐标,存储至我们开辟的二维数组中。
啥意思呢?如下图:
有的同学就要问了,啊为什么要空一行啊,列怎么不往左移动一格?
那是因为我们是要用dp[i][j]来存储相同字符的位置,所以要保证初始值dp[0][0] = 0,故开辟的空间大小也应该是Str1.size()+1和Str2.size()+1
具体代码:
class Solution {
public:
string OneStr = "";
string TwoStr = "";
string GetStringResult(size_t i,size_t j,vector<vector<int>>& tempNum)
{
string res = "";
if (i==0 || j==0) {
return res;
}
if (tempNum[i][j]== 1) {
res += GetStringResult(i-1, j-1,tempNum);//相同传入上一个字符位置,继续遍历
res += TwoStr[j-1];//这里也可以写res += OneStr[i-1] 效果是一样的,因为我们每一级的字符位置,存的是上一个字符元素,所以叠加res时,要拿到字符元素内容
}
else if(tempNum[i][j]==2) { res += GetStringResult(i - 1, j, tempNum);//因为是来自于左边,所以我要继续跑到左边直到找到相同字符的标志位}
else if(tempNum[i][j] == 3) { res += GetStringResult(i,j - 1, tempNum);}
return res;
}
string LCS(string s1, string s2) {
OneStr = s1;
TwoStr = s2;
if (s1.length() == 0 || s2.length() ==0) {
return "-1";
}
vector<vector<int>> dp(s1.size()+1,vector<int>(s2.size()+1,0));//存储相同长度大小
vector<vector<int>> tempNum(s1.size()+1,vector<int>(s2.size()+1,0));//存储标志位
for (int i = 1; i<=s1.size();i++) {
for (int j = 1;j<=s2.size();j++) {
if(s1[i-1] == s2[j-1]) {//表示字符相同,暂存位置
dp[i][j] = dp[i-1][j-1]+1;//基于上一个字符相同的长度+1
tempNum[i][j] = 1;//将本级位置设置flag
}
else {
if(dp[i-1][j] > dp[i][j-1]) { //不相同,来自于左边
dp[i][j] = dp[i-1][j];//延续左边的字符长度
tempNum[i][j] = 2;//将本级位置设置flag
} else {
dp[i][j] = dp[i][j-1];//延续上边的字符长度
tempNum[i][j] = 3;//将本级位置设置flag
}
}
}
}
string res = GetStringResult(s1.size(),s2.size(),tempNum);
return res != "" ? res : "-1";
}
};
综上,我们使用抽象和具象化讲解关于动态规划的思想。如有觉得不合理的地方,欢迎找博主讨论