|
发表于 2016-2-4 22:43:49
|
显示全部楼层
回帖奖励 +3 鱼币
本帖最后由 DingRan 于 2016-2-14 23:11 编辑
想了半天不知道该怎么写(智商余额不足…我就先占个位吧…
说一下目前的一些理解:
如果乒乓球个数是固定的,如题目给出的100,那么在num1=100的条件下定义一个find函数,使称重的次数最少,这是可以做到的。
但是根据我对题目的理解,要求定义一个函数,其第一个参数num1并非一定是100,这就要求在任意传入一个num1值的情况下find函数都是一个最优的算法,这就有点难了。
因为根据初步分析,我感觉最优的算法对num1的依赖性使非常大的,即对于不同性质的num1(奇数偶数素数平方数n次方数等等)可能会有着不同的最优算法…当然这一点还有待继续考证,但如果真是如此的话,要定义一个对任何num1都适合的最优算法,就很困难了…
以上是一些不成熟的个人看法,希望大家一起研究,一起进步~^O^
【2月10日填坑】
先上代码
这里用的“三均分法”,但后来证明这并不总是最优算法
1.0版
- import random
- import time
- def step(start, total, secret):
- '''
- 单步称量函数:分成三组,前两组数量要相等,剩下是第三组;比较前两组,确定目标所在组,缩小范围。
- 参数:start是最小编号减一,total是球的数量,两者决定待测范围;secret是重球的编号。
- '''
- group = round(total/3)#前两组球的个数
- ex_start = start#这里copy一个称量之前的start,待用…
- if start < secret <= start+group:
- total = group
- result = '前者重'
- elif start+group < secret <= start+2*group:
- start = start+group
- total = group
- result = '后者重'
- else:
- start = start+2*group
- total = total - 2*group
- result = '重量相同'
- return start, total, secret, ex_start, group, result
- #返回缩小范围后的start和total,以及其他有用的值…
- def display(i, ex_start, group, result):
- '''
- 输出单步测量结果:
- '''
- if group != 1:#如果是多个球则显示范围
- string = '第 %d 次称量 %d-%d 球和 %d-%d 球,'\
- %(i, ex_start+1, ex_start+group, ex_start+group+1, ex_start+2*group)\
- + result + '。'
- else:#如果是单个球则显示球号
- string = '第 %d 次称量 %d 球和 %d 球,'\
- %(i, ex_start+1, ex_start+2)\
- + result + '。'
- print(string)
- def find(num1, num2, output=False):
- '''
- 主函数
- '''
- #初始化参数
- start = 0
- total = num1
- secret = num2
- para = [start, total, secret]
- i = 0
- #循环单步称量并输出
- while para[1] != 1:
- para = step(para[0], para[1], para[2])
- i +=1
- if output == True:
- display(i, para[3], para[4], para[5])
- #输出最后结果
- if output == True:
- print('重球为 %d 球。共计称量 %d 次。'%(para[0]+1,i))
- #看看猜对没…
- if para[0]+1 != para[2]:
- print('猜错了!!!!!!!===========================')
- return i
- #测试
- m = 100#总球数
- gameRound = 100000#轮数
- #调用find函数一次显示称量过程
- n = random.randint(1, m)#随机生成重球
- print('====================单次测试====================')
- find(m, n, output=True)
- #做gameRound=100000次测试
- print('====================多次测试====================')
- begin = time.clock()#计时开始
- times = 0
- for j in range(gameRound):
- n = random.randint(1, m)
- times += find(m, n)#调用find函数不显示称量过程
- end = time.clock()#计数结束
- print('平均 %.3f 次称出重球。'%(times/gameRound))#输出平均几次称出重球
- print('用时 %.3f 秒。'%(end-begin))#输出用时
复制代码
运行结果:
- >>>
- ====================单次测试====================
- 第 1 次称量 1-33 球和 34-66 球,前者重。
- 第 2 次称量 1-11 球和 12-22 球,后者重。
- 第 3 次称量 12-15 球和 16-19 球,重量相同。
- 第 4 次称量 20 球和 21 球,重量相同。
- 重球为 22 球。共计称量 4 次。
- ====================多次测试====================
- 平均 4.378 次称出重球。
- 用时 1.640 秒。
- >>>
复制代码
然后我来说一下“三均分法”和最优解的一些想法:
0.
【我们先来比较两个概念:“三分法”“三均分法”】
为了说明的方便,我萌做如下定义:
<1>“三分法”是把所有的球任意分成三组,比如10个球分成(1,1,8)或者(2,2,6)等等都是“三分法”
<2>“三均分法”是把球平均分成三组,如果遇到除不尽的情况,则尽量做到均分,比如9个球必然是(3,3,3),7个球除不尽就只能(2,2,3),但(1,1,5)就不行了(它也太不“平均”了。。)
无论“三分法”还是“三均分法”都要有两组是个数相等的,因为我们要称量嘛~
1.
【所有的方法,本质上都是循环使用“三分法”】
所以我萌就可以不用纠结“到底是用二分法呢?还是三分法呢?还是n分法呢?。。。”这样的问题乐!
有童鞋可能不服:这不坑爹么!我有好多方法呢!我明明可以分成3组4组6组10组…你凭啥说所有的方法都是“三分法”?
待我细细道来~
在这个问题中,不论多么复杂的算法,都可以归结为许多个基本操作的重复,这个基本操作就是单次称量。
而单次称量说白了就是:
(1)篮子里有好多球,左手抓起一把球,右手抓起一把球,并且要保证两只手的球数相等,然后放天平上称一称。
(2)如果左(右)边重,重球就在左(右)手抓的那一把里面,如果一样重,重球就在篮子剩下的球里。
(3)接下来,我们把存在犯罪嫌疑球的那一堆球放在一个新篮子里。
(1*)新的轮回~新篮子里有好多球…左手抓起一把球……右手一个慢动作重复。。。
看~我们发现,每一次基本操作,无形中都把球分成了三组(其中至少有两组个数相等),所以基本操作都是“三分法”!
那么,总的算法,都是基本操作的重复,本质上都是在循环使用“三分法”!
所以,不是我们选择了“三分法”,而是这个世界上除了“三分法”再没有其他方法
对于每一次基本操作,我们用(a,a,T-2a)表示,其中T为总数。这样一来模型的建立是不是就简单多了~~
2.
【寻找最优算法,本质上就是给定T,寻找一个最优的a】
怎样的a才是最优的a呢?
每一次基本操作,都会排除一部分良民球,缩小犯罪嫌疑球的范围。所以我一开始的想法(后来证明是不准确的)是:
如果这个a能排除尽可能多的良民球,它就是最优的a,对应的算法就是最优算法。
怎样排除尽可能多的良民球呢?推导如下:
不妨设基本操作为(a,a,T-2a),T为总数已知。
犯罪嫌疑球出现在前两组的概率为2a/T,此时我们能排除的良民球为T-a
犯罪嫌疑球出现在第三组的概率为(T-2a)/T,此时我们能排除的良民球为2a
于是我们能排除的良民球的数学期望E=(T-a)*2a/T + 2a*(T-2a)/T
整理一下,E=(-6a^2+4Ta)/T
求导…求极值…得出:当a=T/3时,E存在最大值
但是问题来了,T/3可能不是整数呀??!!
这简单~因为数学期望E的表达式是二次函数,左右对称,所以我们取最接近T/3的整数,同样能达到效果:
即a=round(T/3)
也就是说,当我们取a的值为round(T/3)时,可以排除最多的良民球,也就是最优的算法!
所以,我们在“三分法”的基础上选择了“三均分法”~鼓掌~~
【回来填坑 2月14日】
3.
【然而令人失望的是,“三均分法”并不总是最优算法】
口说无凭,先上代码。
2.0版
- import random
- import time
- def step(start, total, secret):
- '''
- 单步称量函数:分成三组,前两组数量要相等,剩下是第三组;比较前两组,确定目标所在组,缩小范围。
- 参数:start是最小编号减一,total是球的数量,两者决定待测范围;secret是重球的编号。
- '''
- s0 = {1:0,
- 2:1,
- 3:1,
- 4:1,
- 5:1,
- 11:3,
- 12:4,
- 33:11,
- 34:11,
- 100:33}
-
- group = s0[total]#最优算法
- ex_start = start#这里copy一个称量之前的start,待用…
- if start < secret <= start+group:
- total = group
- result = '前者重'
- elif start+group < secret <= start+2*group:
- start = start+group
- total = group
- result = '后者重'
- else:
- start = start+2*group
- total = total - 2*group
- result = '重量相同'
- return start, total, secret, ex_start, group, result
- #返回缩小范围后的start和total,以及其他有用的值…
- def display(i, ex_start, group, result):
- '''
- 输出单步测量结果:
- '''
- if group != 1:#如果是多个球则显示范围
- string = '第 %d 次称量 %d-%d 球和 %d-%d 球,'\
- %(i, ex_start+1, ex_start+group, ex_start+group+1, ex_start+2*group)\
- + result + '。'
- else:#如果是单个球则显示球号
- string = '第 %d 次称量 %d 球和 %d 球,'\
- %(i, ex_start+1, ex_start+2)\
- + result + '。'
- print(string)
- def find(num1, num2, output=False):
- '''
- 主函数
- '''
- #初始化参数
- start = 0
- total = num1
- secret = num2
- para = [start, total, secret]
- i = 0
- #循环单步称量并输出
- while para[1] != 1:
- para = step(para[0], para[1], para[2])
- i +=1
- if output == True:
- display(i, para[3], para[4], para[5])
- #输出最后结果
- if output == True:
- print('重球为 %d 球。共计称量 %d 次。'%(para[0]+1,i))
- #看看猜对没…
- if para[0]+1 != para[2]:
- print('猜错了!!!!!!!===========================')
- return i
- #测试
- m = 100#总球数
- gameRound = 100000#轮数
- #调用find函数一次显示称量过程
- n = random.randint(1, m)#随机生成重球
- print('====================单次测试====================')
- find(m, n, output=True)
- #做gameRound=100000次测试
- print('====================多次测试====================')
- begin = time.clock()#计时开始
- times = 0
- for j in range(gameRound):
- n = random.randint(1, m)
- times += find(m, n)#调用find函数不显示称量过程
- end = time.clock()#计数结束
- print('平均 %.3f 次称出重球。'%(times/gameRound))#输出平均几次称出重球
- print('用时 %.3f 秒。'%(end-begin))#输出用时
复制代码
运行结果
- >>>
- ====================单次测试====================
- 第 1 次称量 1-33 球和 34-66 球,后者重。
- 第 2 次称量 34-44 球和 45-55 球,前者重。
- 第 3 次称量 34-36 球和 37-39 球,重量相同。
- 第 4 次称量 40 球和 41 球,前者重。
- 重球为 40 球。共计称量 4 次。
- ====================多次测试====================
- 平均 4.299 次称出重球。
- 用时 1.709 秒。
- >>>
复制代码
首先来比较一下两段代码有什么不同:
代码2.0新增了一个字典s0,这个字典用在了哪儿呢?请看第22行,它由1.0版本里的“group = round(total/3)”改为现在的“group = s0[total]”,引用了这个字典。我们说过,“寻找最优算法,本质上就是给定T,寻找一个最优的a”,之前的“三均分法”被我们换成了这个字典,而这个字典的键就是不同的T,对应的值就是针对每个T的最优的a。
下面我来帮大家吐槽。
“坑爹呢吧!原来你的新算法已经事先写好了,做成字典放在那里现用现查啊??!!”
“你那字典怎么来的啊??凭啥就说里面的a就是最优的a啊!!??”
“干嘛要把T和a都列出来啊??就没个表达式么??!!”
然后我来解释…
这个字典里面的a值的确都是最优算法,不是我信手写的,而是由另外一段用来“寻找最优算法”的代码生成的,因为图省事我就先把结果手动复制粘贴来了,另外关于“寻找最优算法”之后会详细说…为啥不用表达式?废话!要是能推出表达式的话谁愿意费这劲全都列出来啊T_T
呼…再看看运行结果吧:
嗯,代码2.0的新算法,平均次数在4.30附近
代码1.0的“三均分法”,平均次数在4.38附近
事实上,之后我们可以严格证明:
旧算法“三均分法”称量次数的数学期望为4.38,新算法的为4.3,并且新算法是最优算法。
显然,“三均分法”悲惨地降级为次优算法,它也有马失前蹄的时候诶~
写到这里可能会有童鞋说:诶~~其实“三均分法”与最优算法也没差多少嘛~~就差0.08而已也不用再麻烦啦~~次优算法就次优算法吧!
这样想的童鞋那就可以愉快的return啦~~(说实话我也觉得次优算法足够了…要不是强迫症又犯了才不来填这个坑……
ps:
1).我感觉吧…之后的东西与Python本身不怎么相关了,倒像是在借用强大的Python暴力解决一道数学题的过程…
2).还有啊…因为智商余额不足…虽然得出了最优算法,却没有得到数学意义上的完美论证(说了是暴力解决T_T对,没有表达式…)
所以请童鞋们特别是数学好的同学要多多交流批评指正啊!!!
4.
【更好的评价标准】
又回到最初的起点:给定T,寻找一个最优的a
为什么“三均分法”得到的a不是最优的a呢?这是一个评价标准的问题。
我们之前曾假定:如果某个a所代表的基本操作,干掉的良民球是最多的(或者说剩下的包含犯罪嫌疑球的球堆是最少的),那么这个a就是最优的a。可见那时的评价标准是:剩下的球数最少。
然而我们的最终目的是什么?是称量的次数最少。
所以问题来了,有两个评价标准,“剩下球数最少”和“称量次数最少”,它俩是不是等价的呢?如果是等价的,我们就可以用前者代替后者,如果是等价的,“三均分法”就可以拍着胸脯说自己是最优算法了…(要是那样我也就不会写这一段了是不是~
所以很显然…很遗憾…并不等价T_T
并不等价的意思就是:假设有一堆球,我最少可以用平均1.857次称量找到目标球;而另一堆球,球数要比前面的少,但是无论我怎么做,最少也得用平均2次称量找到目标球。
球少了反而次数多了?!看上去有违常理是不是?但这是个真实的栗子…第一组球数为7,第二组球数为6,大家可以试一下~
如此这般,就必须老老实实地用“称量次数最少”作评价标准去寻找最优的a
5.
【重寻最优算法】
给定T,每取一个a,都会有一个对应的平均次数(即数学期望),用ES(a,T)表示。
对于所有可能取到的a,必有ES(a,T)的一个最小值,用ES0(T)表示。即T个球的最小称量次数为:
ES0(T) = min{ES(a,T)},其中a历遍(0,T/2]的所有整数
下面计算ES(a,T)的表达式:
给定T,不妨设首次基本操作为(a,a,T-2a)
犯罪嫌疑球出现在前两组的概率为2a/T,称量之后我们剩下的球数为a,而a个球所需要的最小称量次数为ES0(a),所以总的次数为1+ES0(a);
犯罪嫌疑球出现在第三组的概率为(T-2a)/T,称量之后我们剩下的球数为T-2a,而T-2a个球所需要的最小称量次数为ES0(T-2a),所以总的次数为1+ES0(T-2a)
于是称量次数的数学期望
ES(a,T) = [1+ES0(a)]*2a/T + [1+ES0(T-2a)]*(T-2a)/T
= ES0(a)*2a/T + ES0(T-2a)*(T-2a)/T + 1
接下来有两条路可走:
1)计算ES0(T)的数学表达式,代入ES(a,T)的表达式,求最小值,得到a的最优解;
2)将所有可能取到的a代入ES(a,T)的表达式,求最小的ES(a,T)所对应的a,就是a的最优解。
方法1)做下去可以给本案例一个完美的数学论证,但是臣妾实在做不到啊,这也就是为什么代码2.0没有表达式的原因…求数学大神指点!!!
方法2)属于暴力求解,而且计算过程中涉及到递归,在大计算量面前,只能让代码做苦力了~
上代码
寻找最优算法的代码:
- from fractions import Fraction #浮点数在递归过程中误差会比较大,所以用分数
- #这个字典用来储存对于不同的T,最优算法的所有参数
- #键就是T,值是一个列表,列表的零号元素是ES0(T),一号元素是距离t/3最近的那个最优的a,二号元素是所有最优的a组成的列表。
- #事先写好的这两个键值对是初始条件
- solution = {0:[0,0,[0]],
- 1:[0,0,[0]]}
- #这个函数用来求,在给定T的情况下,最少次数ES0(T)、距离t/3最近的那个最优的a、所有最优的a
- #在本函数中,这三个变量分别用esMax,aThird,aBest表示
- def es0(t):
- if t in solution:
- return solution[t]
- else:
- aMax = t // 2
- esMax = float('inf')
- for a in range(1,aMax+1):
- b = t - 2*a
- esTemp = Fraction((2*a*(1+es0(a)[0]) + b*(1+es0(b)[0]))/t)
- if esTemp < esMax:
- esMax = esTemp
- aBest = [a]
- elif esTemp == esMax:
- aBest.append(a)
- cmp = []
- for each in aBest:
- cmp.append(abs(each-t/3))
- aThird = aBest[cmp.index(min(cmp))]
- global solution
- solution[t] = [esMax,aThird,aBest]
- return [esMax,aThird,aBest]
- #把之前得到的最优算法的所有参数简化一下,储存在这个字典中
- #键是T,值是距离t/3最近的那个最优的a
- #只储存总函数里用得着的键值对,而不是全部
- s0 = {}
- #这个函数用来挑取有用的数据,储存在s0
- def disp(t):
- if t == 0 or t == 1 or t in s0:
- return
- a = es0(t)[1]
- b = t - 2*a
- global s0
- s0[t] = a
- disp(a)
- disp(b)
-
- #测试
- M = 100#总球数
- disp(M)
- print(s0)
复制代码
运行结果
- >>>
- {33: 11, 34: 11, 3: 1, 100: 33, 5: 1, 4: 1, 11: 3, 12: 4, 2: 1}
- >>>
复制代码
看!我们的代码把需要的最优算法都做出来啦~鼓掌~~
6.
【合体!合体!合体!】
最后我们把“寻找最优算法”的代码和“称量乒乓球”的代码连接起来起来,让前者作为一个函数出现在后者中:
- import random
- import time
- from fractions import Fraction
- def bestSolution(t):
- solution = {0:[0,0,[0]],
- 1:[0,0,[0]]}
- def es0(t):
- if t in solution:
- return solution[t]
- else:
- aMax = t // 2
- esMax = float('inf')
- for a in range(1,aMax+1):
- b = t - 2*a
- esTemp = Fraction((2*a*(1+es0(a)[0]) + b*(1+es0(b)[0]))/t)
- if esTemp < esMax:
- esMax = esTemp
- aBest = [a]
- elif esTemp == esMax:
- aBest.append(a)
- cmp = []
- for each in aBest:
- cmp.append(abs(each-t/3))
- aThird = aBest[cmp.index(min(cmp))]
- nonlocal solution
- solution[t] = [esMax,aThird,aBest]
- return [esMax,aThird,aBest]
- s0 = {}
- def disp(t):
- if t == 0 or t == 1 or t in s0:
- return
- a = es0(t)[1]
- b = t - 2*a
- nonlocal s0
- s0[t] = a
- disp(a)
- disp(b)
-
- disp(t)
- return s0
- def step(start, total, secret,s0):
- '''
- 单步称量函数:分成三组,前两组数量要相等,剩下是第三组;比较前两组,确定目标所在组,缩小范围。
- 参数:start是最小编号减一,total是球的数量,两者决定待测范围;secret是重球的编号。
- '''
-
- group = s0[total]#前两组球的个数
- ex_start = start#这里copy一个称量之前的start,待用…
- if start < secret <= start+group:
- total = group
- result = '前者重'
- elif start+group < secret <= start+2*group:
- start = start+group
- total = group
- result = '后者重'
- else:
- start = start+2*group
- total = total - 2*group
- result = '重量相同'
- return start, total, secret, ex_start, group, result
- #返回缩小范围后的start和total,以及其他有用的值…
- def display(i, ex_start, group, result):
- '''
- 输出单步测量结果:
- '''
- if group != 1:#如果是多个球则显示范围
- string = '第 %d 次称量 %d-%d 球和 %d-%d 球,'\
- %(i, ex_start+1, ex_start+group, ex_start+group+1, ex_start+2*group)\
- + result + '。'
- else:#如果是单个球则显示球号
- string = '第 %d 次称量 %d 球和 %d 球,'\
- %(i, ex_start+1, ex_start+2)\
- + result + '。'
- print(string)
- def find(num1, num2, output=False):
- '''
- 主函数
- '''
- #初始化参数
- start = 0
- total = num1
- secret = num2
- para = [start, total, secret]
- i = 0
-
- #循环单步称量并输出
- while para[1] != 1:
- para = step(para[0], para[1], para[2],s0)
- i +=1
- if output == True:
- display(i, para[3], para[4], para[5])
- #输出最后结果
- if output == True:
- print('重球为 %d 球。共计称量 %d 次。'%(para[0]+1,i))
- #看看猜对没…
- if para[0]+1 != para[2]:
- print('猜错了!!!!!!!===========================')
- return i
- #测试
- m = 100#总球数
- gameRound = 100000#轮数
- s0 = bestSolution(m)#先导出最优算法
- #调用find函数一次显示称量过程
- n = random.randint(1, m)#随机生成重球
- print('====================单次测试====================')
- find(m, n, output=True)
- #做gameRound=100000次测试
- print('====================多次测试====================')
- begin = time.clock()#计时开始
- times = 0
- for j in range(gameRound):
- n = random.randint(1, m)
- times += find(m, n)#调用find函数不显示称量过程
- end = time.clock()#计数结束
- print('平均 %.3f 次称出重球。'%(times/gameRound))#输出平均几次称出重球
- print('用时 %.3f 秒。'%(end-begin))#输出用时
-
复制代码
运行结果
- >>>
- ====================单次测试====================
- 第 1 次称量 1-33 球和 34-66 球,重量相同。
- 第 2 次称量 67-77 球和 78-88 球,重量相同。
- 第 3 次称量 89-92 球和 93-96 球,前者重。
- 第 4 次称量 89 球和 90 球,后者重。
- 重球为 90 球。共计称量 4 次。
- ====================多次测试====================
- 平均 4.301 次称出重球。
- 用时 1.263 秒。
- >>>
复制代码
ps:
另外有些注释还写的不好,有空回来修改
其实,对于寻找最优算法我还有一个优化的算法,可以让程序运行更快(当然这得球数达到1000以上才能体现出来),如果有机会(应该概率不大)再来填坑吧。。。
-1.
【至此,对于乒乓球这个案例,终于有了一个不怎么圆满,但总算是完整的解答】
呼……原谅我的啰嗦和强迫症哈哈哈~~~ |
评分
-
查看全部评分
|