# 演算法課程題解 - 浮點數 # UVa 10370 ## 題目 http://domen111.github.io/UVa-Easy-Viewer/?10370 有 $C$ 個班級,每個班級有 $N$ 個學生 給定 $N$ 個學生的成績,輸出每個班級有多少百分比的學生成績比班平均還高 ## 解法 By Koios1143 ### 想法 每個班級的成績都可以算出一個平均,算出成績後一個一個比對誰的成績比平均高,每遇到一個就紀錄下來 有了紀錄的數量以及班級學生的總數,就可以算出答案囉! 不過要特別注意兩個地方 1. 我們的平均以及答案的比例都可能會有小數,所以要用 `double` 來儲存 2. 輸出要四捨五入到小數點後第三位,記得要用 `fixed` 和 `setprecision` ### 程式碼 ```cpp= #include<iostream> #include<iomanip> using namespace std; int c,n,arr[1005]; int main(){ cin>>c; while(c--){ cin>>n; double avg=0; for(int i=0 ; i<n ; i++){ cin>>arr[i]; avg+=arr[i];// 先將成績加總 } avg/=n;// 再來算平均 int cnt=0; for(int i=0 ; i<n ; i++){ if(arr[i]>avg) cnt++;// 記錄大於平均的數量 } cout<<fixed<<setprecision(3)<<((double)cnt/(double)n)*100.0<<"%\n"; } return 0; } ``` ### 時間複雜度分析 有 $C$ 個班級,每個班級有 $N$ 筆成績要輸入,輸入的時間複雜度為 $O(CN)$ 每次算出平均後,需要一個一個比對有多少大於平均的成績,時間複雜度為 $O(N)$ 總時間複雜度為 $O(CN+N)$ , 約為 $O(CN)$ # UVa 10491 ## 題目 http://domen111.github.io/UVa-Easy-Viewer/?10491 現在有一場遊戲,遊戲的規則是這樣的: 舞台上有許多扇門,每個門後都有一輛車或是一隻牛 身為參賽者的你,目的就是要選到有車的那扇門。在遊戲的一開始,你會選擇一扇門 接下來主持人會開啟幾扇在你選擇的門以外,藏有牛的門,題目保證這扇門是存在的 問題如下: 在有 $N$ 隻牛, $M$ 輛車,主持人開啟 $S$ 扇門的情況下,你贏得車的機率是多少,輸出到小數點後第 5 位 ## 解法 By Koios1143 ### 想法 這題其實是數學題喔!在高中的機率當中也有類似的題目,想法是這樣的 一開始參賽者選擇的情況有兩個 1. 選擇了藏有牛的門 一開始選擇藏有牛的門的機率為 $\frac{N}{N+M}$ 此時,剩下的門的數量為 $(N-1)+M-S$ ,車的數量為 $M$ 在這個前提下,選到車的機率為 $\frac{N}{N+M} \times \frac{M}{N+M-S-1}$ 2. 選擇了藏有車的門 一開始選擇藏有車的門的機率為 $\frac{M}{N+M}$ 此時,剩下的門的數量為 $N+(M-1)-S$ ,車的數量為 $M-1$ 在這個前提下,選到車的機率為 $\frac{N}{N+M} \times \frac{M-1}{N+M-S-1}$ 最後將這兩種情況的機率相加,就會是我們的答案 記得,這裡的機率可能會含有小數,要使用 `double` 儲存 並且輸出要到小數點後第 5 位,要使用 `fixed` 和 `setprecision` ### 程式碼 ```cpp= #include<iostream> #include<iomanip> using namespace std; double NCOWS, NCARS, NSHOW; int main(){ while(cin>>NCOWS>>NCARS>>NSHOW){ double tot=NCOWS+NCARS; //choose cow double p1 = (NCOWS/tot) * (NCARS/(tot-1-NSHOW)); double p2 = (NCARS/tot) * ((NCARS-1)/(tot-1-NSHOW)); cout<<fixed<<setprecision(5)<<p1+p2<<"\n"; } return 0; } ``` ### 時間複雜度分析 每一筆輸入在計算上時間複雜度為 $O(1)$ 總時間複雜度為 $O(測資數量)$ # TOJ 99 ## 題目 https://toj.tfcis.org/oj/pro/99/ 給一個二階矩陣 \begin{vmatrix} a&b\\c&d \end{vmatrix} 求 $\left| a*d - b*c \right|$ 是否非零,誤差值在小數點後 7 位 ## 解法 By Koios1143 ### 想法 這題是要測試是否知道使用 eps 在 C/C++ 當中,小數點只能是"大致上"準確的,所以像是小數再判斷是否為零不能直接判斷 我們通常會設定一個誤差值,像是本題就是 $10^{-7}$ ,當我們的值小於該誤差值就會被判定為 0 所以我們只需要改用 eps 來判斷就可以囉 ### 程式碼 ```cpp= //By Koios1143 #include<iostream> #include<cmath> using namespace std; const double eps=1e-7; double a,b,c,d; int main(){ cin>>a>>b>>c>>d; double result = a*d-b*c; if(fabs(result)>=eps){ cout<<"1\n"; } else{ cout<<"0\n"; } return 0; } ``` ### 時間複雜度分析 計算以及判斷都只有一次,時間複雜度為 $O(1)$ # UVa 12195 ## 題目 http://domen111.github.io/UVa-Easy-Viewer/?12195 小李在學作曲,他想要做出每一小節長度都是 1 拍的曲子 可以用的音符有這些: ![](https://i.imgur.com/O4PyW43.png) 我們會拿到一串以 `/` 切割的字串,每個 `/` 之間代表一個小節的內容 請你判斷出其中有多少小節的長度剛好是 1 拍 ## 解法 By Koios1143 ### 想法1 每個小節我們都拿一個變數紀錄音符的總長度 我們可以跟著題目的想法做,遇到相對應的音符代號,就將長度記錄下來 在每個小節結束後,如果長度剛好為 1 拍,就記錄下來 輸入的部分我們可以這樣處理: 每次遇到 `/` 就看看我們紀錄音符長度的值是不是剛好為 1 拍,如果是就紀錄下來 無論有沒有剛好為 1 拍,都必須要將記錄長度的變數歸零 ### 想法2 如果不想要處理麻煩的小數問題,可以這樣想 觀察可以用的音符,可以發現到所有音符長度的最小公倍數是 64 將全部的音符長度都乘上 64 後,都會變成整數,這樣就完美的忽略小數的問題了! 那麼一個小節的長度也就變成 64 囉! ### 程式碼1 ```cpp= #include<iostream> #include<iomanip> using namespace std; int main(){ string s; while(cin>>s){ double cnt=0; int ans=0; if(s=="*") break; for(int i=0 ; i<s.size() ; i++){ if(s[i] == '/'){ if(cnt == 1) ans++; cnt=0; } else{ if(s[i] == 'W'){ cnt+=1.0; } else if(s[i] == 'H'){ cnt+=0.5; } else if(s[i] == 'Q'){ cnt+=0.25; } else if(s[i] == 'E'){ cnt+=0.125; } else if(s[i] == 'S'){ cnt+=0.0625; } else if(s[i] == 'T'){ cnt+=0.03125; } else if(s[i] == 'X'){ cnt+=0.015625; } } } cout<<ans<<'\n'; } return 0; } ``` ### 程式碼2 ```cpp= #include<iostream> #include<iomanip> using namespace std; int main(){ string s; while(cin>>s){ int cnt=0; int ans=0; if(s=="*") break; for(int i=0 ; i<s.size() ; i++){ if(s[i] == '/'){ if(cnt == 64) ans++; cnt=0; } else{ if(s[i] == 'W'){ cnt+=64; } else if(s[i] == 'H'){ cnt+=32; } else if(s[i] == 'Q'){ cnt+=16; } else if(s[i] == 'E'){ cnt+=8; } else if(s[i] == 'S'){ cnt+=4; } else if(s[i] == 'T'){ cnt+=2; } else if(s[i] == 'X'){ cnt+=1; } } } cout<<ans<<'\n'; } return 0; } ``` ### 時間複雜度分析 每次輸入會針對字串的每個字原作相對應的計算 而每個計算的時間複雜度為 $O(1)$ 每筆測資的時間複雜度為 $O(len(s))$ # UVa 113 ## 題目 http://domen111.github.io/UVa-Easy-Viewer/?113 給定兩個整數 $n$, $p$ ,求 $p$ 的 $n$ 次方根 $k$ ,且保證 $k$ 為整數 ## 解法 By Koios1143 ### 想法 $p$ 的 $n$ 次方根等同於 $p$ 的 $\frac{1}{n}$ 次方 直接使用內建的 `pow` 即可 有一點需要特別小心,因為 C++ 預設在輸出小數時如果太小,可能會變成以科學記號表示 要排除這種狀況,我們可以用之前學過的 `setprecision` 來解決 `setprecision(n)` 等同四捨五入取到小數點後第 $n$ 位,這裡我們要取整數,所以放 0 ### 程式碼 ```cpp= #include<iostream> #include<iomanip> #include<cmath> using namespace std; double n,p; int main(){ while(cin>>n>>p){ cout<<fixed<<setprecision(0)<<pow(p,1.0/n)<<'\n'; } return 0; } ``` ### 時間複雜度分析 每筆測資時間複雜度可以視為 $O(1)$ 不過詳細可以參考這篇: https://stackoverflow.com/questions/4638473/how-to-powreal-real-in-x86 # UVa 1185 ## 題目 http://domen111.github.io/UVa-Easy-Viewer/?1185 有 $n$ 筆測資,每筆測資包含一個數字 $m$ ,求 $m!$ 是幾位數 ## 解法 By Koios1143 ### 想法 如果只給一個數字 $p$ ,要求 $p$ 是幾位數我們可以用高中學到的 $log_{10}p +1$ 的整數得到答案 根據 $log$ 的性質, $log_{10}{(a*b)}$ = $log_{10}{a} + log_{10}{b}$ 在階乘計算上數字成長速度很快就會超過我們能儲存的大小,所以我們要利用前面提到的 $log$ 性質 要計算 $m!$ 的位數,等同於 $floor(1+\sum_{x=1}^{m}{log_{10}{x}})$ 其中, $floor$ 表示取整數 ### 程式碼1 ```cpp= #include<iostream> #include<cmath> using namespace std; int n,m; int main(){ cin>>n; while(n--){ cin>>m; double ans=0; for(int i=1 ; i<=m ; i++){ ans+=log10(i); } cout<<(int)ans+1<<'\n'; } return 0; } ``` ### 時間複雜度分析 每筆測資都需要花 $O(m)$ 的時間計算 總時間複雜度 $O(nm)$ ### 程式碼2 我們可以將所有答案都先算起來,這樣每次詢問就可以直接回答 ```cpp= #include<iostream> #include<cmath> using namespace std; const int N = 10000005; int n,m; int ans[N]; double tmp; int main(){ for(int i=1 ; i<N ; i++){ tmp+=log10(i); ans[i]=(int)tmp+1; } cin>>n; while(n--){ cin>>m; cout<<ans[m]<<'\n'; } return 0; } ``` ### 時間複雜度分析 預先處理答案的時間複雜度為 $O(N)$ 每筆詢問的時間複雜度為 $O(1)$ 總時間複雜度為 $O(N + n)$ # UVa 10061 ## 題目 http://domen111.github.io/UVa-Easy-Viewer/?10061 給兩個數 $n$, $m$ ,求 1. $n!$ 在 $m$ 進位制下尾數有多少個 $0$ 2. $n!$ 在 $m$ 進位制下是幾位數 ## 解法 By Koios1143 ### 想法 1. 幾位數 問幾位數的問題比較簡單,如同前一題,只是進位制不同 要計算 在 $m$ 進位制下是幾位數,等同於計算 $log_{m}{n}+1$ 取整數 由於內建的函式庫並沒有直接計算 $log$ 底數為任意數的方式,這裡要使用換底公式 $$log_{a}{b} = \frac{log_{n}{b}}{log_{n}{a}} $$ 根據 $logab = loga + logb$ 的性質,可以得到 $$\sum_{i=1}^{n}{log_{m}{i}} $$ 兩個性質合起來 $$\sum_{i=1}^{n}{\frac{log_{10}{i}}{log_{10}{m}}} $$ --- 2. 尾數有多少個 先從 10 進位開始想,尾數要是 0 則它必定會是 10 的整數倍 > 例如: 10, 100, 1000, 1020 ... **這個數一定要有 10 的因數** ,也就是他必須要至少同時有一個 2 和 5 的質因數 換到其他進位制仍然相同 所以說,我們只需要紀錄 $n!, m$ 的質因數各有多少個 如果說 $m$ 的質因數有 $P=\{p_1, p_2, p_3\}$ 那麼 $n!$ 至少要包含整數倍的 $P$ ,才能是 $m$ 的整數倍 所以接下來針對 $m$ 的每個質因數 $i$ ,計算 $i$ 在 $n!$ 中的個數與在 $m$ 中的個數之比值,其中最小值就會是答案 > 例如: $n=7,\ m=16$ 整理一下 $n!$ 的質因數個數 > > | tbl | 2 | 3 | 4 | 5 | 6 | 7 | > | ---- | --- | --- | --- | --- | --- | --- | > | 個數 | 4 | 2 | 0 | 1 | 0 | 1 | > > 整理一下 $m$ 的質因數個數 > > | mPrime | 2 | 3 | 4 | 5 | 6 | 7 | > | ---- | --- | --- | --- | --- | --- | --- | > | 個數 | 4 | 0 | 0 | 0 | 0 | 0 | > > 所以說,必須至少含有 4 個 2 ,尾數才能有一個 0 > 這裡可以發現答案是 $tbl[2]/mPrime[2] = 1$ --- 3. 質因數個數 至於質因數個數的部分,我們可以先預先建立一個表格,紀錄每個數字內的最小質數 > 例如: 10 -> 2 ; 7 -> 7 ; 9 -> 3 接下來,我們每次除以最小質因數,經過的就會是他的質因數了 例如: 36 --(/2)--> 18 --(/2)--> 9 --(/3)--> 3 --(/3)--> 1 求得 $36 = 2^2\times3^2$ 4. 數字內最小質因數 他就會像是這樣,將每個是自己倍數的數值紀錄成自己 例如: 偶數都會被記錄成 2 , 質數都會記錄成自己 ![](https://i.imgur.com/0ghjKd8.gif) ### 程式碼 ```cpp= #include<iostream> #include<iomanip> #include<cmath> using namespace std; const int MaxN = 1048577; int n,m,prime[MaxN],tbl[MaxN],mPrime[MaxN]; void init(){ for(int i=1 ; i<MaxN ; i++){ tbl[i]=0; mPrime[i]=0; } int tmp=m; while(tmp>1){ mPrime[prime[tmp]]++; tmp/=prime[tmp]; } } void update_tbl(int x){ while(x>1){ tbl[prime[x]]++; x/=prime[x]; } } void update_prime(){ prime[1]=1; for(int i=2 ; i<=MaxN ; i++){ if(prime[i]) continue; for(int j=i ; j<=MaxN ; j+=i){ if(!prime[j]) prime[j]=i; } } } int main(){ update_prime(); while(cin>>n>>m){ init(); for(int i=2 ; i<=n ; i++){ update_tbl(i); } int ans1=2147483647; for(int i=2 ; i<=m ; i++){ if(mPrime[i]){ ans1=min(ans1, tbl[i]/mPrime[i]); } } double ans2=0; for(int i=1 ; i<=n ; i++){ ans2+=log(i)/log(m); } ans2=(int)(ans2)+1; cout<<fixed<<setprecision(0)<<ans1<<' '<<ans2<<"\n"; } return 0; } ``` ### 時間複雜度計算 預處理質數時間複雜度約為 $O(NlogN)$ 每次初始化的時間複雜度約為 $O(N+logm)$ 計算尾數的時間複雜度約為 $O(m)$ 計算位數的時間複雜度約為 $O(n)$ 總時間複雜度約為 $O(NlogN+(測資數量)\times(N+logm+m+n))$ 大約為 $O(NlogN+(測資數量)\times(N))$ ## 解法 By sa072686 這題數字小,就來提供比較單純的解法。 找質因數的部份因為數字不大,其實可以亂做; 從 $2$ 開始把每個整除的數字全部除到不整除,就可以了。 這可以保證找到任何整除的數字 $i$ 時,所有比它小的數字都已不整除, 那麼 $i$ 一定是質數,否則必存在比它小的數字整除,矛盾。 $n!$ 由於是 $1$ 到 $n$ 的連乘,假設今天要找質因數 $3$ 出現幾次, 由於每 $3^1$ 個數字就有一個擁有 $3^1$ 所以除以 $3^1$ 即可; 這 $n/3$ 個 $3$ 的倍數數字中,每 $3$ 個就有一個擁有 $3^2$, 或者說每 $3^2$ 個整數就有一個擁 $3^2$ 也可以,一樣意思, 這樣可以很快找到 $n!$ 對任意質因數 $i$ 總共出現幾次。 ### 程式碼 ```cpp= #include<iostream> #include <iomanip> #include<cmath> using namespace std; const int N = 1048576; int n,m, i, j, t, times, ans1; int calc(int n, int i) { int ans; for (ans=0; n; ans+=n/=i); return ans; } int main(){ while(cin>>n>>m){ for (i=2, t=m, ans1=2147483647; i*i<=t; i++) { if (t % i == 0) { for (times=0; t%i==0; times++, t/=i); j = calc(n, i) / times; if (j < ans1) { ans1 = j; } } } if (t != 1) { j = calc(n, t); if (j < ans1) { ans1 = j; } } // 計算位數 double ans2=0; for(int i=1 ; i<=n ; i++){ ans2+=log10(i); } ans2 /= log10(m); cout<<ans1<<' '<<(int)(ans2+1e-10)+1<<"\n"; } return 0; } ``` ### 時間複雜度計算 每組需要質因數最糟 $O(\sqrt{b})$ 質因數最多 $log\ b$ 個,每個最糟要 $log\ a$ 的計算量 計算位數時 $O(a)$ 統合一下大約每組測資是 $O(a+\sqrt{b})$ # UVa 10219 ## 題目 http://domen111.github.io/UVa-Easy-Viewer/?10219 求 $C_{m}^{n}$ 的數值是幾位數 ## 解法 By Koios1143 ### 想法 我們要求的是 $log_{10}{(C_{m}^{n})}+1=log_{10}{(\frac{n!}{(n-m)!m!})}+1$ 取整數後的值 已知 - $log{\frac{a}{b}}$ = $loga-logb$ - $logab = loga + logb$ 則$$ log_{10}{(\frac{n!}{((n-m)!m!)})} \\ = log_{10}{n!}-log_{10}{((n-m)!m!)} \\ = \sum_{i=1}^{n}(log_{10}{i}) - log_{10}{(n-m)!}-log_{10}{m!} \\ = \sum_{i=1}^{n}log_{10}{i}-\sum_{i=1}^{n-m}log_{10}{i}-\sum_{i=1}^{m}log_{10}{i} \\ = \sum_{i=n-m+1}^{n}log_{10}{i}-\sum_{i=1}^{m}log_{10}{i} $$ 又因為 $n-(n-m+1) = m-1$ ,也就是說可以保證兩個 Sigma 跑的次數是相同的 所以在實作上只需要一個迴圈即可 另外,計算 $C_{m}^{n}$ 時,當我們的 $n\ /\ 2>m$ 時,可以轉換成 $C_{n-m}^{n}$ ,讓數值變得更小一點 ### 程式碼 ```cpp= #include<iostream> #include<cmath> using namespace std; int n,m; int main(){ while(cin>>n>>m){ double ans=0; if(n/2 <= m){ // 需要轉換 m=n-m; } // 從1開始跑到m int i=1; while(i<=m) ans += (log10(n--) - log10(i++)); cout<<(int)ans+1<<'\n'; } return 0; } ``` ### 時間複雜度分析 每次計算時間約為 $O(1)$,而需要計算 $m$ 次,每筆測資時間複雜度為 $O(m)$ # UVa 10387 ## 題目 http://domen111.github.io/UVa-Easy-Viewer/?10387 有一張長寬分別為 $a, b$ 的桌子,在他的正中央有一顆球,這顆球會以某個速度 $v$ ,以角度 $A$ 在桌子碰撞,分別在桌子的直向與橫向處撞擊 $m, n$ 次後回到初始位置 求這顆球的初速 $v$ 以及初始角度 $A$ 並且假定球再碰撞過程中並無能量損耗,一切都是完美狀況 ## 解法 By Koios1143 ### 想法 可以先畫一張圖看看,會發現到球在碰撞的過程當中與橫向之間的角度,也就是初始角度 $A$ 都不會改變 並且由於無論如何球都必須要回到原點,每次碰撞過後為了回到原點,必須移動朝向方向(橫向或直向)移動一個長或寬的距離 所以我們可以透過 $a \times m$ 和 $b \times n$ 求到球在桌面上的橫向與直向的移動總距離 透過畢氏定理可以求得球的移動距離,再除以時間就會是速度 $$v = \frac{\sqrt{(am)^2+(bn)^2}}{s} $$ 角度的部分可以透過 $atan$ 求得 由於 C++ 經過 $atan$ 求到的是弧度,而題目要的是角度,所以要再另外計算一個弧度是多少角度 $angle=asin(1.0)/90$ $$A = \frac{atan(\frac{bn}{am})}{asin(1.0)/90} $$ ### 程式碼 ```cpp= #include<iostream> #include<iomanip> #include<cmath> using namespace std; const double radian = asin(1.0)/90; int a,b,s,m,n; int main(){ while(cin>>a>>b>>s>>m>>n && (a||b||s||m||n)){ double h_len = a*m; double v_len = b*n; double angle = atan(v_len/h_len)/radian; double v = sqrt(pow(h_len,2)+pow(v_len,2))/s; cout<<fixed<<setprecision(2)<<angle<<" "<<v<<"\n"; } return 0; } ``` ### 時間複雜度分析 每次輸入的計算時間可估為 $O(1)$ ###### tags: `SCIST 演算法 題解`