一文带你学习,动态规划算法

简介: 背包问题(Knapsack problem)是一种组合优化的NP完全问题。 问题可以描述为:给定一组物品,每种物品都有自己的重量和价格,在限定的总重量内,我们如何选择,才能使得物品的总价格最高。 问题的名称来源于如何选择最合适的物品放置于给定背包中。


1.什么是动态规划(DP)


动态规划其实就是,给定一个问题,我们把它拆成一个个子问题,直到子问题可以直接解决。然后呢,把子问题答案保存起来,以减少重复计算。再根据子问题答案反推,得出原问题解的一种方法


一般这些子问题很相似,可以通过函数关系式(DP方程)递推出来。动态规划就致力于解决每个子问题一次,减少重复计算。其核心思想就是:拆分子问题,记住过往,减少重复计算


一个具体的例子:


A : “1+1+1+1+1+1+1+1 =?”

A : “上面等式的值是多少”

B : 计算 “8”

A : 在上面等式的左边写上 “1+” 呢?

A : “此时等式的值为多少”

B : 很快得出答案 “9”

A : “你怎么这么快就知道答案了”

A : “只要在8的基础上加1就行了”

A : “所以你不用重新计算,因为你记住了第一个等式的值为8!动态规划算法也可以说是 ‘记住求过的解来节省时间’”


2.青蛙跳台阶问题


青蛙跳台阶问题



暴力递归解法


要想到第10级台阶,要么是先跳到第9级,然后再跳1级台阶上去;要么是先跳到第8级,然后一次迈2级台阶上去。


同理,要想到第9级台阶,要么是先跳到第8级,然后再跳1级台阶上去;要么是先跳到第7级,然后一次迈2级台阶上去。


要想到第8级台阶,要么是先跳到第7级,然后再跳1级台阶上去;要么是先跳到第6级,然后一次迈2级台阶上去。


假设跳到第n级台阶的跳数我们定义为f(n),很显然就可以得出以下公式:

f(10) = f(9) + f(8)
f (9)  = f(8) + f(7)
f (8)  = f(7) + f(6)
...
f(3) = f(2) + f(1)
即通用公式为: f(n) = f(n-1) + f(n-2)

那f(2) 或者 f(1) 等于多少呢?显然f(2) = 2,f(1) = 1

于是我们萌生了使用暴力递归解题的方法:

class Solution {
public:
    int numWays(int n) {
        if(n == 1 || n == 0) {
            return 1;
        }
        if(n == 2) {
            return 2;
        }
        return (numWays(n-1) + numWays(n-2)) % 1000000007;
    }
};

一发提交直接TLE:



分析算法的时间复杂度:



递归时间复杂度 = 解决一个子问题时间(本例为1)*子问题个数


问题个数 = 递归树节点的总数,递归树的总节点 = 2^n-1,所以是复杂度O(2^n)


本题数据范围n的最大值为100,而2的100次方等于1.2676506e+30,惊人的数字!


自顶向下的递归解法


一般使用一个数组或者一个哈希map充当一个备忘录,保存之前求解过的值,避免重复计算


第一步,f(10)= f(9) + f(8),f(9) 和f(8)都需要计算出来,然后再加到备忘录中,如下:



第二步, f(9) = f(8) + f(7),f(8) = f(7) + f(6), 因为 f(8) 已经在备忘录中啦,所以可以省掉,f(7),f(6)都需要计算出来,加到备忘录中



第三步, f(8) = f(7) + f(6),发现f(8),f(7),f(6)全部都在备忘录上了,所以都可以剪掉



实现算法:

class Solution {
public:
    int arr[100 + 5];  // 这个数组用作备忘录
    int numWays(int n) {
        if(n == 1 || n == 0) {
            return 1;
        }
        if(n == 2) {
            return 2;
        }
        if(arr[n] !=0) {
            return arr[n];
        } else {
            arr[n] = (numWays(n - 1) + numWays(n - 2)) % 1000000007;
            return arr[n];
        }
    }
};


自底向上的动态规划解法


动态规划跟带备忘录的递归解法基本思想是一致的,都是减少重复计算,时间复杂度也都是差不多


带备忘录的递归,是从f(10)往f(1)方向延伸求解的,所以也称为自顶向下的解法。

动态规划从较小问题的解,由交叠性质,逐步决策出较大问题的解,它是从f(1)往f(10)方向,往上推求解,所以称为自底向上的解法。



实现算法:

class Solution {
public:
    int dp[100 + 5]; // DP数组
    int numWays(int n) {
        if(n <= 1) {
            return 1;
        }
        dp[0] = 1;
        dp[1] = 1;
        for(int i = 2 ; i <= n ; i++) {
            dp[i] = (dp[i - 1] + dp[i - 2]) % 1000000007;
        }
        return dp[n];
    }
};

算法的世界多么美妙!


