程式設計藝術家經典試題解讀:猜生日問題

鍾超發表於2012-01-15

這是一道很多人知道的經典題目,其中的邏輯推理堪稱短小精悍試題的典範。


題目:

張老師的生日為M月D日,他將M值告訴給了小明,將D值告訴給了小強。然後給出如下這些日期:

3月4日,3月5日,3月8日,6月4日,6月7日,9月1日,9月5日,12月1日,12月2日,12月8日。

張老師:你們知道我的生日是哪天嗎?

小明:如果小強不知道,那我也不知道。

小強:剛才我不知道,現在我知道了。

小明:我也知道了。


分析:

這是一類典型的條件推理問題。通常採用的方式,是通過條件的有序疊加,篩選出最終答案。


標記:

引入如下標記:

M0:張老師生日日期的月份,

D0:張老師生日日期的日子,

M:表示常量月,

D:表示常量日,

m:表示變數月,

d:表示變數日,

{MD}:張老師給出的所有日期的集合,

{d*{MD}}:表示 {d|任意日期md屬於{MD},有md的日為d} 這樣的集合,

{d~M0*{MD}}:表示 {d|任意日期md屬於{MD},有md的月為M0,md的日為d} 這樣的集合,

{d`M0*{MD}}:表示 {d|任意日期md屬於{MD},有md的月為M0,md的日不為d} 這樣的集合,

{m*{MD}}:表示 {m|任意日期md屬於{MD},有md的月為m} 這樣的集合,

{m~D0*{MD}}:表示 {m|任意日期md屬於{MD},有md的日為D0,md的月為m} 這樣的集合,

{m`D0*{MD}}:表示{m|任意日期md屬於{MD},有md的日為D0,md的日不為d} 這樣的集合。


解答:

(1)條件1:如果小強不知道,那小明也不知道。

首先此時,還不知道小強知道不知道。

如果小強不知道,根據條件1,有{d~M0|MD}包含於{d`M0|MD}。則由此得到一個日期集合{MD1a}。

如果小強知道,則條件1不可用,不過這時更容易推理,{m~D0|MD}與{m`D0|MD}無交集,則由此得到一個日期集合{MD1b}。

因此,{MD1a}和{MD1b}共同構成了條件1能推理出的日期集合{MD1}。


(2)條件2:小強在得知條件1前,不知道;在得知條件1後,知道。

首先,小強在得知條件1前不知道,說明{MD1b}可以排除。

然後,由於得知了條件1,小強也知道了目前的答案在{MD1a}中。

接著,考慮條件2,說明{m~D0|{MD1a}}只有一個元素,即單元集或稱模為1,且該元素即為M0,這樣小強就知道了M0和D0,就知道了正確的日期。


(3)條件3:小明在得知條件2後,知道了正確的日期。

首先,小明在聽小強說“剛才不知道”後(即條件2中的“小強在得知條件1前,不知道”),就知道小強在得知條件1前並不知道答案,即小強此時知道了正確的日期在{MDa1}中,排除了{MD1b}。

然後,小明聽小強說“現在知道”後(即條件2中的“小強在得知條件1後,知道”),就知道對於{d*{MD1a}}中個任意元素dx,有{m~dx*{MD1a}}只有一個元素,即是一個單元集或稱模為1。


(4)旁觀者的邏輯推理

在小明和小強的對話中,小強是在小明第一句話之後就知道了正確的日期。而小明是在小強的話之後才知道的,並說出了整個對話中的最後一句話。而旁觀者,是在得知最後一句話後,才唯一確定了正確答案的。具體的邏輯過程,可依照(1)至(3)中的推理。


(5)程式原始碼:

package com.sinosuperman.test;

import java.util.ArrayList;
import java.util.List;

public class Test {
	/**
	 * @param args
	 */
	public static void main(String[] args) {
		List<MonDay> dates = new ArrayList<MonDay>();
		dates.add(new MonDay(3, 4));
		dates.add(new MonDay(3, 5));
		dates.add(new MonDay(3, 8));
		dates.add(new MonDay(6, 4));
		dates.add(new MonDay(6, 7));
		dates.add(new MonDay(9, 1));
		dates.add(new MonDay(9, 5));
		dates.add(new MonDay(12, 1));
		dates.add(new MonDay(12, 2));
		dates.add(new MonDay(12, 8));
		
		System.out.println("張老師:小明知道月,小強知道日,現在你們能猜出來我的生日嗎?就在這些裡面:");
		System.out.println(dates);
		
		System.out.println("\n小明:如果小強不知道,那我也不知道。");
		System.out.print("通過這句話可以知道:\n如果小強知道的話,一定在這些日期之中:");
		List<MonDay> dates1 = MonDayUtil.getInstance().getDatesWithDuplicateDays(dates);
		System.out.println(dates1);
		System.out.print("如果小強不知道的話,一定在這些日期之中:");
		List<MonDay> dates11 = MonDayUtil.getInstance().getDatesWithDistinctMon(dates);
		System.out.println(dates11);
		
		System.out.println("\n小強:剛才我不知道,現在我知道了。");
		System.out.print("通過這句話可以知道:\n一定在這些日期之中:");
		List<MonDay> dates2 = MonDayUtil.getInstance().getDatesWithDistinctMon(dates1);
		System.out.println(dates2);
		
		System.out.println("\n小明:我也知道了。");
		System.out.print("通過這句話知道:\n一定在這些日期之中:");
		List<MonDay> dates3 = MonDayUtil.getInstance().getDatesWithDistinctDay(dates2);
		System.out.println(dates3);
		System.out.println("-注意:\n如果只有一個,說明題目可解;\n如果多個日期,說明題目條件不全;\n如果沒有日期,說明題目錯誤。)");
	}
}

