# [競程] 艾狄胥數 (Erdős Numbers) ###### tags: `競程` * Online Judge: [10044](https://onlinejudge.org/index.php?option=onlinejudge&Itemid=8&page=show_problem&problem=985) ![](https://hackmd.io/_uploads/H1OzzVHfs.png) **艾狄胥數**(簡稱**艾數**)(Erdős number),根據現代匈牙利數學家艾狄胥·帕爾,這個最多產的數學家命名,是描述論文中一個作者與艾狄胥的「合作距離」的一種方式。被視為數學界諾貝爾獎的菲爾茨獎,其獲得者的艾數中位數最低時為 $3$。 定義艾狄胥本人的艾數是 $0$,與其合寫過論文的人的艾數是 $1$,一個人至少要 $k$ 個中間人(合寫論文的關係)才能與艾狄胥有關聯,則他的艾數是 $k+1$。 ![](https://hackmd.io/_uploads/HkYnOVBGj.png) 一些著名科學家的艾數如下: * Albert Einstein: 2 * Erwin Schrödinger: 3 * Richard Feynman: 3 * Claude Elwood Shannon: 3 * John von Neumann: 3 * Peter Shor: 2 在學術圈以外,也有其他類似的數: * 貝肯數:以演員凱文·貝肯為中心,以是否一起演出描述與凱文·貝肯的距離,也因此產生著名的遊戲:六度空間(Six Degrees of Kevin Bacon)。 * 秀策數:圍棋中用來描述玩家和棋聖本因坊秀策之間的距離。 * Stringfield數:描述幽浮學的研究者與第一位幽浮學家Leonard H. Stringfield之間的距離。 **六度分隔理論** (Six Degrees of Separation) 認為,世界上任何互不相識的兩人,只需要很少的中間人就能夠建立起聯繫。哈佛大學心理學教授Stanley Milgram於1967年根據這個概念做過一次連鎖信實驗,嘗試證明平均只需要6步就可以聯繫任何兩個互不相識的人。也就是說,今天如果小明發明了一個「小明數」,小明的朋友都擁有「小明數 = 1」,小明朋友的朋友擁有「小明數 = 2」,以此類推,則世界上的所有人的小明數,根據六度分隔理論,最多就是6。 雖然六度分隔理論目前仍沒辦法很嚴謹的用數學證明出來,但是它在實務上運作得很好,特別是在保險及直銷等領域。甚至基於一項Facebook在2016年的研究,由於社群網路的發展,每個人與其他人間隔僅有4.57人。 ## 題目 講了這麼多艾數的背景,現在我們要來實際計算這個數了。給定一系列的文章的作者名單,基於這些發表,要計算出這些名單出現的所有人(艾迪胥本人除外)的艾數。 ## 範例 原始的題目的輸入資料是一個文件,裡面包含了所有文章的資料,包含作者名單與標題,需要進行一些字串處理才能計算。為了簡化題目,專注在演算法上,我們在這邊把作者名單抓出來成為一個姓名的list,一篇文章用一個作者的list代表,則全部的輸入資料就是一個list of list: ```python [ ["Smith, M.N.", "Martin, G.", "Erdos, P."], ["Erdos, P.", "Reisig, W."], ["Smith, M.N.", "Chen, X".], ["Jablonski, T.", "Hsueh, Z."] ] ``` ## 思路 這題直覺就是要用graph去做:把人際網路建構出來,再計算各節點對於艾狄胥本人的距離。所以整個思路可以分為兩個步驟: 1. 使用鄰接表 (adjacency list) 建構人際網路的graph 2. 透過廣度優先搜尋 (BFS) 計算各節點對於艾狄胥的距離 ## 解答 ### 建構人際網路 在這裡我們採取python的dictionary來建構鄰接表。這邊我們選用`collection`模組的`defaultdict`而不是普通的dictionary,這樣就不用管目前遞增的值對應的key是不是已經在dictionary裡面了。舉例來說,我們希望對於每個key,它的初始值都是一個空的list,不管我們是否已經把這個元素添加到這個dictionary裡面,我們就可以這樣做: ```python phone_num = defaultdict(list) ``` 如果要為其中一個key的清單加入元素,就可以直接這樣做: ```python phone_num["Alice"].append("12345678") ``` 那麼這邊我們要怎樣用`defaultdict`來建立鄰接表呢?我們可以用一個預設值是empty set(空集合)的`defaultdict`來做這件事,因為邊的清單順序並不重要所以使用`set`: ```python= author_graph = defaultdict(set) erdos_nums = {} for row in authors: for a in row: erdos_nums[a] = 999999 for b in row: if b != a: author_graph[a].add(b) author_graph[b].add(a) ``` 注意這邊我們一次在`author_graph`中添加了兩個方向的邊,因為合作論文是一個雙向的關係,所以人際網路是無向的圖。這邊還初始化了一個`erdos_nums`的普通dictionary,並把所有value通通初始化為`999999`。這是什麼意思呢?事實上,這個dictionary在等等的BFS尋訪中,有兩個功能: 1. 用來標記已經尋訪過的節點 2. 用來儲存各節點的艾數 至於初始化的值我設定成`999999`,只是因為在這題目中`999999`是一個大到離譜、不可能出現的數字而已,等等就會知道為什麼要這樣設了。 ### 計算艾數 接下來的步驟就是大家最喜歡的尋訪環節了。因為是BFS,所以使用queue來實作: ```python= queue = [("Erdos, P.", 0)] while queue: node, dist = queue.pop(0) erdos_nums[node] = min(erdos_nums[node], dist) if node in author_graph and author_graph[node]: for a in list(author_graph[node]): if erdos_nums[a] == 999999: queue.append((a, dist + 1)) ``` 很明顯可以看出,對於沒尋訪過的節點`a`,我們有`erdos_nums[a] == 999999`,所以就可以用這個條件來排除掉重複尋訪同一個節點的情形。 完整的程式碼如下: ```python= from collections import defaultdict authors = [\ ["Smith, M.N.", "Martin, G.", "Erdos, P."],\ ["Erdos, P.", "Reisig, W."],\ ["Smith, M.N.", "Chen, X."],\ ["Jablonski, T.", "Hsueh, Z."]\ ] def erdos_number(authors): author_graph = defaultdict(set) erdos_nums = {} for row in authors: for a in row: erdos_nums[a] = 999999 for b in row: if b != a: author_graph[a].add(b) author_graph[b].add(a) queue = [("Erdos, P.", 0)] while queue: node, dist = queue.pop(0) erdos_nums[node] = min(erdos_nums[node], dist) if node in author_graph and author_graph[node]: for a in list(author_graph[node]): if erdos_nums[a] == 999999: queue.append((a, dist + 1)) del erdos_nums["Erdos, P."] return erdos_nums print(erdos_number(authors)) ```