3.动态规划的解题套路


如果一个问题,可以把所有可能的答案穷举出来,并且穷举出来后,发现存在重叠子问题,就可以考虑使用动态规划。如最长递增子序列、最小编辑距离、背包问题、凑零钱问题等等,都是动态规划的经典应用场景


穷举分析


当台阶数是1的时候,有一种跳法,f(1) =1

当只有2级台阶时,有两种跳法,第一种是直接跳两级,第二种是先跳一级,然后再跳一级。即f(2) = 2

当台阶是3级时,想跳到第3级台阶,要么是先跳到第2级,然后再跳1级台阶上去,要么是先跳到第 1级,然后一次迈 2 级台阶上去。所以f(3) = f(2) + f(1) =3

当台阶是4级时,想跳到第3级台阶,要么是先跳到第3级,然后再跳1级台阶上去,要么是先跳到第 2级,然后一次迈 2 级台阶上去。所以f(4) = f(3) + f(2) =5



确定边界


f(1) =1,f(2) = 2就是青蛙跳阶的边界,因为我们可以明确这两个结果的准确值


确定最优子结构


n>=3时,已经呈现出规律 f(n) = f(n-1) + f(n-2) ,因此,f(n-1)和f(n-2) 称为 f(n) 的最优子结构


一道动态规划问题,其实就是一个递推问题。假设当前决策结果是f(n),则最优子结构就是要让 f(n-k) 最优,最优子结构性质就是能让转移到n的状态是最优的,并且与后面的决策没有关系,即让后面的决策安心地使用前面的局部最优解的一种性质


写出状态转移方程



4.经典线性DP:数字三角形


IMUSTACM:数字三角形



本题是一道非常经典且历史悠久的动态规划题,其作为算法题出现,最早可以追溯到 1994 年的 IOI(国际信息学奥林匹克竞赛)的 The Triangle。时光飞逝,经过 20 多年的沉淀,往日的国际竞赛题如今已经变成了动态规划的入门必做题,不断督促着我们学习和巩固算法


在每一结点可以选择移动至其左下方的结点或移动至其右下方的结点,一直走到底层,要求找出一条路径,使路径上的数字的和最大。



穷举分析


要求总路径的最大值,就要求出走到最后一排数字每个数字的最大值,再对最后一排结果取最大值


同时,以2为例,走到它有两种情况,一个是来自左上角f(i-1,j-1)7,一个是来自右上角f(i-1,j)4,事实上,我们只要分析出f(i-1,j-1)和f(i-1,j)那个较大取之即可


同理,对于2左上角的7,走到它有两种情况,一个是来自左上角f(i-1,j-1)8,一个是来自右上角f(i-1,j)1



那f(1,1) 等于多少呢?显然f(1,1) =数字三角形的第一个数字


确定边界


f(1,1) =a(1,1)就是数字三角形的边界,因为我们可以明确这两个结果的准确值


确定最优子结构,写出状态转移方程

max(f[i-1][j-1] + a[i][j] , f[i-1][j] + a[i][j])

题解代码:

#include <bits/stdc++.h>
using namespace std;
const int N=110;
int a[N][N],f[N][N];
int main()
{
    int n;
    scanf("%d",&n);
    // 输入数字三角形
    for(int i = 1 ;i <= n ; i++)
        for(int j = 1 ; j <= i ; j++)
            scanf("%d",&a[i][j]);
    f[1][1] = a[1][1];
    for(int i = 2 ; i <= n ; i++)
        for(int j = 1 ; j <= i ; j++)
            f[i][j] = max(f[i-1][j-1] + a[i][j] , f[i-1][j] + a[i][j]);
    // 求解最后一排数字的最大值得到结果
    int res = -0x3f3f3f3f;
    for(int i = 1 ; i <= n ; i++)
        res = max(res , f[n][i]);
    printf("%d",res);
    return 0;
}


5.01背包:摘花生


背包问题(Knapsack problem)是一种组合优化的NP完全问题。 问题可以描述为:给定一组物品,每种物品都有自己的重量和价格,在限定的总重量内,我们如何选择,才能使得物品的总价格最高。 问题的名称来源于如何选择最合适的物品放置于给定背包中。


IMUSTACM:摘花生



题解代码:

#include <bits/stdc++.h>
using namespace std;
const int N = 110;
int f[N][N],w[N][N];
int main()
{
    ios::sync_with_stdio(false);
    cin.tie(0); cout.tie(0);
    int t,r,c;
    cin >> t;
    while(t--)
    {
        cin >> r >> c;
        for(int i = 1 ; i <= r ; i++)
            for(int j = 1 ; j <= c ; j++)
                cin >> w[i][j];
        for(int i = 1 ; i <= r ; i++)
            for(int j = 1 ; j <= c ; j++)
                f[i][j] = max(f[i-1][j] , f[i][j-1]) + w[i][j];
        cout << f[r][c] << endl;
    }
    return 0;
}
目录
相关文章
|
2月前
|
算法 开发者 Python
惊呆了!Python算法设计与分析,分治法、贪心、动态规划...这些你都会了吗?不会?那还不快来学!
【7月更文挑战第10天】探索编程巅峰,算法至关重要。Python以其易读性成为学习算法的首选。分治法,如归并排序,将大问题拆解;贪心算法,如找零问题,每步求局部最优;动态规划,如斐波那契数列,利用子问题解。通过示例代码,理解并掌握这些算法,提升编程技能,面对挑战更加从容。动手实践,体验算法的神奇力量吧!
60 8
|
2月前
|
算法 Python
算法不再难!Python分治法、贪心、动态规划实战解析,轻松应对各种算法挑战!
【7月更文挑战第8天】掌握Python算法三剑客:分治、贪心、动态规划。分治如归并排序,将大问题拆解递归解决;贪心策略在每步选最优解,如高效找零;动态规划利用子问题解,避免重复计算,解决最长公共子序列问题。实例展示,助你轻松驾驭算法!**
50 3
|
1月前
|
机器学习/深度学习 人工智能 资源调度
【博士每天一篇文献-算法】连续学习算法之HAT: Overcoming catastrophic forgetting with hard attention to the task
本文介绍了一种名为Hard Attention to the Task (HAT)的连续学习算法,通过学习几乎二值的注意力向量来克服灾难性遗忘问题,同时不影响当前任务的学习,并通过实验验证了其在减少遗忘方面的有效性。
39 12
|
29天前
|
算法 Java
掌握算法学习之字符串经典用法
文章总结了字符串在算法领域的经典用法,特别是通过双指针法来实现字符串的反转操作,并提供了LeetCode上相关题目的Java代码实现,强调了掌握这些技巧对于提升算法思维的重要性。
|
30天前
|
机器学习/深度学习 算法 Java
算法设计(动态规划应用实验报告)实现基于贪婪技术思想的Prim算法、Dijkstra算法
这篇文章介绍了基于贪婪技术思想的Prim算法和Dijkstra算法,包括它们的伪代码描述、Java源代码实现、时间效率分析,并展示了算法的测试用例结果,使读者对贪婪技术及其应用有了更深入的理解。
算法设计(动态规划应用实验报告)实现基于贪婪技术思想的Prim算法、Dijkstra算法
|
30天前
|
算法 Java 测试技术
算法设计(动态规划实验报告) 基于动态规划的背包问题、Warshall算法和Floyd算法
这篇文章介绍了基于动态规划法的三种算法:解决背包问题的递归和自底向上实现、Warshall算法和Floyd算法,并提供了它们的伪代码、Java源代码实现以及时间效率分析。
算法设计(动态规划实验报告) 基于动态规划的背包问题、Warshall算法和Floyd算法
|
1月前
|
算法 NoSQL 中间件
go语言后端开发学习(六) ——基于雪花算法生成用户ID
本文介绍了分布式ID生成中的Snowflake(雪花)算法。为解决用户ID安全性与唯一性问题,Snowflake算法生成的ID具备全局唯一性、递增性、高可用性和高性能性等特点。64位ID由符号位(固定为0)、41位时间戳、10位标识位(含数据中心与机器ID)及12位序列号组成。面对ID重复风险,可通过预分配、动态或统一分配标识位解决。Go语言实现示例展示了如何使用第三方包`sonyflake`生成ID,确保不同节点产生的ID始终唯一。
go语言后端开发学习(六) ——基于雪花算法生成用户ID
|
1月前
|
存储 机器学习/深度学习 算法
【博士每天一篇文献-算法】连续学习算法之HNet:Continual learning with hypernetworks
本文提出了一种基于任务条件超网络(Hypernetworks)的持续学习模型,通过超网络生成目标网络权重并结合正则化技术减少灾难性遗忘,实现有效的任务顺序学习与长期记忆保持。
24 4
|
1月前
|
存储 机器学习/深度学习 算法
【博士每天一篇文献-算法】连续学习算法之RWalk:Riemannian Walk for Incremental Learning Understanding
RWalk算法是一种增量学习框架,通过结合EWC++和修改版的Path Integral算法,并采用不同的采样策略存储先前任务的代表性子集,以量化和平衡遗忘和固执,实现在学习新任务的同时保留旧任务的知识。
67 3
|
1月前
|
存储 机器学习/深度学习 算法
【博士每天一篇文献-综述】基于脑启发的连续学习算法有哪些?附思维导图
这篇博客文章总结了连续学习的分类,包括经典方法(重放、正则化和稀疏化方法)和脑启发方法(突触启发、双系统启发、睡眠启发和模块化启发方法),并讨论了它们在解决灾难性遗忘问题上的优势和局限性。
22 2