# KSPGC 前測題解
###### tags: `題解`
## pA 歡迎加入程式設計社
源自去年前測
### 常見錯誤:忘記有驚嘆號
==
:::spoiler 錯誤code (python)
```python
print("Hello KSPGC")
```
:::
### 正確做法
其實不管你有沒有讀取輸入,你都會過
:::spoiler 官解 (C\++)
```cpp
#include <iostream>
using namespace std;
int main(){
cout << "Hello KSPGC!";
}
```
:::
:::spoiler 官解 (python)
```python
print("Hello KSPGC!")
```
:::
## pB 買月餅
Idea by niter
### 常見錯誤:用"/"運算符來計算除法(python)
python有"/"和"//"兩種除法運算符,
第一種是用來計算浮點數的,不論是否整除,它總是輸出浮點數
第二種是用來計算整數的,它會無視餘數,總是輸出整數
如果使用"/",最後輸出的答案就會含有小數點(必吃WA)
### 常見錯誤:以為買盒裝的總是比買單個的便宜
:::spoiler 錯誤code (C\++)
```cpp
#include <iostream>
using namespace std;
int main(){
int n, m, a, b;
cin >> n;
for(int i=0; i<n; i++){
cin >> m >> a >> b;
cout << (m/6)*a+(m%6)*b << endl;
}
return 0;
}
```
:::
可以卡掉這個解法的測資:
m = 6, a = 100, b = 1
這段錯誤的程式碼會輸出100,但答案是6
### 正確做法
我們要先判斷買越多盒裝的越好,還是全買單個的比較好
:::spoiler 官解 (C\++)
```cpp
#include <iostream>
using namespace std;
int main(){
int n;
cin >> n;
for(int i = 0; i < n; i++){
int m, a ,b;
cin >> m >> a >> b;
int ans = (m/6)*a + m%6*b;
if(a > 6*b) ans = m*b;
cout << ans << endl;
}
return 0;
}
```
:::
判斷a是否大於6b,就相當於判斷:買盒裝跟單個,哪一種平均一個月餅比較貴
如果你要寫a/6>b,就要用浮點數計算,
因為C\++的整數除法會捨去小數點後的所有數字,因此在C\++中,7/3和2相等
另一個可行的寫法:
:::spoiler code (C\++)
```cpp
#include <iostream>
using namespace std;
int main(){
int n;
cin >> n;
for(int i = 0; i < n; i++){
int m, a ,b;
cin >> m >> a >> b;
int ans = (m/6)*a + m%6*b;
if(((float)(a))/6 > b) ans = m*b; // 這一行的條件式
cout << ans << endl;
}
return 0;
}
```
:::
## pC 二次方程式
語法經典題,因式分解用得到
因為judge的部分我沒用好,所以我rejudge了
部份的人TLE只剩55分
### 常見錯誤:用int來讀取輸入
你不能用int來讀取浮點數輸入,會出現意想不到的結果
你應該用double存取浮點數
### 關於用python然後TLE這件事
我發現如果使用eval()函數把浮點數字串轉成整數,會消耗大量的時間,然後就TLE了
你應該使用float()函數
::: spoiler AC code (python)
```python
while True:
try:
a = input()
except:
break
print(int((int(float(a)))**0.5))
```
:::
如果再搭配輸入輸出優化,效率可以提升到超過10倍
### 正確做法
直接讀取double輸入,因為它的精度很高
然後再利用一個特性:把浮點數指派給整數變數時,小數部分會被自動捨去(剛好如題目要求)
註:sqrt函數的功能是開根號
:::spoiler 官解 (C\++)
```cpp
#include <bits/stdc++.h>
using namespace std;
int main(){
double n;
while(cin >> n){
int ans = sqrt(n); // 自動捨去小數部分
cout << ans << "\n";
}
}
```
:::
## pD 找X
Idea by 鄭勝宇(sean9575)
他原本給我的題目其實更難,下次再出好了(可以先猜猜是什麼)
### 常見錯誤:雙迴圈(每次詢問時都搜尋一次整個陣列)
如果$n,q$給到最大($10^6$),且每次詢問的位置都是最後一項呢?
那麼就要跑$10^6\times10^6=10^{12}$次迴圈!!!
如果電腦1秒可以跑$10^8$次加法(通常寫題目的時候都是用這個值來算會不會過),
那麼這個程式就要跑$\approx\frac{10^{12}}{10^8}=10^4$秒,一定會TLE
### 正確做法
注意到$a_i$的值域不大
所以我們可以建一個$10^6$項的陣列left,left[x]代表數字x在陣列a中由左到右第1個出現的位置的索引值
一開始,我們設定整個陣列left的值都是-1,代表還沒有出現任何數字
然後,我們由左到右搜索整個陣列a,
對於每個a[i],我們檢查left[a[i]]是否為-1,
如果是-1,表示在目前的索引值i之前還沒有出現任何跟a[i]相同的數,
我們就可以把left[a[i]]的值設為i,表示a[i]這個數在索引值i的地方第一次出現
如果不是-1,表示a[i]這個數已經在比索引值i更前面的地方出現過了,
因此我們保留這個值,不作任何更動
在詢問時,對於輸入的數字x,我們可以直接輸出left[x],因為它就是答案
因為題目保證不會沒有答案,所以left[x]不可能是-1
:::spoiler 官解 (C\++)
```cpp
#include <bits/stdc++.h>
using namespace std;
int main(){
ios::sync_with_stdio(false); cin.tie(0);
int n, q;
cin >> n >> q;
int left[1000010], a[1000010];
for(int i = 0; i <= 1000000; i++){
left[i] = -1;
}
for(int i = 0; i < n; i++){
cin >> a[i];
}
for(int i = 0; i < n; i++){
if(left[a[i]] == -1){
left[a[i]] = i;
}
}
int x;
for(int i = 0; i < q; i++){
cin >> x;
cout << left[x] << "\n";
}
return 0;
}
```
:::
<br>其實你可以一邊輸入一邊對left陣列做處理,少開一個a陣列
:::spoiler 優化後的code (C\++)
```cpp
#include <bits/stdc++.h>
using namespace std;
int main(){
ios::sync_with_stdio(false); cin.tie(0);
int n, q;
cin >> n >> q;
int left[1000010], in;
for(int i = 0; i <= 1000000; i++){
left[i] = -1;
}
for(int i = 0; i < n; i++){
cin >> in; // in 就是 a[i]
if(left[in] == -1){
left[in] = i;
}
}
int x;
for(int i = 0; i < q; i++){
cin >> x;
cout << left[x] << "\n";
}
return 0;
}
```
:::
## pE 陣列搜尋
Idea by niter
雖然我很想把lower bound($O(n\log n+n\log n)$)卡掉,但其實不太可能(官解$O(n\log n)$)
### 常見錯誤:直接模擬(雙迴圈)
遍歷整個陣列,尋找最大的數(最多$n$圈),然後標記它
以上動作重複$n$次
這樣最差情況下需要$n^2$次計算,總共跑了$(10^5)^2=10^{10}$次,
也就是$\approx100$秒,這樣就會TLE
(有沒有發現這個做法很像Selection sort?)
(知道要排序之後,是不是可以改用比Selection sort更好的排序法?)
### 正確做法
注意到$a_i$的初始值必大於0,所以已經變成0的數就不會再改變了,
因此這些數字變成0的順序就只跟它原本的大小有關
最大的一定第一個變成0,最小的一定最後變成0,
我們只需要`排序`就能得知這些數的大小順序(變成0的順序)
(C\++的內建排序是$O(n\log n$))
再來就是實作的問題了,要注意一樣的數會同時變成0
我們用pair資料結構,在排序的同時紀錄數字原本的位置($i$),
把數字的值放在pair的第一項,他原本的位置放在第二項
這樣排序時就會先排數值,再排位置
我們用for迴圈遍歷排序後的陣列,並以run變數來記錄這個數字會在第幾次操作後變成0,
因為相同的數值會被排在一起(可以把排序後的陣列print出來觀察看看),
所以只要跟前一項作比較,就能知道是不是相同的數
如果是,run變數就不要增加,代表兩個數會同時變成0
否則run變數加1,代表目前的數會在前一個數變成0之後的下一次操作才變成0
:::spoiler 官解 (C\++)
```cpp
#include <bits/stdc++.h>
using namespace std;
int main(){
ios::sync_with_stdio(false); cin.tie(0);
int n;
cin >> n;
int Ans[n];
pair<int,int> a[n];
for(int i = 0; i < n; i++){
cin >> a[i].first;
a[i].second = i;
}
sort(a, a+n, greater<pair<int,int>>());
int run = 1;
Ans[a[0].second] = run;
for(int i = 1; i < n; i++){
if(a[i].first != a[i-1].first) // 相同的數會同時變成0
run++; // run變數+1表示是不同的數
Ans[a[i].second] = run;
}
for(int i = 0; i < n; i++){
cout << Ans[i] << " ";
}
cout << "\n";
}
```
:::
:::spoiler lower bound解 by 2006lennon (C\++)
cv陣列表示原本的a陣列
r陣列是a陣列排序後且刪除重複項所形成的陣列
```cpp
#include <bits/stdc++.h>
using namespace std;
int main(){
int n;
cin>>n;
vector <int> cv(n),v(n),r;
for(int i=0;i<n;i++) cin>>v[i];
cv=v;
sort(v.begin(),v.end(),greater<int>());
r.push_back(v[0]);
for(int i=1;i<n;i++){
if(v[i]!=*(r.end()-1)) r.push_back(v[i]);
}
for(int i=0;i<n;i++){
int it=lower_bound(r.begin(),r.end(),cv[i],greater<int>())-r.begin();
cout<<it+1<<" ";
}
return 0;
}
```
:::
:::spoiler 2nlogn解 by cmj0415, niter (python)
改自 `wayne.tw` 的code
```python
import bisect
from sys import stdin, stdout
x = int(stdin.readline())
y = list(map(int,stdin.readline().split()))
a = set(y)
z = sorted(set(y))
#print(z)
for i in y:
stdout.write(str(len(a)-bisect.bisect(z,i)+1))
stdout.write(" ")
```
:::
## pF 1x2骨牌
由陳連恩(2006lennon)提供(後來我發現網路上也有這題)
### 常見錯誤:沒有$mod10^9+7$
mod就是取餘數的意思
第11行的%符號很重要,沒有mod你就只剩10分 :(
:::spoiler 100分解法 by khhsu (C\++)
```cpp=
#include<iostream>
using namespace std;
int main(){
int n,a=1,b=2,c;
cin>>n;
if(n==1||n==2){cout<<n;}
else{
for(int i=3;i<=n;i++){
c=(a+b)%1000000007;
a=b;
b=c;
}
cout<<c;
}
}
```
:::
### 正確做法
假設圖形的寬(上下)是2、長(左右)是n
我們可以從`遞迴`的方向思考
你會發現所有的排列都是由兩種最基本的排法組成的
第一種是擺一個直的(2x1)
第二種是擺兩個橫的(並排2x2)
假設$2\times n$的格子有$f(n)$不同的排法
( $f(1)=1,f(2)=2$ )
則$f(x)+f(x+1)=f(x+2)$ ($x\geq 1$)
上面的兩種基本排法中,
第一種代表從$f(x+1)$的狀態轉移到$f(x+2)$的狀態
第二種代表從$f(x)$的狀態轉移到$f(x+2)$的狀態
100分解法請見上方的常見錯誤
### 150分做法
其實從上面的遞迴式可以觀察到:$f(x)$其實就是費氏數列的第$x+1$項
而上面的100分解法,複雜度是$O(n)$
在$1 \leq n \leq 10^6$的情況下不會TLE
但$n$到了$10^{15}$時,這樣的複雜度是不夠快的
所以我們要使用矩陣快速冪($O(\log n)$),以更快的方法求費氏數列
矩陣快速冪暫時不需要學,有興趣再去查就好了,我們還不確定何時會教到