设有N种面值的硬币,要求用最小的硬币数找M元零钱。这里给出三种解答:1、动态规划。2、贪婪、3、暴力搜索
1、动态规划
package my.dynamicprogram;
public class DynamicSolveConitsProblem {
/**
* 只有三种硬币0是为了让数组在使用的时候下标从1开始算
*/
public static final int[] COINTS = new int[] {0, 1, 3, 5, 7, 13};
/**
* 需要找零的钱数
*/
public static final int MONEY = 20;
/**
* 构造最优解数据矩阵
*
* @return
*/
public static int[][] initBestResult() {
int[][] arr = new int[COINTS.length][MONEY + 1];
// 如果硬币的面额是0元,则凑够i元,需要无穷多的硬币
for (int i = 1; i <= MONEY; i++) {
arr[0][i] = Integer.MAX_VALUE;
}
// 如果钱数是0,则凑够0元只需要0个硬币
for (int i = 0; i < COINTS.length; i++) {
arr[i][0] = 0;
}
for (int i = 1; i < COINTS.length; i++) {// 硬币面额数组,遍历
for (int j = 1; j <= MONEY; j++) {
if (j < COINTS[i]) {// 如果钱数j小于硬币COINTS[i]的面额,则就取硬币面额为COINTS[i-1],凑够j元的硬币个数,即arr[i][j] = arr[i-1][j];
arr[i][j] = arr[i - 1][j];
} else if (j == COINTS[i]) {// 如果钱数j刚好等于硬币COINTS[i]的面额,则
arr[i][j] = 1;
} else {// 如果硬币COINTS[i]的面额大于钱数j,则有两种处理方法。1、取硬币COINTS[i]。2、不取COINTS[i]。然后取结果较小的情况即可
/**
* 1、如果取当前面额COINTS[i]的硬币,则当前已经有1个面额为COINTS[i]的硬币,那么要凑够j元还需要j-COINTS[i]元,
* 要凑够j-COINTS[i],在硬币数组COINTS的下标为i,钱数是j-COINTS[i]时的硬币个数arr[i][j-COINTS[i]] 此时硬币个数arr[i][j-COINTS[i]]+1.
* 2、如果不取当前面额COINTS[i]的硬币,则凑够j元,就需要arr[i-1][j]
*/
if (arr[i][j - COINTS[i]] + 1 < arr[i - 1][j]) {
arr[i][j] = arr[i][j - COINTS[i]] + 1;
} else {
arr[i][j] = arr[i - 1][j];
}
}
}
}
return arr;
}
/**
* 打印最优解数组矩阵
*
* @param arr
*/
public static void printMatrix(int arr[][]) {
System.out.print(String.format("%-5s", "") + " ");
for (int i = 1; i <= MONEY; i++) {
System.out.print(String.format("%-5s", String.valueOf(i)) + " ");
}
System.out.print("\n");
for (int i = 1; i < COINTS.length; i++) {
System.out.print(String.format("%-5s", COINTS[i]) + " ");
for (int j = 1; j <= MONEY; j++) {
System.out.print(String.format("%-5s", String.valueOf(arr[i][j])) + " ");
}
System.out.print("\n");
}
}
/**
* 回溯最优解 按照构建矩阵的逻辑可知arr[i][j](i、硬币种类数组COINTS下标。j、钱数)取值有两种情况。
* 1、arr[i][j]=a[i-1][j]。此时并未选择第i种硬币
* 2、arr[i][j]=a[i][j-COUNTS[i]]+1。此时选择了第i种硬币 由此作为回溯的基本原理,实现结果如下:
*
* @param arr
*/
public static void printBestPlan(int arr[][]) {
String selectConintsTs = "";// 被选择的硬币的数组下标
int cointsCnt = arr[COINTS.length - 1][MONEY];
System.out.println("找"+MONEY+"元最少需要" + cointsCnt + "个硬币");
int money = MONEY;
for (int i = COINTS.length - 1; i > 0; i--) {
while (arr[i - 1][money] != arr[i][money]) {
selectConintsTs += i;
money -= COINTS[i];
}
}
String[] str = selectConintsTs.split("");
System.out.println("选择的硬币为:");
for (int i = 0; i < str.length; i++) {
System.out.print(String.format("%-5s", COINTS[Integer.parseInt(str[i])] + "元"));
}
}
public static void main(String ... args) {
System.out.println("有面值1元、3元、5元、7元、13元五种硬币,求找20元零钱所使用的最小硬币数量。");
int arr[][] = initBestResult();
printMatrix(arr);
printBestPlan(arr);
}
}
2、贪婪
package my.dynamicprogram;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
public class GreedySolveCointsProblem {
/**
* 只有三种硬币0是为了让数组在使用的时候下标从1开始算
*/
public static final int[] COINTS = new int[] {1, 3, 5};
public static Map<Integer, Integer> solve(int money) {
Map<Integer, Integer> selectMap = new HashMap<Integer, Integer>();
if (money < 1) {
return selectMap;
}
for (int i = COINTS.length - 1; i >= 0; i--) {
int currentCnt = money / COINTS[i];
if (currentCnt > 0) {
selectMap.put(COINTS[i], currentCnt);
}
money = money % COINTS[i];
if (money == 0) {
break;
}
}
return selectMap;
}
public static void print(Map<Integer, Integer> result) {
Iterator<Integer> key = result.keySet().iterator();
int cnt = 0;
System.out.println("选择硬币为:");
while (key.hasNext()) {
int tem = key.next();
cnt += result.get(tem);
System.out.println(tem + "元选择了" + result.get(tem) + "个 ");
}
System.out.print("\n总共选择了" + cnt + "个");
}
public static void main(String ... args) {
System.out.println("有面值1元、3元、5元、7元、13元五种硬币,求找11元零钱所使用的最小硬币数量。");
Map<Integer, Integer> selectMap = solve(11);
print(selectMap);
}
}
3、暴力搜索
package my.dynamicprogram;
/**
*
* 如果我们有面值为1元、3元和5元的硬币若干枚,如何用最少的硬币凑够11元?
*/
public class CointsProblem {
public static int[] DEALUT_COINTS = {1, 3, 5};
public static int solve(int money) {
int[] arr = new int[money + 1];
for (int i = 0; i < arr.length; i++) {
arr[i] = i;
}
if(money == 1||money ==2){
return arr[money];
}
for (int i = 3; i <= money; i++) {
for (int j = i - 1; j >= 0; j--) {
for (int k = 0; k < DEALUT_COINTS.length; k++) {
if (j + DEALUT_COINTS[k] == i && arr[j] + 1 < arr[i]) {
arr[i] = arr[j] + 1;
}
}
}
System.out.println(i+"元需要"+arr[i]+"个硬币");
}
return arr[money];
}
public static void main(String ...args){
solve(100);
}
}
从文中可以看出,贪婪算法是最直接最简单的方式,也是我们常人容易接受和理解的。但是这里不能就此否定动态规划的作用,在此题中,我们的硬币只有一种属性,那就是面额,如果硬币有了其他的属性作为限制条件,比如重量作为限制条件(一个人负重有限,只能带走一定重量的硬币),那么贪婪算法就不再适用了。此类问题,典型的题目如01背包问题,背包容量是有限的,物品则有价值和重量两种属性,而一个人一次性只能带走固定重量的物品,那么在求一次性能带走的最大价值的物品的时候,就无法只用物品的一个纬度去计算了,比如按照每次拿的物品价值最大的唯独,或者每次拿物品重量最小的纬度,这样都不准确,此时动态规划算法的优势就体现出来了。
暴力搜索算法,则是最基础的算法,有利于去理解动态规划算法。也是我们平时在解决问题最容易想到的方案。