【面试/竞赛必备算法】动态规划

这里是KK爱Coding 团队的paoxiaomo,一个现役ACMer,之后将会持续更新算法笔记系列以及笔试题题解系列

本文章面向想打ICPC/蓝桥杯/天梯赛等程序设计竞赛,以及各个大厂笔试的选手

各大大厂近年笔试题汇总以及题解合集可以私信 KK爱Coding

感谢大家的订阅➕ 和 喜欢💗

动态规划是一种解决多阶段决策过程最优化问题的数学方法。不论是信息学竞赛还是大厂面试题都是常客,今天就来讲解一下主要的做题步骤,并通过一些例题来帮助各位理解。

  1. 理解问题:首先,要充分理解问题的要求和限制条件。明确问题是什么,需要求解的是什么,以及问题的约束条件是什么。

  2. 定义状态:动态规划的核心在于定义合适的状态。状态应该包含问题的关键信息,并且能够刻画问题的特征。通常情况下,状态可以是一个或多个变量的组合。

  3. 找到状态转移方程:一旦定义了状态,接下来就是确定状态之间的转移关系,即状态转移方程。状态转移方程描述了问题从一个状态转移到下一个状态的过程,并且可以通过递推的方式求解最终的解。

  4. 初始化:在开始状态转移之前,需要初始化一些状态。这些初始化的状态通常是最简单、最基础的状态,为后续的状态转移提供基础。

  5. 状态转移:根据状态转移方程,通过递推或迭代的方式求解出所有状态的值。这个过程可以采用自底向上的方法,从最简单的状态开始逐步求解,也可以采用自顶向下的方法,从问题的初始状态开始递归地求解。

  6. 求解目标状态:一旦所有状态的值都求解出来了,就可以根据问题的要求找到目标状态的值,即最终的解。

  7. 分析复杂度:最后,要分析算法的时间复杂度和空间复杂度,确保算法在实际应用中的可行性。

直接看这些步骤可能会不太明白,所以我们可以先通过几道例题来进行讲解。

首先就是最经典的背包问题:

01背包问题

给定一个背包,其容量为m,以及一组物品,每个物品有两个属性:重量(weight)和价值(value)。现在需要从这些物品中选择一些放入背包中,使得放入背包的物品总重量不超过背包容量,同时使得放入背包的物品总价值最大。每个物品只能选择放入一次(即要么放入背包,要么不放入)。

01背包问题解题过程:

  1. 定义状态:定义一个二维数组dp[i][j],其中dp[i][j]表示在前i个物品中选择,且背包容量为j时,能够获得的最大价值。

  2. 找到状态转移方程:对于每个物品i,考虑两种情况:

    • 如果不放入第i个物品,则dp[i][j] = dp[i-1][j]。
    • 如果放入第i个物品,则dp[i][j] = dp[i-1][j-weight[i]] + value[i]。 最终,取这两种情况中的最大值作为dp[i][j]的值。
  3. 初始化:将dp数组初始化为0,即dp[0][j] = 0和dp[i][0] = 0。

  4. 状态转移:根据状态转移方程,从第一个物品开始逐步计算dp数组的值,直到计算出dp[n][m],其中n表示物品的数量。

  5. 求解目标状态:由于背包可以不放满,所以dp[n][m]不一定为最优解,我们直接遍历整个数组dp[n]寻找最大值即为最优解。

  6. 分析复杂度:动态规划算法的时间复杂度为O(nm),其中n为物品的数量,m为背包的容量。

参考代码(cpp)

 int n,m;cin>>n>>m;
    vector<vector<int>> dp(n + 1, vector<int>(m + 1, 0));
    for(int i=1;i<=n;i++){
        int weight,value;
        cin>>weight>>value;
        for(int j=m;j>=0;j--){
            if(j<weight){//无法放入背包,则此时什么都干不了
                dp[i][j]=dp[i-1][j];
            }else{//如果能放入背包,则有两个选择,即两个转移式,要么不放,要么找一个正好比当前枚举到的状态小weight的背包放入
                dp[i][j]=max(dp[i-1][j],dp[i-1][j-weight]+value);
            }
        }
    }int ans=0;
    for(int i=0;i<=m;i++)ans=max(ans,dp[n][i]);
    cout<<ans<<endl;

