题目:给一个数组Arr=[2,3,5,7,5],aim=10;得出数组中组成aim的元素个数最少是多少;
下面从三个方面依次解题,逐步从递归解法过渡到动态规划
1. 暴力递归
- 思路
设f(index,aim)表示从index开始的硬币中,组成aim的最小硬币数;那么我们的目标为f(0,10);
f(index,aim) = min(f(index+1,aim),1+f(index+1,aim-Arr[index]))
第一项表示不用第index个硬币;第二项表示使用index个硬币
/********************暴力递归****************/
//index表示从index个硬币开始选择
//rest表示还剩下多少钱数需要凑
//返回index开始以后的硬币还需要使用rest个
int process1(vector<int>& Arr, int index, int rest)
{
if (rest < 0)
return -1;
if (rest == 0)
return 0;
//rest>0但是硬币没了
if (index == Arr.size())
return -1;
//rest>0并且还有硬币
int p1 = process1(Arr, index + 1, rest);
int p2Next = process1(Arr, index + 1, rest - Arr[index]);
if (p1 == -1 && p2Next == -1)
return -1;
else if (p1 == -1)
return p2Next + 1;
else if (p2Next == -1)
return p1;
return min(p1, 1 + p2Next);
}
//得到最小的硬币数
int get_mincoin1(vector<int>& Arr, int aim)
{
return process1(Arr, 0, aim);
}
2. 记忆搜索解法
第一种解法,会出现重复计算的情况,如同f(3,5)会在两个分支上都计算一遍,这就造成了计算资源的浪费;所以记忆搜索解法相比第一种暴力递归解法添加存储机制,保存已经计算过的f(index,rest)的值;当再次遇到的时候,直接使用,不在重复计算
- code
/*************记忆搜索*************/
int process2(vector<int>& Arr, int index, int rest,vector<vector<int>>& dp)
{
if (rest < 0)
return -1;
if (dp[index][rest] != -2)
return dp[index][rest];
if (rest == 0)
dp[index][rest] = 0;
//rest>0
else if (index == Arr.size())
dp[index][rest] = -1;
else
{
//rest>0 还有硬币
int p1 = process2(Arr, index + 1, rest,dp);
int p2Next = process2(Arr, index + 1, rest - Arr[index],dp);
if (p1 == -1 && p2Next == -1)
dp[index][rest] = -1;
else if (p1 == -1)
dp[index][rest] = p2Next + 1;
else if (p2Next == -1)
dp[index][rest] = p1;
else
dp[index][rest] = min(p1, 1 + p2Next);
}
return dp[index][rest];
}
int get_mincoin2(vector<int>& Arr, int aim)
{
vector<vector<int>> dp(Arr.size() + 1, vector<int>(aim + 1, -2));
return process2(Arr, 0, aim, dp);
}
3. 严格表结构
严格表结构就是大家说的动态规划,状态转移矩阵;在从记忆搜索解法往严格表结构转换的时候,需要有一下几个步骤
1) 找出可变参数个数和范围,用于定义dp数组;
2) 需要根据记忆搜索算法的if条件,确定dp数组中不需要计算的位置;
3) 寻找表中其他位置的位置依赖的规律
状态转移矩阵的大概形式(还需要根据细节划分)
dp(index,aim) = dp(f(index+1,aim),1+dp(index+1,aim-Arr[index]))
- code
/*************严格表结构*************/
int get_mincoin3(vector<int>& Arr, int aim)
{
vector<vector<int>> dp(Arr.size() + 1, vector<int>(aim + 1,0));
for (int index = 0; index < Arr.size(); index++)
dp[index][0] = 0;
for (int rest = 1; rest <= aim; rest++)
dp[Arr.size()][rest] = -1;
for (int index = Arr.size() - 1; index >= 0; index--)
{
for (int rest = 1; rest <= aim; rest++)
{
int p1 = dp[index + 1][rest];
int p2Next = -1;
if(rest - Arr[index] >= 0)
p2Next = dp[index + 1][rest - Arr[index]];
if (p1 == -1 && p2Next == -1)
dp[index][rest] = -1;
else if (p1 == -1)
dp[index][rest] = p2Next + 1;
else if (p2Next == -1)
dp[index][rest] = p1;
else
dp[index][rest] = min(p1, 1 + p2Next);
}
}
return dp[0][aim];;
}
4. 总结
实际上,动态规划题目最终的状态转移矩阵的本质都是由第一解法暴力递归转化过来的;最重要的还是要确定递归的思路,而递归思路的确定的前提,就是找到变换的参数index和rest;所以最重要的就是找到这两个变化的参数和递归的思路,然后状态转移矩阵就可以迎刃而解;
动态规划中,重要的还有遍历的顺序和变换参数的范围