黑马程序员技术交流社区
标题:
论解决问题的正确姿势——从《海盗分金》说开去
[打印本页]
作者:
龙骑将杨影枫
时间:
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来表示在此轮分金中是否要给他钱。
public class Pirate {
public int gold=0;
public boolean isGiven=false;
}
复制代码
然后我又创建了一个PirateThinking的类,模拟思考过程。
public class PiratesThinking {
public Pirate[] pirates=new Pirate[10];//首先,初始化一排海盗。
public int gold=100;//设定总金额为100块钱
public PiratesThinking(){
this.init();//初始化
}
private void init() {
for(int i=0;i<this.pirates.length;i++){
pirates=new Pirate();
}
}
//模拟分钱思考过程
public Pirate[] spilitGold(int n){
Pirate[] temp=null;
Pirate[] temp2=null;
temp=new Pirate[n];
temp=this.initPirates(temp);
if(n==3){
temp[0].gold=100;
}else if(n>3){
temp2=this.spilitGold(n-1);//洞悉下一个人的分配方式
temp2=this.removeSpilit(temp2);//抹掉下一个人分配的痕迹
temp2=this.getMins(temp2,n/2);//从身后获取半数选票
temp=this.copyArray(temp2);//再加上本人,形成最终的分配序列
temp=this.bribe(temp);//分钱
}
this.showPirates(temp);
return temp;
}
//去除上一次的分配信息
private Pirate[] removeSpilit(Pirate[] temp2) {
for(int i=0;i<temp2.length;i++)
temp2.isGiven=false;
return temp2;
}
//显示分配方式
public void showPirates(Pirate[] pirate2){
System.out.printf("%2d个海盗时分配模式为:",pirate2.length);
Pirate[] temp3=new Pirate[10];
int j=pirate2.length-1;
for(int i=temp3.length-1;i>=0;i--){
try {
temp3=pirate2[j];
} catch (RuntimeException e) {
temp3=new Pirate();
temp3.gold=0;
}
j--;
}
for(int i=0;i<temp3.length;i++){
if (i==0)
System.out.printf("%3d",temp3.gold);
else
System.out.printf("%3d,",temp3.gold);
}
System.out.println();
}
//从m个数中取出n个比较小的数值来
private Pirate[] getMins(Pirate[] temp2, int count) {
for (int i=0;i<count;i++){
int compare=101;
for(int j=0;j<temp2.length;j++){
if(!temp2[j].isGiven && temp2[j].gold<compare)
compare=temp2[j].gold;
}
temp2=this.findPirateByGold(temp2,compare);
}
return temp2;
}
//根据所分钱数确定海盗的位序
private Pirate[] findPirateByGold(Pirate[] temp2, int compare) {
for(int i=0;i<temp2.length;i++){
if (temp2.gold==compare && !temp2.isGiven){
temp2.isGiven=true;
break;
}
}
return temp2;
}
//数组复制
private Pirate[] copyArray(Pirate[] temp2) {
Pirate[] temp=new Pirate[temp2.length+1];
for(int i=0;i<temp2.length;i++){
temp[i+1]=temp2;
}
temp[0]=new Pirate();
temp[0].gold=0;
temp[0].isGiven=false;
return temp;
}
//初始化数组
public Pirate[] initPirates(Pirate[] temp){
for(int i=0;i<temp.length;i++){
temp=new Pirate();
}
return temp;
}
//按照isGiven的值来进行分钱
private Pirate[] bribe(Pirate[] pirates2) {
int sum=0;
for(int i=1;i<pirates2.length;i++){
if (pirates2.isGiven){
pirates2.gold++;
sum+=pirates2.gold;
}else{
pirates2.gold=0;
}
}
pirates2[0].gold=100-sum;
return pirates2;
}
}
复制代码
最后编写一个演示类,进行运行。
public class Demo {
/**
* @param args
*/
public static void main(String[] args) {
PiratesThinking pt=new PiratesThinking();
pt.spilitGold(5);
}
}
复制代码
任务完成,经过验证符合推理结果。(注:偷懒,写的定长的数组,最多可以验证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