黑马程序员技术交流社区

标题: 论解决问题的正确姿势——从《海盗分金》说开去 [打印本页]

作者: 龙骑将杨影枫    时间: 2014-12-5 00:54
标题: 论解决问题的正确姿势——从《海盗分金》说开去

闲的筋疼的时候曾经用代码处理过这么一个例子,经济学中非常有名的“海盗分金”模型。

5个海盗抢得100枚金币,他们按抽签的顺序依次提方案:首先由1号提出分配方案,然后5人表决,超过半数(注意此处)同意方案才被通过,否则他将被扔入大海喂鲨鱼,依此类推。
然后在假设所有海盗都绝顶聪明,并且足够残忍,你是第一个海盗,如何提出分配方案才能使得自己的利益最大化?

我一开始看到这个题,也是老虎咬刺猬无从下嘴,甚至觉得假如我是第一个海盗,无论如何分配钱财都会被同伙踹下海(得益于我的2B性格)。 无奈之下看了一眼答案,明白了。

说到底,还是个假设法推断的问题。

从后向前推,如果1至3号强盗都喂了鲨鱼,只剩4号和5号的话,5号一定投反对票让4号喂鲨鱼,以独吞全部金币。所以,4号惟有支持3号才能保命。
3号知道这一点,就会提出“100,0,0”的分配方案,对4号、5号一毛不拔而将全部金币归为已有,因为他知道4号一无所获但还是会投赞成票,再加上自己一票,他的方案即可通过。
不过,2号推知3号的方案,就会提出“98,0,1,1”的方案,即放弃3号,而给予4号和5号各一枚金币。由于该方案对于4号和5号来说比在3号分配时更为有利,他们将支持他而不希望他出局而由3号来分配。这样,2号将拿走98枚金币。
同样,2号的方案也会被1号所洞悉,1号并将提出(97,0,1,2,0)或(97,0,1,0,2)的方案,即放弃2号,而给3号一枚金币,同时给4号(或5号)2枚金币。由于1号的这一方案对于3号和4号(或5号)来说,相比2号分配时更优,他们将投1号的赞成票,再加上1号自己的票,1号的方案可获通过,97枚金币可轻松落入囊中。这无疑是1号能够获取最大收益的方案了!答案是:1号强盗分给3号1枚金币,分给4号或5号强盗2枚,自己独得97枚。分配方案可写成(97,0,1,2,0)或(97,0,1,0,2)。”

推理结束(蒙圈的同学可以再回头捋一遍),于是问题来了:如何用代码实现呢?我又抓耳挠腮好久,做猴子状。

蒙圈许久(不是指推理过程 而是思考代码描述的过程),我决定抛掉所有复杂的算法,重新去研究问题的内在规律。于是,我发现了如下几点:
1
排在前面的海盗要考虑排在后面海盗的分配方式。1号要考虑2号,2号要考虑3号,3号要考虑4号。当然4号不用考虑,无论他怎么分,他都会被5号踹下海滴。
换句话说,1号的分钱计划要考虑2号分钱计划,2号分钱计划要考虑3号分钱计划....
再换句哈说,f(n)和f(n-1)有关,f(n-1)和f(n-2)有关.....依次类推。
于是我恍然大悟豁然开朗:我艹,这不就是递归嘛?我居然还试图用博弈论的算法去分析,实在是上帝为我关上一扇窗户的时候还顺便夹了我的脑子啊。
于是,我决定采用递归结构做程序的主体。
2
排在前面的海盗在考虑完后面海盗的分配方式的目的,是为了获得更后面的海盗的支持(再次强调,超过半数的支持)。
换句话说,1号海盗要从身后的4个海盗中获得2票支持,2号海盗要从身后的3个海盗里获得2支持....
如果身后有m个海盗,那么前面的海盗要获得的票数是m/2(注:向下取整)。
3
前面的海盗要如何获得这些人的票数呢?很简单,从身后m个人里选出m/2来,在他们原有的收益上多给1块钱就可以了。
而且,为了保证自己收益最大化,那么这m/2个人,必然是m个数里比较小的数。
换个不抽象的话说,1号海盗要从2号分钱方案的4个人里,选出2个分钱最少的海盗,各给1元钱,进行拉拢。
4
方案考虑完了,人也确定了,分赃。
5
分赃完毕,程序结束,问题解决,写代码。


