凑硬币(58同城2017校招笔试题)

凑硬币(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

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值