动态规划优化过程:
暴力递归 —>通过记忆计算过的数值减少迭代过程—>给计算过程规定好计算路线,使得后面的计算可以直接利用前面的结果。
第一道例题会列出这一优化过程(后面的只给出动态规划的求解)
Question1:有数组penny,penny中所有的值都为正数且不重复。每个值代表一种面值的货币,每种面值的货币可以使用任意张,再给定一个整数aim(小于等于1000)代表要找的钱数,求换钱有多少种方法。
暴力递归:
int force(const vector<int>&penny,int index,int aim)
{
//检测异常
if(penny.size()<0||aim<0)return 0;
int counts=0;
//递归终点:确定了最后一种货币的数量。
//并且如果aim为0 说明当前货币组合OK,counts记为1;
if(index==penny.size())
return counts=(aim==0?1:0);
else{
//penny[index]使用1个 使用2个 使用3个....
for(int i=0;i<=aim/penny[index];++i)
//penny[index+1]使用1个,使用2个 使用3个 对应aim-ipenny[index]
counts+=force(penny,index+1,aim-i*penny[index]);
}
return counts;
}
记忆计算过的值:
重复计算的情况: 使用了0张1元 0张 2元 2张5元 需要递归计算force(penny,3,90)//假设目标钱数是100
当使用0张1元 5张2元 0张5元 有需要递归计算force(penny,3,90)所以利用哈希表记录这个结果即可
int memory(const vector<int> &penny,int index,int aim)
{
if(penny.size()<0||aim<0)
return 0;
map<pair<int,int>,int>memory;
return recursion(penny,index,aim,memory);
}
int recursion(const vector<int> &penny,int index,int aim,
map<pair<int,int>,int>memory)
{
int counts=0;
if(index==penny.size())
return counts=(aim==0?1:0);
else
{
for(int i=0;i<=aim/penny[index];++i){
//!=0 说明这个pair出现过,那么如果是-1 说明行不通 counts+0 如果是1那么counts+这个pair的值
if(memory[make_pair(index+1,aim)]!=0)
counts+=memory[make_pair(index+1,aim)]==-1?0:memory[make_pair(index+1,aim)];
else
counts+=force(penny,index+1,aim-i*penny[index]);
}
}
//关于哈希表的下标操作阔以看看书
memory[make_pair(index,aim)]=(counts==0?-1:counts);
return counts;
}
动态规划:关键就是构造dp矩阵:
1)两个可变参数就用二维矩阵,一个就用一维,后面的题会看到。
2)搞清楚dp[i][j]代表什么 比如此题 dp[i][j]代表 使用i种货币,换取j有多少种取法。最后dp[penny.size()][aim+1]就是答案
//动态规划的实现
int DP(const vector<int> &money_type,int aim)
{
if(money_type.size()<0||aim<0)
return 0;
vector<vector<int>>dp_arry(money_type.size(),vector<int>(aim+1,0));
//因为二维数组已经被初始化为0了 所以需要置0的不需要再处理了
//直接置1就可以了
for(int j=0;j<=aim;++j){
if(j%money_type[0]==0)
dp_arry[0][j]=1;
}
for(int i=1;i<money_type.size();++i)
{
//这里要注意 对于钱数是0 我们是有解决方案的 就是各拿0张所以方案数是1 但是对于钱数为1(2,5)是没有办法的 所以是0
dp_arry[i][0]=1;
for(int j=1;j<=aim;++j)
dp_arry[i][j]=dp_arry[i-1][j]+(j-money_type[i]<0?0:dp_arry[i][j-money_type[i]]);
}
return dp_arry[money_type.size()-1][aim];
}
Question2:有n级台阶,一个人每次上一级或者两级,问有多少种走完n级台阶的方法。
int countWays(int n){
/*暴力递归
//处理异常
if(n<=0)return 0;
//递归终止条件:指定一个台阶或者两个台阶的方法数
if(n==1||n==2)
return n;
//每个台阶,都有两种方式可以到达
//一步上来的,或者两步上来的。
else{
return countWays(n-1)+countWays(n-2);
}*/
vector<int>dp(n+1,0);
//因为只有一个可变参数,台阶数,所以dp是一维的
//dp[i]代表i阶台阶的方法数
dp[1]=1;dp[2]=2;
for(int i=3;i<n+1;++i)
dp[i]=(dp[i-1]+dp[i-2]);
return dp[n];
Question3:这是一个经典的LIS(即最长上升子序列)问题,请设计一个尽量优的解法求出序列的最长上升子序列的长度。
给定一个序列A及它的长度n(长度小于等于500),请返回LIS的长度。
变量只有一个,就是序列的长度,所以dp仍旧是一维的。dp[i]的意义:以A[i]元素作为最大元素的最长递增子序列的长度。
因为最长上升子序列并不一定包括最后一个数,所以最终答案也不是dp[n-1] 而是dp中的最大值
int getLIS(vector<int> A, int n){
// write code here
//初始化dp矩阵
vector<int>dp(n,0);
//第一个字符的最大递增子序列自然为1
dp[0]=1;
if(n==1)return 1;
for(int loc=1;loc<n;++loc)
{
int max=0;
for(int i=loc-1;i>=0;--i)
{
if(A[i]<A[loc]&&dp[i]>max)
max=dp[i];
}
//这里如果dp[loc]是目前出现最小的,自然dp[loc]=0+1=1;
//如果不是,就是取前面出现的比他小的那些值里对应dp最大的。dp[loc]=max+1;
dp[loc]=max+1;
}
return *max_element(dp.cbegin(),dp.cend());
}
Question4: 有一个矩阵map,它每个格子有一个权值。从左上角的格子开始每次只能向右或者向下走,最后到达右下角的位置,路径上所有的数字累加起来就是路径和,返回所有的路径中最小的路径和。
dp[i][j]的意义是: 到[i][j]位置的最小的路径和;
因为移动的方式是左—>右,上—>下。所以对于第一行的每个位置,都只能从第一行的左面移动而来,第一列的都只能是从第一列的上面移动而来。(即累加和)
根据第一行第一列的值,来求其他位置的值。 dp[i][j]可能是dp[i][j-1]+map[i][j] 即从左面移动而来,或者从上面移动而来dp[i-1][j]+map[i][j] 选取最小的作为结果。
int getMin(vector<vector<int> > map, int n, int m)
{
// write code here
//构建dp矩阵 并且全部初始化为0
vector<vector<int>>dp(n,vector<int>(m,0));
//构造dp矩阵的第一行
for(int j=0;j<m;++j)
dp[0][j]=map[0][j]+(j>0?dp[0][j-1]:0);
//构造dp矩阵第一列
for(int i=0;i<n;++i)
dp[i][0]=map[i][0]+(i>0?dp[i-1][0]:0);
//求其他位置的dp值
for(int i=1;i<n;++i)
{
for(int j=1;j<m;++j)
{
//因为移动顺序是左到右,上到下
//所以任一格,都是由他上面一格或者左面一格移动过来的。
//再加上该位置本身的权重。
dp[i][j]=map[i][j]+min(dp[i-1][j],dp[i][j-1]);
}
}
return dp[n-1][m-1];
}
Question5:给定两个字符串A和B,返回两个字符串的最长公共子序列的长度。
例如,A="1A2C3D4B56”,B="B1D23CA45B6A”,”123456"或者"12C4B6"都是最长公共子序列。
两个字符串,dp自然是二维的。dp[i][j]的含义,A[0..i]和B[0..j]的最长公共子序列
首行:第一个元素:A[0]和B[0]的最长公共子序列,0/1 第二个元素: A[0]和B[0..1]的最长公共子序列 0/1 ...
其中一旦有一个值为1,自然后面的全是1,不需要再去判读。
首列同理。
对于dp[i][j] 如果A[i]=B[j] 那么dp[i][j]=dp[i-1][j-1]+1; 如果A[i]!=B[j] 则dp[i][j] 可能等于A[0..i]和B[0..j-1]的最长公共子串也可能等于A[0..i-1]和B[0...j]的最长公共子串,取二者的最大值。
在代码上通过添加一行一列的0 ,可以使首行首列的取值和其他行列的取值行为统一。
int findLCS(string A, int n, string B, int m){
// write code here
vector<vector<int>>dp(n+1,vector<int>(m+1,0));
//设置第一例的值
/* bool flag=false;
for(int row=0;row<n;++row){
if(A[row]==B[0]||flag){
dp[row][0]=1;
flag=true;
}
}
flag=false;
//设置第一行的值
for(int col=0;col<m;++col){
if(B[col]==A[0]||flag){
dp[0][col]=1;
flag=true;
}
}*/
//设置其他位置的值
for(int row=1;row<=n;++row){
for(int col=1;col<=m;++col){
if(A[row-1]==B[col-1])
dp[row][col]=dp[row-1][col-1]+1;
else
dp[row][col]=max(dp[row-1][col],dp[row][col-1]);
}
}
return dp[n][m];
}
Question6:背包问题。一个背包有一定的承重cap,有N件物品,每件都有自己的价值,记录在数组v中,也都有自己的重量,记录在数组w中,每件物品只能选择要装入背包还是不装入背包,要求在不超过背包承重的前提下,选出物品的总价值最大。给定物品的重量w价值v及物品数n和承重cap。请返回最大总价值。
变量仍为两个 物品数和cap( w 和v 都是附属于物品的)。
dp[i][cur_cap]的意义是 有i种物品可选时,在当前的背包承重下,背包的最大价值。同样加入一行一列的0 来使每一个位置的操作统一。
对于dp[i][cur_cap] 如果将第i件物品放进去(前提是他放得进去):dp[i][cur_cap]=dp[i-1][cur_cap-w[i-1]]+v[i-1]背包减去i的重量,放i-1种物品时的最大价值+i的价值。不放第i件物品:那么i-1种物品在当前承重下的最大价值。取两者的最大值。
int maxValue(vector<int> w, vector<int> v, int n, int cap) {
// write code here
vector<vector<int>>dp(n+1,vector<int>(cap+1,0));
//dp[row][col]的意义是 前row件物品在重量为col时的最大价值
for(int i=1;i<=n;++i)
{
for(int cur_cap=1;cur_cap<=cap;++cur_cap)
{
//两种情况,第i件物品 加或者不加
//在第i件物品可以放进背包的情况下,选取放或者不放的最大值
if(cur_cap-w[i-1]>=0)
dp[i][cur_cap]=
max(dp[i-1][cur_cap],dp[i-1][cur_cap-w[i-1]]+v[i-1]);
//放不进去的话,就是不用比较,直接取不放第i件物品的值
else
dp[i][cur_cap]=dp[i-1][cur_cap];
}
}
return dp[n][cap];
}
通过上面的题也不难发现,动态规划其实和递归一样,也需要第一个基础的确定的值,(首行首列的值,或者第一个位置的值的定义)。
Question7: 对于两个字符串A和B,我们需要进行插入、删除和修改操作将A串变为B串,定义c0,c1,c2分别为三种操作的代价,请设计一个高效算法,求出将A串变为B串所需要的最少代价。
dp[i][j]代表什么大家可以结合上面的自己定义一下,这个题有四种可能的情况,算是最复杂的了,咱们李辉老师也讲过。
int findMinCost(string A, int n, string B, int m, int c0, int c1, int c2) {
// write code here
vector<vector<int>>dp(n+1,vector<int>(m+1,0));
//首行代表:由空串变为目标串(一个字符组成的) 即插入操作
for(int col=0;col<=m;++col)
dp[0][col]=col*c0;
//首列代表:由一个字符组成的串 变为目标串(空串) 即删除操作
for(int row=0;row<=n;++row)
dp[row][0]=row*c1;
//定义其余位置
for(int i=1;i<=n;++i)
{
for(int j=1;j<=m;++j)
{
//dp[row][col]有四种情况
//两大类A[i-1]?=B[j-1]
if(A[i-1]==B[j-1])
dp[i][j]=dp[i-1][j-1];
else{
//插入,删除,替换
int temp=min(dp[i][j-1]+c0,dp[i-1][j]+c1);
dp[i][j]=min(temp,dp[i-1][j-1]+c2);
}
}
}
return dp[n][m];
}