# 2020q3 Homework5 (quiz5) contributed by < `nelsonlai1` > ## 測驗 1 ```c= #include <stdio.h> #include <stdlib.h> double divop(double orig, int slots) { if (slots == 1 || orig == 0) return orig; int od = slots & 1; double result = divop(orig / 2, od ? (slots + 1) >> 1 : slots >> 1); if (od) result += divop(result, slots); return result; } ``` 這題除法依照除數 slots 的奇偶來分成兩個 result。 slots 為偶數時 : (這裡將 orig 設成 m, slots 設成 n) $\dfrac{m / 2}{n / 2}= \dfrac{m}{n}$ slots 為奇數時 : $\dfrac{m/2}{(n+1)/2} + \dfrac{\dfrac{m/2}{(n+1)/2}}{n} = \dfrac{n \times m/2 + m/2}{\dfrac{n \times (n + 1)}{2}} = \dfrac{(n + 1) \times m/2}{\dfrac{n \times(n+1)}{2}} = \dfrac{m}{n}$ 接著一直做遞迴直到終止條件 `(slots == 1 || orig == 0)`。 ## 測驗 2 :::spoiler 題目 ```c= #include <stdint.h> /* A union allowing us to convert between a float and a 32-bit integers.*/ typedef union { float value; uint32_t v_int; } ieee_float_shape_type; /* Set a float from a 32 bit int. */ #define INSERT_WORDS(d, ix0) \ do { \ ieee_float_shape_type iw_u; \ iw_u.v_int = (ix0); \ (d) = iw_u.value; \ } while (0) /* Get a 32 bit int from a float. */ #define EXTRACT_WORDS(ix0, d) \ do { \ ieee_float_shape_type ew_u; \ ew_u.value = (d); \ (ix0) = ew_u.v_int; \ } while (0) static const float one = 1.0, tiny = 1.0e-30; float ieee754_sqrt(float x) { float z; int32_t sign = 0x80000000; uint32_t r; int32_t ix0, s0, q, m, t, i; EXTRACT_WORDS(ix0, x); /* take care of zero */ if (ix0 <= 0) { if ((ix0 & (~sign)) == 0) return x; /* sqrt(+-0) = +-0 */ if (ix0 < 0) return (x - x) / (x - x); /* sqrt(-ve) = sNaN */ } /* take care of +INF and NaN */ if ((ix0 & 0x7f800000) == 0x7f800000) { /* sqrt(NaN) = NaN, sqrt(+INF) = +INF,*/ return x; } /* normalize x */ m = (ix0 >> 23); if (m == 0) { /* subnormal x */ for (i = 0; (ix0 & 0x00800000) == 0; i++) ix0 <<= 1; m -= i - 1; } m -= 127; /* unbias exponent */ ix0 = (ix0 & 0x007fffff) | 0x00800000; if (m & 1) { /* odd m, double x to make it even */ ix0 += ix0; } m >>= 1; /* m = [m/2] */ /* generate sqrt(x) bit by bit */ ix0 += ix0; q = s0 = 0; /* [q] = sqrt(x) */ r = QQ0; /* r = moving bit from right to left */ while (r != 0) { t = s0 + r; if (t <= ix0) { s0 = t + r; ix0 -= t; q += r; } ix0 += ix0; r >>= 1; } /* use floating add to find out rounding direction */ if (ix0 != 0) { z = one - tiny; /* trigger inexact flag */ if (z >= one) { z = one + tiny; if (z > one) q += 2; else q += (q & 1); } } ix0 = (q >> 1) + QQ1; ix0 += (m << QQ2); INSERT_WORDS(z, ix0); return z; } ``` ::: 這題做的是浮點數的開根號,EXTRACT_WORDS 將 float 改用 int 表示(位元不變動),INSERT_WORDS 則相反。這題先將 float 改成 int 來方便做運算,算好值之後再轉換成 float。 ```c=36 /* take care of zero */ if (ix0 <= 0) { if ((ix0 & (~sign)) == 0) return x; /* sqrt(+-0) = +-0 */ if (ix0 < 0) return (x - x) / (x - x); /* sqrt(-ve) = sNaN */ } ``` 這段在處理 0 和負數的問題,如果除了 sign bit 以外都是 0,代表此數是 0 (雖然 ix0 是 int,但實際上表示的是 float,所以有 +-0),直接 return 原數。而如果是負數的話,則 return NaN。 ```c=43 /* take care of +INF and NaN */ if ((ix0 & 0x7f800000) == 0x7f800000) { /* sqrt(NaN) = NaN, sqrt(+INF) = +INF,*/ return x; } ``` 這段對 infinity 和 NaN 做處理,由於這兩數開根號後依然不變,所以也是直接返回原數。 ```c=48 /* normalize x */ m = (ix0 >> 23); if (m == 0) { /* subnormal x */ for (i = 0; (ix0 & 0x00800000) == 0; i++) ix0 <<= 1; m -= i - 1; } m -= 127; /* unbias exponent */ ix0 = (ix0 & 0x007fffff) | 0x00800000; if (m & 1) { /* odd m, double x to make it even */ ix0 += ix0; } m >>= 1; /* m = [m/2] */ ``` `m = (ix0 >> 23);` 取出 exponent 部分。 由於浮點數有 normalize 和 denormalize 模式,50 ~ 54 行將 denormalize 轉換成 normalize, `ix0 <<= 1;` 將 denormalize 的數乘以二(由於這裡一定是正數,所以這邊不用將 sign bit 補回), `m -= i - 1;` 則是將乘過的次數補回來,之所以會 +1 是因為 denormalize 時是 $\times 2^{-126}$,但轉換成 normalize 後為 $\times 2^{exponent-127}$,以上部分可以參考[浮點數運算和定點數操作](https://hackmd.io/@NwaynhhKTK6joWHmoUxc7Q/H1SVhbETQ) `m -= 127;` 將原本的 exponent 改成更直觀的寫法,`ix0 = (ix0 & 0x007fffff) | 0x00800000;` 把 fraction 部分取出並加上 1,到這邊為止,被開根號的數表示為 $ix0 \times 2^m,1 \leq ix0 < 2$ 開根號後,我們可以輕易地處理 $2^m$ 的部分。 如果 m 是偶數,則最後的結果是 $\sqrt{ix0} \times 2^{\lfloor m/2 \rfloor}$ 如果 m 是奇數,則最後的結果是 $\sqrt{2 \times ix0} \times 2^{\lfloor m/2\rfloor}$ 也就是 57 ~ 60 行的內容。 ```c=62 /* generate sqrt(x) bit by bit */ ix0 += ix0; q = s0 = 0; /* [q] = sqrt(x) */ r = QQ0; /* r = moving bit from right to left */ while (r != 0) { t = s0 + r; //t = s_i + 2^(-(i+1)) if (t <= ix0) { //s_i + 2^(-(i+1)) <= y_i s0 = t + r; //s_(i+1) = s_i + 2^(-i) ix0 -= t; //y_(i+1) = y_i - s_i - 2^(-(i+1)) (* 2 later) q += r; //q_(i+1) = q_i + 2^(-(i+1)) } ix0 += ix0; //y_(i+1) = y_i * 2 r >>= 1; } ``` 這裡部分參考 [RinHizakura](https://hackmd.io/@RinHizakura/SJnXiSewD#%E6%B8%AC%E9%A9%97-2) 的解釋。 第 62 行我想了蠻久的,我覺得這裡應該不是對數值本身乘二,而是將小數點往左移一格,所以目前的小數點在(右邊數來)第 24, 25 位之間,因此 r = 0x01000000; $q_i$ 表示為 $\sqrt y$ 到小數點後第 i 位的近似值,$q_0$ = 1。 而依照 ${(q_{i} + 2^{-(i + 1)})}^2 \leq y$ 判斷式來決定 $q_{i+1}$ 的值,若上式成立時,$q_{i+1} = q_i + 2^{-(i+1)}$ (下一位補 1),反之 $q_{i+1} = q_i$ (下一位補 0)。 把上式做展開後,可得到 $$ q_i^2 + q_i \times2^{-i} + 2^{-2(i+1)}\leq y \\ q_i \times2^{-i} + 2^{-2(i+1)}\leq y -q_i^2 \\ q_i \times 2 + 2^{-(i+1)}\leq (y-q_i^2)\times 2^{i+1} $$ 令以下兩個變數 $$ s_i = q_i\times 2 \\ y_i = (y-q_i^2)\times 2^{i+1} $$ 於是原本的不等式變成 $$ s_i + 2^{-(i+1)}\leq y_i\space $$ 若以上式子成立時 $$ s_{i+1} = s_i + 2^{-i} \\ y_{i+1} = (y-q_{i+1}^2)\times 2^{i+2} = (y - q_i^2 - q_i \times2^{-i} - 2^{-2(i+1)})\times2^{i+2} = (y_i-s_i-2^{-(i+1)})\times 2 $$ 不成立時 $$ s_{i+1} = s_i \\ y_{i+1} = y_i \times 2 $$ ```c=78 /* use floating add to find out rounding direction */ if (ix0 != 0) { z = one - tiny; /* trigger inexact flag */ if (z >= one) { z = one + tiny; if (z > one) q += 2; else q += (q & 1); } } ``` 這裡在判斷系統是哪種進位方式,一開始先把 `z = one - tiny`,由於 tiny 遠小於 one,所以如果此時 `z >= one` 成立,代表系統可能為無條件進位或四捨五入,再透過 `z = one + tiny` 以及 `z > one` 來分辨無條件進位以及四捨五入。 由於後面會將 `q >> 1`,所以這裡需要考慮進位問題,如果系統是無條件進位的話,就直接把第 2 位,也就是最後結果的第 1 位加 1, 而如果是四捨五入的話,就會做 `q += (q & 1)` ,如此一來,如果第 1 位是 1 的話會進位,反之則不進位。 ```c=90 ix0 = (q >> 1) + QQ1; ix0 += (m << QQ2); ``` 經過上面的了解後,最後這部分就比較容易回答了。由於前面把小數點往前移了一格,所以這裡做 `q >> 1` 把小數點移到 float 格式中的位置,又因為原本把 exponent 部分減 127,所以這裡要加回來,因為 `q >> 1` 的 MSB 必定是第 24 位 ($1\leq \sqrt y < 2$),所以這裡的 QQ1 = 0x3f000000。再把 $2^m$ 乘回來,所以 QQ2 = 23 ### 延伸問題 #### 練習 LeetCode 69. Sqrt(x),提交不需要 FPU 指令的實作 ```c= int mySqrt(int x) { if (x == 0 || x == 1) return x; int a = x / 2; int b = 1 << (((32 - __builtin_clz(x)) >> 1) - 1); while(b <= a) { int c = (a + b) / 2; if (c == x / c) return c; if (c < x / c) b = c + 1; if (c > x / c) a = c - 1; } return a; } ``` ![](https://i.imgur.com/dOKlPwk.png) 利用二分法逼近 x 的平方根,在 x >= 2 條件下,x / 2 >= $\lfloor \sqrt x\rfloor$, 網路上查到的方法中,b 多半設為 1,這裡用了這種算法可以逼近得更快。 ## 測驗 3 給定一個正整數 N,問 N 能寫成多少種連續正整數之和,假設正整數從 x 開始,且個數共有 k 個,則可寫出該等差數列為: $$ x, x+1, x+2, ..., x+(k-1) $$ 令其和為 N,根據等差數列的求和公式,可寫出以下等式: $$ kx + \frac{(k-1)k}{2} = N $$ $$ kx = N - \frac{(k-1)k}{2} $$ 對任意 k 值,倘若 x 能得到正整數解,就表示必定會有個對應的等差數列和為 N。由於 k 是等差數列的長度,首先必定大於 0,即為下限。由於 x 也必是正整數,可得: $$ N - \frac{(k-1)k}{2} > 0 $$ 從而得到近似解: $$ k < \sqrt{2N} $$ ```c= int consecutiveNumbersSum(int N) { if (N < 1) return 0; int ret = 1; int x = N; for (int i = 2; i < x; i++) { x += 1 - i; if (x % i == 0) ret++; } return ret; } ``` 第七行的 i 就是上面的 k,從 2 開始試,因為 k == 1 一定成立 (x == N)。 $\dfrac{(k-1)k}{2} = 1 + 2 + ... +(k - 1)$,所以函式中的 $X = N - \dfrac{(k-1)k}{2} > 0$ (初始值為N,之後在第八行加 (1 - i))(為了避免跟前面的正整數起始 x 混淆,這裡用大寫表示),而第九行在判斷 $N - \dfrac{(k-1)k}{2}$ 是否等於 $kx$,如果是的話代表 k 等於該值的時候有解,所以 `ret++` 在第七行有一個 `i < X` 的終止條件,因為 $k <= N - \dfrac{(k-1)k}{2}$,且 $k = N - \dfrac{(k-1)k}{2}$ 的情況已在第九行時就有判斷過了,所以成立。