算法题中的动态规划

首先了解下背包问题,动态规划的一个实例就是解决背包问题。

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

组合优化:就是在一个有限的对象集中找到最优对象的一类课题,组合优化的问题特征是可行解的集是离散或者可以简化到离散,目标是找到最优解。

条件:有n种物品,物品j的重量为wj,价格为pj。我们假定所有物品的重量和价格都是非负的。背包所能承受的最大重量为W

  • 0-1背包问题:限定每种物体只能选择0个或1个
  • 有界背包问题:限定物品j最多只能选择bj个
  • 无界背包问题:不限定每种物品的数量

动态规划#

通过把原问题分解为相对简单的子问题的方式求解复杂问题的方法

适用情况#

  1. 最优子结构性质。如果问题的最优解所包含的子问题的解也是最优的,我们就称该问题具有最优子结构性质(即满足最优化原理)。最优子结构性质为动态规划算法解决问题提供了重要线索。
  2. 无后效性。即子问题的解一旦确定,就不再改变,不受在这之后、包含它的更大的问题的求解决策影响。
  3. 子问题重叠性质。子问题重叠性质是指在用递归算法自顶向下对问题进行求解时,每次产生的子问题并不总是新问题,有些子问题会被重复计算多次。动态规划算法正是利用了这种子问题的重叠性质,对每一个子问题只计算一次,然后将其计算结果保存在一个表格中,当再次需要计算已经计算过的子问题时,只是在表格中简单地查看一下结果,从而获得较高的效率。

实例#

斐波那契数列

