PTA 7-4 换硬币 | 双层循环 | 动态规划 | 递归 | 回溯
7-4 换硬币
分数 7
作者 C课程组
单位 浙江大学
将一笔零钱换成5分、2分和1分的硬币,要求每种硬币至少有一枚,有几种不同的换法?
输入格式:
输入在一行中给出待换的零钱数额x∈(8,100)。
输出格式:
要求按5分、2分和1分硬币的数量依次从大到小的顺序,输出各种换法。每行输出一种换法,格式为:“fen5:5分硬币数量, fen2:2分硬币数量, fen1:1分硬币数量, total:硬币总数量”。最后一行输出“count = 换法个数”。
输入样例:
13
输出样例:
fen5:2, fen2:1, fen1:1, total:4
fen5:1, fen2:3, fen1:2, total:6
fen5:1, fen2:2, fen1:4, total:7
fen5:1, fen2:1, fen1:6, total:8
count = 4
导语
在解答本题的时候,很多同学很难想到实现的思路,网上大多也是根据输出样例使用双重循环来实现的,本题实际上是一题较为经典的题目,一开始我尝试使用动态规划中的多重背包解题,后面发现本题要求是输出每一个组合,而不是求价值为n的方案数,那么我又尝试使用递归来求解,结果是求出了按硬币面额最大数量的唯一方案,其他方案没有求出,经过我不停尝试,最后我使用回溯算法,将所有方案求出,也确定了本题最适合的解法是使用回溯算法,下面我将具体分析各种解法。
一、传统双重循环解法
由于总和total公式为,fen5*5+fen2*2+fen1*1,所以我们从每种币得最大值开始递减去组合,比如5分的币,最大值就是总价值n-2-1/5,就是5分出现数量最大值,依此类推两分和一分的,然后我们求和运算判断是否等于总价值n即可。
时间复杂度分析
外层循环中,i的取值范围为(1, (n-2)/5],即i最多只需要循环(n/5)次。
内层循环中,o的取值范围为(1, (n-5)/2],即o最多只需要循环(n/2)次。
因此,整个算法的时间复杂度为O((n/5) * (n/2)),即O(n^2)。
代码实现
#include <stdio.h>
int main()
{
int i, n, t, o, p, sum, amount;
scanf("%d", &n);
for (i = (n - 2 - 1) / 5; i > 0; i--)
{
for (o = (n - 5 - 1) / 2; o > 0; o--)
{
p = n - (i * 5 + o * 2);
sum = i * 5 + o * 2 + p;
if (sum == n && p > 0)
{
amount++;
printf("fen5:%d, fen2:%d, fen1:%d, total:%d\n", i, o, p, i + o + p);
}
}
}
printf("count = %d\n", amount);
return 0;
}
二、动态规划思想求解面值为n的方案数
动态规划(Dynamic Programming)是一种解决多阶段决策过程最优化问题的算法思想。通常采用自底向上的方式,将问题分解成多个阶段,每个阶段都需要根据之前阶段的状态或决策结果进行决策,最终达到全局最优解的目标。
使用多重背包思想求方案数是经典的应用,但是注意本题,每一枚硬币至少使用一次,在初始化状态数组dp的时候需要将物品coins的所有面值减去一次。定义状态数组dp[i],表示面值为i的方案数是dp[i]个。
代码中首先计算出所有硬币总价值,即将所有硬币的面值相加并减去n,得到的结果就是需要找零的钱数。
接下来,代码创建了一个大小为n+1的一维数组dp,其中dp[i]表示找零i元的硬币方案数。初始化时,将dp[0]设为1,因为找零0元只有1种方案,即不找零。
接着,使用两个for循环来计算dp数组。外层循环遍历所有硬币,内层循环遍历从硬币面值到n的所有金额,这样可以保证dp[j - nums[i]]已经被计算过了。对于每一个内层循环,将dp[j]的值增加dp[j - nums[i]],表示将当前硬币的面值nums[i]加入方案后,找零金额为j的方案数就增加了dp[j - nums[i]]。
最后,返回dp[n],表示找零金额为n元的方案数。
时间复杂度分析
因此,该算法的时间复杂度为O(N*M),其中N为硬币面值的个数,M为需要找零的金额。
代码实现
#include <iostream>
#include <vector>
using namespace std;
// 使用动态规划求解硬币总价值为n的方案数
int exchangeCoins(vector<int>& nums, int n)
{
for (auto item : nums) n -= item;
vector<int> dp(n + 1, 0);
dp[0] = 1;
for (int i = 0; i < nums.size(); i++)
{
for (int j = nums[i]; j <= n; j++)
{
dp[j] += dp[j - nums[i]];
}
}
return dp[n];
}
int main() {
vector<int> nums = { 1,2,5 };
int ans = exchangeCoins(nums,13);
cout << ans << endl;
}
三、递归解法求按硬币面额最大数量的唯一方案
我们这里尝试使用递归来不停的将当前金额按照从面值大到小减去,直到当前金额为0,此时我们便找到了面额最大数量的唯一方案。
不过和题目要求不同,这里只需要求出一种硬币组合方案即可。而且,这个方案中硬币的个数是唯一的,即只有一种硬币组合方案可以达到目标金额。
代码中使用了一个名为backtrack的递归函数来实现回溯算法。当money小于等于0时,说明已经找到了一种硬币组合方案,这时函数会直接返回。
当money大于等于5时,说明可以用5元硬币来凑零钱,那么backtrack()函数就会递归调用自身,并将money减去5。随后,程序会将fen5的值加1,表示已经用了1枚5元硬币。
如果money小于5但是大于等于2,说明可以用2元硬币来凑零钱,那么backtrack()函数就会递归调用自身,并将money减去2。随后,程序会将fen2的值加1,表示已经用了1枚2元硬币。
如果money小于2,说明只能用1元硬币来凑零钱,那么backtrack()函数就会递归调用自身,并将money减去1。随后,程序会将fen1的值加1,表示已经用了1枚1元硬币。
最后,当函数返回时,fen5、fen2和fen1分别记录了使用5元、2元和1元硬币的个数,即为硬币组合方案。这个方案就是唯一的硬币组合方案。程序会输出这个方案所用的硬币个数。
时间复杂度分析
假设硬币的面值最大值为C,找零的金额为N,那么回溯的深度最多为N/C,因此backtrack函数的时间复杂度是O(N/C)。
代码实现
#include <iostream>
using namespace std;
int fen5 = 0;
int fen2 = 0;
int fen1 = 0;
void backtrack(int money)
{
if (money <= 0)
return;
if (money >= 5)
{
backtrack(money - 5);
fen5++;
}
else if (money >= 2)
{
backtrack(money - 2);
fen2++;
}
else
{
backtrack(money - 1);
fen1++;
}
}
int main() {
int ans = backtrack(13);
cout << ans << endl;
}
四、回溯算法求解满足价值为n并且至少使用一次的所有方案
此方法求解本题最合适,能够满足全部的题目要求,我们的整体思路很简单,就是通过回溯来不停的求解各种组合,最后满足每一枚硬币至少用一次的情况就将结果集加入到二维数组result里面。
在下面这段代码中,有一个长度为6的整型向量path,用于记录当前硬币组合的情况。同时,还有一个向量result,用于存储所有的硬币组合方案。
backtracking()函数实现了回溯算法。它通过递归函数的方式,不断地向向量path中添加硬币,直到达到目标金额money。如果组合方案中包含1元、2元和5元硬币,那么这个方案就是有效的。在这种情况下,会将这个方案加入到result向量中。如果向量result中已经存在了这个方案,就不会重复添加。
在回溯过程中,程序会遍历nums向量中的所有硬币,将它们一个个添加到path向量中,再递归调用backtracking()函数,直到找到所有的方案或者无法找到更多的方案为止。在递归返回时,程序会将path向量中添加的最后一个硬币删除,以便继续寻找其他方案。
时间复杂度分析
假设硬币的面值个数为N,找零的金额为M,最坏情况下所有硬币都可以使用,那么回溯的深度最多为M,每个硬币都可以被选或不选,因此时间复杂度是O(2^N * M)。这个时间复杂度是指最坏情况下的时间复杂度,实际运行时间可能会更快。
代码实现
#include <iostream>
#include <vector>
#include <unordered_set>
#include <algorithm>
using namespace std;
vector<int> path(6, 0);
vector<vector<int>> result;
void backtracking(int money, vector<int>& nums)
{
if (money == 0 && path[1] && path[2] && path[5])
{
vector<vector<int>>::iterator it = find(result.begin(), result.end(), path);
if (it == result.end())
{
result.insert(result.begin(), path);
return;
}
return;
}
else if (money < 0) return;
for (int i = 0; i < nums.size(); i++)
{
path[nums[i]]++;
backtracking(money - nums[i], nums);
path[nums[i]]--;
}
}
int main() {
vector<int> nums = { 1,2,5 };
backtracking(13, nums);
for (auto item : result)
{
cout << "fen5:" << item[5] << ", ";
cout << "fen2:" << item[2] << ", ";
cout << "fen1:" << item[1] << ", ";
cout << "total:" << item[5] + item[2] + item[1] << endl;
}
cout << "count = " << result.size() << endl;
return 0;
}

浙公网安备 33010602011771号