近期在做程序员面试指南,里面有一道《换钱的方法数》问题,我觉得对理解动态规划和递归优化搜索很有帮助。
给定数组arr,arr中所有的值都为正数且不重复。每一个值代表一种面值的货币,每种面值的货币可以使用任意张,再给定一个整数CoinNum代表要找的钱数,求换钱的有多少种方法。
arr=[5,10,25,1],CoinNum=0;
组成0元的方法有1种,就是所有货币都不用。所以返回1。
arr=[5,10,25,1],CoinNum=15;
组成15的方法有6种,分别为:
(1)3张5元
(2)1张10元+1张5元
(3)1张10元+5张1元
(4)10张1元+1张5元
(5)2张5元+5张1元
(6)15张1元
arr=[3,5],CoinNum=2;
任何方法都无法组成2元,所以返回0。
#include "stdafx.h"
#include
#include
#include
using namespace std;
//leetcode518
int process1(vectorarr, int index, int aim)
{
int res = 0;
if (index == arr.size())
{
res = aim == 0 ? 1 : 0;
}
else
{
for (int i = 0; arr[index] * i <=aim; i++)
{
res += process1(arr, index + 1, aim - arr[index] * i);
}
}
return res;
}
int coinsl(vectorarr, int aim)
{
int res;
if (arr.size() == 0 || aim < 0)
{
res = 0;
return res;
}
else
{
return process1(arr, 0, aim);
}
}
int _tmain(int argc, _TCHAR* argv[])
{
vectorarr = { 5, 10, 25, 1 };
int aim = 15;
int res = coinsl(arr, aim);
cout << res;
return 0;
}
在我看来,解决这个问题如果用普通递归的话,代码写好三点就好了:
代码递归的初识条件,在普通递归代码中,我们写的递归的初识条件就是
int coinsl(vectorarr, int aim)
{
int res;
if (arr.size() == 0 || aim < 0)
{
res = 0;
return res;
}
else
{
return process1(arr, 0, aim);
}
}
这样理解相当于在普通递归外套了一个“壳”
2.代码递归的结束条件,在普通递归代码中,我们写的递归的初识条件就是
if (index == arr.size())
{
res = aim == 0 ? 1 : 0;
}
要给定递归结束的条件,比如说当前几个数已经组成完成钱数,这样就算1种方法
3、(最关键)循环条件
for (int i = 0; arr[index] * i <=aim; i++)
{
res += process1(arr, index + 1, aim - arr[index] * i);
}
普通递归时间复杂度为O(coinnum^n)
#include"stdafx.h"
#include
#include
#include
#include
using namespace std;
int process2(vectorarr, int index, int aim, vector>map)
{
int res=0;
int i, j;
int mapValue = 0;
if (index == arr.size())
{
if (aim == 0)
{
res = 1;
}
else
{
res = 0;
}
}
else
{
for (i = 0; i*arr[index] <= aim; i++)
{
mapValue == map[index + 1][aim - i*arr[index]];
if (mapValue == 0)
{
res += process2(arr, index + 1, aim - arr[index] * i, map);
}
else
{
if (mapValue == -1)
{
res += 0;
}
else
{
res += mapValue;
}
}
}
}
if (res == 0)
{
map[index][aim] = -1;
}
else
{
map[index][aim] = res;
}
return res;
}
int coinsl(vectorarr, int aim)
{
vector>map(arr.size()+1,vector(aim+1));
if (arr.size() == 0 || aim < 0)
{
return -2;
}
else
{
return process2(arr, 0, aim, map);
}
}
int main()
{
int a = 91;
int aa = 92;
vectorarr = { 5, 10, 25, 1};
int aim = 15;
int res = coinsl(arr, aim);
cout << "换钱的方法数为:" << res << endl;
return 0;
}
记忆优化时间复杂为O(N*coinnum^2)
简述:暴力递归存在大量的重复计算,比如上面的例子,当已经使用0张5张+1张10元和使用2张5元和0张10元两种情况,剩下的钱组成15的可能,产生重复的计算,在暴力递归的过程中,这种计算会大量发生。记忆优化就是建立二维表map,每计算完一个递归过程,都将结果记录到map中,当下次进行也同样的递归过程之前,现在map中查询这个递归是否已经计算过。
#include "stdafx.h"
#include
#include
#include
using namespace std;
int coinsl(vectorarr, int coinnum)
{
if (arr.size() == 0 || coinnum < 0)
{
return 0;
}
vector>dp(arr.size()+1 , vector(coinnum + 1));
int i, j,k;
for (i = 0; i < arr.size() + 1; i++)
{
dp[i][0] = 1;
}
for (j = 0; j*arr[0]-coinnum<=0; j++)//这里要注意条件
{
dp[0][j*arr[0]] = 1;
}
int num = 0;
for (i = 1; i < arr.size(); i++)
{
for (j = 1; j <=coinnum; j++)
{
num = 0;
for (k = 0; j - k*arr[i] >= 0; k++)
{
num += dp[i - 1][j - k*arr[i]];
}
dp[i][j] = num;
}
}
return dp[arr.size()-1][coinnum];
}
int main()
{
int a = 91;
int aa = 92;
vectorarr = { 5, 10, 25, 1 ,2};
int coinnum = 15;
int res = coinsl(arr, coinnum);
cout << "换钱的方法数为:"<< res<
动态规划算法,时间复杂度为O(N*coinnum^2)生成行数为arr.size(),列数为coinnum+1的二维向量,
dp[i][j]
表示在使用arr[0…i]货币的情况下,组成钱数j有多少种方法。
二维向量第一行和第一列的值好确定
剩下的值为以下的值的累加
完全不用arr[i],只使用arr[0…i-1]货币时,方法数为dp(i-1,j)
用1张arr[i]货币,剩下的钱用arr[0…i-1]货币组成时,方法数为dp(i-1,j-arr[i])
用2张arr[i]货币,剩下的钱用arr[0…i-1]货币组成时,方法数为dp(i-1,j-2*arr[i])
…
用k张arr[i]货币时,剩下的钱用arr[0…i-1]货币组成时,方法数为dp(i-1,j-k*arr[i])
for (i = 1; i < arr.size(); i++)
{
for (j = 1; j <= coinnum; j++)
{
dp[i][j] = dp[i - 1][j];
if (j - arr[i] >= 0)
{
dp[i][j]+= dp[i][j - arr[i]];//在这一步进行优化,第1种情况的方法数为dp[i][j-arr[i]]的值,从第2种情况到第k种情况的方法数其实就是累加dp[i][j-arr[i]]的值。
}
else
{
dp[i][j] += 0;
}
}
}
在使用动态规划优化算法时,其实就是省去了枚举的流程,与普通动态规划相比,不用k的变量了,时间复杂度为O(N*coinnum)。
for (int j = 0; arr[0] * j <= coinnum; j++)
{
dp[arr[0] * j] = 1;
}
for (int i = 1; i < arr.size(); i++)
{
for (int j = 1; j < coinnum; j++)
{
if (j - arr[i] >= 0)
{
dp[j] += dp[j - arr[i]];
}
else
{
dp[j] += 0;
}
}
return dp[coinnum];
}
在上一步优化的基础上,利用空间压缩原理,时间复杂度为O(N*coinnum),空间复杂度为O(coinnum)
通过一个数组滚动更新的方式节省了空间,这样很好。