用文字来说,斐波那契数列就是从0和1开始,之后的斐波那契数列就是由之前两数相加而得出(简单的说就是递归的方式定义)。

 function fib(n)
       if n = 0 or n = 1
           return n
       return fib(n − 1) + fib(n − 2

这就是以递归的方式计算第n个值,但是这种递归的方式拆分开就会发现做了重复的计算,每一个n(n>1)最后都会变成f(0)和f(1)相加的结果,这种算法的运算时间是以指数级增长的。采用的动态规划优化的方式就是将前n个已经算出的数保存在数组中,在后面的计算中直接应用前面的结果,从而避免了重复计算。算法的运算时间变为O(n)。

array map [0...n] = { 0 => 0, 1 => 1 }
fib(n)
    ifmap m does not contain key n)
        m[n] := fib(n − 1) + fib(n − 2return m[n]
背包问题

动态规划,可以用伪多项式时间解决背包问题。

  • 无界背包

    我们假定重量都是正数(wj > 0)。在总重量不超过W的前提下,我们希望总价格最高。对于YW,我们将在总重量不超过Y的前提下,总价格所能达到的最高值定义为A(Y)。A(W)即为问题的答案。

    显然,A(Y)满足:

    • A(0) = 0
    • A(Y) = max { A(Y - 1), { pj + A(Y - wj) | wjY } }

    其中,pj为第j种物品的价格。关于第二个公式的一个解释:总重量为Y时背包的最高价值可能有两种情况,第一种是该重量无法被完全填满,这对应于表达式A(Y - 1)。第二种是刚好填满,这对应于一个包含一系列刚好填满的可能性的集合,其中的可能性是指当最后放进包中的物品恰好是重量为wj的物品时背包填满并达到最高价值。而这时的背包价值等于重量为wj物品的价值和当没有放入该物品时背包的最高价值之和。故归纳为表达式pj + A(Y - wj)。最后把所有上述情况中背包价值的最大值求出就得到了A(Y)的值。

    复杂度分析:如果总重量为0,总价值也为0。然后依次计算A(0), A(1), ..., A(W),并把每一步骤的结果存入表中供后续步骤使用,完成这些步骤后A(W)即为最终结果。由于每次计算A(Y)都需要检查n种物品,并且需要计算WA(Y)值,因此动态规划解法的时间复杂度为O(nW)

  • 0-1背包问题

    同样的前提:假定w1, ..., wnW都是正整数。我们将在总重量不超过Y的前提下,前j种物品的总价格所能达到的最高值定义为A(j, Y)。

    A(j, Y)的递推关系为:

    • A(0, Y) = 0
    • 如果wj > Y, A(j, Y) = A(j - 1, Y)
    • 如果wjY, A(j, Y) = max { A(j - 1, Y), pj + A(j - 1, Y - wj)}

    通过计算A(n, W)即得到最终结果。为提高算法性能,我们把先前计算的结果存入表中。因此算法需要的时间和空间都为O(nW),通过对算法的改进,空间的消耗可以降至O(W)。

简单的面试题#

题目:有1分2分5分的硬币组成1角,共有多少种组合?

  • 第一种解法(也是最直观的解法):暴力枚举法,就是直接定义循环累加或者递归

    void main(){
      int n = 0;
      // 5分硬币最多有i个
      for (int i=0; i<3; i++)
      {
          // 2分硬币最多有10-5*i个
          for (int j=0; j<=(10-5*i)/2; j++)
          {
              // 1分硬币的个数
              for (int k=0; k<= 10 - 5*i - 2*j; k++)
              {
                  if (10 == 5*i + 2*j +k)
                  {
                      n++;
                      printf("5分:%d个,2分:%d个,1分:%d个",i,j,k);
                  }
              }
          }
       }
       printf("所有组合有%d种",n);
    
    int fun(s,n){
        int count =0;
        int a[3] = { 1,2,5 };
        if(n>2){
            if(s == 0) return 1;
            else return 0;
        }else{
            for(int i=0;s>=i*a[n];i++){
                count += fun(s-i*a[n],n+1);
            }
        }
        return count;
    }
    int main(){
        printf("所有组合有%d种",fun(10,0));
        return 0;
    }
    

    递归的方法我也没有理解。

  • 第二种解法:

    简单分析,不难看出这就是个无界背包问题,同时也是Fibonacci的动态规划解法。

    int coinCombinations(int coins[], int coinKinds, int sum)
    {
        vector<vector<int> > dp(coinKinds + 1, vector<int>(sum+1,0));
    
        for (int i = 0; i <= coinKinds; ++i)    //递推初始条件
        {
            dp[i][0] = 1;
        }
        for (int i = 1; i <= coinKinds; ++i)
        {
            for (int j = 1; j <= sum; ++j)
            {
                dp[i][j] = 0;
                //j / coins[i-1]表示能取的该硬币的最大数量。
                for (int k = 0; k <= j / coins[i-1]; ++k)   //i-1是因为coins是从0开始算起的。
                {
                    dp[i][j] += dp[i-1][j - k * coins[i-1]];
                }
            }
        }
    
        return dp[coinKinds][sum];
    }
    

    dp[i][sum] = 用前i种硬币构成sum 的所有组合数。

    状态转移方程:
    dp[i][sum] = dp[i-1][sum - 0*Vm] + dp[i-1][sum - 1*Vm] + dp[i-1][sum - 2*Vm] + … + dp[i-1][sum - K*Vm]; 即前i中硬币构成sum的组合数是由前i-1中硬币构成sum中减去第i种硬币使用次数乘以币值的差的组合数累加。

    其中K = sum / Vm,dp[i][0] = 1 where sum= 0 ,dp[0][sum] = 0 where i=0

    其实本题就是Fibonacci问题,对应1分2分5分的硬币三种,组合成1角,共有多少种组合? 这个问题就是:

    f(n) = f(n-1) + f(n-2) + f(n-5) .其中f(0) = 1 , f(1) = 1, f(2) = 2, f(3) = 2, f(4) = 3.

    还有一种类似无界背包问题解决的算法(我没有理解):

    int main(){
        int weight[] =[1,2,5];
        int dp[0]=1;
        for(int i=0;i<3;i++){
            for(int j=weight[i];j<10;j++){
                dp[j] += dp[j-weight[i]];
            }
        }
        pritf("所有组合有%d种",dp[10]);
        return 0;
    }
    

作者:EGBDFACE

出处:https://www.cnblogs.com/EGBDFACE/p/16271443.html

版权:本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」许可协议进行许可。

posted @   EGBDFACE  阅读(33)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
more_horiz
keyboard_arrow_up dark_mode palette
选择主题
menu
点击右上角即可分享
微信分享提示