硬币找钱问题
Problem Description
设有六种不同面值的硬币,各硬币的面值分别为 5分,1角,2角,5角,1元,2元。现要用这些面值的硬币来购物和找钱。购物时可以使用的各面值的硬币个数存于数组 C o i n [ 1 ; 6 ] Coin[1;6] Coin[1;6] 中,商店里各面值的硬币有足够多。在一次购物中希望使用最少的硬币个数。
例如,一次购物需要付款 0.55 元,没有 5 角的硬币,只好用 2 × 20 + 10 + 5 2 \times 20+10+5 2×20+10+5 共 4 枚硬币来付款。如果付出 1 元,找回 4 角 5 分,同样需要 4 枚硬币。但是如果付出 1.05 元(1枚1元和1枚5分),找回 5 角,只需要 3 枚硬币,这个方案用的硬币个数最少。
对于给定的各种面值的硬币个数和付款金额,编程计算使用硬币个数最少的交易方案,如果顾客手中的硬币总量不够付款金额,则输出impossible
。
Sample Input
文件input.txt
给出输入数据。每一行有6个整数和一个有2位小数的实数,分别表示可以使用的各种面值的硬币个数和付款金额。
2 4 2 2 1 0 0.95
2 4 2 0 1 0 0.55
0 0 0 0 0 0 1
Sample Output
输出文件 output.txt
2
3
impossible
算法源代码
#include <iostream>
#include <fstream>
#include <algorithm>
using namespace std;
int Coins[6]; // 各面值硬币数
int CoinNum = 0; // 需要的最少硬币数
int Pay; // 需要支付的钱数
int CoinValue[6] = {5, 10, 20, 50, 100, 200}; // 硬币面值,以分为单位
int ChangeValue[7] = {0, 5, 10, 20, 50, 100, 200}; // 用来找零的硬币面值,0为不找零的情况
int RealMoney = CoinValue[0] - ChangeValue[0]; // 实际支付金额 = 支付硬币 - 找零硬币 【贪心变量】
ifstream input("input.txt");
ofstream output("output.txt");
/*****************************************************************
* 函数描述: 从文件中读取测试数据
*****************************************************************/
void getData()
{
for (int i = 0; i < 6; ++i)
input >> Coins[i];
double costt;
input >> costt;
cout << "\n目标金额:" << costt << "元"
<< "\n各币值数量:" << endl;
Pay = (int)(costt * 100); //将输入的钱转为分为单位
for (int i = 0; i < 6; ++i)
cout << Coins[i] << " ";
}
/*****************************************************************
* 函数描述: 格式化输出结果
*****************************************************************/
void outputResult()
{
if (CoinNum == 0 || Pay != 0)
{
cout << "\n============== impossible" << endl;
output << "impossible" << endl;
}
else
{
cout << endl
<< "各币值剩余数量:" << endl;
for (int i = 0; i < 6; i++)
cout << Coins[i] << " ";
cout << endl
<< "============== 支付和找零需要的最少总硬币数量:" << CoinNum << endl;
output << CoinNum << endl;
}
}
/*****************************************************************
* 函数描述:查看顾客手中是否还有 a 面值的硬币,
* 如果有则返回面值索引,否则返回 -1
* 函数返回:面值索引 or -1
*****************************************************************/
int contains(int a)
{
for (int i = 0; i < 6; ++i)
if (CoinValue[i] == a && Coins[i] > 0)
return i;
return -1;
}
/*****************************************************************
* 函数描述: 贪心法实现硬币找零问题,以实际支付金额作为贪心变量
*****************************************************************/
void Greed()
{
// 顾客手中的硬币面值从大到小遍历选取
for (int i = 5; i >= 0; --i)
{
if (Coins[i] > 0) // 如果顾客手中当前面值硬币有剩余
{
// 商家手中的硬币从小到大遍历选取,进行找零操作
for (int j = 0; j <= i; ++j)
{
RealMoney = CoinValue[i] - ChangeValue[j]; // 实际支付金额 = 支付硬币 - 找零硬币
// 只有当实际支付金额小于目标金额的时候才进行选取,否则需要增加找零硬币的金额(即,继续本层循环 j)
if (Pay >= RealMoney)
{
if (Coins[i] >= Pay / RealMoney) //如果当前面值硬币找零后足够支付余额
{
int TempCoinNum = Pay / RealMoney; // 此步骤顾客消耗硬币数量
CoinNum += TempCoinNum * 2;
// 如果顾客具有该硬币,则无需使用找零,因为找零需要耗费2个硬币,而支付只需要一个硬币
if (contains(RealMoney) != -1)
{
TempCoinNum = min(TempCoinNum, Coins[contains(RealMoney)]);
CoinNum -= TempCoinNum;
Coins[contains(RealMoney)] -= TempCoinNum; // 顾客手中相应硬币数减少
}
else
Coins[i] -= Pay / RealMoney; // 顾客手中当前面值硬币数减少
Pay = Pay % RealMoney; // 更新还需支付的金额
if (contains(CoinValue[i]) == -1)
break; // 如果顾客手中该硬币用完,直接用下一个小面值
}
else // 如果当前币值不够支付,则用完该币值
{
CoinNum += Coins[i];
Pay = Pay - CoinValue[i] * Coins[i];
Coins[i] = 0;
}
}
}
}
}
}
int main()
{
while (!input.eof())
{
CoinNum = 0;
getData(); // 读取测试数据
Greed(); // 贪心法实现硬币找零问题
outputResult(); // 输出结果
}
input.close();
output.close();
}
程序运行截图
贪心算法
所谓贪心算法是指在对问题求解时,总是做出在当前看来是最好的选择。也就是说,不从整体最优上加以考虑,它所做出的仅仅是在某种意义上的局部最优解。
贪心算法没有固定的算法框架,算法设计的关键是贪心策略的选择。必须注意的是,贪心算法不是对所有问题都能得到整体最优解,选择的贪心策略必须具备无后效性。
核心思路
解决本问题在于贪心变量的找取。原始的硬币问题的贪心变量很直观,就是硬币从大到小选取(原始硬币问题的描述与解决见此链接)。而加上了找零的操作后,贪心变量就不那么直观了。本问题中的贪心变量是每个硬币的实际支付金额,加上找零的本质就是通过找零的排列组合,将硬币的面值变多了。
实 际 支 付 金 额 = 支 付 硬 币 − 找 零 硬 币 实际支付金额 = 支付硬币 - 找零硬币 实际支付金额=支付硬币−找零硬币
注意的“坑”
其中有一点需要注意的就是,如果找零的排列组合(当前硬币的实际支付金额)存在等面值的真实硬币,则不用找零的操作,直接取硬币,因为找零需要耗费 2 枚硬币,而支付只需要一枚硬币。
博主为学生,正在学习,如博文内容有误,欢迎大家在评论区更正,共同进步~