文章作者:Yx.Ac    文章来源:勇幸|Thinking (http://www.ahathinking.com)   转载请注明,谢谢合作。 

---

话说这道题还是三年前径点公司来学院笔试中的一道题目,当时刚进入实验室,师兄在带着我们做新生培训的时候做过这道题,最近回顾DP的一些基础,翻找以前写的程序,发现了这道题,就贴一下,给出两种方法的代码,并对比了它们在不同规模问题下的效率。

题目:20个桶,每个桶中有10条鱼,用网从每个桶中抓鱼,每次可以抓住的条数随机,每个桶只能抓一次,问一共抓到180条的排列有多少种 (也可求概率)。

分析:我们要在20个桶中一共抓取180条鱼,每个桶能抓到鱼的条数为0-10,仔细想下,这个问题是可以分解成子问题的:假设我们在前i个桶中抓取了k(0<=k<=10*i)条鱼,那么抓取180条鱼的排列种数就等于在剩下的(20-i)个桶中抓取(180-k)条鱼的方法加上前i个桶中抓取k条鱼的方法。

例如,在第一个桶中抓取了2条鱼,那么总的排列数等于在剩下19个桶中抓取178条鱼的排列种数;如果在第一个桶中抓取了10条鱼,那么总的排列数等于在剩下19个桶中抓取170条鱼的排列数,,,依次分解该问题,总的排列数就等于所有这些排列数的总和。有点DP的感觉。

换个思维,在实现上这个题目可以有更为简洁的方法,我们看看这个问题的对偶问题,抓取了180条鱼之后,20个桶中剩下了20条鱼,不同的抓取的方法就对应着这些鱼在20个桶中不同的分布,于是问题转化为将20条鱼分到20个桶中有多少中不同的排列方法(这个问题当然也等价于180条鱼分到20个桶中有多少种不同的方法)?其中,每个桶最多放10条,最少放0条。这样一转化,无论是用搜索还是DP,问题规模都缩小了很大一块。

按照这个分析,最直接的方法就是用递归了,递归实现DP问题,自顶向下,为了防止重复计算子问题(例如求19个桶放12条鱼的方法数时算了一遍子问题17个桶放10条鱼的方法数,在算18个桶,17个桶时就不用再计算17个桶放10条鱼的情况了),一般设置一个备忘录,记录已经计算过的子问题,其实这个备忘录就是在自底向上实现DP时的状态转移矩阵。

递归实现,如果桶没了,鱼还有,说明这种排列不符合要求,应该结束并返回0;如果桶还有,鱼没了,说明这种排列也不符合要求;只有在桶没了,鱼也没了的情况下才说明20条鱼恰好分放到了20个桶。根据上面分析我们知道每个桶有11种情况,代码如下:

#include <iostream>
using namespace std;/*捞鱼:将20条鱼放在20个桶中,每个桶最多可以放10条求得所有的排列方法DP自顶向下递归 备忘录
*/int dp[21][21]; /* 备忘录,存储子问题的解; 表示前i个桶放j条鱼的方法数 */int allocate(int bucketN, int fishN)
{if(bucketN == 0 && fishN == 0){return 1;}if(bucketN == 0 || fishN < 0){return 0;}/* 如果子问题没有计算就计算,否则直接返回即可 */if(dp[bucketN][fishN] == 0){for(int i = 0; i <= 10; ++i){dp[bucketN][fishN] += allocate(bucketN-1,fishN-i);}}return dp[bucketN][fishN];
}void main()
{int bucketN, fishN;while(scanf("%d %d", &bucketN, &fishN)!= EOF){memset(dp,0,sizeof(dp));printf("%d\n",allocate(bucketN,fishN));}
}

输出:

结果如图,先输入一个小数据验证解是否正确,可以看出这个解是非常大的,最初实现的两种情况都是等了好久都没有出来结果,一种是没有使用备忘录,单纯递归的搜索,非常非常非常慢,等了两分钟都没有结果;一种是没有求对偶问题,而是求dp[20][180]也是相当的慢。

既然可以用DP,我们通常使用自底向上的方法,下面来看看非递归实现的方法。自底向上就需要考虑合法状态的初始化问题,从小规模去考虑,20个桶太大,考虑零个桶,一个桶,零个桶装多少鱼都是非法的,故就是0;一个桶装鱼,装0-10条鱼都是合法的,其余的就不合法了; dp[i][j]:前i个桶放j条鱼的方法共分为11种情况:前i-1个桶放j-k(0<=k<=10)条鱼的方法总和。我们可以得到状态方程:

1
f(i,j) = sum{ f(i-1,j-k), 0<=k<=10}

考虑到这,dp的程序就出来了,代码如下:

#include <iostream>
using namespace std;/*捞鱼:将20条鱼放在20个桶中,每个桶最多可以放10条求得所有的排列方法自底向上DP f(i,j) = sum{ f(i-1,j-k), 0<=k<=10}该方法中测试 20个桶 180条鱼,与递归速度做对比
*//* 实现1 */int dp[21][200]; /* 前i个桶放j条鱼的方法数 */
int i, j, k;void main()
{int bucketN, fishN;while(scanf("%d %d", &bucketN, &fishN)!= EOF){memset(dp,0,sizeof(dp));for(int i = 0; i <= 10; ++i)  /* 初始化合法状态 */{dp[1][i] = 1;}for(int i = 2; i <= bucketN; ++i)  /* 从第二个桶开始 */{for(int j = 0; j <= fishN; ++j){for(int k = 0; k <= 10 && j-k >= 0; ++k){dp[i][j] += dp[i-1][j-k];}}}printf("%d\n",dp[bucketN][fishN]);}
}

输出:

当我们测试20个桶放180条鱼的方法,结果立即就算出来了,而用递归则是等了半天没反应,由此我们可以看出效率的差别有多大。同时,两个对偶问题的答案是一样的,说明我们的分析是没错的,:-)。

