赞
踩
动态规划算法是一个分治的方法,把重叠子问题从底层到顶层拆解后,基于已经求解的子问题来求解目标问题的算法,过程清晰明了,且具有记忆化功能,在某些问题中可以避免很多重复计算,效率高,故受到很多程序员的青睐。
考虑斐波那契数列的算法:
- #include <vector>
- #include <iostream>
- using namespace std;
-
-
- int fib(int n) {
- if (n == 1) return 1;
- if (n == 2) return 1;
- return fib(n - 1) + fib(n - 2);
- }
-
-
- int main() {
- int result = fib(10);
- cout << result;
- }
上面的这个算法是从 n = 10 开始一步一步递归,直到 n = 1的时候停止,从终态(未知态)开始推回起始状态(已知状态),就是从顶层到底层的算法。
- #include <vector>
- #include <iostream>
- using namespace std;
- int fib(int n) {
- if (n <= 0) exit(EXIT_FAILURE); // 防止输入无效数据
- if (n == 1) return 1; // 特殊数据
- // 创建容器vec并进行初始化
- vector<int> vec(n + 1, 1);
- vec[0] = 0;
- // 初始化结束
-
- for (int i = 2; i <= n; i++)
- {
- vec[i] = vec[i - 1] + vec[i - 2];
- }
- return vec[n];
-
- }
-
- int main() {
- int result = fib(10);
- cout << result;
- }
而这个算法是先确定所需要的数据,列表填表,从最底层的数据开始一点点的向上推导,直到达到题目所需要求的值或者找到这个问题的路径,这个就是动态规划的核心思想——分治法。
普通的递归算法就是自上而下的递归算法,动态规划对比其的优点就是列表一般都是提前规划好的,是记忆化的计算,且在数据庞杂的背景下减少了很多没有意义的风险,从而时间效率高,没有堆栈溢出的风险。缺点就是对比普通递归算法而言较复杂且很考验程序员的思维,如果填表部分没有规划好容易造成空间上的损失。
1.状态表示
2.写出状态转移方程
3.初始化
4.填表
5.返回值
题目链接:. - 力扣(LeetCode)
以LeetCode377. 组合总和 Ⅳ为例:
状态表示为达到某中间态的路径数,状态转移方程是nums数组中某一个数(总路径数记为1)或者某中间态(总路径数记为表中对应索引的值)的和,起始状态下,如果nums中没有元素加起来的值可以刚好等于目标值,则为0,所以我们可以把表中的值全部初始化为0,但是又要考虑当目标值为0时,已知我们初始化为0,且题目给出条件是nums中每个数都至少大于0,且为整数,所以得到nums中所有元素全部是正整数。所以接下来我们只需要分析每个中间态的路径数然后把每两个中间态相加得到下一个总路径数(例 : dp[5] = dp[2] + dp[3]),返回dp[target]即可得到最终结果。
代码如下:
- class Solution {
- public:
- int combinationSum4(vector<int>& nums, int target) {
- vector<unsigned int> dp(target + 1, 0);
- // 初始化dp数组,当target的值为0时,因为题给条件为nums数组里面每一个数都是正数,所以可以初始化dp[0]为1
- dp[0] = 1;
- for (int i = 1; i <= target; i++)
- {
- for (int& num : nums)
- {
- if (num <= i)
- {
- dp[i] += dp[i - num];
- }
- }
- }
- return dp[target];
- }
- };
最终代码是正确的,但是中间对中间态进行求解的过程中,索引是从1开始加1,假设target是接近于INT_MAX的值,且nums是一个数据相差较大的稀疏向量呢?这就比较浪费时间和空间了,这就需要用到接下来的方法了。
在上文的环境下,如果数据足够庞大或者数据够稀疏的情况下,从1开始递加1直到target会做出很多无效的操作,每次中间态的增加的量不会小于nums的最小值,所以不在这个区间的中间态是没有用的,可以省略。此时可以很轻松的想到哈希表。而C++中给出了两种哈希表的底层容器。但<unordered_map>是按照键值来排列的,大概率并不一定是添加顺序。所以为了确保确定性,需要使用map来作为容器。
代码如下:
- class Solution {
- public:
- int combinationSum4(vector<int>& nums, int target) {
- map<int, int> dpTable;
- dpTable[0] = 1; // 初始化dpTable[0] = 1
-
-
- // 动态规划计算,题目给出nums向量中不会有负数和0
- map<int, int>::iterator it = dpTable.begin();//前面的int是键值,后面的int是值
- while (it != dpTable.end())
- {
- for (int num : nums)
- {
- // 进行初始化,如果 num > target,则其值必为0,可以忽略
- // 所以需要判断是否满足 num <= target,其值才能为有效数值
- if (num <= target)
- {
- // 先判断 原键值 和 num 加起来是否会爆范围,是否小于 target,再对值进行爆范围的判断。
- // 一定要先判断是否溢出!!否则程序会报错无法运行!!
- // 由于map的特性,没有的键会被自动创建,并且其值的值会被初始化为0
-
-
- if (INT_MAX - it->first >= num && target - it->first >= num && INT_MAX - dpTable[it->first] >= dpTable[num + it->first])
- {
- dpTable[num + it->first] += dpTable[it->first];
- }
-
- }
-
- }
- it++;
- }
- return dpTable[target];
- }
- };
本文是本系列的第一篇文章,仅作为一个浅浅的引入,后续肯定会分享更多的算法方面的题。如果大家喜欢我的文章的话,欢迎点赞收藏关注,谢谢大家!
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。