--- title: 【NCPC】112解題筆記 tags: C++ --- # 【NCPC】112解題筆記 [113官網](https://ncpc.nsysu.edu.tw/index.php) | [112初賽PDF](https://ncpc.nsysu.edu.tw/static/file/62/1062/img/4478/832180131.pdf) | [112初賽計分板](https://reg.ncpc.ntnu.edu.tw/ncpc2023/NCPC_scorboard/NCPC2023_preliminary_result.html) | [112決賽PDF](https://ncpc.nsysu.edu.tw/static/file/62/1062/img/4478/506520321.pdf) | [112決賽計分板](https://reg.ncpc.ntnu.edu.tw/ncpc2023/NCPC_scorboard/NCPC2023_final_result.html) ## 前言 - 這裡的測資執行時間指的是執行官網提供的.in和.out檔所需時間,使用CP Editor計時。為什麼不寫時間複雜度呢?因為我不太懂那個,怕錯估自己code的複雜度會很尷尬。 - 關鍵字是指跟我的做法有關的一些資料結構、演算法或數學觀念,看不懂可以先google它們,當然也可以問我。 - 如果你看完想到什麼酷炫的優化方式也請跟我說! ## 初賽 ### Problem A: Cake Game **關鍵字:** 線段相交(segment intersection) **測資執行時間:** 163ms 這題最麻煩的部分就是判斷兩線段是否相交。我這裡使用定向(orientation)計算,簡單來說就是考量兩個相交的線段a, b,線段b的兩端點一定在a的兩側,比如a1-a2-b1是順時針方向旋轉的話,a1-a2-b2就會是逆時針方向。詳細做法可以參考[這篇文章](https://www.geeksforgeeks.org/check-if-two-given-line-segments-intersect/)。 ```cpp= #include <bits/stdc++.h> using namespace std; using ll = long long; int t; struct candle{ int x; int y; }; candle red[4], green[4]; const int g0[] = {0,0,0}; const int g1[] = {1,2,3}; const int g2[] = {2,1,1}; const int g3[] = {3,3,2}; // 用向量算三點位置關係 int orientation(candle p, candle q, candle r){ int val = (q.y-p.y)*(r.x-q.x) - (q.x-p.x)*(r.y-q.y); if(val==0) return 0; // 共線 return (val>0)?1:2; } // 檢查點r是否在線段pq上 bool on_segment(candle p, candle q, candle r){ if(r.x < max(p.x, q.x) && q.x > min(p.x, q.x) && r.y < max(p.y, q.y) && r.y > min(p.y, q.y)){ return true; } return false; } // 檢查線段是否相交 bool intersect(candle a1, candle a2, candle b1, candle b2){ int o1, o2, o3, o4; // 一般情況: b1,b2在線段a不同側,a1,a2在線段b不同側 o1 = orientation(a1, a2, b1); o2 = orientation(a1, a2, b2); o3 = orientation(b1, b2, a1); o4 = orientation(b1, b2, a2); if(o1!=o2 && o3!=o4) return true; // 特殊情況: ab共線且有重疊 if(o1==0 && on_segment(a1,a2,b1)) return true; if(o2==0 && on_segment(a1,a2,b2)) return true; if(o3==0 && on_segment(b1,b2,a1)) return true; if(o4==0 && on_segment(b1,b2,a2)) return true; return false; } int main(){ ios::sync_with_stdio(0); cin.tie(0); cin >> t; while(t--){ bool g = false; bool rg1, rg2, g1g2; for(int i=0; i<4; i++) cin >> red[i].x >> red[i].y; for(int i=0; i<4; i++) cin >> green[i].x >> green[i].y; // 窮舉紅蠟燭連線 for(int i=0; i<4; i++){ for(int j=i+1; j<4; j++){ // 檢查線段red[i]red[j] if(g) break; int bfail = 0; // 窮舉綠蠟燭兩條連線組合 (只考慮不會相交的情況) for(int k=0; k<3; k++){ rg1 = intersect(red[i], red[j], green[g0[k]], green[g1[k]]); rg2 = intersect(red[i], red[j], green[g2[k]], green[g3[k]]); g1g2 = intersect(green[g0[k]], green[g1[k]], green[g2[k]], green[g3[k]]); if(rg1 || rg2 || g1g2) bfail++; } if(bfail==3) g = true; } } if(g) cout << "G\n"; else cout << "B\n"; } return 0; } ``` ### Problem B: Subarray Sums **關鍵字:** 前綴和(prefix sum) **測資執行時間:** 66ms ```cpp= #include <bits/stdc++.h> using namespace std; using ll = long long; int n, s, d; int ary[5005], pre[5005]; int main(){ ios::sync_with_stdio(0); cin.tie(0); cin >> n; while(n--){ cin >> s; cin >> ary[0]; pre[0] = ary[0]; // 處理輸入順便做前綴和 for(int i=1; i<s; i++){ cin >> ary[i]; pre[i] = pre[i-1] + ary[i]; } cin >> d; // 窮舉子序列並求和 int sum=0, curr=0, maxr=0, cnt=0; for(int i=0; i<s; i++){ for(int j=i; j<s; j++){ sum = pre[j] - pre[i] + ary[i]; curr = sum%d; // 記錄最大值和出現次數 if(curr > maxr){ maxr = curr; cnt = 1; } else if(curr == maxr) cnt++; } } cout << cnt << '\n'; } return 0; } ``` ### Problem D: Little Red Riding Hood **關鍵字:** 廣度優先搜尋(breadth-first search / BFS), 戴克斯特拉演算法(Dijkstra's algorithm), 優先佇列(priority queue) **測資執行時間:** 485ms ```cpp= #include <bits/stdc++.h> using namespace std; using ll = long long; const int maxN = 2e5+1; const int maxW = 1e9+1; struct road{ int dest; // 這條路通向的村莊 int hour; // 走完這條路所需的時間 }; struct state{ int time; // 目前走了幾小時 int village; // 到幾號村莊了 int hour_left; // 白天還剩幾小時 }; // 自定義比較運算子,priority_queue才知道怎麼排序道路 bool operator>(const state& x, const state& y){ return x.time > y.time; } //紀錄道路連通情況,adj[i]是一個vector,裡面存了從第i村出發的所有路 vector<road> adj[maxN]; int dijkstra(int n, int a, int b){ vector<int> min_hour(n+1, maxW); vector<int> hour_left(n+1, a); priority_queue<state, vector<state>, greater<state>> pq; // 先從第1村出發 min_hour[1] = 0; // 到第1村最少就是0小時 state start; start.time = 0; // 目前用時0小時 start.village = 1; // 我們在第1村 start.hour_left = a; // 白天還剩a小時 pq.push(start); while(!pq.empty()){ state cur = pq.top(); // 沿著目前用時最短的路徑繼續前進 pq.pop(); int cur_time = cur.time; int village = cur.village; int day_hours_left = cur.hour_left; if(village==n) return cur_time; // 到阿嬤家了 // 目前村莊出去的每條路都走走看 for(road r : adj[village]){ int next_village = r.dest; int travel_time = r.hour; int new_time, new_hours_left; // 天黑前可以走完的路就走一走 if(travel_time <= day_hours_left){ new_time = cur_time + travel_time; new_hours_left = day_hours_left - travel_time; } // 天黑前走不完的路就先過夜再出發 else{ new_time = cur_time + day_hours_left + b + travel_time; new_hours_left = a - travel_time; } // 如果這次到下個村莊的時間比之前紀錄的min_hour[next_village]還短就更新 if(new_time < min_hour[next_village]){ min_hour[next_village] = new_time; hour_left[next_village] = new_hours_left; pq.push({new_time, next_village, new_hours_left}); } } } return -1; // 走完還是沒到終點就回傳-1 } int main(){ ios::sync_with_stdio(0); cin.tie(0); int t; cin >> t; while(t--){ int n, m, a, b, res; cin >> n >> m >> a >> b; // 針對每筆測資重置道路連通情況的紀錄 for(int i = 1; i <= n; i++){ adj[i].clear(); } for(int i = 0; i < m; i++){ int u, v, w; road r1, r2; // 路沒有方向性,來回都可以通 cin >> u >> v >> w; r1.dest = v; r1.hour = w; r2.dest = u; r2.hour = w; adj[u].push_back(r1); adj[v].push_back(r2); } res = dijkstra(n, a, b); cout << res << '\n'; } return 0; } ``` ### Problem F: Drones **關鍵字:** 動態規劃(dynamic programming) **測資執行時間:** 31ms 我覺得DP題最難的部分是看出可以用DP...這題我花了兩個多小時寫出一個錯誤解法,又想了半天才發現可以DP。 狀態```dp[i]```代表從```0```到```i```的最低花費,轉移式為```dp[j] = min(dp[], dp[i]+cost(i+1,j))``` ```cpp= #include<bits/stdc++.h> using namespace std; int t, n; const int maxN = 1001; int outpost[maxN], dp[maxN]; //dp[i]代表從0到i的最低花費 int cost(int i, int j){ int dis = outpost[j] - outpost[i]; return 10+dis*dis; } int main(){ ios::sync_with_stdio(0); cin.tie(0); cin >> t; while(t--){ cin >> n; for(int i=0; i<n; i++){ cin >> outpost[i]; // 一開始先假設0到i的最低花費都是用一台無人機巡邏整段 dp[i] = cost(0, i); } for(int i=1; i<n; i++){ for(int j=0; j<i; j++){ // 看j+1到i切出來有沒有比較便宜 dp[i] = min(dp[i], dp[j]+cost(j+1,i)); } } cout << dp[n-1] << '\n'; } return 0; } ``` ## 決賽 太難了我寫個兩題簡單的就要去寫111了。 ### Problem A: Spiral Game **關鍵字:** 模擬(simulation) **測資執行時間:** 665ms ```cpp= #include<bits/stdc++.h> using namespace std; int t, n, m; int pl[151], must[5001]; int main(){ ios::sync_with_stdio(0); cin.tie(0); int s, d, cur, sim, winner; bool move; cin >> t; while(t--){ winner = -1; memset(must, 0, sizeof(must)); memset(pl, 0, sizeof(pl)); cin >> n >> m; while(cin >> s){ if(s==0) break; must[s] = 1; } must[n] = 1; cur = 0; while(cin >> d){ if(d==0) break; move = true; sim = pl[cur]; while(sim<pl[cur]+d-1){ sim++; if(must[sim]) move = false; } if(move) pl[cur] += d; if(pl[cur]==n && winner==-1) winner = cur; cur = (cur+1) % m; } cout << winner+1 << '\n'; } return 0; } ``` ### Problem D: Jacobi Symbol **關鍵字:** 遞迴(recursion) **測資執行時間:** 26ms ```cpp= #include<bits/stdc++.h> using namespace std; using ll = long long; ll a, n; ll gcd(ll x, ll y){ if(!y) return x; else return gcd(y, x%y); } ll solve(ll a, ll n){ a %= n; if(gcd(a, n) != 1) return 0; if(a == 1) return 1; if(a == 2){ if(n%8==1 || n%8==7) return 1; return -1; } if(a%2){ // odd if(a%4==3 && n%4==3) return -(solve(n, a)); return solve(n, a); } int res = 1; while(a%2==0){ res *= solve(2, n); a /= 2; } res *= solve(a, n); return res; } int main(){ ios::sync_with_stdio(0); cin.tie(0); bool first; while(cin >> n){ if(!n) break; first = true; while(cin >> a){ if(!a){ cout << '\n'; break; } if(!first) cout << ' '; cout << solve(a, n); first = false; } } return 0; } ```