其实,代码还可以更简练,仔细想想,就是初始化状态的方法;其实初始化合法状态完全可以这样想,问题始终都是分解成子问题的,根据递归的实现方法,只有分解到0个桶装0条鱼才是合法的,那么我们就初始化这一个状态为合法即可,然后从第一个桶开始向上计算,代码如下:

/* 实现2 */int dp[21][200];
int i, j, k;void main()
{int bucketN, fishN;scanf("%d %d", &bucketN, &fishN);dp[0][0] = 1;  /* 初始化合法状态 */for(int i = 1; i <= bucketN; ++i)  /* 从第一个桶开始 */{for(int j = 0; j <= fishN; ++j){for(int k = 0; k <= 10 && j-k >= 0; ++k){dp[i][j] += dp[i-1][j-k];}}}printf("%d\n",dp[bucketN][fishN]);
}

从递归到非递归再到现在,一个看似规模很大很复杂的问题只用简单的几行代码就可以解决,关键在于怎么思考,要好好修炼。

总结:

  • 问题分解成子问题
  • 寻对偶问题减少问题规模
  • 不断Thinking,追求简炼代码

本文相关代码可以到这里下载。

(全文完)

有趣的算法:捞鱼问题相关推荐

  1. 冒泡排序出现的问题_停课不停学 | 有趣的算法——冒泡排序

    停课不停学 有趣的算法--冒泡排序 01 生活中处处都有算法 每个人每天都会用到一些算法,算法也是人类使用计算机解决问题的技巧之一,但是算法并不是仅仅用于计算机领域中,包括在数学.物理甚至是每天的生活 ...

  2. DP问题之 捞鱼问题

    捞鱼问题: http://blog.163.com/zhaohai_1988/blog/static/209510085201271743020919/ 模型总结 http://www.cnblogs ...

  3. 自媒体6字箴言:广撒网,多捞鱼

    今天,写一篇自媒体运营类专业文章,主题是"广撒网,多捞鱼". 我想,但凡是我们做自媒体这行的朋友,肯定都开了很多个号,比如头条号.公众号.百家号.一点号.网易号.搜狐号.大鱼号.知 ...

  4. 闲鱼X-Sign算法闲鱼抓包方法

    闲鱼X-Sign算法闲鱼抓包方法 闲鱼X-Sign 抓包效果图 闲鱼X-Sign 最近研究了下闲鱼的APP,抓包方法和X-Sign算法,左上角用户名是我的Q,去掉w是我的WX,欢迎大家交流. 废话不多 ...

  5. 从零到一,这些有趣的算法畅销书,你看过吗?

    还记得许多年前的夏天, 那时的我还是满头黑发, 拼命学习还研究算法, 还有一直鼓励我的她, 当时的我是那么快乐, 虽然刚刚踏入算法大门, 在教室.在宿舍.在食堂里, 研究着别人看不懂的公式. 如果有一 ...

  6. 《算法图解-像小说一样有趣的算法入门书》最全读书笔记--Binrry(冰蕊)

    点击关注,期待Binrry(冰蕊)带给你更多更全的读书笔记-- 可点击下面链接下载本书具体代码执行辅助学习噢: https://download.csdn.net/download/qq_408598 ...

  7. 【图书阅读】《Aditya Bhargava-算法图解:像小说一样有趣的算法入门书》

    这本书主要讲述了算法基础,包括二分查找.大O表示法.两种基本的数据结构等,后续也面对具体问题时的技巧,例如贪婪算法或动态规划:散列表的应用:图算法:K最近邻算法.该篇博文主要记录阅读完的一些重点回顾! ...

  8. 计算阶乘的另一些有趣的算法

    一个正整数n的阶乘就是前n个正整数的乘积,我们通常需要n-1次乘法操作来算出精确的值.不像等差数列求和.a的n次幂之类的东西,目前求阶乘还没有什么巨牛无比的高效算法,我们所能做的仅仅是做一些小的优化. ...

  9. 一个有趣的算法问题:如何定义一个分数类

    一个来自于C++程序设计的经典问题.如何定义一个分数类,实现分数的约分化简,分数之间的加法.减法.乘法.除法四则运算? 1.初见 刚看到这道题的时候,第一感觉是挺简单的啊,就是基本的面向对象,定义对应 ...

  10. 用Java编写约分最简公式_一个有趣的算法问题:如何定义一个分数类

    一个来自于C++程序设计的经典问题.如何定义一个分数类,实现分数的约分化简,分数之间的加法.减法.乘法.除法四则运算? 1.初见 刚看到这道题的时候,第一感觉是挺简单的啊,就是基本的面向对象,定义对应 ...

