算法学习-简单dp1-找零与数字三角形问题

硬币找零和数字三角形问题


前言

动态规划是什么?
现实中遇到一个复杂的问题时不能分解为几个简单的子问题,而是会分解成一系列的子问题,如果使用分治法,可能会使得递归调用的次数呈指数增长,如斐波那契数列数列的计算。

那就斐波那契数列来说,若果我么要计算 \(F_{40}\),则我们先要吧它拆成 \(F_{39}\)\(F_{38}\),而\(F_{39}\)\(F_{38}\)有需要拆成\(F_{38},F_{37},F_{37},F_{36}\)......,这样对于\(F_{38}\)就调用了两次,&F_{37}&会调用3次,最后指数爆发,让运行时间大幅度增加。

解决这个问题,也就是如何处理大量的冗余的递归调用,我们可以引入一个数组,从最小规模开始求解,先把 \(F_1\)\(F_2\)存入数组,之后从小到大每计算一个项就把该项的值存入数组,这样计算具体某一项 \(F_k\)时,由于 \(F_k-1\)\(F_k-2\)是已知的,直接相加就可以得出结果了。

比如已知\(F_1\)\(F_2\)我们可以直接得出\(F_3\),已知 \(F_2\)\(F_3\)可以直接得出 \(F_4\)......已知 \(F_{39}\)\(F_{40}\)就可以直接得出 \(F_{41}\)。不难得出,但我们计算\(F_n\)时,我们只需要用 n-1 次加法。

问题的最优解如果可以由子问题的最优解推导得到,则可以先求解子问题的最优解,再构造原问题的最优解;若子问题有较多的重复出现,则可以自底向上从最终子问题向原问题逐步求解。 这就是动态规划原理的基本思想==。

<硬币找零问题>


题目: 如果有面值 1,5,10,21,25 分的硬币,最少需要多少个银币来找出 k 分钱的零钱。

一、解决思路


对于这一道题,使用分治法同样会造成大量冗余计算,如果采用如下代码:

int MakeChange(k)
{
    int i=0,coins=0;
    
    if (如果能被找零成一枚银币) return 1;
    
    min=k;

    for (i=1;i<k;++k)
    {
        coins=void(i)+void(k-i);
        if (coins<min) min=coins;
    }

    return coins;
}

那肯定会 tle 。

所以,这里要使用动态规划的思想。如果数值为 k 的零钱,我们可以先求 1 零钱的最优解,再是 2,再是 3,一直计算到 k。每种零钱的最优解可以用coins[]来存放。

那求最优解的方法是什么?对于数值为 i 的零钱,既然是找零,那我们都可以把它拆成面值 i ( i<=k ) 的硬币和 k-i 的的零钱。这样对于 i 的最优解,就是k-i的最优解+1。

实现起来也不难,递归也不需要,变量开始定义好就行,直接上代码了:

二、代码实现


int makechangs(k)
{
    int i=0,j=0,min=0,kind[6]={0,1,5,10,21,25},coins[1000]={0};//i是零钱,从1变化至k;前一个数组存不同面额硬币,后一个存解。

    coins[0]=0;     //给求硬币面值与零钱相同的留一条路
    
    for (i=1;i<=k;++i)
    {
        min=i; //假设全用 1 面值找零
        if (j=1;j<=5;++j)
        {
            if (kind[j]>i) continue;    //直到硬币面值小于等于零钱时往下走
            if(coins[i-kind[j]]+1<min)  min=coins[i-kind[j]]+1;     //上面解释了
        }
        coins[i]=min;   
    }

    return coins[k];
}

算是一道简单的dp,入门很好用,大概

<数字三角形问题>


题目 :
观察下面的数字金字塔。

写一个程序来查找从最高点到底部任意处结束的路径,使路径经过数字的和最大。每一步可以走到左下方的点也可以到达右下方的点。

        7  
      3   8 
    8   1   0 
  2   7   4   4 
4   5   2   6   5 

在上面的样例中,从7→3→8→7→5 的路径产生了最大 .

一、思路


一眼看过去第一反应就是从上往下加,但这可这不是贪心,要枚举尝试也过于精污,但是!我们可是掌握简单dp核心技术的人,我们应该逆向思维从下往上,这样思路就能豁然开朗,前途一片光明依然没有,从下往上要怎么加?

累加。

逆向思考,不是最后一排加上一排,而是上一排挑选最后一排去做加法。

第一次挑选:

              7  
            3   8 
          8   1   0 
        7   12  10  10 
      4   5   2   6    5 

第二次挑选:

              7  
            3   8 
          20   13  10 
        7   12  10   10 
      4   5   2   6     5 

第三次:

              7  
            23   21 
          20  13  10 
        7   12  10   10 
      4   5   2    6    5

第四次:

              30  
            23   21 
          20  13  10 
        7   12  10   10 
      4   5   2    6    5

好了,结果就是30。


等等,,,为什么是挑最大的加?

如果某次加法时挑小的加,然后从路线A走到第一行,最后得到的值,总会比挑大的加,同样从路线A回到第一行的得到的值要小,也就是说,挑小的相加,然后累加到最后的值,永远不可能是最大值,相当于筛除。进行到第二行的时候只剩两个数,那么这一次筛除后就会得到最大值。

具体的做法出来了,代码也就水到渠成。

二、代码实现


#include<iostream>
using namespace std;

int main()
{
    int n=0,i=0,j=0,k=0,arr[1005][1005];

    cin>>n;

    for (i=1,j=1;i<=n;++i,++j)
    {
        for (k=1;k<=j;++k)
        {
            cin>>arr[i][k];
        }
    }

    for (i=n-1,j=n-1;i>=1;--i,--j)
    {
        for (k=1;k<=j;++k)
        {
            arr[i][k]+=max(arr[i+1][k],arr[i+1][k+1]);
        }
    }
    cout<<arr[1][1];

    return 0;
}

不用解释,思路有了就是打字的问题。

顺着dp的思路,会发现,从上往下加也可行,不过是多加了一条比较大小的循环。如下:

//自上往下
#include<iostream>
using namespace std;

int main()
{
    int n=0,i=0,j=0,k=0,arr[100][100]={0},Max=0;

    cin>>n;

    for (i=1,j=1;i<=n;++i,++j)
    {
        for (k=1;k<=j;++k)
        {
            cin>>arr[i][k];
        }
    }

    for (i=2,j=2;i<=n;++i,++j)
    {
        for (k=1;k<=j;++k)
        {
            arr[i][k]+=max(arr[i-1][k-1],arr[i-1][k]);
        }
    }

    Max=arr[n][n];

    for (k=1;k<=n;++k)
    {
        if (arr[n][k]>Max) Max=arr[n][k];
    }

    cout<<Max;

    return 0;
}

最后 --- *** 上手简单,持续受苦。


学习自:

C艹程序设计|思想与方法

洛谷P1216


codevein绝赞捏脸进行中。

posted @ 2020-11-09 18:15  七铭的魔法师  阅读(101)  评论(0编辑  收藏  举报