用程式消除一道概率題的二義性
無意在維基看到了一個關於概率悖論的討論Boy or Girl paradox。有爭議的的題目如下:
史密斯先生有兩個孩子,至少其中之一是男孩,請問兩個孩子都是男孩的可能性有多大?
原文如下:
Mr. Smith has two children. At least one of them is a boy. What is the probability that both children are boys?
一部分人的答案是1/3,一部分人的答案是1/2.為什麼會產生這樣的結果呢,文中說得很清楚是因為問題本身存在歧義,對“至少一個孩子是男孩”的資訊做不同的假設,導致了不同的結果。問題歸根結底是由於自然語言的二義性,將問題轉化成了兩個不同的數學模型,從而產生了兩個不同的結果。我們知道設計一種程式語言的任務之一就是消除其二義性,因此一般而言程式語言是一種沒有歧義的語言(所以程式設計師都喜歡寫程式碼不喜歡說話麼),如果用程式語言來描述這一問題,會不會好理解點呢?以java為例,我們來看看。
以下是對史密斯先生有兩個孩子的可能情況進行描述,其中random.nextBoolean()函式隨機返回true或false的概率均為1/2,用於模擬現實中生男孩女孩的概率各一半。可見程式不僅描述了所有的組合,還明確了題目中暗含的條件。
class TwoChildren
{
Child child1;
Child child2;
public TwoChildren()//一個孩子是男孩或女孩的概率是50%
{
child1 = random.nextBoolean()?Child.BOY:Child.GIRL;
child2 = random.nextBoolean()?Child.BOY:Child.GIRL;
}
}
接著描述“至少一個孩子是男孩”,因為這裡是存在歧義的地方,所以轉化成程式碼描述時,我們會根據兩個不同的假定,得到不同的程式碼。1/3結果的假定條件:觀察了史密斯的兩個孩子,其中一個是男孩:
boolean isObserved(TwoChildren draw)
{
return draw.child1 == Child.BOY || draw.child2 == Child.BOY;
}
熟悉java的程式設計師會注意到,如果child1為BOY的話,child2不會被觀察(||運算子後面的程式碼不會被執行),是否和自然語言描述不一樣?我們從邏輯或運算定義可知二者是等價的,即使||運算子後面的程式碼被執行,也不影響程式結果,換成自然語言就是我們觀察史密斯的兩個孩子時,一個一個的觀察,如果發現其中一個是男孩,就已經保證了“至少一個是男孩”,就沒必要接著觀察了。
接著描述1/2的結果假定條件:隨機觀察了史密斯的一個孩子,其中一個是男孩:
boolean isObserved(TwoChildren draw)
{
return random.nextBoolean()?draw.child1 == Child.BOY:draw.child2 == Child.BOY;
}
其中random.nextBoolean()函式隨機返回true或false的概率均為1/2,就是兩個孩子被選中觀察的概率是1/2.
最後我們描述問題的提出
if(isObserved(draw))
{
observedCount ++;
if(draw.child1 == Child.BOY && draw.child2 == Child.BOY)
{//兩個孩子都是男孩
matchedCount++;
//經過很多次運算後,((double)matchedCount/observedCount)最可能的值是多少?
}
}
至此我們將一個以自然語言描述的問題,轉化成了一個以程式語言描述的問題。這個轉化是否正確呢,我們做10萬次測試,看結果是否滿足數學推導的預期。以下是一個完整的測試程式碼:
package hermitdl.test2;
import java.util.Random;
/**
* Test for <<Boy or Girl paradox>>
*/
public class App
{
static enum Child
{
BOY,
GIRL
};
static Random random = new Random();
static class TwoChildren
{
Child child1;
Child child2;
public TwoChildren()//一個孩子是男孩或女孩的概率是50%
{
child1 = random.nextBoolean()?Child.BOY:Child.GIRL;
child2 = random.nextBoolean()?Child.BOY:Child.GIRL;
}
}
//隨機檢查一個孩子的性別是否是男孩
static class PeekOneTest extends ProbabilityTest
{
@Override
boolean isObserved(TwoChildren draw)
{
return random.nextBoolean()?draw.child1 == Child.BOY:draw.child2 == Child.BOY;
}
}
//檢查兩個孩子的性別,是否其中之一是男孩
static class PeekTwoTest extends ProbabilityTest
{
@Override
boolean isObserved(TwoChildren draw)
{
return draw.child1 == Child.BOY || draw.child2 == Child.BOY;
}
}
static abstract class ProbabilityTest
{
int observedCount = 0;
int matchedCount = 0;
//在isObserved為真的情況下計數,以及兩個孩子均是女孩的情況計數。
void test(TwoChildren draw)
{
if(isObserved(draw))
{
observedCount ++;
if(draw.child1 == Child.BOY && draw.child2 == Child.BOY)
{
////兩個孩子都是男孩
matchedCount++;
}
}
}
abstract boolean isObserved(TwoChildren draw);
void printResult()
{
System.out.printf(this.getClass().getSimpleName() +"=%d/%d=%f\n"
,matchedCount
,observedCount
,((double)matchedCount/observedCount));
}
}
public static void main( String[] args )
{
PeekOneTest peekOneTest = new PeekOneTest();
PeekTwoTest peekTwoTest = new PeekTwoTest();
TwoChildren draw;
for(int i = 0;i < 1000000; i++)
{
draw = new TwoChildren();
peekOneTest.test(draw);
peekTwoTest.test(draw);
}
peekOneTest.printResult();
peekTwoTest.printResult();
}
}
以下為程式的幾次執行結果:
PeekOneTest=249436/499301=0.499570
PeekTwoTest=249436/750234=0.332478
PeekOneTest=250209/500229=0.500189
PeekTwoTest=250209/749846=0.333681
PeekOneTest=249963/500234=0.499692
PeekTwoTest=249963/749712=0.333412
可見模擬結果始終分別在1/2與1/3附近波動,是符合數學預期的。