算法分享(贪心+动态规划)

算法

解决问题的方法。就好比同样是从长沙到北京,坐火车可能需要几天,坐高铁6小时,坐飞机两个半小时,当然交通工具不同,所消耗的成本也不同,算法就是在成本与时间中不断权衡,一个好的算法衡量标准就是用尽可能小的代价成本实现较短时间到达。

算法有什么用?

  • 面试大厂必备技能。
  • 更快的性能。计算机中主要的计算组件就是CPU,假设某个问题可以通过两种算法(简称为算法1、算法2)解决,它们的时间复杂度分为O(n)和O(n²),假设现在某型号CPU的运算速度提升了10倍,我们分别配置稍微低一点的CPU去跑时间复杂度为O(n)的算法,和使用提升了10倍的CPU去跑时间时间复杂度为O(n²)的算法,我们会发现一个结果:在n>10以后,使用配置低的硬件运行的效率要比配置高的硬件效率更好(假设n=20, CPU1: 20 <CPU2 20²/10)

贪心

每一步选择中都采取当前状态下最优的选择,追求局部最优解,从而希望达到全局最优解

动态规划

将问题分解为多个阶段,每个阶段对应一个决策。我们记录每个阶段可达的状态集合(去掉重复的),然后通过当前阶段的状态集合推导下一个阶段的状态集合,动态地往前推进,直至到达最终阶段,得到全局最优解

小兴兴兑换兴币问题

题目

用户有80个小兴兴,有四个兑换场次,分别是50个小兴兴兑换10个兴币,30个小兴兴兑换6个兴币,30个小兴兴兑换5个兴币,20个小兴兴兑换5个兴币

兑换限制一人/次

贪心

分别计算三场兑换的比率

场次 比率
50->10.5 0.21
30->6 0.2
30->5 0.18
20->5 0.25

先兑换20->5,再兑换50->11,得15.5个兴币

动态规划(递归+存储)

20 30 40 50 60 70 80
50->10.5 0 0 0 10.5 10.5 10.5 10.5
30->6 0 6 6 10.5 10.5 10.5 16.5
30->5 0 6 6 10.5 11 11 16.5
20->5 5 6 6 11 11 15.5 16.5

寻找递推关系式

面对当前场次有两种可能性:

小兴兴余额不足,总兴币与上一个场次兴币个数一致

小兴兴满赠门槛,但兑换了当前场次不一定是当前最优,所以需判断兑换的兴币是不是最大值

关键代码:

java 复制代码
int[] w = { 0 , 50 , 30 ,30 , 20 };       //小兴兴兑换值0、50、30、30、20
float[] v = { 0 , 10.5f , 6 , 5 , 5 };       //兴币面额 10.5、6、5、5
int balance  = 80;                     //用户小兴兴总余额
float[][] dp = new float[5][7];               //动态规划表
for (int i = 1; i <= 4; i++) {
    for (int j = 20; j <= balance ; j+=10) {
        if (j < w[i]){
            dp[i][j] = dp[i - 1][j];
        } else{
            dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - w[i]] + v[i]);
        }
    }
}

兑换无限制

贪心

兑换20->5 *4 ,得20个兴币

动态规划

20 30 40 50 60 70 80
50->10.5 0 0 0 10.5 10.5 10.5 10.5
30->6 0 6 6 10.5 12 12 12
30->5 0 6 6 10.5 12 12 12
20->5 5 6 10 11 15 16 20

区别:可以兑换多次,即判断最大值时可以乘上K次,k*v[i] < j

dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - K*w[i]] + k*v[i]);

关键代码

java 复制代码
for (int i = 1; i <= 4; i++) {
    for (int j = 20; j <= balance ; j+=10) {
        if (j < w[i]){
            dp[i][j] = dp[i - 1][j];
        } else{
            for (int k = 1; k * w[i] <= j; k++) {
                dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - k * w[i]] + k * v[i]);
            }
        }
    }
}

时间复杂度:O(N^3)

优化:
java 复制代码
dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - K*w[i]] + k*v[i]);

拆开得:

java 复制代码
dp[i][j] = Math.max(dp[i-1][j],  dp[i-1][j - w[i]]+v[i], dp[i-1][j-2*w[i]]+2*v[i] ,......)

用代入法j=j-w[i]带入上面的方程中得到:

dp[i][j-w[i]] = Math.max(dp[i-1][j-w[i]],  dp[i-1][j - 2*w[i]]+v[i], dp[i-1][j-3*w[i]]+2*v[i] ,......)

对比第二个和第一个方程,我们发现,方程1可以简化成:

java 复制代码
dp[i][j] = Math.max(dp[i-1][j],dp[i][j-w[i]]+v[i]);

dp(i)(j - w(i)) 已经将除去1场当前兑换场次时的所有最优解已经求出来了,因此在计算dp(i)(j)时,无需再重复计算k=2,3,4,5...时的值了。

简化代码

for (int i = 1; i <= 4; i++) {
    for (int j = 20; j <= balance ; j+=10) {
        if (j < w[i]){
            dp[i][j] = dp[i - 1][j];
        } else{
            dp[i][j] = Math.max(dp[i-1][j],dp[i][j-w[i]]+v[i]);
        }
    }
}

时间复杂度O(N^2)

贪心+动态规划

动态规划缺点:时间复杂度高,耗内存。当前用户小兴兴数为80,使用动态规划成本不是很高,如果是800,成本就上去了

贪心算法缺点,在全局最优时不一定是最优解。

核心思路:保证贪心算法在能实现全局最优的前提下,将计算的小兴兴数目最大化,在将剩余小兴兴使用动态规划去计算。

保证贪心算法能实现全局最优的前提:当前小兴兴数目大于50

即800-50=750

750:20*37 =740,得185兴币

余下60:得15兴币

总共:200个兴币

结语

**硬件的速度提升并不是我们不去考虑开发出更高效率应用的借口,作为一名开发者,至少对自己开发的应用要有所标准!所以,开发一个程序时使用更好性能的算法是一名合格程序员应该考虑的因素。