目录
前言
各位看官早上好中午好晚上好,今天为大家带来的是动态规划的入门级介绍,本文其实是我自己看了多篇讲解后自己做的总结笔记,用讲解的口吻复述,希望也能为你带来些许收获
动态规划是很多朋友刚学习算法时比较先接触的一种算法。
那么什么是动态规划呢?
《算法导论》是这样解释动态规划的:动态规划与分治法相似,都是通过组合子问题的解来求解原问题答案,将问题划分为互不相交的子问题,递归的求解子问题,最后合并子问题的答案,得到原问题的答案。
翻译成人话就是:计算并存储小问题的解,并将这些解组合成大问题的解。
一、动态规划题目特点:
学习动态规划我们需要明白:当题目写了什么时?我们要知道这个题可能需要用到动态规划了。
毕竟题目上一般不会说请你用动态规划做这题啥的。
涉及的题干主要有三类:
1、计数类,如:有多少种方式走到右下角?
有多少种方法选出K个数使得合为SUM?
2、求最大最小值,如:从左上角走到右下角路径的最大数字和
求最长上升子序列长度
3、求存在性,如:取石子游戏,先手是否必胜?
能不能选出K个数使得合为SUM?
二、例题讲解
我们直接用一道例题来为大家讲解这种算法
描述:
给出不同面额的硬币以及一个总金额. 写一个方法来计算给出的总金额可以换取的最少的硬币数量. 如果已有硬币的任意组合均无法与总金额面额相等, 那么返回
-1
.你可以假设每种硬币均有无数个
总金额不会超过10000
硬币的种类数不会超过500, 每种硬币的面额不会超过100样例输入: [2, 5, 7] 27 输出: 5 解释: 27 = 5 + 5 + 5 + 5 + 7
1.为什么是动态规划?
那么在本题中,我们是如何判断出需要用到动态规划的呢?没错,就是 “ 最少 ”
为便于理解,我们用样例来讲解这一思想。
样例输出的问题就是:
你有三种硬币,面值分别为2元、5元和7元,每种硬币都足够多
买一本书需要27元,如何用最少的硬币组合,正好不需要对方找钱。
2.动态规划如何思考:
就三个确定:确定状态、确定转移方程、确定初始条件和边界情况
2.1第一步:确定状态:
状态可以说是动态规划的核心,是动态规划能够解决问题的根本机理
简单来说、我们在动态规划中都会开一个数组用来储存问题的解,假设这个数组是f[ ],状态就是数组中的每个元素f[ i ]代表的意义,这也类似与我们解应用题时候设的未知数x,y,z。
那么怎么确定状态呢?确定状态其实就两件事:
2.1.1最后一步:
在本题中,虽然我们不知道最优策略到底用到了多少枚硬币、也不知道他到底是怎么做到的,假设最优策略为用了K枚硬币、那么我们就可以确定这K枚硬币:a1,a2…ak的面值加起来是27
那么在最后一步,我们用到了最后一枚硬币ak
除去这枚硬币,前面硬币的面值之和就为27-ak。
示意如图:
同时我们要知道,因为我们找到的是最优策略,所以拼出27-ak这么大的面值使用的硬币数一定是最少的,不然与最优矛盾。
2.1.2子问题:
所以我们要求解的问题就变成了:最少用多少枚硬币可以拼出27-ak。
那么这个时候原问题的27就缩小为了27-ak,问题的规模缩小了,这样的问题我们称为子问题。
这时状态就可以确定了:
我们设状态最少用多少枚硬币拼出。
还有一个问题,最后一枚硬币是多少呢?
这当然是我们已知的,在样例中,最后一枚硬币只可能是给出的2、5或7了。
那么最后一枚硬币ak与最优解 f[x] 就只有这三种情况。
那么最有方案用到的最少硬币数
有的朋友看到这个方程就会觉得:这不是递归吗?那我拿递归不就做出来了,没什么大不了的啊.但是我们要想清楚了,如果用递归,我们实际上会将前面用到的某些数字多次计算,例如f[20],你可以想一想要算几次,再想想f[20]前面的那些数,很恐怖的好吧!
那么这里就显示出我们动态规划算法的巧妙了:将计算过的结果保存下来,用到时直接计算即可。
2.2第二步:转移方程:
设状态f[x]=最少用多少枚硬币拼出x。
则
2.3第三步:初始状态和边界情况:
写出状态转移方程,并不是就已经万事大吉了
看着上面这个式子,我们还要思考两个问题:
1、当x-2、x-5或者x-7小于0怎么办?数组可没有负数啊。
2、总不可能一直算下去,什么时候停下来呢?
2.3.1初始状态:
初始状态就是我们用转移方程算不出来的状态需要手动定义
在本题中,我们需要定义f[0]=0,有了f[0],那么f[2]、f[5]、f[7]就都有了,这就是转移方程计算的起点了。
2.3.2边界情况:
边界情况说白了就是要防止数组越界,很简单,如果计算时马上要用到负数,我们直接叫停,表示拼不出来即可,那么我们就可以把拼不出来的数记为正无穷即可。
那么我们发现:
没错,我们成功记录出了拼不出来的数字。
2.3第四步:确定计算顺序:
在本题以及大多数题中都是从小到大计算的
确定原则:让每次计算时需要用到的数已知
3.样例的实现:
以上所有知识性内容就讲解完毕了,俗话说的好:纸上得来终觉浅,绝知此事要躬行。
鼓励大家有条件的先自己想一想怎么解决样例这一小问题。
先解决样例,算算2,5,7拼27最优解、
#include<iostream>
#include<limits.h> //c++用到无穷大时的头文件
using namespace std;
int main()
{
int coin[3]={2,5,7}; //这里记录我们的硬币种类
int f[28]; //我们要用到f[27],故数组开到28
f[0]=0; //初始条件不能忘
for(int i=1;i<=27;i++)
{
f[i]=INT_MAX;
for(int j=0;j<3;j++)
if(i>=coin[j]&&f[i-coin[j]]!=INT_MAX) //这里要注意!
{
f[i]=min(f[i-coin[j]]+1,f[i]);
}
}
if(f[27]==INT_MAX)
{
f[27]=-1;
}
cout<<f[27];
return 0;
}
注意我代码中标注需要注意的地方,想一想自己是否想到了这个判断句?
加这两个判断的原因:
防止数组越界
如果我们只用这一式子
理论上是可行的
但是我们不可避免的要给无穷大加一,给无穷大加一当然是无穷大,学过数学的都知道,但是计算机不知道,编译系统不知道,在编译系统中给正无穷大加一大部分是会掉到负无穷大去的
前半句:i>=coin[j]也就是说我们目前要拼的面额数应该是不小于我们准备将要用到的硬币的,我们不可能用5块钱拼2块钱,那注定不行。
后半句:f[i-coin[j]]!=无穷大、同样的以避免用到拼不出的面额来避免用到无穷大。
输出结果:
到这里我们对于这一思想就有了一个初步的了解。
4.典例的实现:
这个题相对复杂一些,我们放到下面,有兴趣的可以了解一下
#include<iostream>
#include<limits.h>
using namespace std;
int main()
{
int n,a;
int coins=0;
int coin[500];
while(cin>>a)
{
coin[coins]=a;
coins++;
if(cin.get()=='\n')
{
break;
}
}
cin>>n;
int *f=new int[n+1];
f[0]=0;
for(int i=1;i<=n;i++)
{
f[i]=INT_MAX;
for(int j=0;j<coins;j++)
if(i>=coin[j]&&f[i-coin[j]]!=INT_MAX)
{
f[i]=min(f[i-coin[j]]+1,f[i]);
}
}
if(f[n]==INT_MAX)
{
f[n]=-1;
}
cout<<f[n];
delete []f;
return 0;
}
总结
算法特点:时间复杂度n*m,相对较低。
与递归相比,没有重复计算,非常快。
祝大家学习进步 我去打游戏了