数据结构与算法之动态规划

  双十一马上就要来了,晓明心中的女神在购物车加了N个东西,突然她中了一个奖可以清空购物车5000元的东西(不能找零),每个东西只能买一件,那么她应该如何选择物品使之中奖的额度能最大利用呢?如果存在多种最优组合你只需要给出一种即可,现在女神来问你,你该怎么办?

  问题来了,上述问题可以用贪心算法解决吗?

  贪心算法又叫贪婪算法,它在求解某个问题时,总是做出眼前最大利益,也就是说只顾眼前不顾大局,所以它是局部最优解。核心:通过局部最优推出全局最优。贪心算法不是对所有问题都能得到整体最优解,关键是贪心策略的选择,选择的贪心策略必须具备无后效性,即某个状态以前的过程不会影响以后的状态,只与当前的状态有关。

  一般通过以下问题就可以通过贪心算法解决:

  1. 针对某个问题有限制值,以及有一个期望的最好结果,通常是从某些数据中选出其中一些,达到最好的结果。
  2. 一般会有一个排序,找出贡献最大的。
  3. 举例看贪心是否可以解决。

  一般用在任务调度,教师排课等系统。

回看上述问题,如果购物车里面的商品价格为2000,3000,4000,那么用贪心算法求得的最大价值为4000,不符合要求。既然贪心解决不了,那么该如何解决呢?这就要说到我们常说到的动态规划。

  一般动态规划可以解决的问题:

  1. 局部最优解:也就是它会有一个最优子结构
  2. 子问题可以重复
  3. 状态转移方程:通过把问题分成很多小阶段一段段的转移。从而得出最优解。状态转移方程是解决动态规划的关键。如果我们能写出状态转移方程,那动态规划问题基本就解决了一大半了,但是很多动态规划问题的状态本身就不好定义,状态转移方程也就更不好想到。

举个简单的小例子:

物品:

钱:6  10  12

Kg1  2   4

我们把5kg的袋子,拆分成1kg1kg这样子计算,里面的表格就表示当前重量下能装的最多的钱。表格的数列就表示是要装的物品

1kg

2kg

3kg

4kg

5kg

加入物品1

6

6

6

6

6

加入物品2

6

10

10+6=16

10+6=16

16

加入物品3

6

10

16

16

18

加入物品2时,袋子当前为1kg 的容量时,我们发现物品2装不进去。那我们应该取多少呢?是不是只要取物品进来时1kg最大钱?,当袋子为2kg时,我们发现物品2可以装下去,此时可以得到10块钱,之前物品1进来时2kg最大是6吧,那我们肯定要选择大的这个10,而不是6.此时袋子还剩0kg可以装。

袋子为3kg时,我们还是可以装下这个物品2,得到10块,袋子还剩下1kg

10+1kg能装的东西。

物品3来了,袋子为4kg时,物品3可以转进来,得到12块钱,袋子还剩0kg

我发现我不装物品3 还能得到16

物品3来了,袋子为5kg时,物品3可以转进来,得到12块钱,袋子还剩1kg。那么装了物品3就能得到12+6=18块钱

我发现我不装物品3 能得到16,比18小,所以决定装。

上面这一个递推过程总结起来就是一个东西------状态转移方程

能装的时候 每次和上面的比较,大我就装,否则就不装。

Max(money[i]+res[i-1][w-weight[i]],res[i-1][w]);

money[i]+res[i-1][w-weight[i]]:装这个物品

w-weight[i] :表示装完还剩下的空间

res[i-1][w-weight[i]]:表示装完后剩下的空间还能装的最大值,取上一次的结果。

Res[i-1][w]表示不装这个物品的值

简单代码实现:

 1 public class Dp {
 2 
 3     public static void main(String[] args) {
 4         
 5         int value [] ={60,100,120};
 6         int weigth[] = {10,20,40};    //购物车那个问题 只需要一个价值就行了,重量都都没有。
 7         
 8         int w = 50;
 9         int n = 3;
10         int dp[][] = new int[n+1][w+1];        //n表示是物品,w表示重量,初始化全是0
11         
12         for(int i = 1; i<= n; i++){    //每次加的物品
13             for(int cw = 1 ; cw <= w ; cw ++){        //分割的背包
14                 if(weigth[i - 1] <= cw){        //表示这个物品可以装进去
15                     dp[i][cw] = Math.max(
16                             value[i-1] + dp[i-1][cw-weigth[i-1]],
17                             dp[i-1][cw]
18                             );
19                 }else{
20                     dp[i][cw] = dp[i-1][cw];    //不能装
21                 }
22             }
23         }
24         System.out.println(dp[n][w]);
25         
26     }
27 }

和遍历的比较及优化:

遍历每次在物品加进来的时候都会保存选择与不选择两种状态那么这样下去越到后面状态保存的就越多其实就是2^n次,因为每个物品都有选与不选两种情况。而动态规划是每次都会把当前情况下的最优解计算出来,层层递推,下一层的最优解都是基于它上一次结果存下来的,所以最后一个结果就一定是最优解。

其实也是把问题都分解成了一个子问题,然后通过子问题去求解全局最优解。

动归和贪心的比较:

贪心是只管眼前不会管后的情况,而动归不一样,它的每次递推都是基于上一次的最优解进行。所以往往动归是一定能求出最优解的,而贪心不一定,这也是贪心算法的缺点,但是大家都看到了动归的时间复杂度是O(n*m)而贪心是O(nlogn),所以贪心算法的是高效的,动归如果子问题太多的话 就容易算不出结果,而且能用动归的问题往往用贪心都能解决一部分,甚至很大一部分。因此如果在实际项目中要求不是特别严的话,建议使用贪心算法求最优解,其实我们很多时候并不用保证100%的准确,能尽量准确就可以了,贪心恰恰是符合这个规则的。

原文地址:https://www.cnblogs.com/frspring/p/14853922.html