动态规划有多重要?
- 科技公司面试必考算法
- 根据面试经验,一半失败的面试都与动态规划有关
动态规划题目特点
1、计数题
-有多少种方式走到右下角
-有多少种方法选出k个数使得和是Sum
2、求最大最小值
-从左上角走到右下角路径的最大数字和
-最长上升子序列长度
3、求存在性
-取石子游戏,先手是否必胜
-能不能选出k个数使得和是Sum
动态规划的特点
1、将原问题分解为相似的子问题
2、所有的子问题都只需解一次(即存储解过的问题)
3、使用空间换时间
4、自底向上求解
目录
一、Coin Change题
科大讯飞笔试题
题目:
有三种硬币,分别面值2元、5元和7元,每种硬币都有足够多,现在买一本书需要27元,问如何用最少的硬币组合正好付清,不需要对方找钱?
你是否同意下面的想法?
题目要求用最少的硬币,也就是尽量用大的硬币,最后用一种硬币付清即可;
如:7+7+7=21,21+2+2+2=27;
用了6枚硬币。
但正确答案是:7+5+5+5+5=27,共5枚硬币。
而正确的做法是使用动态规划解,如何使用动态规划?动态规划的组成部分有哪些?
1、确定状态
确定状态即使用一个数组,数组的每个元素f[i]或f[i][j]代表什么。
确定状态需要做两件事:最后一步、子问题。
最后一步
先忽略解题的最优策略是什么,但最优策略肯定是K枚硬币a1...ak加起来等于27;
所以必存在一枚最后的硬币:ak;
去掉这枚硬币,前面的硬币加起来就是27-ak。
由以上说法可以得出两个关键点。
1、不关心前面的k-1枚硬币是如何拼出27-ak的,但是确定前面的硬币拼出了27-ak。
2、拼出的27-ak硬币数一定最少,否则就不是最优策略了。
子问题
所以可以将原问题“最少用多少枚硬币拼出27”转换为子问题“最少用多少枚硬币可以拼出27-ak”。
为了简化,可以设状态f(x)=最少用多少枚硬币拼出x。
现在还不知道最后那枚硬币ak是多少?
ak只可能是2、5或7;
如果ak=2,f(27)=f(27-2)+1 (f表示的是个数,所以加上最后一枚硬币是+1)
如果ak=5,f(27)=f(27-5)+1
如果ak=7,f(27)=f(27-7)+1
需要求最少的硬币数,所以可得出以下公式:
f(27)=min{f(27-2)+1,f(27-5)+1,f(27-7)+1}
看到上面的公式,一定有人问:为什么不用递归?
递归解法的问题:
由图可得:递归解法做了很多重复计算,效率低下。
那么如何避免重复计算的问题?将计算结果保存下来,并改变计算顺序。
2、转移方程
设状态f[x]=最少用多少枚硬币拼出x
对于任意x,有:
现在思考一下,如果要求使用最多的硬币拼呢?
那么可以将方程的min改为max
如果写出了转移方程,那么就成功了一半。
仅仅写出转移方程还是不够,在编写程序前还需做两件事。
3、初始条件和边界情况
在得出方程后衍生两个问题:如果x-2、x-5、x-7小于0怎么办?什么时候停下?
如果不能拼出Y,就定义f[Y]=正无穷,此处的正无穷是尽可能大的数。
初始条件:f[0]=0,因为方程不能得出x=0的正确答案,需要手动定义。
4、计算顺序
从初始后开始计算;
初始条件:f[0]=0;
然后计算f[1]....f[27]
当计算到f[x]时,前面的f[x-2]、f[x-5]、f[x-7]已经得到结果,可以直接使用(这里体现了动态规划的一个特点即动态的存储计算过的数据)。
使用动态规划解此题过程如下:
每一步尝试三种硬币,一共27步。
如果有n种硬币,拼m块硬币
动态规划解此题的时间复杂度:n*m
而递归时间复杂度是指数级别,要>>n*m
5、代码
int a[MAX],m;//a存放硬币种类,M为求解的总硬币数
int n;
int i,j;
int f[m+1];
f[0]=0;
for(i=1;i<=m;i++){
f[i]=MAX;
for(j=0;j<n;j++){
if(i>=a[j]&&f[i-a[j]]!=MAX)
{
if(f[i-a[j]]+1<f[i])
f[i]=f[i-a[j]]+1;
}
}
}
if(f[m]==MAX){
return -1;
}
printf("%d",f[m]);
}
二、Unique Paths
题目:
给定m行n列网格,有一个机器人从左上角(0,0)出发,每一步可以向下或向右走一步,问有多少种不同的方式走到右下角?
1、子问题
如果机器人有X种方式从左上角走到(m-2,n-1),有Y种方式从左上角走到(m-1,n-2),则机器人有X+Y种方式走到(m-1,n-1)。
可以由原题“有多少种方式从左上角走到(m-1,n-1)”转化为子问题“机器人有多少种方式从左上角到(m-2,n-1)和(m-1,n-2)”
2、转移方程
对于任意一个格子(i,j),有方程:
3、初始条件和边界情况
初始条件
f[0][[0]=1,因为机器人只有一种方式到左上角
边界情况
i=0或j=0(即第一行和第一列),前一步只能有一个方向过来,使f[0][j]=1,f[i][0]=1。
4、计算顺序
f[0][0]=1
计算第0行:f[0][0]、f[0][1]....f[0][n-1]
....
计算第m-1行:f[m-1][0]、f[m-1][1]......f[m-1][n-1]
答案为:f[m-1][n-1]
为什么要按行计算?
因为对于任意一个格子,如果按行计算,那么它的上边和左边的格子都已经计算过了。
时间复杂度:O(MN)
5、代码
int n,m;
int f[m][n];
int i,j;
for(i=0;i<m;i++){
for(j=0;j<n;j++){
if(i==0||j==0)//第一行和第一列是边界情况,因为第一行的格子只能由左方向来,第一列只能从上来
f[i][j]=1;
else
f[i][j]=f[i-1][j]+f[i][j-1];
}
}
printf("%d",f[m-1][n-1]);
三、Jump Game
网易笔试题
题目:
有n块石头分别在0,1,.....,n-1位置,有一只青蛙在石头0,想跳到石头n-1,如果青蛙在第i块石头上,它最多可以向右条距离ai,问青蛙能否跳到石头n-1?例:
输入:a[2,3,1,1,4]
输出:True
输入:a=[3,2,1,0,4]
输出:False
此题可用动态规划也可用其他算法如贪心。
1、确定状态
假设存在一个石头i可以跳到石头n-1,则可将原问题“能不能跳到石头n-1”转换为子问题“能不能跳到石头i”。
设状态f[j]表示能不能跳到石头i。
2、转移方程
思考如何判断是否能跳到石头i?
需要考虑以下步骤:
1、枚举i前面的石头j
2、判断前面的石头能否跳到石头j(如果前面的都跳不到j,那j能跳到i也没用了)
3、如果j有效,则判断j+a[j]>=i,否则跳不到i
3、初始条件和边界情况
初始条件
f[0]=True,因为青蛙一开始就在石头0
边界情况
无边界情况,不会越界。
4、计算顺序
初始化f[0]=True
计算f[1],f[2]....f[n-1]
答案是f[n-1]
时间复杂度:O(N^2)
5、代码
int a[num];
int i,j;
bool f[num];
f[0]=true;
for(i=1;i<num;i++){
f[i]=false;
for(j=0;j<i;j++){
if(f[j]&&j+a[j]>=i){
f[i]=true;
break;
}
}
}
printf("%d",f[num-1]);