# 2018q1 Homework2 contributed by <`e94046165`> 重做前三周測驗題 ## 第 1 週測驗題_測驗`1` 題目如下: - [ ]考慮以下程式碼: ```C #include <stdlib.h> int isMultN(unsigned int n) { int odd_c = 0, even_c = 0; /* variables to count odd and even SET bits */ if (n == 0) // return true if difference is 0. return 1; if (n == 1) // return false if the difference is not 0. return 0; while (n) { if (n & 1) // odd bit is SET, increment odd_C odd_c++; n >>= 1; if (n & 1) // even bit is SET, increment even_c even_c++; n = n >> 1; } /* Recursive call till you get 0/1 */ return(isMultN(abs(odd_c - even_c))); } ``` 其作用為檢查輸入整數是否為 N 的倍數,那麼 N 為多少? ### 想法 & 思考 最一開始看到這段程式碼時,腦海中第一個浮現的念頭是 N = 11,會有這樣的想法是因為看到程式碼中比對 __奇偶數位__ ,而在十進位中,判斷某數是否為11的倍數方法如下: 將奇偶數位數字分別相加後,兩者相減mod11 == 0 則為11 的倍數,下面是舉例。 ``` 例: 1221 是否為 11 的倍數? 奇數位總和 = 1+2 = 3 偶數位總和 = 1+2 = 3 (3-3) % 11 = 0 ---> 1221 為 11的倍數 ``` 顯然我忽略了要以二進位的角度去思考,所以這題在第一次作答的時候我選錯了。這題的答案應該是 N = 3 ,但答對此題同學在上台講解的時候說了: ``` 因為 3 以二進位表示是 011 所以所有 3 的倍數必定符合: 奇數位元為 1 的個數與偶數位元為 1 的個數相等,這個條件。 ``` 我認為這樣的講法是不夠嚴謹的,接著我將試著以數學歸納法作證明。 ### 數學歸納法 這邊先簡單的解釋數學歸納法: __定義__ from [wiki](https://zh.wikipedia.org/zh-tw/%E6%95%B0%E5%AD%A6%E5%BD%92%E7%BA%B3%E6%B3%95) ``` 最簡單和常見的數學歸納法是證明當_n_等於任意一個自然數時某命題成立。證明分下面兩步: 1. 證明當 n = 1時命題成立。 2. 證明如果在 n = m 時命題成立,那麼可以推導出在 n = m +1 時命題也成立。( m 代表任意自然數) ``` 而更容易理解的解釋方法是 ``` 假設有一列骨牌,我們只要證明 1.第一張骨牌會倒 2.假設在已知第 m 張骨牌會倒的情況下,證明第 m + 1 張骨牌也會倒 (m 為 != 1 的任意自然數) 則我們可以說,整列骨牌都會倒。(即證明成立) ``` __證明__ 試證:所有 3 的倍數,以二進位表示時奇偶位`1`數量相等 1. 第一張骨牌: 3 = 011 奇偶數位皆有一個 1,3 為 3 的倍數 假設成立 2. 假設 m 為一個非 3 的 3 的倍數,且奇偶數位的 `1` 個數相等,分別考慮以下兩種情況:(以下進位皆以二進位為主) * m + 3 無進位: m = ...00 奇偶數位的 `1` 數量相等 m + 3 = ...11 奇偶數位的 `1` 數量仍相等 ---->符合命題:m + 3 是三的倍數且奇偶位 `1` 數量相等 * m + 3 有一次進位: m = ...0001 m + 3 = ...0100 奇偶數`1`個數與原來相同,命題成立 * m + 3 有兩次進位: m = ...0101 m + 3 = ...1000 奇偶數`1`個數改變,命題不成立...? 由上列證明可得知, 命題:所有 3 的倍數,以二進位表示時奇偶位`1`數量相等 是有缺陷的,但這個缺陷來在於我沒有看懂程式碼,還是程式碼寫錯了呢? ### 測試&重讀程式碼 ``` root@acnes-X550JX:~/test#./test 165 165 是三的倍數 168 168 是三的倍數 1 1 不是三的倍數 ``` 若是程式碼錯了,那麼應該沒辦法判斷 168 是三的倍數,理由如下: 165 = 10100101 168 = 10101000 奇偶數 `1`個數不相等 但由上面的測試結果可發現,這個 function 可以正確的判斷 在重新檢視程式碼後,才發現是我忽略了最後那行遞迴呼叫: ```C /* Recursive call till you get 0/1 */ return(isMultN(abs(odd_c - even_c))); ``` 當奇偶數位 `1` 不相等時,會遞迴呼叫,直至得到 0 或 1 為止。也就是說,如果奇偶數位的 `1` 個數差是三的倍數,依然能判斷出它是三的倍數。 因此,原假設應修正成: 所有奇偶數位 `1` 個數相等的數,皆為三的倍數 但三的倍數,可能為奇偶數位 `1` 個數差為 0 或 3n 的數 現在又遇到同樣的問題,要怎麼證明這個假設在所有情況下都成立呢? __證明__ 試證:三的倍數,皆為奇偶數位 `1` 個數差為 0 或 3n 的數 基數位的 `1` 代表 2^2n^ 也就是 :1, 4, 16, 64...這些數共同的特色是 3p~i~+1 偶數位的 `1` 代表 2^2n+1^ 也就是 :2, 8, 32, 128...這些數共同的特色是 3q~i~-1 因此可得到, 奇偶數位 `1` 的個數相等時:代表 3(P+Q) 為三的倍數 奇偶數位 `1` 的個數相差3的倍數:代表 3P+3 or 3Q-3皆為三的倍數 p.s. P = Σp~i~ , Q = Σq~i~ 得證假設成立。 ### 延伸問題 - 為什麼上述程式碼可運作呢?上列已解釋並證明。 - 將 N 改為其他質數再改寫為對應的程式碼,一樣使用遞迴 試著將 N 改為 5 ```C #include <stdio.h> int isMultN(unsigned int n) { if (n == 5) // return true if only 5 is left return 1; if (n < 5 ) // return false if the number left is less than 5 return 0; //for number which is larger than 5 if(n&1) n = n - 5; else n >>= 1; /* Recursive call till you get 5 or less */ return(isMultN(n)); } ``` __解釋程式碼__ 因為 5 的倍數有此特性: 尾數非 0 即 5 因此重複 n 為偶數則 n = n / 2 n 為奇數則 n = n - 5 由最後 n == 5 or n < 5 可判斷 n 是否為 5 的倍數 由於上列解法實在太平庸了,連我自己都覺得跟發廢文沒兩樣,所以決定丟掉再寫些新的。 ```C #include <stdlib.h> #include <stdio.h> int isMultN(unsigned int n) { if ( (n == 5) || (n == 0) ) // return true if only 5 is left return 1; if ( (n >> 2) < (n & 3) ) // return false if the number left is less than 5 return 0; //for number which is larger than 5 n = (n >> 2) - (n & 3); /* Recursive call till you get 5, 0 or a special terminate condition */ return(isMultN(n)); } ``` 這段 code 一樣用來判斷輸入 n 是否為 5 的倍數,但是因為這次一次處理兩個 bits 所以速度應該會較前一個快上一些。而且這個概念應該也可以拓展到其他的質數。 概念如下: 假設 n = (abcde)~2~ 則 n = (abc00)~2~ + (000de)~2~ = ( (abc)~2~ << 2 ) + (000de)~2~ = ( (abc)~2~ * (5-1)~10~ ) + (000de)~2~ = ( (abc)~2~ * 5 - (abc - de)~2~ 因為(abc)~2~ * 5 必定為 5 的倍數,所以只要判斷(abc - de)~2~是否為 5 的倍數就好,這部份可以交給遞迴呼叫來解決,直到得到結果為止。 現在試著將上面的概念拓展到 N = 7 假設 n = (abcde)~2~ = (ab000)~2~ + (00cde)~2~ = ( (ab)~2~ << 3 ) + (00cde)~2~ = = ( (ab)~2~ * 7 + (ab + cde)~2~ 依照上列概念寫出的程式碼如下: ```C #include <stdlib.h> #include <stdio.h> int isMultN(unsigned int n) { if(n <= 7){ if ( (n == 7) || (n == 0) ) return 1; else return 0; //for number which is larger than 7 } else n = (n >> 3) + (n & 7); /* Recursive call till you get 7 or less */ return(isMultN(n)); } ``` ~~真舒服XD~~ 可以看到判斷式有些不一樣,不過應該挺好理解的。 遞迴呼叫至 n <= 7 ,若 n 等於 0 或 7,則為 7 的倍數,其餘則非。 說來慚愧,會有以上的概念並不是我突然靈光一閃,是在搜尋的過程中看到了一篇[國小科展](http://ccjh.tc.edu.tw/manasystem/Files/download1/9805121042411_48%E5%B1%86%E7%A7%91%E5%AD%B8--%E4%BD%B3%E4%BD%9C--%E6%9D%8E%E7%91%9E%E6%BA%90%E8%80%81%E5%B8%AB.pdf)...才讓我想起 11 倍數判斷的原理並加以推廣。 :::info 太好了,接著你可以繼續想,上面遞迴和 bit-wise 操作可對應到數位邏輯電路是怎麼組合,來練習吧! :notes: jserv ::: ## 第 2 週測驗題_測驗`1` 題目如下: 請完成下方程式碼,依循 IEEE 754 單精度規範,輸出 2^x^的浮點數表達法,而且考慮兩種狀況: - 當 x 過小的時候,回傳 0.0 0.0 - 當 x 過大的時候,回傳 +∞ +∞ 注意:這裡假設 `u2f` 函式返回的浮點數值與其無號數輸入有著相同的位數,也就是至少 32-bit ```C #include <math.h> static inline float u2f(unsigned int x) { return *(float *) &x; } float exp2_fp(int x) { unsigned int exp /* exponent */, frac /* fraction */; /* too small */ if (x < 2 - pow(2, Y0) - 23) { exp = 0; frac = 0; /* denormalize */ } else if (x < Y1 - pow(2, Y2)) { exp = 0; frac = 1 << (unsigned) (x - (2 - pow(2, Y3) - Y4)); /* normalized */ } else if (x < pow(2, Y5)) { exp = pow(2, Y6) - 1 + x; frac = 0; /* too large */ } else { exp = Y7; frac = 0; } /* pack exp and frac into 32 bits */ return u2f((unsigned) exp << 23 | frac); } ``` 將 Y0 ~ Y7 填入相對應的數值,使得該函式依輸入的 x 輸出 2^x^ 的浮點數值。 ### 想法 & 思考 這段 code 註解清楚的將整個程式依照輸入的 x 值,分成 4 個範圍(與浮點數表示法相呼應): * too small 單精度浮點數表示法,能表現的最小正值為 0 0...0 0.......1 也就是 signed bit 為零; exponent bits 全為零; fraction 為 2^-23^ 對應到的 2^x^ ,x = 1 -(128 -1) - 23 = 2 - pow(2, Y0) - 23 ==> Y0 = 7 當 x < 這個值時,則使所有 bits 全為 0 * denormalize 這個範圍介於 too small 與 normalized 之間,也就是 exponent bits 全為零,但 fraction 部份不為零的情況。 因此,x 被限制在 x < Y1 - pow(2, Y2) = 2 - pow(2, 7) 在此範圍內 exp 設定成 0; 而 frac 則依照 x 大小被位移 frac = 1 << (unsigned) (x - (2 - pow(2, Y3) - Y4)); 這裡可以依最小的 denormalize 值來判斷參數 Y3、Y4 最小值 x = -149 ==> frac = 1 ==> -149 - (2 - pow(2, Y3) - Y4)) == 0 ==> Y3 = 7, Y4 = 23 * normalized 單精度浮點數能表示的最大值為 (2−2^−23^) × 2^127^ < 2^128^ 因此 x 值必須小於 128 = pow(2, Y5), Y5 = 7 exp = pow(2, Y6) - 1 + x; 為 x 加上 Bias 127 ==> Y6 = 7 * too large 最後則是輸入的 x 大於單精度能表示的範圍,在此情況下, exp 被設定為 8 bits 全為 1 ,Y7 = 0xff ### 延伸問題 - 給定一個單精度的浮點數值,輸出較大且最接近的 2^x^ 值,需要充分考慮到邊界 延伸題是原本命題的反向,由輸入的浮點數值決定 x。這個功能與原來的程式相比難上不少...原來是想這麼說的,不過突然想起好像一個工具叫作 log? 以下是偷渡 log2 (以二為底) 求 x 的方法,由於題目要求**輸出較大**且最接近的 2^x^,所以在得到 log2 的值後,若 x >= 0 則需要加上 1 以符合此要求。 得到 x 值後再進一步用上面的函式測驗 2^x^ 與輸入的 float 的差距。 ```C int main() { float a; while(1) { printf("Please input a number : "); scanf(" %f", &a); printf("\n"); int x = log2(a); if((x >= 0) && (x < log2(a))) x++; printf("THe log value = %f\n", log2(a)); printf("x = %d\n", x); printf("2^%d = %.20f\n", x, exp2_fp(x)); printf("2^%d = %.20f\n", x-1, exp2_fp(x-1)); } } ``` 以下為簡易的測試結果,此結果不考慮使用者輸入過大或過小的正值,或輸入值為負的 error。 ``` Please input a number : 0.111111 THe log value = -3.169926 x = -3 2^-3 = 0.12500000000000000000 2^-4 = 0.06250000000000000000 Please input a number : 0.222222 THe log value = -2.169926 x = -2 2^-2 = 0.25000000000000000000 2^-3 = 0.12500000000000000000 Please input a number : 0.5555555555555 THe log value = -0.847997 x = 0 2^0 = 1.00000000000000000000 2^-1 = 0.50000000000000000000 Please input a number : 1.5 THe log value = 0.584963 x = 1 2^1 = 2.00000000000000000000 2^0 = 1.00000000000000000000 Please input a number : 0.018521 THe log value = -5.754694 x = -5 2^-5 = 0.03125000000000000000 2^-6 = 0.01562500000000000000 Please input a number : 0.000068 THe log value = -13.844106 x = -13 2^-13 = 0.00012207031250000000 2^-14 = 0.00006103515625000000 ``` 從測試結果可看出,當給定一個單精度的浮點數值,輸出的較大且最接近的 2^x^ 值其實與原輸入的浮點數值有不小差異。若移除命題中 **較大** 的限制,只找出最接近的 2^x^ 值,可使誤差減少。 :::info TODO: 此議題可討論的面向還有很多,例如: - 因我們只要找出最接近的整數 x 值,可以不必使用 log2 函式,改以遞迴呼叫或迴圈的方式找出 x < float < x+1 的 x ,應可提高不少效能。 - 移除 x 必須為 int 的限制,以求更精準的表示所輸入的 float 值。雖然這樣等同於以另一 float 去表示這個 float 值,看似沒有意義,但可能在某些應用場景能以花費空間來換取時間也說不定。 這些額外的議題可能得等我完成其他作業再說... ::: :::danger 很好!有幾點可刺激思考: 1. 現代處理器許多具備 clz (count leading zero) 指令,能加速類似 log2 的運算; 2. 輸入數值的範圍對正確性影響很大,[形式化驗證](https://hackmd.io/s/H1xxp3pF0) 本質上就是做數學層面到工程的探索; :notes: jserv ::: ## 第 2 週測驗題_測驗`3` 題目如下: 考慮到某些實數的二進位表示形式如同 0.y y y y y ... 0.yyyyy... 這樣的無限循環小數,其中 y 是個 k 位的二進位序列,例如 1 / 3 的二進位表示為 0.01010101... 0.01010101... (y = `01`),而 1./ 5 的二進位表示為 0.001100110011... 0.001100110011...(y = `0011`),考慮到以下 y 值,求出對應的十進位分數值。 - y = `010011` => 19/X~1~ - y = `101` => 5/X~2~ - y = `0110` => 2/X~3~ 請分別選出最接近的X~1~、X~2~、X~3~ ### 想法 & 思考 這題在第一次課堂上作答的時候,因為沒想到適當的解法,所以都是用 **估計** 的:先手算出 y 的十進位小數值,再看和選項中哪個數最接近。 事後回想起來,若將 1~2~ >> 1 = 0.1~2~ 、 010~2~ >> 3 = 0.010~2~ 這樣的概念引入,這題解起來就會快很多。 由於0.yyyy...代表無窮循環小數 ==> 0.yyyy... = (y >> k) + (y >> 2k).... = y * (1 / 2^k^ + 1 / 2^2k^...)公比為 2^k^無窮級數 = y * (1 /(2^k^ - 1)) k 取決於 y 是幾個 bits - y = `010011` ==> y = 19, k = 6 得到 X~1~ = 2^6^-1 = 63 - y = `101` ==> y = 5, k = 3 得到 X~2~ = 2^3^-1 = 7 - y = `0110` ==> y = 6, k = 4 得到 0.yyyy.. = 6 / (16 - 1) 約分後得 X~3~ = 5 ### 延伸問題 給定的分數 (即 N/M 形式,當 N 和 M 都是正整數),如何轉換成二進位,而且在表達 bit 數量限制的狀況,轉換的誤差是多少? 由於個人覺得浮點數運算十分麻煩,可以的話能不碰就不碰,因此過去 12 小時一直在想,若我們要算 N/M 並且以二進位表示,可不可以不要跟 float 扯上關係。 以下是我得出的結論: ```C #include <stdlib.h> #include <stdio.h> long int frac(unsigned int x, unsigned int y) { long int i; i = 1; int j; while((i & 0x80000000) != 0x80000000){ if(x == 0){//shift i till we get enough bits while((i & 0x80000000) != 0x80000000) i = i << 1; } else if(x < y){ if((x<<1)< 0xffffffff) x = x<<1; i = i << 1; } else{ x = x - y; i++; } } return i; } int main() { unsigned int a, b; unsigned int Q;//quotient int j; while(1) { printf("Please input two numbers : \n"); scanf(" %u", &a); scanf(" %u", &b); Q = frac(a, b); printf("0."); for(j=1; j<31; j++){ if((Q & (0x80000000 >> j)) != 0) printf("1"); else printf("0"); } printf("\n"); printf("\n"); } return 1; } ``` 測試結果 ``` Please input two numbers : 1 5 0.001100110011001100110011001100 Please input two numbers : 19 63 0.010011010011010011010011010011 Please input two numbers : 5 7 0.101101101101101101101101101101 Please input two numbers : 2 5 0.011001100110011001100110011001 ``` 結果看起來不錯,不過我到底在寫什麼鳥呢? 以十進位為例: 若本來 2 / 7 的過程是 2 / 7 = <font color= "red">0.2</font> + (2 - <font color= "red">0.2</font>*7) / 7 = 0.2<font color= "red">8</font> + (0.6 - <font color= "red">0.08</font>*7) / 7 ... 上列程式碼做的事就是 2 / 7 = 1/10 *(20/7) = 1/10 *(<font color= "red">2</font> + 6/7) = 1/10 * 1/10 *(20 + 60/7) = 1/10 * 1/10 *(<font color= "red">28</font> + 4/7) ... 不斷使被除數進位,直到大於除數,得到一個整數商值後,再重頭做這些動作。而又因為二進位只有 0 與 1 的特性,因此只要做 Shift 與 Sub 就行了,不必考慮 被除數 - **商數** *除數。 程式碼的核心概念如上所述,接下來只要注意以下幾個小細節就好了: 1. i 的 leftmost bit 用來判斷商數 bits 達到指定長度與否,達到則停止,否則會產生 overflow。 2. 在還未達指定商數長度前就 x - Q*y 就等於 0 的話,則會依照 i 的 leftmost bit 向左 Shift 直到達指定長度。 3. 此法理論上可無限拓展商數長度(藉由分次計算後串接) :::info TODO: 將二進位的商數轉換回浮點數,並與浮點數除法比較誤差值。 :::