凑硬币(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
凑硬币(58同城2017校招笔试题)相关推荐
- 58同城秋招笔试题解答 --map模式按value进行排序
求字符串中字母重叠的次数并按要求输出(笔试题) 题目描述: 给定一个字符串,求出字母重叠出现的次数,最后按字典格式输出. 输入示例: aaabccbbbfffaddbaa 输出示例: a : 5 b ...
- 微软2017校招笔试题3 registration day
题目 It's H University's Registration Day for new students. There are M offices in H University, numbe ...
- 今日头条2017校招笔试题
一些出题人出了n道题,每道题有一个难度系数,难度系数满足以下关系的3道题可以组成一套试卷,为了使这n道题使用上且只能使用一次,问出题人最少还要出多少题? a<=b<=c b-a<=1 ...
- 微软2017校招笔试题2 composition
题目 Alice writes an English composition with a length of N characters. However, her teacher requires ...
- 互联网公司招聘--58集团--前端--2017年笔试题1
互联网公司招聘–58集团–前端–2017年笔试题1 互联网公司招聘–58集团–前端–2017年笔试题1 互联网公司招聘–58集团–前端–2017年笔试题1
- 58集团2017校招(第一次正规的笔试)
准备出去实习了,以前都是喜欢写书面笔记的,现在差不多有3本密密麻麻的笔记了,这也是传承了我的导师<每日一篇书面博客>的老传统,现在想想看着自己在这点上的每天坚持,更多的是感谢导师的用心良苦 ...
- 关于python类的继承正确的说法是_2017美团点评的运维岗校招笔试题,测测你会几题?...
原标题:2017美团点评的运维岗校招笔试题,测测你会几题? 1.数据库:以下哪项不是HASH索引的特征? A MySQL不能确定在两个值之间大约有多少行 B 不能使用hash索引来加速ORDER BY ...
- 58同城2020校招转转算法岗笔试编程题
58同城2020校招转转算法岗笔试编程题 第一题 题目描述:年末了要评选优秀员工了,给定一个优秀员工比率,在员工的评分上计算出能被选中的优秀员工的评分阈值.优秀员工人数=员工人数*优秀员工比率.员工人 ...
- 数据分析真题日刷 | 欢聚时代2018校招笔试题-产品经理/数据分析/游戏运营/市场专员 A卷
今日真题 欢聚时代2018校招笔试题-产品经理/数据分析/游戏运营/市场专员 A卷(来源:牛客网) 题型 客观题:单选5道,不定项选择10道 主观题:问答4道 完成时间 90分钟 牛客网评估难度系数 ...
最新文章
- DevExpress z
- 正则表达式--元字符和限定词
- uniapp打包成html5包个ios壳,HBuilder之uni-app打包App方法
- 2016-2017-2 《Java 程序设计》课堂实践项目
- [游戏杂谈]浅谈游戏打击感
- ios第3天的气泡作业
- 如何给PDF文件添加水印?
- STM32 Mbed系列-ADC参考电压设置
- 计算机和信息技术革命,人类历史上的四次信息技术革命
- 安Linux基础入门教程
- Discord教程:Discord账号注册、Discord多账号登录和管理
- 微信刷卡 sdk java_微信支付 Java SDK
- 回归算法(最小二乘法拟合)
- 苍蝇眼睛_桌子的布局像苍蝇一样下降=)
- 应用数据库软件的典型架构有哪些
- 基本初等函数 对数函数
- django1.11 mysql配置_使用Django1.11创建简单的资产管理平台
- OpenERP采购的两种不同情况
- mybatis 如何切割字符串 查询多个值
- 很值得一读精彩语句 转自@红薯