算法学习-简单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绝赞捏脸进行中。