--- title: 'NHDK 程式暑期培訓營' disqus: hackmd --- NHDK 程式暑期培訓營 === [TOC] # 模板 ```csharp= #include <bits/stdc++.h> using namespace std; #define ll long long #define pll pair<ll,ll> #define F first #define S second #define pb push_back #define mp make_pair #define sz(s) (int)s.size() #define max(p,q)((p)>(q)?(p):(q)) #define fast ios::sync_with_stdio(0), cin.tie(0) #define N 10000000 #define MOD 1000000007 struct P{ int x, y, t; P(){} P(int x, int y):x(x), y(y){} P(int x, int y, int t):x(x), y(y), t(t){} int operator-(const P r)const{return abs(x-r.x)+abs(y-r.y);}//曼哈頓距離 }; int main() { fast;cout.tie(0); return 0; } ``` ## 遞迴 ### 遞迴 [zero judge f640: 函數運算式求值](https://zerojudge.tw/ShowProblem?problemid=f640) :::info 解題關鍵: 用遞迴解!! ::: ```csharp= #include <iostream> #include <string> using namespace std; int eval(){ string s; cin>>s; if (s[0]=='f') { int x=eval(); //要宣告在區域,不然宣告在全域會賦值 return 2*x-3; } else if(s[0]=='g') { int x=eval(); int y=eval(); return 2*x+y-7; } else if(s[0]=='h') { int x=eval(); int y=eval(); int z=eval(); return 3*x-2*y+z; } else return stoi(s); } int main() { ios::sync_with_stdio(false); cin.tie(0); cout<<eval()<<endl; return 0; } ``` ## 競賽入門 * 時間複雜度可看成程式的規模 ### 前綴和 [TPR #23 H2 區間求和問題【Two-dimensional Version】(2*)](https://codeforces.com/group/H0qY3QmnOW/contest/383582/problem/H2) :::info 解題關鍵: 題目超長,重點只有幾句話: * 求$(x_1,y_1)$到$(x_2,y_2)$所圍成的矩形內(包括邊上)的所有數字和。 * 其值=(0,0)∼$(x_2,y_2)$ 的總和扣掉 (0,0)∼$(x_1−1,y_2)$ 的總和再扣掉 (0,0)∼$(x_2,y_1−1)$ 的總和,最後再加上 $(x_1−1,y_1−1)$ (多扣掉的要加回來)。 * 關於上式的推導,可以用矩形面積來思考。 ::: ```csharp= #include <bits/stdc++.h> using namespace std; #define ll long long #define maxi 2600 ll mp[maxi][maxi],pre[maxi][maxi]; ll q,n,m,x_1,y_1,x_2,y_2; int main() { ios::sync_with_stdio(false); cin.tie(0); cin>>n>>m; for(int i=1;i<=n;i++){ for(int j=1;j<=m;j++) cin>>mp[i][j]; } for(int i=1;i<=n;i++){ for(int j=1;j<=m;j++) pre[i][j]=pre[i-1][j]+pre[i][j-1]-pre[i-1][j-1]+mp[i][j]; } cin>>q; while(q--){ cin>>x_1>>y_1>>x_2>>y_2; cout<<pre[x_2+1][y_2+1]+pre[x_1][y_1]-pre[x_1][y_2+1]-pre[x_2+1][y_1]<<endl; } return 0; } ``` ### 前綴和ZJ b844: 一堆按鈕(3*) :::success 可能卡關的點: 如果我們每輸入一個數字K就要reset一次array,輸入完的複雜度會是O($n^2$)。接著再查詢Q次,總體複雜度會是O($n^2$+Q),然後就會吃TLE拉。 ::: :::info 解題關鍵: 如果我們把輸入的數值K都先由小至大sort,那我們就可以推算數字a到底是0還是1(只要知道按a~n的按鈕的奇偶性即可) ::: ```csharp= #include <bits/stdc++.h> using namespace std; #define ll long long #define N 500005 ll n,q,i,u,k; vector<ll> v; ll q_v[N]; //存按過按鈕的點 int main() { ios::sync_with_stdio(false); cin.tie(0); while(cin>>n>>q){ v.clear(); ///記得要clear()他 for(i=1;i<=n;i++){ cin>>k; v.push_back(k); } sort(v.begin(),v.end()); //小至大sort for(i=0;i<q;i++){ cin>>q_v[i]; u=upper_bound(v.begin(),v.end(),q_v[i])-v.begin(); //記得-v.beign() //因為以後有包含=,所以要用沒有=的func. if(u%2==0) cout<<0<<'\n'; else cout<<1<<'\n'; } } return 0; } ``` ## 資料結構I ### vector 2022年1月APCS第二題 [h082: 2. 贏家預測-1(4*)](https://zerojudge.tw/ShowProblem?problemid=h082) :::info 解題關鍵: 開三個vector,存贏家、輸家、第i輪的參賽人員,並開一個array去紀錄每個人的失敗次數,超出m則被淘汰。 有些小細節要注意,請看程式碼的註解 ::: ```csharp= #include <bits/stdc++.h> using namespace std; #define ll long long #define N 1050 ll n,m,ind; ll x,y,a,b,c,d; //紀錄對戰者的編號、戰力、應戰力 ll s[N],t[N],lose[N]; //戰力\應戰力\編號 vector<ll> vw,vl,v; //贏家編號\輸家編號\每個人輸了幾次\每輪玩家編號 int main() { ios::sync_with_stdio(false); cin.tie(0); cin>>n>>m; for(int i=1;i<=n;i++) cin>>s[i]; for(int i=1;i<=n;i++) cin>>t[i]; for(int i=0;i<n;i++) { cin>>ind; v.push_back(ind); }//第一輪玩家編號 while(v.size()>1){//每輪遊玩人數>1 for(int i=0;i<v.size()-1;i+=2){//兩個一組pk x=v[i];y=v[i+1]; a=s[x];b=t[x];c=s[y];d=t[y]; //不要寫成s[i],因為s[i]不一定是指第i個人的戰力 if(a*b>=c*d){ s[x]=a+(c*d)/(2*b); t[x]=b+(c*d)/(2*a); s[y]=c+c/2; t[y]=d+d/2; lose[y]++; vw.push_back(x); if(lose[y]<m)vl.push_back(y); //失敗次數不超出m,超出m則淘汰(不會進入敗部復活) } else{ s[y]=c+(a*b)/(2*d); t[y]=d+(a*b)/(2*c); s[x]=a+a/2; t[x]=b+b/2; vw.push_back(y); lose[x]++; if(lose[x]<m)vl.push_back(x); } } if(v.size()%2==1) vw.push_back(v.back()); //落單直接晉級 v=vw; for(ll j:vl) v.push_back(j); vw.clear(); vl.clear(); } cout<<v[0]<<'\n'; return 0; } ``` ### priority_queue [TPR #7 PC 中位數 (3*)](https://www.google.com/url?q=https://codeforces.com/group/H0qY3QmnOW/contest/316436/problem/C&sa=D&source=editors&ust=1659524344077458&usg=AOvVaw3uemvuuLyfJFhcD4ADvAv_) :::info 解題關鍵: 算中位數需要把序列由小至大排列,使序列具有某種單調性,同時元素可能會重複=>所以選擇priority_queue實作啊! 我們可以把元素分成兩半,左邊放小的,右邊放大的。 讓max{左邊數字}<=({右邊數字})、左邊的數字比右邊的數字多,因為這樣可以直接開top算出中位數。 ::: ```csharp= #include <bits/stdc++.h> using namespace std; #define ll long long priority_queue<int,vector<int>,less<int>> pq_l; priority_queue<int,vector<int>,greater<int>> pq_r; //注意優先佇列沒有pq[i]這種東西 int main() { ios::sync_with_stdio(false); cin.tie(0); int a; while(cin>>a){ if(a==0) break; if(pq_l.empty() || a<=pq_l.top()) pq_l.push(a); else pq_r.push(a); while(pq_l.size()>pq_r.size()){ pq_r.push(pq_l.top()); pq_l.pop(); } while(pq_r.size()>pq_l.size()){ //確保左邊的數字比較多 pq_l.push(pq_r.top()); pq_r.pop(); } if(pq_r.size()==pq_l.size()) cout<<(pq_r.top()+pq_l.top())/2<<'\n'; else cout<<pq_l.top()<<'\n'; } return 0; ``` ### set [d129: 00136 - Ugly Numbers](https://zerojudge.tw/ShowProblem?problemid=d129) :::success 可能卡關的點--要用什麼資料結構呢? 原題等價於求$2^a 3^b 5^c$($a,b,c \in Z$)第1500大的數字。 最直觀的方法就是枚舉原本資料結構所有元素,再加入這些元素*2、*3、*5的元素,然後刪掉重複的元素,並按照大小排列。複雜度直接炸開(指數成長)。想起set具有"刪掉重複的元素"、"按照大小排列"的特性,所以當然用set阿!! 同時如果s.size()>=1499也沒差。 ::: :::info 解題關鍵: 注意 * set沒有set[i]這種東西,只能用指標*it代表它的值 * s.begin()、s.end()是一種疊代器interator * auto可以自動判別型體,就不用寫interator了 ::: ```csharp= #include <bits/stdc++.h> using namespace std; #define ll long long #define N 1e9 set<ll> s; int main() { ios::sync_with_stdio(false); cin.tie(0); s.insert(1); ll cnt=0; for(auto it=s.begin();it!=s.end();it++){ cnt++; if(cnt==1500) { cout<<"The 1500'th ugly number is "<<*it<<"."; break; } s.insert(*it *2);//*interator=ptr s.insert(*it *3); s.insert(*it *5); } return 0; } ``` ### set h083: 3. 數位占卜 2022年1月APCS第三題 [h083: 3. 數位占卜](https://zerojudge.tw/ShowProblem?problemid=h083) :::info 解題關鍵: 1. 設字串P可與字串B形成「聖杯」,則P必可切割成sl、B、sr。 稍微比對一下,可推知sl=sr。 2. s.substr(l.r)範圍是[l,r) 3. binary_search回傳bool值(值存在回傳1,反之回傳0) 4. O(n*s.size()*log s.size())=O($10^7$),不會TLE。 5. 因為對於任意兩個相異的字串s[i]、s[j],s[i]!=s[j]。 所以sl.size()=sr.size()>0、B.size()>0 ⇒sl.size()+sr.size()=2*len<P.size。 6. 字串的sort是按照a~z的順序下去排,先比對第一個元素,一路比對到第n個元素。 ::: ```csharp= #include <bits/stdc++.h> using namespace std; #define ll long long #define N 1e9 ll m; string s,st,sl,sr,B; vector<string> str; int main() { ios::sync_with_stdio(false); cin.tie(0); ll ans=0; cin>>m; for(int i=0;i<m;i++){ cin>>st; str.push_back(st); } sort(str.begin(),str.end()); for(int i=0;i<m;i++){ s=str[i]; for(int len=1;len*2<s.size();len++){ sl=s.substr(0,len); sr=s.substr(s.size()-len); if(sl!=sr) continue; B=s.substr(len,s.size()-(2*len)); if(binary_search(str.begin(),str.end(),B)) ans++; } ``` ### bitset [C - Jumping Takahashi(2*)](https://atcoder.jp/contests/abc240/tasks/abc240_c) :::info 解題關鍵 * 怎麼判斷在第i步驟,可不可以跳到第j個數字呢? 因為第i步一定是由前一步跳a或b步而來,所以只要去遞迴他就行了。 可以開一個大小為(n+1)的資料結構,來知道在第i步驟,可不可以跳到第j個數字,而因為記錄的數字不超過20000($a*100+b*100$)個數字,所以每個子資料結構的大小可以開20000,紀錄每個子資料結構的第j個值是否可以跳到。 阿因為範圍固定,然後又只要知道可不可以達成,所以開一個具有bool性質的資料結構--bitset * bitset需要先宣告大小,可初始化成數字、串列、字元等 * 位元運算 跳a或b步,可以想像成位元往左跳了a或b格,所以用"<<"阿。 ::: ```csharp= #include<bits/stdc++.h> using namespace std; #define ll long long bitset<20000> vis[101]; //宣告101個陣列 長度為20000(maxi:100*100+100*100) //,並初始化為0 //判斷第i步驟是否會跳到j(1<=j<=20000) int main(){ vis[0][0]=1; ll n,x,a,b; cin>>n>>x; for(ll i=1;i<=n;i++){ cin>>a>>b; vis[i]=vis[i-1]<<a|vis[i-1]<<b; //<< 和 >> 將一個變數的所有位元向左 / 向右移動。最左位元 / 最右位元消失,最右位元 / 最左位元補 0 //一定要跳a或b步 // "|"位元的or,因為第i步可以由第(i-1)步跳a或b步而來,所以取or } if(vis[n][x]) cout<<"Yes"<<'\n'; //像是stack,越右邊(早)索引值越大 else cout<<"No"<<'\n'; return 0; } ``` ### bitset [zj f630: 5. 共同朋友(3*)](https://zerojudge.tw/ShowProblem?problemid=f630) :::info 解題關鍵: * 我們要去得知兩人是否有共同好友,只需要開兩個bitset去紀錄兩人的共同好友,兩個bitset取&(取兩者好友的交集)(注意道自己跟自己不能為好友,所以某A跟某B共同好友不可能擁有A或B)(因此,共同好友必定與本身互斥,所以取完&不用檢驗c=a或b),然後再看取完&的bitset是否為全0的bit就行啦 * 不能用count!!!(因為時間複雜度是$O(n^3)$,會炸開), 要改成用.any()(時間複雜度$O(1)$) ::: ```csharp= #include <bits/stdc++.h> using namespace std; #define ll long long #define MAXI 2600 bitset<MAXI> k[2600]; ll n,d,f,cnt; int main() { ios::sync_with_stdio(false); cin.tie(0); cin>>n; for(ll i=1;i<=n;i++){ cin>>d; for(ll j=1;j<=d;j++){ cin>>f; k[i][f]=true; } } cnt=0; for(ll a=1;a<n;a++){ for(ll b=a+1;b<=n;b++){ //因為b>a if((k[a]&k[b]).any()!=0)cnt+=1; //兩者好友關係取完and不為全0 bit //any函式:如果有存在一個bit不為0,則回傳1,否則回傳0 } } cout<<cnt; return 0; } ``` ## 枚舉 ### 枚舉所有操作 TNGS IRC Final Exam pE. 倒水問題(2*) :::info 解題關鍵: 看到a、b的執行次數至多為10,直接砸兩個for loop也不會TLE阿~ ::: ```csharp= #include <bits/stdc++.h> using namespace std; #define ll long long #define N 1000000 ll mini,n,k,a,b; ll p1,p2,x; ll i,j; int main() { ios::sync_with_stdio(false); cin.tie(0); mini=N; //所剩差距 cin>>n>>k>>a>>b; for(j=0;j<=10;j++){ for(i=0;i<=10;i++){ x=i*a+j*b; if(n-x>=k && n-x-k<mini){ mini=n-x-k; p1=i; p2=j; } } } cout<<p1<<" "<<p2<<" "<<mini<<'\n'; return 0; } ``` ### 位元枚舉 [CSES Problem Set Apple Division(2*)](https://cses.fi/problemset/task/1623/) :::warning 位元相關知識 1. x & 1 is equivalent to x % 2. 舉例:當x=1011,1=0001,每個位置做and的結果是0001=1,相當於x的奇偶性。 2. <<、>>分別為左移運算子,右移運算子。 3. 1<<x,相當於1向左位移x格 舉例:當x=3。1<<x其值為0001 4. x=x>>1,可簡寫成x>>=1(右移並附值) ::: :::info 因為分推方法數至多為$2^{20}$,所以窮舉他吧。 注意到同一物件分堆情形數有二,所以只要用0、1表示他被分在哪堆即可。 而至多有n件物品,所以窮舉0~$2^n-1$即可。 (全部分在左邊~全部分在右邊) ::: ```csharp= #include <bits/stdc++.h> using namespace std; #define ll long long #define N 1e12 ll apple[25],total,n,gap=N,num,i,p,rt,rl,idx; int main() { ios::sync_with_stdio(false); cin.tie(0); total=0; cin>>n; for(i=0;i<n;i++) { cin>>apple[i]; total+=apple[i]; } p=pow(2,n); for(i=0;i<p;i++){ num=i; idx=0; rt=0; rl=0; while(num!=0){ if((num&1)==0) rt+=apple[idx]; idx++; num/=2; } rl=total-rt; gap=min(gap,abs(rl-rt)); } cout<<gap<<'\n'; return 0; } ``` ### 全排列枚舉 [Zerojudge e446 排列生成(2*)](https://zerojudge.tw/ShowProblem?problemid=e446) :::info 解題關鍵: 你只要知道next_permutation這個內建函式就秒殺了, 值得一提的是這個函式很常搭配do-while loop。 這個loop需要先寫的事項,再寫條件 ::: ```csharp= #include <bits/stdc++.h> using namespace std; #define ll long long #define N 1e9 vector<ll> a; int main() { ios::sync_with_stdio(false); cin.tie(0); ll n; cin>>n; a.clear(); for(ll i=1;i<=n;i++) a.push_back(i); do{ for(ll i=0;i<n;i++) cout<<a[i]<<" "; //要排列的對象 cout<<'\n'; }while(next_permutation(a.begin(),a.end())); //全排列函式//對[first,end)做排列 return 0; } ``` ### 有限度的枚舉 [Ten Point Round #20 飲品調配(2*)](https://codeforces.com/group/H0qY3QmnOW/contest/377732/problem/A) :::info 解題關鍵: 因為c=n-a-b、$a \leq n \leq 5000$,$b \leq n \leq 5000$。 又發現枚舉a、b的複雜度為$O(n^2)$,不會炸開,那就直接枚舉吧! * 有O(1)作法:取a=0=b、c=n。(超級梗) ::: ```csharp= #include <bits/stdc++.h> using namespace std; #define ll long long ll a,b,c,mint=-1,n,t; int main() { ios::sync_with_stdio(false); cin.tie(0); cin>>n; for(a=0;a<=n;a++){ for(b=0;b<=n-a;b++){ c=n-a-b; t=2022+abs(b-c)+c*c+a*b+b*c-abs(b*b-a*a); mint=max(mint,t); } } cout<<mint<<'\n'; return 0; } ``` ### 枚舉答案 [CSES Problem Set Common Divisors(4*)](https://cses.fi/problemset/task/1081/) :::success 可能卡關的點: 1. 枚舉$x_i$、$x_j$,時間複雜度為$O(n^2log(min(x_i, x_j)))$。(炸開) 2. 假設i<j,枚舉$x_i$、$x_j$,時間複雜度為$O(n^2log(min(x_i, x_j)))$。(炸開) 所以上述方法都行不通。 ::: :::info 解題關鍵: 假設a<b,gcd(a,b)=k的充要條件為a,b同時等於k或2k或3k....。 所以我們只要枚舉k,再確認a,b可不可以同時為k或2k或3k.... 。 時間複雜度為$O(\frac{n}{1}+\frac{n}{2}+...+\frac{n}{n})=O(nlogn)$。這樣就不會TLE了!!(注意因為兩個for迴圈是相關的,時間複雜度!=$O(n*nlogn)$) 至於怎麼知道a,b可不可以同時為k或2k或3k....,開一個bool陣列去紀錄數字有沒有出現過就行了!! ::: ```csharp= #include <bits/stdc++.h> using namespace std; #define ll long long #define N 1000005 ll num[N]; ll n,x,g; int main() { ios::sync_with_stdio(false); cin.tie(0); cin>>n; for(ll i=0;i<n;i++){ cin>>x; num[x]++;//紀錄每個數字出現的次數 } g=1; for(ll i=1;i<N;i++){ ll cnt=0; for(int j=i;j<N;j+=i){ cnt+=num[j];//注意數字可能會重複出現 } if(cnt>=2) g=i; } cout<<g<<'\n'; return 0; } ``` ### BFS模板 [Zerojudge d406: 倒水時間](https://zerojudge.tw/ShowProblem?problemid=d406)(3*) ```csharp= #include <bits/stdc++.h> using namespace std; #define ll long long #define pll pair<ll,ll> #define F first #define S second #define pb push_back #define mp make_pair #define sz(a) (int)s.size() #define max(p,q)((p)>(q)?(p):(q)) #define fast ios::sync_with_stdio(0), cin.tie(0) #define N 10000000 #define MOD 1000000007 ll G[110][110],vis[110][110],ans[110][110]; queue<pll> qu; ll dx[4]={0,1,-1,0}; ll dy[4]={1,0,0,-1}; int main() { ll n,m,num,k,sta,cnt=1; fast;cout.tie(0); while(cin>>k>>n>>m){ for(ll i=0;i<n;i++) for(ll j=0;j<m;j++) {ans[i][j]=0;vis[i][j]=0;} for(ll i=0;i<n;i++){ for(ll j=0;j<m;j++){ cin>>G[i][j]; } } for(ll j=0;j<m;j++) if(G[0][j]==1) sta=j; qu.push(mp(0,sta)); vis[0][sta]=1; ans[0][sta]=1; while(!qu.empty()){ pll cur=qu.front();qu.pop(); ll x=cur.first; ll y=cur.second; //cout<<"x="<<x<<"y="<<y<<endl; for(ll i=0;i<4;i++){ if(k==2 && i==2) continue; ll nx=x+dx[i],ny=y+dy[i]; if((-1<nx && nx<n)&&(-1<ny && ny<m) && vis[nx][ny]==0 && G[nx][ny]==1){ //cout<<"nx="<<nx<<"ny="<<ny<<'\n'; //cout<<"YES\n"; vis[nx][ny]=1; ans[nx][ny]=ans[x][y]+1; qu.push(mp(nx,ny)); } } } cout<<"Case "<<cnt<<":\n"; for(ll i=0;i<n;i++){ for(ll j=0;j<m;j++){ cout<<ans[i][j]<<" "; } cout<<"\n"; } cnt+=1; } return 0; } ``` ## 搜尋 ### 二分搜尋 [Codeforces EDU A. Binary Search(2*)](https://codeforces.com/edu/course/2/lesson/6/1/practice/contest/283911/problem/A) :::info 這題只要知道binary_search函式就能秒殺了。 自己手刻一個函式還需要去驗在左還右界,還是開絕招比較容易! ::: ```csharp= #include <bits/stdc++.h> using namespace std; #define ll long long #define N 100005 ll n,k,q,left,right,mid; vector<ll> a; int main() { ios::sync_with_stdio(false); cin.tie(0); cin>>n>>k; for(ll i=0,b;i<n;i++) {cin>>b;a.push_back(b);} sort(a.begin(),a.end()); while(k--){ cin>>q; if(binary_search(a.begin(),a.end(),q)) cout<<"YES"<<'\n'; else cout<<"NO"<<'\n'; } return 0; } ``` ### 雙指針 [CSES Problem Set Sum of Three Values(3*)](https://cses.fi/problemset/task/1641) :::info 破題關鍵: 注意到如果直接寫的話,複雜度為$O(n^3)$。 $\because n<=5000$,帶入進去$O(n^3)=1.25*10^{11}$ $∴$炸開拉。 所以我們可以換個想法,我們可以對其中兩個數開雙指針$(O(n^2))$,剩下一個我們只要開lower_bound$(O(log n))$二分搜他就行了阿~ 有一些點注意: ①同一個數字不能取兩次 ②小心lower_bound不存在的時候會回傳size+1,所以要判斷lower_bound是否存在 ::: ```csharp= #include <bits/stdc++.h> using namespace std; #define ll long long #define N 1e9 ll x,n,A,m,i,j; vector<pair<ll,ll>> a; vector<ll> num; int main() { ios::sync_with_stdio(false); cin.tie(0); bool yes=0; cin>>n>>x; for(ll i=0;i<n;i++){ cin>>A; a.push_back({A,i+1}); } sort(a.begin(),a.end()); for(i=0;i<n;i++) num.push_back(a[i].first); for(i=0;i<n;i++){ for(j=n-1;j>i;j--){ m=lower_bound(num.begin(),num.end(),x-num[i]-num[j])-num.begin(); if(i==m && num[m]==num[m+1] && m+1<n)m+=1; if(m!=i && m!=j && m<n && num[i]+num[m]+num[j]==x){ yes=1; cout<<a[i].second<<" "<<a[m].second<<" "<<a[j].second<<'\n'; break; } } } if(yes==0) cout<<"IMPOSSIBLE\n"; return 0; } ``` ## 分治 0, :::info 解題關鍵: 可以用merge sort來解決他,而merge sort的方法正是分治的精神-- 1. 大問題切割成小問題(利用遞迴) 2. 破解小問題(在小到某一程度也能使用暴力解) 3. 小問題合併成大問題 相信遞迴的力量! ::: ```csharp= #include<bits/stdc++.h> using namespace std; #define ll long long #define N 1000005 ll n; ll a[N]; int main(){ while(cin>>n){ for(ll i=0;i<n;i++){ cin>>a[i]; } sort(a,a+n); for(ll i=0;i<n;i++){ cout<<a[i]<<" "; } cout<<'\n'; } return 0; } ``` ## 數學 **快速幂** 時間複雜度O($log_2$次方) 程式碼(推導請參考wiki) 記憶方法:很像是$n^p=(n^2)^{\frac{p} {2}}$ ```csharp= int fastpow(int n,int p){ int res=1; while(p){ if(p&1) res=res*n%mod; //p等於1嗎//返回n%mod n=n*n%mod; p/=2; //做log_2 p次 } return res; } ``` ### 快速幂 CSES - Exponentiation(2*) ```csharp= #include <bits/stdc++.h> using namespace std; #define ll long long #define mod 1000000007 ll fastpow(ll a,ll b){ int res=1; while(b){ if(b&1) res=res*a%mod; a=a*a%mod; b/=2; } return res; } int main() { ios::sync_with_stdio(false); cin.tie(0); ll a,b,n; cin>>n; while(n--){ cin>>a>>b; if(a==0 & b==0) cout<<1<<'\n'; else if(a==0) cout<<0<<'\n'; else{ cout<<fastpow(a,b)<<'\n'; } } return 0; } ``` **費馬小定理** :::warning 敘述:設a屬於正整數、P屬於質數,則$a^{P-1}≡1(mod P)$ (反之不然) 證明:我懶之後補。 ::: ### 費馬小定理 CSES Problem Set Exponentiation II(2*) :::info 解題關鍵: 令k=b^c mod (P-1),由費馬小定理可以得知 $a^{b^c}≡a^k$($\because a^{b^c}=a^k* a^{P-1} *...* a^{P-1}≡a^k*1*...*1(mod P)$) ::: ```csharp= #include <bits/stdc++.h> using namespace std; #define ll long long #define P 1000000007 #define N 1000000006 ll fastpow(ll a,ll b,ll c){ ll res=1; if(a==0 && b==0) return 1; else if(a==0) return 0; else{ while(b){ if(b&1) res=res*a%c; a=a*a%c; b/=2; } return res; } } int main() { ios::sync_with_stdio(false); cin.tie(0); ll n,x,y,z; cin>>n; while(n--){ cin>>x>>y>>z; cout<<fastpow(x,fastpow(y,z,N),P)<<'\n'; } return 0; } ``` ### 歐拉函數 [ETF - Euler Totient Function(3*)](https://www.spoj.com/problems/ETF/) :::info 解題關鍵 注意到歐拉函數有一個性質-- 若一大於1的正整數a,可質因數分解成$a=p_1^{\alpha_1}*p_2^{\alpha_2}*...*p_n^{\alpha_n}$,則φ(a)=a$\Pi_{k=1}^n(1-\frac{1}{p_k})$。 注意到$p_k|a$,所以先除再乘也沒差~(比較不容易超出range) ::: ```csharp= #include <bits/stdc++.h> using namespace std; #define ll long long #define N 1e9 ll eft(int n){ ll ans=n; for(int i=2;i*i<=n;i++){ if(n%i==0) ans=ans/i*(i-1); while(n%i==0) n/=i; } if(n>1) ans=ans/n*(n-1); //當n為質數時,其因數為本身和1 return ans; } int main() { ios::sync_with_stdio(false); cin.tie(0); ll n,q; cin>>n; while(n--){ cin>>q; cout<<eft(q)<<'\n'; } return 0; } ``` ## 動態規劃(一) ### 線性DP [CSES Problem Set Dice Combinations(2*)](https://cses.fi/problemset/task/1633) :::info 解題關鍵: dp[i]可分別由dp[i-1]、dp[i-2]、dp[i-3]、dp[i-4]、dp[i-5]、dp[i-6]跳1、2、3、4、5、6點而來。(但這只有適用於i>=6時) ::: ```csharp= #include <bits/stdc++.h> using namespace std; #define ll long long #define mod 1000000007 #define MAXI 1000010 ll n; ll dp[MAXI];//湊出點數i的可能情形 int main() { cin>>n; dp[0]=1; for(int i=1;i<=n;i++){ for(int j=1;j<=min(i,6);j++){ //要取min不然可能會爆(點數太多) dp[i]+=dp[i-j]; dp[i]%=mod; } } cout<<dp[n]<<'\n'; return 0; } ``` ### 線性DP [CSES Problem Set Minimizing Coins(2*)](https://cses.fi/problemset/result/4349093/) :::info 破題關鍵: dp[i]:=湊出第i個值,所需要的最少硬幣數 第i個值可由$i-C_0$、$i-C_1$、...、$i-C_n$,分別加上$C_0$、$C_1$、...、$C_n$硬幣(硬幣數+1)湊出。 所以$dp[i]=min(dp[i-C_0],dp[i-C_1],...,dp[i-C_n])+1$ $=min(dp[i],dp[i-c_j]+1)$,記得判斷索引值>=0的case。 ::: ```csharp= #include <bits/stdc++.h> using namespace std; #define ll long long #define MAXI 1000005 //陣列開太大會RE #define coin 105 ll n,x,in; ll dp[MAXI],c[coin];//湊出點數i的可能情形之方法數 int main() { ios::sync_with_stdio(false); cin.tie(0); cin>>n>>x; memset(dp, 0x3F, sizeof(dp)); dp[0]=0; for(int i=0;i<n;i++){ cin>>c[i]; } for(int i=0;i<=x;i++){ for(int j=0;j<n;j++){ if(i-c[j]>=0){ dp[i]=min(dp[i],dp[i-c[j]]+1); } } } if(dp[x]>x)cout<<-1<<'\n'; //dp[x]等於0x3f時 else cout<<dp[x]<<'\n'; return 0; } ``` ### 線性DP [CSES Problem Set Grid Paths(2*)](https://cses.fi/problemset/task/1638/) :::info 解題關鍵: i+c[j]點是由i點加上c[j]點而來,所以dp[i+c[j]]=dp[i];。 ::: ```csharp= #include <bits/stdc++.h> using namespace std; #define ll long long #define MOD 1000000007 ll n,x,c[110],dp[1000010]; int main() { ios::sync_with_stdio(false); cin.tie(0); cin>>n>>x; dp[0]=1; for(int i=0;i<n;i++) cin>>c[i]; //注意幣值相同的硬幣視為相異 for(int i=0;i<x;i++){ for(int j=0;j<n;j++){ if(i+c[j]>x) continue; dp[i+c[j]]+=dp[i]; dp[i+c[j]]%=MOD; } } cout<<dp[x]<<'\n'; return 0; } ``` ### 線性DP [CSES Problem Set Coin Combinations I(5*)](https://cses.fi/problemset/task/1635/) :::info 破題關鍵: 因為只能往右或上有,所以轉移式 * dp[i][j]=dp[i][j-1]+dp[i-1][j] 但記得要判斷起點(1,1)的情形,可能可以走(方法數=1),或者不能走(方法數=0),不能用轉移式遞迴得到!!! ::: ```csharp= #include <bits/stdc++.h> using namespace std; #define ll long long #define MAXI 1010 #define mod 1000000007 int main() { ios::sync_with_stdio(false); cin.tie(0); ll n,i,j; cin>>n; string map[MAXI];//char陣列只有1維的 //map:建圖 ll dp[MAXI][MAXI]; memset(dp,0,MAXI);//初始化成0 for(i=1;i<=n;i++){ cin>>map[i]; for(j=1;j<=n;j++){ if(i==1&&j==1){ if(map[i][j-1]=='*') dp[1][1]=0; else dp[1][1]=1; continue; //要寫continue,不然會做下列事情,dp[1][1]會變成遞迴而得。 //但dp起點不能由遞迴而得!! } if(map[i][j-1]=='*') continue; dp[i][j]=(dp[i][j-1]+dp[i-1][j])%mod; } } cout<<dp[n][n]<<endl; return 0; } ``` ### 線性DP [Atcoder DP Contest pE. Knapsack 2(3*)](https://atcoder.jp/contests/dp/tasks/dp_e) :::info 解題關鍵: 1. 定義dp[i] dp[i]定義成湊出背包價值為i時,所需的最小重量。 注意到:如果定義dp[i]為當重量為i時的最大價值,很容易加一加然後 2. 列出轉移式 dp[j]=min(dp[j-v[i]]+w[i],dp[j]); dp[j]可能由dp[j-v[i]]+w[i] (取第i件物品)而來,或是原來的dp[j] (不取第i件物品)而來。 3. 利用迴圈遞迴 注意要從價值為W開始往回算,不然可會會重複算到,變成無限背包問題。 4. 計算在背包重量$\leq$W的條件下,所能獲得地最大價值。 ::: ```csharp= #include <bits/stdc++.h> using namespace std; #define ll long long int main() { ios::sync_with_stdio(false); cin.tie(0); ll N,W; ll v[1005],w[1005]; ll dp[1000001]; //dp[i]代表湊出背包價值為i時的最小重量 fill(dp,dp+100001,1e18); //取min,記得要先初始化成很大的數字 dp[0]=0; cin>>N>>W; for(ll i=1;i<=N;i++){ cin>>w[i]>>v[i]; for(ll j=100000;j>=v[i];j--){//轉移後背包價值不能為負值 dp[j]=min(dp[j],dp[j-v[i]]+w[i]); //列出轉移式 } } ll ans=0;//計算在背包重量<=W的條件下,所能獲得地最大價值 for(ll i=0;i<=100000;i++) if(dp[i]<=W) ans=max(ans,i);//最大可能的價值 cout<<ans<<'\n'; return 0; } ``` ## 圖論一 ### dfs [CSES Problem Set Building Roads(2*)](https://cses.fi/problemset/task/1666/) :::info 解題關鍵: (相鄰:=只需要走一條edge就能到達) * dfs精神 1. 呼叫起點 2. 枚舉起點所有相鄰連通城市 3. 枚舉符合條件2的所有城市 按上面三步驟,就能知道與起點相鄰的所有連通城市。 接著再來枚舉所有城市,如果與1不相鄰通者,就建路。 接著把建路的城市dfs,得知建路後與1相鄰的所有城市,同理, 做完所有城市。 ::: ```csharp= #include <bits/stdc++.h> using namespace std; #define ll long long #define MAXI 200010 vector<ll> G[MAXI]; //G[i]紀錄第i座城市和哪些城市是connection的 bool vis[MAXI];//記錄哪些城市被dfs過,dfs過表是連通 ll n,m,a,b; vector<pair<ll,ll>> ans; //紀錄哪些路要被建 int dfs(ll i){ //遍歷第i張圖 vis[i]=true; //dfs第i座城市 for(ll e:G[i]){ //枚舉與i相鄰的城市e=1~n,dfs第i座城市的所有相鄰連通城市 if(!vis[e]){ //如果它沒被dfs過,就dfs他,把連通但不相鄰i的城市枚舉 dfs(e); //枚舉與i連通,但不相鄰連通的城市 } } return 0; //記得就算不用return,還是要return。 } //整個做完,如果第j個跟第i城市是相鄰的,vis[j]=1 int main() { ios::sync_with_stdio(false); cin.tie(0); cin>>n>>m; for(ll i=1;i<=m;i++){ //建m條路 cin>>a>>b; G[a].push_back(b); G[b].push_back(a); //因為是無向圖 } dfs(1);//一定要有一個城市與城市1連通 for(ll i=2;i<=n;i++){//dfs n座城市 if(!vis[i]){ //如果第i個城市不與1連通,需要一條edge使其連通 ans.push_back({1,i}); //蓋城市 dfs(i);//找到與i連通的所有城市 } } cout<<ans.size()<<"\n"; for(auto it:ans) cout<<it.first<<" "<<it.second<<'\n'; return 0; } ``` ### DFS [TIOJ 1152 銀河帝國旅行社(4*)](https://tioj.ck.tp.edu.tw/problems/1152) :::info 解題關鍵: 看到這題不難覺得要開DFS,但是要知道最遠距離,該怎麼修改扣呢?(直接看扣理解) 注意事項 * 某個點u,經過點i,走到點v的距離d(u,i,v)怎麼算呢,首先先看i為根結點的情形,距離d(u,i,v)=dep(u)+dep(v)-2。再看i深度=2的情形,距離d(u,i,v)=dep(u)+dep(v)-2*dep(i)。 * 有了這個關係式,接著思考何時d(u,i,v)有maxi。 得知當dep(i)=0、dep(v)、dep(u)時,距離有maxi * 接著我們去dfs整張圖,並記錄最大跟次大深度dep(u)、dep(v),用pair<ll,ll> mx(並要求mx.first $\ge$mx.second)存取。 * 每次dfs回傳最大深度mx.first ::: ```csharp= #include<bits/stdc++.h> using namespace std; #define ll long long #define MAXI 10005 ll n,ans=0,a; ll d[MAXI]; vector<ll> G[MAXI]; ll dfs(ll i,ll dep){//回傳最大深度 d[i]=dep;//記錄每點的深度,雖本題不需要知道 pair<ll,ll> mx(dep,dep); //紀錄dep(u)、dep(v),並使兩者值最大化 for(auto e:G[i]){//dfs他 ll tmp=dfs(e,dep+1);//依序往下dfs,每往下一次,深度+1 if(tmp>mx.first) mx={tmp,mx.first}; //把dep小的換掉,而mx.second<=mx.first,所以換掉mx.second else if(tmp>mx.second) mx={mx.first,tmp}; //把dep小的換掉//mx.first>=mx.second } ans=max(ans,mx.first+mx.second-2*dep);//d(u,i,v)的算法 return mx.first; } int main(){ ios::sync_with_stdio(false); cin.tie(0); cin>>n; for(ll i=0;i<n;i++) while(cin>>a && a>0) G[i].push_back(a); dfs(0,1); cout<<ans<<'\n'; return 0; } ``` ## DP II ### [LIS CSES - Increasing Subsequence](https://cses.fi/problemset/task/1145) ```csharp= #include<bits/stdc++.h> #define pb push_back #define ll long long using namespace std; vector<ll> v,dp; int main(){ ios::sync_with_stdio(false); cin.tie(); ll n; cin>>n; vector<ll> point(n); for(ll i=0;i<n;i++) { cin>>point[i]; ll ind=lower_bound(v.begin(),v.end(),point[i])-v.begin(); if(v.empty() || ind>=(ll)v.size()) v.pb(point[i]); else v[ind]=point[i]; } cout<<v.size(); return 0; } ``` ### [LIS APCS 2021 一月場 p4 - 飛黃騰達 (4*)](https://zerojudge.tw/ShowProblem?problemid=f608) :::info 先對(x,y)排序,然後對y做嚴格LIS。 如何實作LIS 1. 定義v[j]為當dp[i]=j時,$a_i$的最小值。 2. 注意到v[j]為遞增序列,加入新點時,對v做二分搜,把$a_i$插入 ::: ```csharp= #include <bits/stdc++.h> #define ll long long #define pll pair<ll,ll> #define F first #define S second #define pb push_back #define mp make_pair using namespace std; vector<ll> v; vector<pll> point; int main() { ios::sync_with_stdio(false); cin.tie(0),cout.tie(0); ll n;cin>>n; for(ll i=0;i<n;i++){ ll x,y; cin>>x>>y; point.pb(mp(x,y)); } sort(point.begin(),point.end()); for(ll i=0;i<n;i++){ ll ind=upper_bound(v.begin(),v.end(),point[i].S)-v.begin();//嚴格遞增子序列 if(v.empty() || ind>=v.size()) v.pb(point[i].S); else v[ind]=point[i].S;//因為v[ind+1]>=arr[i] } cout<<v.size()<<'\n'; return 0; } ``` ## 計算幾何 ### 向量應用 [CSES Problem Set Polygon Area(2*)](https://cses.fi/problemset/task/2191) :::info 破題關鍵: 怎麼用程式算平面圖形的面積呢? 開鞋帶公式(測量師公式) ::: ```csharp= #include <bits/stdc++.h> using namespace std; #define ll long long vector<ll> x,y; ll n; int main() { cin>>n; ll area=0; for(int i=1,a,b;i<=n;i++){ cin>>a; x.push_back(a); cin>>b; y.push_back(b); } x.push_back(x[0]);y.push_back(y[0]); for(int i=0;i<=n-1;i++){ area+=x[i]*y[i+1]; area-=y[i]*x[i+1]; } cout<<abs(area)<<endl; return 0; } ```