作者: 龙骑将杨影枫    时间: 2014-12-5 00:58
本帖最后由 龙骑将杨影枫 于 2014-12-5 01:05 编辑

首先我创建了一个名为Pirate的类,包含2个公有属性(懒得再去get和set)。其中gold表示该名海盗在一轮分金中需要分多少钱,isGiven来表示在此轮分金中是否要给他钱。

  1.     public class Pirate {
  2.     public int gold=0;
  3.     public boolean isGiven=false;
  4.     }
复制代码


然后我又创建了一个PirateThinking的类,模拟思考过程。

  1.     public class PiratesThinking {
  2.     public Pirate[] pirates=new Pirate[10];//首先,初始化一排海盗。

  3.     public int gold=100;//设定总金额为100块钱

  4.     public PiratesThinking(){
  5.     this.init();//初始化
  6.     }

  7.     private void init() {
  8.     for(int i=0;i<this.pirates.length;i++){
  9.     pirates=new Pirate();
  10.     }
  11.     }

  12.     //模拟分钱思考过程
  13.     public Pirate[] spilitGold(int n){
  14.     Pirate[] temp=null;
  15.     Pirate[] temp2=null;
  16.     temp=new Pirate[n];
  17.     temp=this.initPirates(temp);
  18.     if(n==3){
  19.     temp[0].gold=100;
  20.     }else if(n>3){
  21.     temp2=this.spilitGold(n-1);//洞悉下一个人的分配方式
  22.     temp2=this.removeSpilit(temp2);//抹掉下一个人分配的痕迹
  23.     temp2=this.getMins(temp2,n/2);//从身后获取半数选票
  24.     temp=this.copyArray(temp2);//再加上本人,形成最终的分配序列
  25.     temp=this.bribe(temp);//分钱
  26.     }
  27.     this.showPirates(temp);
  28.     return temp;
  29.     }
  30.     //去除上一次的分配信息
  31.     private Pirate[] removeSpilit(Pirate[] temp2) {

  32.     for(int i=0;i<temp2.length;i++)
  33.     temp2.isGiven=false;
  34.     return temp2;
  35.     }

  36.     //显示分配方式
  37.     public void showPirates(Pirate[] pirate2){
  38.     System.out.printf("%2d个海盗时分配模式为:",pirate2.length);
  39.     Pirate[] temp3=new Pirate[10];
  40.     int j=pirate2.length-1;
  41.     for(int i=temp3.length-1;i>=0;i--){
  42.     try {
  43.     temp3=pirate2[j];
  44.     } catch (RuntimeException e) {
  45.     temp3=new Pirate();
  46.     temp3.gold=0;
  47.     }
  48.     j--;
  49.     }
  50.     for(int i=0;i<temp3.length;i++){
  51.     if (i==0)
  52.     System.out.printf("%3d",temp3.gold);
  53.     else
  54.     System.out.printf("%3d,",temp3.gold);
  55.     }
  56.     System.out.println();
  57.     }

  58.     //从m个数中取出n个比较小的数值来
  59.     private Pirate[] getMins(Pirate[] temp2, int count) {
  60.     for (int i=0;i<count;i++){
  61.     int compare=101;
  62.     for(int j=0;j<temp2.length;j++){
  63.     if(!temp2[j].isGiven && temp2[j].gold<compare)
  64.     compare=temp2[j].gold;
  65.     }

  66.     temp2=this.findPirateByGold(temp2,compare);
  67.     }
  68.     return temp2;
  69.     }

  70.     //根据所分钱数确定海盗的位序
  71.     private Pirate[] findPirateByGold(Pirate[] temp2, int compare) {
  72.     for(int i=0;i<temp2.length;i++){
  73.     if (temp2.gold==compare && !temp2.isGiven){
  74.     temp2.isGiven=true;
  75.     break;
  76.     }
  77.     }

  78.     return temp2;
  79.     }

  80.     //数组复制
  81.     private Pirate[] copyArray(Pirate[] temp2) {
  82.     Pirate[] temp=new Pirate[temp2.length+1];
  83.     for(int i=0;i<temp2.length;i++){
  84.     temp[i+1]=temp2;
  85.     }
  86.     temp[0]=new Pirate();
  87.     temp[0].gold=0;
  88.     temp[0].isGiven=false;
  89.     return temp;
  90.     }

  91.     //初始化数组
  92.     public Pirate[] initPirates(Pirate[] temp){
  93.     for(int i=0;i<temp.length;i++){
  94.     temp=new Pirate();
  95.     }
  96.     return temp;
  97.     }

  98.     //按照isGiven的值来进行分钱
  99.     private Pirate[] bribe(Pirate[] pirates2) {
  100.     int sum=0;
  101.     for(int i=1;i<pirates2.length;i++){
  102.     if (pirates2.isGiven){
  103.     pirates2.gold++;
  104.     sum+=pirates2.gold;
  105.     }else{
  106.     pirates2.gold=0;
  107.     }
  108.     }
  109.     pirates2[0].gold=100-sum;
  110.     return pirates2;
  111.     }

  112.     }
