数据结构与算法简记--动态规划

Posted wod-y

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了数据结构与算法简记--动态规划相关的知识,希望对你有一定的参考价值。

动态规划


 

初识

  • 使用动态规划解决回溯算法中的 0-1背包问题:
    • 把整个求解过程分为 n 个阶段,每个阶段会决策一个物品是否放到背包中。
    • 每个物品决策(放入或者不放入背包)完之后,背包中的物品的重量会有多种情况,也就是说,会达到多种不同的状态,对应到递归树中,就是有很多不同的节点。
    • 把每一层重复的状态(节点)合并,只记录不同的状态,然后基于上一层的状态集合,来推导下一层的状态集合。
    • 通过合并每一层重复的状态,这样就保证每一层不同状态的个数都不会超过 w 个(w 表示背包的承载重量),也就是例子中的 9。于是,我们就成功避免了每层状态个数的指数级增长。
    • 时间复杂度是 O(n*w)。n 表示物品个数,w 表示背包可以承载的总重量。
    • weight:物品重量,n:物品个数,w:背包可承载重量
      public int knapsack(int[] weight, int n, int w) {
        boolean[][] states = new boolean[n][w+1]; // 默认值false
        states[0][0] = true;  // 第一行的数据要特殊处理,可以利用哨兵优化
        if (weight[0] <= w) {
          states[0][weight[0]] = true;
        }
        for (int i = 1; i < n; ++i) { // 动态规划状态转移
          for (int j = 0; j <= w; ++j) {// 不把第i个物品放入背包
            if (states[i-1][j] == true) states[i][j] = states[i-1][j];
          }
          for (int j = 0; j <= w-weight[i]; ++j) {//把第i个物品放入背包
            if (states[i-1][j]==true) states[i][j+weight[i]] = true;
          }
        }
        for (int i = w; i >= 0; --i) { // 输出结果
          if (states[n-1][i] == true) return i;
        }
        return 0;
      }

       

    • 优化:降低空间复杂度
    • public static int knapsack2(int[] items, int n, int w) {
        boolean[] states = new boolean[w+1]; // 默认值false
        states[0] = true;  // 第一行的数据要特殊处理,可以利用哨兵优化
        if (items[0] <= w) {
          states[items[0]] = true;
        }
        for (int i = 1; i < n; ++i) { // 动态规划
          for (int j = w-items[i]; j >= 0; --j) {//把第i个物品放入背包
            if (states[j]==true) states[j+items[i]] = true;
          }
        }
        for (int i = w; i >= 0; --i) { // 输出结果
          if (states[i] == true) return i;
        }
        return 0;
      }

       

  • 总结:把问题分解为多个阶段,每个阶段对应一个决策。我们记录每一个阶段可达的状态集合(去掉重复的),然后通过当前阶段的状态集合,来推导下一个阶段的状态集合,动态地往前推进
  • 时间复杂度: O(n*w)优于回溯算法的O(2^n)
  • 空间复杂度:需要额外申请一个 n 乘以 w+1 的二维数组(优化后为w+1的一维数组),对空间的消耗比较多。动态规划是一种空间换时间的解决思路。

0-1 背包问题升级版

  • 引入物品价值这一变量。对于一组不同重量、不同价值、不可分割的物品,我们选择将某些物品装入背包,在满足背包最大重量限制的前提下,背包中可装入物品的总价值最大是多少呢?
  • 动态规划的解决方法:
    • 把整个求解过程分为 n 个阶段,每个阶段会决策一个物品是否放到背包中。
    • 每个阶段决策完之后,背包中的物品的总重量以及总价值,会有多种情况,也就是会达到多种不同的状态。
    • 我们用一个二维数组 states[n][w+1],来记录每层可以达到的不同状态。不过这里数组存储的值不再是 boolean 类型的了,而是当前状态对应的最大总价值。
    • 我们把每一层中 (i, cw) 重复的状态(节点)合并,只记录 cv 值最大的那个状态,然后基于这些状态来推导下一层的状态。
    • public static int knapsack3(int[] weight, int[] value, int n, int w) {
        int[][] states = new int[n][w+1];
        for (int i = 0; i < n; ++i) { // 初始化states
          for (int j = 0; j < w+1; ++j) {
            states[i][j] = -1;
          }
        }
        states[0][0] = 0;
        if (weight[0] <= w) {
          states[0][weight[0]] = value[0];
        }
        for (int i = 1; i < n; ++i) { //动态规划,状态转移
          for (int j = 0; j <= w; ++j) { // 不选择第i个物品
            if (states[i-1][j] >= 0) states[i][j] = states[i-1][j];
          }
          for (int j = 0; j <= w-weight[i]; ++j) { // 选择第i个物品
            if (states[i-1][j] >= 0) {
              int v = states[i-1][j] + value[i];
              if (v > states[i][j+weight[i]]) {
                states[i][j+weight[i]] = v;
              }
            }
          }
        }
        // 找出最大值
        int maxvalue = -1;
        for (int j = 0; j <= w; ++j) {
          if (states[n-1][j] > maxvalue) maxvalue = states[n-1][j];
        }
        return maxvalue;
      }
    • 时间复杂度是 O(n*w),空间复杂度也是 O(n*w)(可优化)

满减问题:

    • 淘宝的“双十一”购物节有各种促销活动,比如“满 200 元减 50 元”。
    • 假设你女朋友的购物车中有 n 个(n>100)想买的商品,她希望从里面选几个,在凑够满减条件的前提下,让选出来的商品价格总和最大程度地接近满减条件(200 元),这样就可以极大限度地“薅羊毛”。作为程序员的你,能不能编个代码来帮她搞定呢?
    • // items商品价格,n商品个数, w表示满减条件,比如200
      public static void double11advance(int[] items, int n, int w) {
        boolean[][] states = new boolean[n][3*w+1];//超过3倍就没有薅羊毛的价值了
        states[0][0] = true;  // 第一行的数据要特殊处理
        if (items[0] <= 3*w) {
          states[0][items[0]] = true;
        }
        for (int i = 1; i < n; ++i) { // 动态规划
          for (int j = 0; j <= 3*w; ++j) {// 不购买第i个商品
            if (states[i-1][j] == true) states[i][j] = states[i-1][j];
          }
          for (int j = 0; j <= 3*w-items[i]; ++j) {//购买第i个商品
            if (states[i-1][j]==true) states[i][j+items[i]] = true;
          }
        }
      
        int j;
        for (j = w; j < 3*w+1; ++j) { 
          if (states[n-1][j] == true) break; // 输出结果大于等于w的最小值
        }
        if (j == 3*w+1) return; // 没有可行解
        for (int i = n-1; i >= 1; --i) { // i表示二维数组中的行,j表示列
          if(j-items[i] >= 0 && states[i-1][j-items[i]] == true) {
            System.out.print(items[i] + " "); // 购买这个商品
            j = j - items[i];
          } // else 没有购买这个商品,j不变。
        }
        if (j != 0) System.out.print(items[0]);
      }

以上是关于数据结构与算法简记--动态规划的主要内容,如果未能解决你的问题,请参考以下文章

数据结构与算法简记--位图

数据结构与算法简记--红黑树

《数据结构与算法之美》27——初识动态规划

数据结构与算法题解|动态规划|——矩阵型动态规划

数据结构与算法三个经典案例带你了解动态规划

数据结构与算法简记--拓扑排序