--- tags: 進階電腦系統理論與實作 --- # 2020q3 Homework5 (quiz5) contributed by < `johnnycck` > > [第 5 週測驗題](https://hackmd.io/@sysprog/2020-quiz5) ## 測驗 `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 / D1, od ? (slots + D2) >> 1 : slots >> 1); if (od) result += divop(result, slots); return result; } ``` ## 延伸問題1-1: 解釋上述程式碼運作原理 ### 思考流程 1. 使用 od = slots & 1 得到 slots(除數) 是否為奇數 2. 再來會看到使用遞迴的方法完成除法,因此可以猜到 orig / D1 想表達的是被除數每次都除以2,所以 `D1 = (c) 2` 3. 若 slots 為奇數,會將放入遞迴的除數設為:(slots + D2) >> 1,在這裡不難推斷出 `D2 = (d) 1`,因為奇數要 + 1 後再位移 ### 程式邏輯 * 如果除數是偶數,那將被除數 /2 後再將除數右移 1 位放入遞迴中,就會是正確結果 * 但若除數是奇數,那將被除數 /2 後再將除數 + 1 後右移 1 位放入遞迴中,這樣結果會比正確答案來的小,因此要在第 9~10 行把結果加回來,參考以下數學式: $$ \begin{align} \frac{a}{b} =& \frac{a}{b+1}\frac{b+1}{b} \\ =& \frac{a}{b+1}(1+\frac{1}{b}) \\ =& \frac{\frac{a}{2}}{\frac{b+1}{2}} + \frac{\frac{a}{2}}{\frac{b+1}{2}} \frac{1}{b} \end{align} $$ * 所以我們要補加回來的部份就是 $\frac{\text{result}}{b}$ 也就是 `divop(result, slot)` ## 延伸問題 1-2: 以編譯器最佳化的角度,推測上述程式碼是否可透過 [tail call optimization](https://en.wikipedia.org/wiki/Tail_call) 進行最佳化,搭配對應的實驗 搭配閱讀 [C 語言: 編譯器和最佳化原理](https://hackmd.io/s/Hy72937Me) 及 [C 語言: 遞迴呼叫](https://hackmd.io/s/rJ8BOjGGl) ### 思考流程 ### divop.c 組合語言 程式碼是否有做 tail call optimization 最佳化可以直接從組合語言得知,傳統的遞迴會執行 "call divop" 此動作,因此以下列出有無執行編譯器最佳化的組合語言來做比較,指令分別是 gcc -S -O0 -o divop_O0 divop.c 跟 gcc -S -O3 -o divop_O3 divop.c * gcc -S -O0 ```clike= .file "divop.c" .text .globl divop .type divop, @function divop: .LFB6: .cfi_startproc pushq %rbp .cfi_def_cfa_offset 16 .cfi_offset 6, -16 movq %rsp, %rbp .cfi_def_cfa_register 6 subq $48, %rsp movsd %xmm0, -24(%rbp) movl %edi, -28(%rbp) cmpl $1, -28(%rbp) je .L2 pxor %xmm0, %xmm0 ucomisd -24(%rbp), %xmm0 jp .L3 pxor %xmm0, %xmm0 ucomisd -24(%rbp), %xmm0 jne .L3 .L2: movsd -24(%rbp), %xmm0 jmp .L5 .L3: movl -28(%rbp), %eax andl $1, %eax movl %eax, -12(%rbp) cmpl $0, -12(%rbp) je .L6 movl -28(%rbp), %eax addl $1, %eax sarl %eax jmp .L7 .L6: movl -28(%rbp), %eax sarl %eax .L7: movsd -24(%rbp), %xmm0 movsd .LC1(%rip), %xmm1 divsd %xmm1, %xmm0 movl %eax, %edi call divop // 第一個遞迴呼叫 movq %xmm0, %rax movq %rax, -8(%rbp) cmpl $0, -12(%rbp) je .L8 movl -28(%rbp), %edx movq -8(%rbp), %rax movl %edx, %edi movq %rax, -40(%rbp) movsd -40(%rbp), %xmm0 call divop // 第二個遞迴呼叫 movapd %xmm0, %xmm1 movsd -8(%rbp), %xmm0 addsd %xmm1, %xmm0 movsd %xmm0, -8(%rbp) .L8: movsd -8(%rbp), %xmm0 .L5: leave .cfi_def_cfa 7, 8 ret .cfi_endproc .LFE6: .size divop, .-divop .section .rodata .align 8 .LC1: .long 0 .long 1073741824 .ident "GCC: (Ubuntu 7.5.0-6ubuntu2) 7.5.0" .section .note.GNU-stack,"",@progbits ``` 可以發現,GCC 沒有進行最佳化的情況下,只是單純的把 c 轉換成組合語言,而上面的組合語言中,單純的跟原始碼一樣做了兩次遞迴呼叫 * gcc -S -O3 ```clike= .file "divop.c" .text .p2align 4,,15 .globl divop .type divop, @function divop: .LFB39: .cfi_startproc cmpl $1, %edi movapd %xmm0, %xmm1 je .L12 pxor %xmm2, %xmm2 sete %dl ucomisd %xmm2, %xmm0 setnp %al cmovne %edx, %eax testb %al, %al je .L16 .L12: movapd %xmm1, %xmm0 ret .p2align 4,,10 .p2align 3 .L16: pushq %r12 .cfi_def_cfa_offset 16 .cfi_offset 12, -16 pushq %rbp .cfi_def_cfa_offset 24 .cfi_offset 6, -24 movl %edi, %r12d pushq %rbx .cfi_def_cfa_offset 32 .cfi_offset 3, -32 movl %edi, %ebx subq $16, %rsp .cfi_def_cfa_offset 48 movsd .LC1(%rip), %xmm0 mulsd %xmm0, %xmm1 ucomisd %xmm2, %xmm1 setnp %dl cmove %edx, %eax andl $1, %r12d je .L3 leal 1(%rdi), %ebp sarl %ebp cmpl $1, %ebp je .L4 testb %al, %al jne .L4 .L5: testb $1, %bpl mulsd %xmm1, %xmm0 jne .L17 movl %ebp, %edi sarl %edi call divop movapd %xmm0, %xmm1 .L7: testl %r12d, %r12d je .L2 .p2align 4,,10 .p2align 3 .L4: movapd %xmm1, %xmm0 movl %ebx, %edi movsd %xmm1, 8(%rsp) call divop movsd 8(%rsp), %xmm1 addsd %xmm0, %xmm1 jmp .L2 .p2align 4,,10 .p2align 3 .L3: movl %edi, %ebp sarl %ebp cmpl $1, %ebp je .L2 testb %al, %al je .L5 .L2: addq $16, %rsp .cfi_remember_state .cfi_def_cfa_offset 32 movapd %xmm1, %xmm0 popq %rbx .cfi_def_cfa_offset 24 popq %rbp .cfi_def_cfa_offset 16 popq %r12 .cfi_def_cfa_offset 8 ret .p2align 4,,10 .p2align 3 .L17: .cfi_restore_state leal 1(%rbp), %edi sarl %edi call divop movl %ebp, %edi movsd %xmm0, 8(%rsp) call divop movsd 8(%rsp), %xmm1 addsd %xmm0, %xmm1 jmp .L7 .cfi_endproc .LFE39: .size divop, .-divop .section .rodata.cst8,"aM",@progbits,8 .align 8 .LC1: .long 0 .long 1071644672 .ident "GCC: (Ubuntu 7.5.0-6ubuntu2) 7.5.0" .section .note.GNU-stack,"",@progbits ``` * 在此解說 O3 下組合語言的流程 - 原先在 O0 下組合語言只有出現 2 次 "call divop",這是因為編譯器單純將 c code 轉換成對應的組合語言 - O3 下出現了 4 次 "call divop",這是因為它將步驟進行優化,依照 od 是否為 '0',來區分兩種處理的模式 - 若編譯器判斷 od 為 '0',則先執行第一次的 call divop 呼叫(.L5),執行後再檢查一次 od 是否為 '0',若是,則直接回傳,若不是,則執行 "result += divop(result, slots)"(.L4),進行第二次 "call divop" - 若 od 為 '1',則編譯器將 "divop(orig / D1, (slots + 1) >> 1 )" 跟 "result += divop(result, slots)" 放在同一個 block 操作,也就是 .L17 * 由以上編譯器追蹤的結果,可以發現**並沒有執行 "tail recursive 的優化"** ## 測驗 `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 = 0x01000000; /* 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) + 0x3f000000; ix0 += (m << 23); INSERT_WORDS(z, ix0); return z; } ``` ::: ## 延伸問題 2-1: 解釋上述程式碼何以運作 搭配研讀 [以牛頓法求整數開二次方根的近似值](http://science.hsjh.chc.edu.tw/upload_works/106/44f6ce7960aee6ef868ae3896ced46eb.pdf) (第 19 頁) ### 程式碼解析 ```clike= /* 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; ``` - union 的結構特性讓結構中的各變數是共用記憶體位置 - 因此 `ieee_float_shape_type` 可以把一段 IEEE 754 編碼的 float,轉換成 uint32_t 來達成 float 跟 uint32_t 的轉換 ```clike= /* 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) ``` - `INSERT_WORDS(d, ix0)` 能將型態為 `float` 的 'd' 轉成型態為 `int` 的 'ix0' - `EXTRACT_WORDS(ix0, d)` 能將型態為 `int` 的 'ix0'轉成型態為 `float` 的 'd' * 參考 IEEE 754 規範 ![](https://i.imgur.com/rHhl97x.png) ![](https://i.imgur.com/ZkJvAZH.png) ![](https://i.imgur.com/x6sNXSV.png) -- [src](https://hackmd.io/@sysprog/c-floating-point) - 首先將傳入的 `float x` 透過 EXTRACT_WORDS(ix0, x) 轉換成 `int` 型態 - 若是 ix0 <=0 且 ix0 & (~sign) == 0,代表除了 sign bit 之外都是 0,這樣子的浮點數可能為 +-0,開根號會是自己,因此回傳 x - 若是進入 ix0 < 0 這個判斷式,代表 x 並非 0,則代表是負浮點數,負浮點數無法開根號,因此回傳 (x-x)/(x-x),代表 NaN ```clike= int32_t sign = 0x80000000; 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 */ } ``` - 剛剛已經排除 +-0 跟負浮點數的情況,在此透過 ix0 & 0x7f800000 == 0x7f800000 來判斷浮點數是否為 +infinity 或 NaN,如果成立,則回傳 x,因為 +infinity 跟 NaN 開根號都是得到自己 ```clike= /* take care of +INF and NaN */ if ((ix0 & 0x7f800000) == 0x7f800000) { /* sqrt(NaN) = NaN, sqrt(+INF) = +INF,*/ return x; } ``` - 目前已經排除 +-0 ,負浮點數, +INF, NaN 等特殊狀況,接下來考慮 denormalize number 的狀況 - 透過右移 23 位,m 只有保留 sign bit + exponent bits(9 bits) - 如果 m==0,因為在前面已經排除浮點數是 0 的情況,因此代表此浮點數是 denormalize number,此浮點數是 $0.x * 2^{-126}$,denormalize number 的處理方式是要將 $0.x * 2^{-126}$ 變成 $1.Fraction * 2^{exponent}$ 這種 normalize number 的方式 - 透過 for loop,將 ix0 做左移,直到 exponent 位元 > 0 (此步驟讓 0.x 變成 $1.Fraction$ - m-= i-1 ( 此步驟以 'i' 記錄位移量,又因為原先 denormalize 表示法 exponent 是 -126,在此要 m+1-i 才會正確 - m-=127,得到 IEEE 規範中的 exponent - ix0 保留 Fraction ,並在 exponent 的最低位加入 1,表達 $1.Fraction * 2^{exponent}$ - 因為目標是求 x 的根號,$1.Fraction * 2^{exponent}$ 開根號會是 $\sqrt{1.Fraction} * 2^{exponent/2}$ - 如果 m 是奇數,讓 $1.Fraction * 2^{exponent}$ * 2,變成 $2.Fraction * 2^{exponent-1}$ - m >>= 1,表達 exponent/2 ```clike= /* 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] */ ``` - 以上的程式碼讓目前要求的 $x * 2^{exponent}$ 的 x 落在 1<=x<4 的範圍 - 因此求 x 的根號範圍會落在 1 < $\sqrt{x}$ < 2,這樣的範圍可以允許從 1 開始往上加,並使用 q 來代表解的精度 - 在此部分參考 `RusselCK` 的推導解釋: * Generate ${(\sqrt{x})}_2$ Bit by Bit ( 數學推導 ): * 假設 $1 \leq x < 2^2 = 4$ * 令 $q_i={(\sqrt{x})}_2$ 到小數點後第 $i$ 位的精確值, $i \geq 0$ $\Rightarrow q_0=1$ * 考慮 $q_{i+1}$ : * 若 ${(q_i + 2^{-(i+1)})}^2 \leq x$ ,則 $q_{i+1}= q_i + 2^{-(i+1)}$ * 即 $q_i$ 後面補 1 * 若 ${(q_i + 2^{-(i+1)})}^2 > x$ ,則 $q_{i+1} = q_i$ * 即 $q_i$ 後面補 0 * 整理 ${(q_i + 2^{-(i+1)})}^2 \leq x$: $$ \begin{split} {(q_i + 2^{-(i+1)})}^2 \leq x &\Rightarrow q_i^2 + 2\times q_i \times (2^{-(i+1)}) + (2^{-(i+1)})^2 \leq x \\ &\Rightarrow q_i \times (2^{-i}) + 2^{-2(i+1)} \leq x - q_i^2 \\ &\Rightarrow q_i \times 2 \space + 2^{-(i+1)} \leq [x - q_i^2] \times 2^{(i+1)} \\ &\Rightarrow s_i + r_i \leq x_i, \\ &\begin{split} where \space &s_i = 2 \times q_i, \\ &r_i = 2^{-(i+1)}, \\ &x_i = [x - q_i^2] \times 2^{(i+1)} = [x - q_i^2]r_i^{-1} \end{split} \end{split} $$ * $Thus,$ * 若 $s_i + r_i \leq x_i$,則 * $q_{i+1}= q_i + r_i$ * $s_{i+1}=(s_i + r_i) + r_i$ * $x_{i+1}=2x_i - 2(s_i + r_i) = 2[x_i - (s_i + r_i)]$ :::spoiler 推導過程 $$ \begin{split} s_{i+1} &= 2 \times q_{i+1} \\ &= 2 \times [q_i + 2^{-(i+1)}] \\ &= (2 \times q_i) + 2^{-i} \\ &= s_i + 2^{-i} \\ &= s_i + 2^{-(i+1)} + 2^{-(i+1)} \\ &= (s_i + r_i) + r_i \\ \end{split} $$ $$ \begin{split} x_{i+1} &=[x - q_{i+1}^2] \times 2^{((i+1)+1)} \\ &= [x - (q_i + r_i)^2] \times 2^{(i+2)} \\ &= [x - (q_i^2 + 2q_ir_i + r_i^2)] \times 2r_i^{-1} \\ &= [x - q_i^2] \times 2r_i^{-1} - 2q_ir_i \times 2r_i^{-1} - r_i^2 \times 2r_i^{-1} \\ &= 2x_i - 2s_i - 2r_i \\ &= 2x_i - 2(s_i + r_i) \\ \end{split} $$ ::: * 若 $s_i + r_i > x_i$,則 * $q_{i+1} = q_i$ * $s_{i+1}=s_i$ * $x_{i+1}=2x_i$ :::spoiler 推導過程 $$ \begin{split} s_{i+1} &= 2 \times q_{i+1} \\ &= 2 \times q_i \\ &= s_i \\ \end{split} $$ $$ \begin{split} x_{i+1} &=[x - q_{i+1}^2] \times 2^{((i+1)+1)} \\ &= [x - q_i^2] \times 2^{(i+2)} \\ &= [x - q_i^2] \times 2r_i^{-1} \\ &= 2x_i \end{split} $$ ::: ```clike= /* generate sqrt(x) bit by bit */ ix0 += ix0; q = s0 = 0; /* [q] = sqrt(x) */ r = 0x01000000; /* 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; } ``` ## 延伸問題 2-3: 練習 LeetCode 69. Sqrt(x),提交不需要 FPU 指令的實作 #### 思考流程 * Naive Solution 一開始使用 naive 的想法,因為只需要回傳 input 的 sqrt() 整數解,所以使用 for 迴圈從 2 開始往上加,而終止條件則設定為 i*i > input,此解法的壞處為錯誤次數會很多,需要許多的乘法運算,造成效能低落,也因此在 leetcode 上只贏過 23% 的人 ```c= int mySqrt(int x){ if(x==0 || x==1) return x; else { long long i;// 此處為 long long 原因是 i*i 有可能過大到超過 int 能表示的範圍 for(i=2; i*i<=x; i++); return (i-1); } } ``` Leetcode 結果: ![](https://i.imgur.com/bSpi1uT.png) * Binary Tree Solution 原先的解法是慢慢的往上加,這個尋找 root 的過程其實類似於搜尋已排序好的遞增數列,若是套用二元搜尋的概念,就能減少錯誤的次數,進而加快搜尋速度,因此更改為二元搜尋的方式來完成,最後發現比原先解法快速很多 ```c= int mySqrt(int x){ if(x==0 || x==1) return x; else { // end = x/2 因為若 x>1 的情況,floor(sqrt(x)) 不可能 > x/2 long long start = 1, end = x/2, ans; while (start <= end) { long long mid = (start + end) / 2; if (mid*mid == x) return mid; if (mid*mid < x) { start = mid + 1; ans = mid; } else end = mid-1; } return ans; } } ``` Leetcode 結果: ![](https://i.imgur.com/1wMWUjU.png) * 因為 memory usage 的大小只贏過 23.19% 的提交,因此把 mid 這個變數刪掉,改用 (start+end)/2 代替,最後執行時間贏過 100% 的提交,空間贏過 98.35% 的提交 ```clike= int mySqrt(int x){ if(x==0 || x==1) return x; else { long start = 1, end = x/2; while (start <= end) { //long long mid = ((start + end) / 2); if (((start + end) / 2) * ((start + end) / 2) == x) return ((start + end) / 2); if (((start + end) / 2) * ((start + end) / 2) < x) { //ans = ((start + end) / 2); start = ((start + end) / 2) + 1; } else end = ((start + end) / 2)-1; } return (((start - 1) * 2 ) / 2); } } ``` * leetcode 結果 ![](https://i.imgur.com/laku2uP.png) ![](https://i.imgur.com/SYEk8wj.png) :::warning TODO: 1. 引入快取表格,將 Sqrt 的結果保存在表格中,避免之後重複運算,並運用 cache replacement policy 2. LeetCode [146. LRU Cache](https://leetcode.com/problems/lru-cache/) ::: --- ## 測驗 `3` ### 題目 ```c= int consecutiveNumbersSum(int N) { if (N < 1) return 0; int ret = 1; int x = N; for (int i = 2; i < x; i++) { x += ZZZ; if (x % i == 0) ret++; } return ret; } ``` ## 延伸問題 3-1: 解釋上述程式碼何以運作 ### 思考流程 * 數學式 1. $$ kx = N - \frac{k(k-1)}{2} $$ ------- 2. $$ N - \frac{(k-1)k}{2} = kx $$ * 參考 [RinHizakura](https://hackmd.io/@RinHizakura/SJnXiSewD),將上述公式(在 k 和 x 皆為正整數的前提下,使得等號成立的整數 x 和 k 組合有多少個),轉換概念為 $$ N - \{從 \space 0 \space 加到\space (k - 1)\space 之和\} = kx $$ * 每次的 for 迴圈代表 N-{0 加到 k-1 之和},因此 k 從 2 開始往上加 * 第一次 interation,x (在一開始等於 N),會需要減掉前述:{0 加到 k-1 之和},並透過跟 k 取餘數,得知是否有正整數解,若有,則代表 N 可由 2 個連續整數相加來表達 * 在接下來的 iteration,透過 x += (1-k),也就是 x -= (k-1),可以計算 N-{0 加到 k-1 之和},直到 k>=x,代表連續 k 整數已經超過 N-{0 加到 k-1 之和},因此結束計算 * 因此,`ZZZ = (d) 1 - i`。 回顧原程式(下面 `i` 改成 `k`,與前式子做對應): ```c= int consecutiveNumbersSum(int N) { if (N < 1) return 0; int ret = 1; int x = N; for (int k = 2; k < x; k++) { x += (1 - k); // = x -= (k - 1) if (x % k == 0) ret++; } return ret; } ``` ## 延伸問題 3-2: 嘗試改寫為更有效率的實作 ### 思考流程 * 在前述題目中,有提到迴圈的結束條件是連續 k 整數的 k,已經超過 N-{0 加到 k-1 之和},這樣子的結束條件是可以縮小的,以之前公式可以得出 $$ \Rightarrow k< \sqrt{2N}\\ \Rightarrow k^2 < 2N\\ $$ 因此將程式碼改寫為: ```c= int consecutiveNumbersSum(int N){ if (N < 1) return 0; int ret = 1; int x = N; for (int i = 2; i*i < 2*N; i++) { x += (1-i); if (x % i == 0) ret++; } return ret; } ``` * 結果 ![](https://i.imgur.com/xhdE3EQ.png)