还有一个最适合理解动态规划的题就是最长公共子序列:

最长公共子序列(LCS)

  1. 定义状态:我们可以定义一个二维数组dp[i][j],其中dp[i][j]表示字符串s1的前i个字符与字符串s2的前j个字符的最长公共子序列的长度。

  2. 找到状态转移方程:对于字符串s1的第i个字符和字符串s2的第j个字符:

    • 如果两个字符相等,则dp[i][j] = dp[i-1][j-1] + 1。
    • 如果两个字符不相等,则dp[i][j] = max(dp[i-1][j], dp[i][j-1])。 最终,dp[m][n]即为问题的解,其中m为字符串s1的长度,n为字符串s2的长度。
  3. 初始化:将dp数组初始化为0,即dp[i][0] = 0和dp[0][j] = 0。

  4. 状态转移:根据状态转移方程,从第一个字符开始逐步计算dp数组的值,直到计算出dp[m][n],其中m和n分别为字符串s1和s2的长度。

  5. 求解目标状态:dp[m][n]即为问题的解,表示字符串s1和s2的最长公共子序列的长度。

  6. 分析复杂度:动态规划算法的时间复杂度为O(mn),其中m和n分别为字符串s1和s2的长度。

     string s1,s2;
    cin>>s1>>s2;
    int n=s1.size(),m=s2.size();
    // 创建动态规划数组,dp[i][j]表示s1的前i个字符与s2的前j个字符的最长公共子序列的长度
    vector<vector<int>> dp(m + 1, vector<int>(n + 1, 0));
    for (int i = 1; i <= m; ++i) {
        for (int j = 1; j <= n; ++j) {
            if (s1[i - 1] == s2[j - 1]) {
                dp[i][j] = dp[i - 1][j - 1] + 1;
            } else {
                dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);
            }
        }
    }
    cout<<dp[m][n]<<endl;

最后还有个经典的一维动态规划问题:

青蛙跳台阶

有一只青蛙,它每次可以跳上1级台阶或2级台阶。如果台阶数为n,求青蛙跳上这个n级台阶总共有多少种跳法。

  1. 定义状态:定义一个一维数组dp[i],其中dp[i]表示跳上第i级台阶的总跳法。

  2. 找到状态转移方程:青蛙跳上第i级台阶可以从第i-1级台阶跳上来,也可以从第i-2级台阶跳上来。因此,状态转移方程为:dp[i] = dp[i-1] + dp[i-2]。

  3. 初始化:由于青蛙跳上1级台阶只有一种跳法(跳1级),跳上2级台阶有两种跳法(跳2次1级或一次跳2级),所以可以初始化dp[1] = 1和dp[2] = 2。

  4. 状态转移:根据状态转移方程,从第3级台阶开始逐步计算dp数组的值,直到计算出dp[n],其中n为台阶的总数。

  5. 求解目标状态:dp[n]即为问题的解,表示青蛙跳上n级台阶的总跳法。

  6. 分析复杂度:动态规划算法的时间复杂度为O(n),其中n为台阶的总数。

int n;cin>>n;
     if(n<3){
        cout<<n<<endl;exit(0);
     }
    vector<int> dp(n + 1, 0);
    // 初始化前两级台阶的跳法
    dp[1] = 1;
    dp[2] = 2;
    // 计算跳上第i级台阶的总跳法
    for (int i = 3; i <= n; ++i) {
        dp[i] = dp[i - 1] + dp[i - 2];
    }
    // 返回跳上n级台阶的总跳法
    cout<<dp[n]<<endl;

不积硅步,无以至千里,只有学好了这些简单的动态规划入门题,才有机会在比赛/笔试中解决难题。

  • 10
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值