用程式碼理解黎曼猜想
關於黎曼猜想的話題,最近真的很紅。好多文章真是又臭又長,雲裡霧裡看它半小時都不到正題。倒底黎曼猜想是啥,還沒看到有能說清楚的。
面試工程師,是團隊建立初期要做的最重要的事情。我在做技術面試時,是會請面試人當場在白板上手寫程式碼作為考題(whiteboard coding interview)。如果是遠端不方便用白板,也會通過 ofollow,noindex" target="_blank">https://codepad.remoteinterview.io/ 或者 Skype 共享螢幕來進行面試。
我會提出一個簡單的演算法題,不是對類庫和方法的記憶力比賽,而是看解決問題的思路和能力。然後請面試的童鞋在白板上用自己擅長的語言作答,甚至偽碼也可以。
真實世界中,我是不會用黎曼ζ函式這麼複雜的公式做面試問題。但是一直想聊聊手寫程式碼面試這個話題。所以,在這篇文章中,我打算一邊解釋黎曼ζ函式和黎曼猜想,一邊說說手寫程式碼面試。

黎曼ζ函式(簡單版)
黎曼ζ函式是黎曼猜想的背景。先說說 ζ 這個發音是 zeta ,所以正確的讀法是:黎曼zei(賊)ta(塔)函式(Riemann zeta function)。
黎曼ζ函式,又寫為 ζ(s)。先看面試第一題:
我們知道當 s > 1 , 1 < n < ∞ 時,黎曼ζ函式定義為下面的公式:

寫一段程式碼,打印出 s = 2, 3, 4, 5 …. 10, n = 1000 的,所有 ζ(s) 的結果。
作為參考,我會告訴面試者,冪計算在個語言中的寫法,比如 nodejs 中是 Math.pow(x, y)。
關於手寫程式碼
不考慮本來黎曼ζ函式還能涵蓋的負數和複數領域,前面這公式並不難。手寫程式碼題只是為了觀察工程師幾個基本素質:
1. 能不能寫程式碼
不要懷疑,有不少面試者真的是無法按題目的意思寫程式碼。
2. 會不會硬來(brute force)
硬來(brute force) 在我看是加分項。工程師首先要解決問題,過渡思考數學演算法卻沒有能解決問題,是無效思考。
3. 能不能讓程式碼輸出正確的結果
肯定要給面試者充足的檢查時間。但是我還是常常驚訝於,只有如此少的人能寫出能輸出正確結果的程式碼。
4. 程式碼風格
面試當場手寫程式碼是有心理壓力的,但也更容易展現出第一反應下的程式碼書寫風格習慣。沒有IDE或者自動處理的幫助時,如果還能保持整齊的縮排風格,變數名走心,程式碼整齊,足以說明這是一個對程式碼質量有追求的人。
5. 能不能清楚地講述自己的邏輯思路
在完成程式碼之後,我會請面試者逐行解釋自己的程式碼。擁有技術溝通能力在團隊合作中會有極大的加成。
6. 程式碼可讀性
如果有習慣對程式碼結構進行拆解,降低程式碼複雜度,達到更高的可讀性。也是重要的加分項。
黎曼ζ函式的程式碼與解讀
在 s > 1 區間域的黎曼ζ函式,10行左右程式碼就可以了。例如:
function zeta(s, n) { sum = 0; for (var i = 1; i <= n; i++) { sum += 1/(Math.pow(i, s)); } return sum; } function main() { for (var i = 2; i <= 10; i++) { console.log(`zeta(${i}) = ` + zeta(i, 1000)); } } main();
將 zeta 單獨封裝成一個函式,而不是像下面兩個 for 迴圈向下面這樣的寫法,就是前面說的對程式碼結構進行拆解,來降低程式碼複雜度,以達到更高的可讀性。
for (var i = 2; i <= 10; i++) { // 下面這樣的寫法雖然更短,但是可讀性更差, // 增加別人看懂程式碼的難度,是扣分的 sum = 0; for (var j = 1; j <= 1000; j++) { sum += 1/(Math.pow(j, i)); } console.log(`zeta(${i}) = ${sum}`); }
正確的輸出結果應該是:
zeta(2) = 1.6439345666815615 zeta(3) = 1.2020564036593433 zeta(4) = 1.082323233378306 zeta(5) = 1.0369277551431222 zeta(6) = 1.017343061984441 zeta(7) = 1.0083492773819207 zeta(8) = 1.004077356197943 zeta(9) = 1.0020083928260817 zeta(10) = 1.0009945751278182
黎曼ζ函式(完整高難版)
黎曼ζ函式的引數 s,可以是複數,也就是說可以是實數加虛數。如果我們將 s 的實數部分簡寫為 Re(s) 。當 Re(s) > 1 時,也就是引數 s 的實數部分大於1時,黎曼ζ函式還是前面那個簡單版本。
當黎曼ζ函式延展到 Re(s) < 0 負數領域中,表達就不同了:

其中 Γ 讀為 Gamma (伽瑪函式),是一種支援複數(實數加虛數)的階乘。
當 0 < Re(s) < 1 時,表達又不同:

三個公式分別有各自的作用域,組合起來就是完整形態的黎曼ζ函式。至於說為什麼 s < 1 時黎曼ζ函式突然就像變形金剛一樣的展開了,這是以一種叫 Analytic continuation 的技巧得來的唯一優雅解法。這裡我推薦一個 Visualizing the Riemann hypothesis and analytic continuation 的視訊解說。
黎曼猜想的程式碼表達
題目:寫一段程式碼,n 為 1000,分別計算 s = -1, -2, 1/2+14.1347i, -3 的結果。這裡的 i 是虛數 (Imaginary number)。
題目會附帶提示可以操作複數的完整數學函式 math.js | an extensive math library for JavaScript and Node.js 的方法文件做參考。
將前面的公式程式碼化:
const math = require('mathjs'); function zetaComplex(s, n) { if (math.re(s) > 1) { var sum = math.complex(0,0); for (var i = 1; i <= n; i++) { sum = math.add(sum, math.divide(1, math.pow(i, s))); } return sum; } else if (math.re(s) > 0) { var sum = math.complex(0,0); for (var i = 1; i <= n; i++) { sum = math.add(sum, math.divide(math.pow(-1, i+1), math.pow(i, s))); } return math.prod(math.divide(1, math.subtract(1, math.pow(2, math.subtract(1, s)))), sum); } else { return math.pow(2, s) * math.pow(math.PI, math.subtract(s, 1)) * math.sin( math.divide(math.PI * s, 2)) * math.gamma(math.subtract(1, s)) * zetaComplex(math.subtract(1, s), n); } } var input = [-1, -2, math.complex(1/2, 14.1347), -3]; input.forEach(function(el) { console.log(`zeta(${el}) = ` + zetaComplex(el, 1000)); });
ζ(-1) 應該等於 -1/12,先人已經算好。
ζ(-2) 就是黎曼ζ函式中常說的負偶數,結果應該等於 0。
ζ(1/2+14.1347i) 就是黎曼猜想的部分: 除了引數s為負偶數的情形外,令ζ函式結果為0的引數s的實數部分必然是 1/2。
ζ(-3) 應該等於 -1/120。
為了更直白的解釋,程式碼精度有限,所以輸出結果是近似值,大約是:
zeta(-1) = -0.08328269806336473 zeta(-2) = -2.3738653665425513e-18 zeta(0.5 + 14.1347i) = 0.00665796802251104 - 0.00027328853234271006i zeta(-3) = 0.008333333330770697
雜記
面試時以黎曼猜想為題是不人道的。所以真實的情況中,我的面試題會簡單的多得多。但會是一道和數學有關,和記憶類庫或方法無關的題目。
如果面試者確實能做出正確的結果,我還會請面試者嘗試優化一下程式碼的效能。一個優秀的程式員應該是非常關注效能優化的。這裡我會希望看到面試者對計算開銷的理解:是不是對效能優化的方向有思路,有沒有尋找程式碼中被執行次數最多的計算,是否嘗試去減少它的使用次數,能夠倍數級甚至呈指數級的減少效能開銷。
有好奇心。有責任心。能解決問題。就是個好工程師。
能夠不斷的突破自我,會不斷的優化自己所做的事情,尋找更好的解決方案。就是個優秀的工程師。
更多參考:
Riemann zeta function - Wikipedia
Visualizing the Riemann hypothesis and analytic continuation - YouTube