什么是动态规划:
1、本质就是利用申请的空间来记录每一个暴力搜索的计算结果,下次要用结果的时候直接使用,而不再进行重复的递归过程
2、动态规划每一中递归状态的计算顺序,依次进行计算。
面试中遇到暴力递归题目可以优化成动态规划方法的大体过程:
1、实现暴力递归方法
2、在暴力搜索方法的函数中看那些参数可以代表递归过程
3、找到代表递归过程的参数之后,记忆化搜索的方法非常容易实现。
4、通过分析记忆化搜索的依赖路径,从而实现动态规划
5、根据记忆化搜索方法改出动态规划的方法,进而看看能否能简化,如果能简化,还能实现时间复杂度更低的动态规划方法
动态规划方法的关键点:
1、最优化原理,也就是最优子结构性质,这是指一个最优化策略具有这样的性质,不论过去状态和决策如何,对前面的决策所形成的状态而言,余下的诸决策必须构成最优策略,简单来说就是一个最优化策略的子决策总是最优的,如果一个问题满足最优化原理,就称其具有最优子结构特征
2、无后效性,指在某状态下决策的收益,只与状态和决策相关,而与到达该状态的方式无关
3、子问题的重叠性,动态规划将指数级复杂度的暴力搜索算法,改进成了具有多项式时间复杂度的算法,关键在于解决冗余,这是动态规划算法的根本目的。
这个“最优化原理”如果用数学化一点的语言来描述的话,就是:假设为了解决某一优化问题,需要依次作出n个决策D1,D2,…,Dn,如若这个决策序列是最优的,对于任何一个整数k,1 < k < n,不论前面k个决策是怎样的,以后的最优决策只取决于由前面决策所确定的当前状态,即以后的决策Dk+1,Dk+2,…,Dn也是最优的。
自我感觉难点在于如何设计策略,有了策略后面的过程就很简单了。
有数组penny,penny中所有的值都为正数且不重复。每个值代表一种面值的货币,每种面值的货币可以使用任意张,再给定一个整数aim(小于等于1000)代表要找的钱数,求换钱有多少种方法。
给定数组penny及它的大小(小于等于50),同时给定一个整数aim,请返回有多少种方法可以凑成aim。
测试样例:
[1,2,4],3,3
返回:2
方法一:
暴力递归搜索算法:
假设arr={5,10,25,1}aim=1000;
1、用0张5元的货币,让{10,25,1}组成剩下的1000,最终方法记为res1;
2、用1张5元的货币,让{10,25,1}组成剩下的995,最终方法记为res2;
。。。。。
以此类推,并最终叠加就是结果,每一个单独的问题又可以进行迭代。代码如下:
class Exchange {
public:
int countWays(vector<int> penny, int n, int aim) {
return recurcount(penny,0,n,aim);
}
int recurcount(vector<int> penny,int begin,int n,int aim)
{
if(begin==n-1)
{
if(aim%penny[begin]==0)
return 1;
else
return 0;
}
int sum=0;
for(int i=0;i*penny[begin]<=aim;++i)
{
sum+=recurcount(penny,begin+1,n,aim-i*penny[begin]);
}
return sum;
}
};
方法二:记忆搜索算法
暴力搜索之所以复杂度高,就是因为有很多重复计算,比如2张5元0张10元组成的和0张五元,1张10元组成的是相同的结果,在暴力搜索中计算了两次。所以可以通过一个全局变量进行记录,从而减少迭代的次数,加快程序的指向速度。或者想迭代函数传递一个数组保存计算过的值。代码如下:
class Exchange {
public:
int a[60][1100];
int countWays(vector<int> penny, int n, int aim) {
memset(a,-1,sizeof(a));
return recurcount(penny,0,n,aim);
}
int recurcount(vector<int> penny,int begin,int n,int aim)
{
if(begin==n-1)
{
if(aim%penny[begin]==0)
return 1;
else
return 0;
}
int sum=0;
for(int i=0;i*penny[begin]<=aim;++i)
{
if(a[begin+1][aim-i*penny[begin]]==-1)
a[begin+1][aim-i*penny[begin]]=recurcount(penny,begin+1,n,aim-i*penny[begin]);
sum+=a[begin+1][aim-i*penny[begin]];
}
return sum;
}
};
方法三 未优化的动态规划
如果arr长度为N,生成行数为N,列数为aim+1的矩阵dp,dp[i][j]的含义是在使用arr[0….i]货币的情况下,组成钱数j有多少种方法
dp[i][j]=dp[i-1][j]+dp[i-1][j-1*arr[i]]+dp[i-1][j-2*arr[i]]+………
记忆搜索方法与动态规划方法的联系:
1、记忆化搜索方法就是某种形态的动态规划方法;
2 、记忆化搜索方法不关心到达某一个递归过程的路径,只是单纯的对计算过的递归过程进行记录,避免重复的递归过程
3、动态规划的方法则是规定好每一个递归过程的计算顺序,依次进行计算,后面的计算过程严格依赖前面的计算过程。
4、两者都是空间换时间的放大,也都有枚举的过程,区别就在于动态规划规定计算顺序,而即系搜索不用规定。
注意组成0元的方法也有1种,就是一个都没有
class Exchange {
public:
int countWays(vector<int> penny, int n, int aim) {
int dp[52][1003];
memset(dp,0,sizeof(dp));
for(int i=0;i<=aim;++i)
{
if(i%penny[0]==0)
dp[0][i]=1;
}
for(int i=1;ifor(int j=0;j<=aim;++j)
{
for(int k=j;k>=0;k=k-penny[i])
dp[i][j]+=dp[i-1][k];
}
}
return dp[n-1][aim];
}
};
方法四:对上面的动态规划过程进行优化:
根据推导公式可得
dp[i][j]=dp[i-1][j]+dp[i-1][j-1*arr[i]]+dp[i-1][j-2*arr[i]]+………
dp[i][j-arr[i]]=dp[i-1][j-1*arr[i]]+dp[i-1][j-2*arr[i]]+………
所以
dp[i][j]=dp[i-1][j]+dp[i][j-arr[i]]
从而将时间复杂度由O(N*aim^2)降低到O(N*aim);
代码如下:
class Exchange {
public:
int countWays(vector<int> penny, int n, int aim) {
int dp[52][1003];
memset(dp,0,sizeof(dp));
for(int i=0;i<=aim;++i)
{
if(i%penny[0]==0)
dp[0][i]=1;
}
for(int i=1;ifor(int j=0;j<=aim;++j)
{
int k=j;
if(k-penny[i]>=0)
dp[i][j]+=dp[i-1][k]+dp[i][k-penny[i]];
else
dp[i][j]+=dp[i-1][k];
}
}
return dp[n-1][aim];
}
};