最新文章

  1. 复制文件以及异常处理
  2. 闲话网名之“jrfly331”
  3. mysql5.7.14多实例安装
  4. html实体转换成xa0,关于javascript:反应道具:在JSX动态内容中使用HTML实体?
  5. Android开发之自定义的ProgressDialog
  6. centos web服务器---sysctl.conf调优参数
  7. Spark —— RDD、DataFrame 与 Dataset
  8. ORACLE的程序包1-程序包的基
  9. 项目管理工具_项目管理工具MS Project使用经验分享
  10. Service服务学习(SimpleRandomServiceDemo)
  11. 数论和有限域的基本概念
  12. Navicat注册机报错No all pattern found! file already patched
  13. mybatis 文档 学习
  14. java下载天地图数据,天地图离线地图,可指定经纬度范围
  15. 闺蜜生日c语言代码,祝闺蜜生日快乐的说说大全 2018最新祝朋友生日快乐经典说说...
  16. java 时分秒转毫秒_运行时间(Java版本)—转换毫秒到时分秒日期
  17. 支付宝身份认证初始化服务40004未知的错误码
  18. LitsModer —— 开发日志(上)
  19. 百度地图 公交线路查询
  20. C++ 使用Poco库操作 json 文件

热门文章

  1. EditPlus 使用技巧以及快捷键
  2. google play测试内购流程
  3. Codeforces 1023G:Pisces(最长反链)
  4. [thrift] thrift基本原理及使用
  5. 爬虫实战之爬取电影天堂全部电影信息
  6. 音视频开发之Android端native层播放音频三种方式
  7. 洛谷P1308统计单词数C语言
  8. fastlane build 版本号自增
  9. Windows平台Qt添加OpenCV模块
  10. 【Docker系列】 Docker secrets