复制代码



最后编写一个演示类,进行运行。

  1.     public class Demo {

  2.     /**
  3.     * @param args
  4.     */
  5.     public static void main(String[] args) {
  6.     PiratesThinking pt=new PiratesThinking();
  7.     pt.spilitGold(5);
  8.     }

  9.     }
复制代码

任务完成,经过验证符合推理结果。(注:偷懒,写的定长的数组,最多可以验证10个人)

说明:
1
关于如何从m个数里选出n个比较小的数又不移动他们本身的位置,我采取的是这种办法。
首先取一个足够大的常量(这这次我取的是101,因为只有100块钱,不可能有人会分101块钱),进行遍历比较。如果发现有数比常量小,用此数代替常量。遍历一圈后,即可找到此列数字中最小的一个数,也就是海盗们所应该分的钱。然后我通过findPirateByGold去定位该名海盗,将他的isGiven设为true。isGiven为true的海盗不参与与常量的比较。进行下一轮比较。一共进行m/2轮比较。
2
为何要有抹掉下一个人分配的痕迹操作。因为我是按照递归的方法进行调用,所以如果后面的海盗分完钱,必然会有海盗isGiven是true的情况,会影响到本次分配。因此每递归一次都要将isGiven置空。
3
我本人编程时养成的习惯,写代码前考虑清楚,写代码时分割清楚。宁愿多写几个函数名称,也要把大函数分割成小函数。第一清晰易读。第二降低耦合度,防止一错错一堆。可能诸位现在写还习惯于写在一起。我真诚的建议:模块化函数从现在开始。不然等以后遇到一旦写出超过200行(我本人是100行)的大函数,其他程序员修改调试时会有拿刀劈了你的冲动。

以上就是我在处理海盗分金时的思考过程与编码过程,希望能对大家有所帮助。实际上编程说难不难说简单也很简单,下次如果碰到不知如何下手的问题时,不如先把代码和算法以及对弱智电脑的不满扔到一边。泡杯蜂蜜柚子茶,然后找纸笔划拉划拉,静下心来想想,如果是我,我会怎么处理?

能想清楚,划拉清楚,剩下的就是写代码的功夫,没什么大不了的。
作者: Mr.Ni    时间: 2014-12-5 01:30
膜拜!!!




欢迎光临 黑马程序员技术交流社区 (http://bbs.itheima.com/) 黑马程序员IT技术论坛 X3.2