class MonDayUtil {
	private static MonDayUtil instance = new MonDayUtil();
	public static MonDayUtil getInstance() { return instance; }
	public List<MonDay> getDatesWithDuplicateDays(List<MonDay> srcList) {
		
		List<MonDay> resultList = new ArrayList<MonDay>();
		
		List<Integer> monList = getMonNum(srcList);
		for (Integer m : monList) {
			
			List<MonDay> datesOfMon = getDatesOfMon(m, srcList);
			List<Integer> daysOfDates = getDaysOfDates(datesOfMon);
			
			List<MonDay> otherDatesOfMon = getOtherDatesOfMon(m, srcList);
			List<Integer> otherDaysOfDates = getDaysOfDates(otherDatesOfMon);
			
			if (otherDaysOfDates.containsAll(daysOfDates)) {
				resultList.addAll(datesOfMon);
			}
		}
		
		return resultList;
	}
	
	public List<MonDay> getDatesWithDistinctMon(List<MonDay> srcList) {
		
		List<MonDay> resultList = new ArrayList<MonDay>();
		
		List<Integer> dayList = getDayNum(srcList);
		for (Integer d : dayList) {
			
			List<MonDay> datesOfDay = getDatesOfDay(d, srcList);
			
			if (datesOfDay.size() == 1) {
				resultList.addAll(datesOfDay);
			}
		}
		
		return resultList;
	}

	public List<MonDay> getDatesWithDistinctDay(List<MonDay> srcList) {
		List<MonDay> resultList = new ArrayList<MonDay>();
		List<Integer> monList = getMonNum(srcList);
		for (Integer m : monList) {
			List<MonDay> datesOfMon = getDatesOfMon(m, srcList);
			if (datesOfMon.size() == 1) {
				resultList.addAll(datesOfMon);
			}
		}
		return resultList;
	}
	private List<MonDay> getDatesOfDay(int day, List<MonDay> srcList) {
		List<MonDay> resultList = new ArrayList<MonDay>();
		for (MonDay md : srcList) {
			if (md.getDay() == day) {
				resultList.add(md);
			}
		}
		return resultList;
	}
	
	private List<Integer> getDayNum(List<MonDay> srcList) {
		List<Integer> resultList = new ArrayList<Integer>();
		for (MonDay md : srcList) {
			if (!resultList.contains(md.getDay())) {
				resultList.add(md.getDay());
			}
		}
		return resultList;
	}
	
	private List<Integer> getDaysOfDates(List<MonDay> srcList) {
		List<Integer> resultList = new ArrayList<Integer>();
		for (MonDay md : srcList) {
			if (!resultList.contains(md.getDay())) {
				resultList.add(md.getDay());
			}
		}
		return resultList;
	}
	
	private List<Integer> getMonNum(List<MonDay> srcList) {
		List<Integer> resultList = new ArrayList<Integer>();
		for (MonDay md : srcList) {
			if (!resultList.contains(md.getMon())) {
				resultList.add(md.getMon());
			}
		}
		return resultList;
	}
	
	private List<MonDay> getDatesOfMon(int mon, List<MonDay> srcList) {
		List<MonDay> resultList = new ArrayList<MonDay>();
		for (MonDay md : srcList) {
			if (md.getMon() == mon) {
				resultList.add(md);
			}
		}
		return resultList;
	}
	
	private List<MonDay> getOtherDatesOfMon(int mon, List<MonDay> srcList) {
		List<MonDay> resultList = new ArrayList<MonDay>();
		for (MonDay md : srcList) {
			if (md.getMon() != mon) {
				resultList.add(md);
			}
		}
		return resultList;
	}
}

class MonDay {
	private int mon;
	private int day;
	public MonDay(int mon, int day) { this.mon = mon; this.day = day; }
	public int getMon() { return mon; }
	public int getDay() { return day; }
	public String toString() { return mon + "/" + day; }
}

(6)程式解讀

類MonDay用於表示日期。

類MonDayUtil中提供了邏輯推理中可以能用到的推理方式的工具,比如:

getDatesWithDuplicateDays:輸入{MD},輸出{MD}中滿足m是md的日,且{d~m*{MD}}與{d`m*{MD}}有交集的md構成的集合。

getDatesWithDistinctMon:輸入{MD},輸出{MD}中滿足d是md的月,且{m~d*{MD}}與{m`d*{MD}}無交集的md構成的集合。

getDatesWithDistinctDay:輸入{MD},輸出{MD}中滿足m是md的月,且{d~m*{MD}}與{d`m*{MD}}無交集的md構成的集合。

具體的邏輯過程,與邏輯分析一致,只是把數學語言,轉化為計算機語言。





相關文章