凑硬币(58同城2017校招笔试题)
暴力破解,循环递归实现,代码如下:
/*
*
* 暴力破解,循环递归,找出了所有可能的组合并进行了存储,
* 在循环递归的时候,因为选取的分类相互是有重叠的,生成的递归树分支出现重复,而递归函数最终返回的就是总的拼凑数目,最终会导致总的数目重复,不得不进行判断,效率低下。。
*/
package others;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
public class CoinCoin58 {
//测试用例
public static void main(String[] args) {
int m = 20;
int[] a = {1, 5, 10, 20, 50, 100};
ArrayList<ArrayList<String>> arr = coinCoin58(m, a);
System.out.println(arr);
System.out.println("总数 = " + arr.size());
}
//找出最少的钱的数目
private static ArrayList<ArrayList<String>> coinCoin58(int m, int[] a) {
ArrayList<ArrayList<String>> arr = new ArrayList<ArrayList<String>>();
HashMap<String, Integer> h = new HashMap<String, Integer>();
coinCoin58(m, a, arr, h);
return arr;
}
private static void coinCoin58(int m, int[] a,
ArrayList<ArrayList<String>> arr, HashMap<String, Integer> h) {
//递归结束条件
if (m == 0) {
ArrayList<String> arr_temp = new ArrayList<String>();
// 将HashMap中的键值对全部取出,存储到arr中。
for (Iterator<String> it = h.keySet().iterator(); it.hasNext();) {
String key = (String) it.next();
for (int j = 0; j <= h.get(key); j++) {
// 键所对应的值位2,就增加两个键,否则增加一个键
arr_temp.add(key);
}
}
if (!arr.contains(arr_temp)) {
arr.add(arr_temp);
}
}
for(int i = 0; i < a.length; i++) {
if(m - a[i] >= 0) {
if(!h.containsKey(Integer.toString(a[i]))) {
h.put(Integer.toString(a[i]), 0);
coinCoin58(m-a[i], a, arr, h);
h.remove(Integer.toString(a[i]));
} else {
h.put(Integer.toString(a[i]), h.get(Integer.toString(a[i]))+1);
coinCoin58(m-a[i], a, arr, h);
h.put(Integer.toString(a[i]), h.get(Integer.toString(a[i]))-1);
}
}
}
}
}
上面是比较常规的思路,但是并不实用,下面采用动态规划,自顶向下解决,注释已经写在代码里面了。
//############################ 动态规划,自顶向下 ####################################
/*
* 循环递归来实现,注意这里分类是按照是否使用某个币值来划分的,这样划分不会在递归树中出现重复分支
* 举个例子来说:m = 10,一共有四中凑硬币的方案。
* {10, 1}
* {5, 5, 1}
* {5, 1, 1, 1, 1, 1, 1}
* {1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1}
* 第一步,是否使用了币值为100的硬币,显然都没有使用 ;
*
* 第二步,是否使用了币值为50的硬币,显然都没有使用;
*
* 第三步,是否使用了币值为20的硬币,显然都没有不使用;
*
* 第四步,是否使用了币值为10的硬币,这时就将集合划分成了两个子集:
* 使用了10: {10, 1}
* 没使用10 {5, 5, 1}、{5, 1, 1, 1, 1, 1, 1}、{1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1}
* 第五步,针对使用了10的情况:此时应该让m = m -10,然后继续对m进行是否使用了币值为10的硬币。
* 针对没使用10的情况,继续判断是否使用了币值为5的硬币
*
* 第六步,针对第五步出现的可能的多种结果,继续进行判断
* 。。。。。。
* 。。。。。。
* 重复上述结果,结果集合中的4个元素会落入各自的类别中。
*
* 注意一点,在计算拼凑m可能的组合方案时,会出现重复计算,可以通过维护一个HashMap来提高效率。
* 也出现了大量的重复情况,可以通过维护一个HashMap来避免重复,提高效率,但是牺牲了空间复杂度。
*/
import java.util.HashMap;
public class CoinCoin58 {
// 测试用例
public static void main(String[] args) {
int m = 1000;
int[] a = { 1, 5, 10, 20, 50, 100 };
//h用于优化代码,牺牲空间复杂度,换取时间复杂度
HashMap<String, Integer> h = new HashMap<String, Integer>();
int result = coinCoin58(m, a, a.length - 1, h);
System.out.println("总数= " + result);
}
private static int coinCoin58(int m, int[] a, int length, HashMap<String, Integer> h) {
if(m == 0){
return 1;
}
if(m < 0) {
return 0;
}
if(length < 0) {
return 0;
}
// /*
// * 方法一:
// * 优化,拿空间复杂度换取时间复杂度
// */
// int temp = 0;
// if(!h.containsKey(Integer.toString(m) + "&" + Integer.toString(length))) {
// temp = coinCoin58(m, a, length - 1, h) + coinCoin58(m-a[length], a, length, h);
// h.put(Integer.toString(m) + "&" + Integer.toString(length), temp);
// return temp;
// } else {
// return h.get(Integer.toString(m) + "&" + Integer.toString(length));
// }
/*
* 方法二
* 不优化
*/
return coinCoin58(m, a, length - 1, h) + coinCoin58(m-a[length], a, length, h);
}
}
现在,我们换个思路,既然自顶向下可以,那么逆转思维,自底向上自然也可以解决。
//############################## 动态规划 自底向上 ###################
public class CoinCoin58 {
// 测试用例
public static void main(String[] args) {
int m = 1000;
int[] a = { 1, 5, 10, 20, 50, 100 };
int result = coinCoin58(m, a);
System.out.println("总数= " + result);
}
private static int coinCoin58(int m, int[] a) {
int[][] table = new int[m + 1][a.length];
// table[i][j]表示钱数为i、且使用币值为a[0]、a[1]....a[j]的硬币来拼凑的总方案。
for (int i = 0; i < a.length; i++) {
table[0][i] = 1;
}
for (int j = 1; j < m + 1; j++) {
for (int k = 0; k < a.length; k++) {
// 包括 a[k] 的方案数
int x = (j - a[k] >= 0) ? table[j - a[k]][k] : 0;
// 不包括 a[k] 的方案数
int y = (k >= 1) ? table[j][k - 1] : 0;
table[j][k] = x + y;
}
}
return table[m][a.length-1];
}
}
到这里时间和空间复杂度,已经比较小了,但是还有神一样的解法。
//######################### 神一样的解法 ########################
/*
* 这两个for循环用的十分巧妙,其循环过程描述
* 当只有0元硬币可以使用时,拼凑n元的方案数显然等于0。
*
* 当只有0、1元硬币可以使用时,拼凑n元的方案数 =
* 当只有0元硬币可以使用时,拼凑n元的方案数 +
* 当只有0、1元硬币可以使用时,拼凑n-1元的方案数 +
*
* 当只有0、1、5元硬币可以使用时,拼凑n元的方案数 =
* 当只有0、1元硬币可以使用时,拼凑n元的方案数 +
* 当只有0、1、5元硬币可以使用时,拼凑n-5元的方案数
*
* 当只有0、1、5、10元硬币可以使用时,拼凑n元的方案数 =
* 当只有0、1、5元硬币可以使用时,拼凑n元的方案数 +
* 当只有0、1、5、10元硬币可以使用时,拼凑n-10元的方案数
*
* 。。。。。。。
*
* 如何理解上述过程呢?
* 还是一个集合分类的思想,比如我们的钱的数目是17,那么可使用的币值为1,5,10
* 我们把结果集合按照是否使用了10分成了两类:第一类是使用了10币值的方案, 第二类是只使用了1,5币值的方案
* 第二类只使用了1,5币值的方案,在上一次循环中已经求出,那么第一类使用了10币值的方案如何求解呢?
* 这里我们用17-10等于7,然后使用币值为1,5,10拼凑7的方案已知。
* 减10就保证了第一类方案中一定会出现10,从而不与第二种方案重复
*
*/
public class CoinCoin58 {
// 测试用例
public static void main(String[] args) {
int m = 1000;
int[] a = { 1, 5, 10, 20, 50, 100 };
int result = coinCoin58(m, a);
System.out.println("总数= " + result);
}
private static int coinCoin58(int m, int[] a) {
int[] table = new int[m + 1];
table[0] = 1;
for (int i = 1; i < m + 1; i++) { //赋初值
table[i] = 0;
}
for (int i = 0; i < a.length; i++) {
for (int j = a[i]; j <= m; j++) {
table[j] += table[j - a[i]];
}
}
return table[m];
}
}
* 到这里,就彻底结束了,一共四种方法,其中动态规划的两种方法,在思想上是相同的,都是把一个大问题进行了划分。但是这个划分是关键点,因为我们的第一种方法暴力破解,它也是把问题进行了划分,先取一个1或5或10或20或50或100这一个硬币,然后再拼凑剩下的硬币,但是这个为什么就复杂了呢?因为我们在对问题进行划分的时候,实际上是把问题的解答集合划分成子集合,而上面那种方式划分的子集合中出现了重复的元素。
比如,拼凑16,问题的解答集合:
{10,5,1}
{5,5,5,1}
{5,5,1,1,1,1,1,1,1,1,1,1}
{10,1,1,1,1,1,}
{5,1,1,1,1,1,1,1,1,1,1}
{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,}
一共是6中可能,注意集合里面元素的排列顺序是无关紧要的,只看元素的种类和个数是否一样。而在第一种方法暴力破解中,按先选取1,5。。。,再拼凑剩下的硬币的方案,显然不能把上述集合分离开,比如,上述{10,5,1}按照第一份方案的分解方式,就会以
{1,5,10}和{1,10,5}的形式出现在先取一个币值为1的硬币子集中,
{5,1,10}和{5,10,1}的形式出现在先取一个币值为5的硬币子集中,
{10,5,1}和{10,1,5}的形式出现在先取一个币值为10的硬币子集中,
显然,上述6种形式是同一种拼凑方案!这样就让递归树的分支出现了重复,而题目求解的就是总数目,导致结果出现错误。
而按照是否使用某个币值来划分,就能把集合完美的分成两个子集合,然后再进一步分解,例如按照是否使用币值5来划分:
第一类:{1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,} {10,1,1,1,1,1,}
第二类:{10,5,1} {5,5,5,1} {5,5,1,1,1,1,1,1,1,1,1,1} {5,1,1,1,1,1,1,1,1,1,1}
显然,上述两个集合中无重复!
综合上述分析过程会发现,在运用动态规划时,集合的划分
应该首先遵循一个无重复元素的原则,尤其是重复划分会影响最终结果的正确性的时候;其次,也可以采用重复划分,当出现重复不会影响结果的正确性,只会降低效率的时候,这时可以通过后期优化来降低重复。切记!
这样写出的代码才是真正属于动态规划的代码!
参考资料:http://blog.csdn.net/jiyanfeng1/article/details/40559111