APCS 11011 實作題題解 == ## P1 - [link](https://zerojudge.tw/ShowProblem?problemid=g595) #### 題意 給定一個陣列,有些位置為 0,將所有為 0 的位置填上左右相鄰值的 min,求填完後陣列總和。 #### 題解 基礎實作題,照題目規則填數字即可,遇到 0 填上左右的 min。 #### 需注意的點 - 此題為基礎語法題,考驗基礎語法 - 注意只有一個鄰居,也就是陣列頭尾為 0 的情況。 #### 範例解答 ``` cpp= #include <bits/stdc++.h> using namespace std; #define ll long long #define fr(i,j,k) for(int i=j;i<k;i++) #define f(n) fr(i,0,n) #define f1(n) fr(i,1,n+1) #define pb push_back #define F first #define S second #define all(x) x.begin(), x.end() const int mod = 1e9 + 7; const int maxn = 100005; int main() { ios_base::sync_with_stdio(0); cin.tie(0); int n; cin >> n; vector<int>v(n); int sum = 0; for (auto &i : v) { cin >> i; } if (v[0] == 0) { v[0] = v[1]; sum += v[0]; } if (v[n - 1] == 0) { v[n - 1] = v[n - 2]; sum += v[n - 1]; } for (int i = 1 ; i < n - 1 ; i++) { if (!v[i]) { v[i] = min(v[i - 1], v[i + 1]); sum += v[i]; } } cout << sum << '\n'; } ``` ## P2 - [link](https://zerojudge.tw/ShowProblem?problemid=g596) #### 題意 題目給定 n×m 的矩形,使用木樁和線來排動線,有下列兩種操作 加入木樁 r c 0 加一木樁在 (r,c), 並且向他的上下左右盡量找離最近的木樁連線, 若 (r,c) 有線經過則先將線拆掉後再來連線 移除木樁 r c 1 把位於 (r,c) 的木樁拔掉, 並把與他相關線拔掉, 保證 (r,c) 上一定有木樁 總共有 h 次操作,輸出過程中有線和有木樁佔據空間的面積最大是多少, 以及 h 次操作後有線和有木樁佔據空間的面積 #### 題解 進階實作題,依然是照題目需求實作,但是此題的實作十分複雜,下列為一建議寫法。 分別用陣列紀錄此格是否有橫線, 直線, 木樁。 將直線橫線分開紀錄的原因,將通過某格的直線拆掉,該格可能還有橫線,如以下 case。 remove(4, 3) _ _ @ _ _ @ * * * @ _ _ * _ _ _ _ @ _ _ 可發現 (2, 3) 的直線被移除,但仍有橫線 - 加入木樁 - 將該點設為有木樁。 - 將該點的直線, 橫線移除。 - 找到上下鄰居, 路徑上加入直線。 - 找到左右鄰居, 路徑上加入橫線。 - 移除木樁 - 將該點設為無木樁。 - 將該點的直線, 橫線移除。 - 找到上下鄰居, 路徑上直線移除。 - 找到左右鄰居, 路徑上橫線移除。 #### 需注意的點 - 此題進階語法題,考點為進階的實作與語法,開始實作前務必熟讀題目了解題意並確實想清楚實作流程。 - 四個方向找鄰居的流程相似,可寫成迴圈精簡 code。 - 實作過程中有很多細節,如找鄰居超界,橫線直線的加入移除順序流程。 #### 範例解答 ``` cpp= #include <bits/stdc++.h> using namespace std; int b[105][105]; int line[105][105][2]; int dx[] = {-1, 0, 1, 0}; int dy[] = {0, 1, 0, -1}; int main() { ios_base::sync_with_stdio(0); cin.tie(0); int n, m, h; cin >> n >> m >> h; int maxv = 0; for (int i = 0 ; i < h ; i++) { int x, y, op; cin >> x >> y >> op; if (!op) { b[x][y] = 1; line[x][y][0] = line[x][y][1] = 0; for (int j = 0 ; j < 4 ; j++) { int nx = x + dx[j]; int ny = y + dy[j]; while (nx >= 0 && nx < n && ny >= 0 && ny < m && !b[nx][ny]) { nx += dx[j]; ny += dy[j]; } if (nx >= 0 && nx < n && ny >= 0 && ny < m) { int px = x + dx[j]; int py = y + dy[j]; while (px != nx || py != ny) { line[px][py][j % 2] = 1; px += dx[j]; py += dy[j]; } } } } else { b[x][y] = 0; for (int j = 0 ; j < 4 ; j++) { int nx = x + dx[j]; int ny = y + dy[j]; while (nx >= 0 && nx < n && ny >= 0 && ny < m && !b[nx][ny]) { nx += dx[j]; ny += dy[j]; } if (nx >= 0 && nx < n && ny >= 0 && ny < m) { int px = x + dx[j]; int py = y + dy[j]; while (px != nx || py != ny) { line[px][py][j % 2] = 0; px += dx[j]; py += dy[j]; } } } } int cur = 0; for (int i = 0 ; i < n ; i++) { for (int j = 0 ; j < m ; j++) { if (b[i][j] || line[i][j][0] || line[i][j][1]) { cur++; } } } maxv = max(maxv, cur); } int ans = 0; for (int i = 0 ; i < n ; i++) { for (int j = 0 ; j < m ; j++) { if (b[i][j] || line[i][j][0] || line[i][j][1]) { ans++; } } } cout << maxv << '\n' << ans << '\n'; } ``` ## P3.生產線 - [link](https://zerojudge.tw/ShowProblem?problemid=g597) #### 題意 n 台機器排成一直線, 每一個機器有 t[i], 代表該台機器要產出一單位的資料需要 t[i] 單位的時間 有 m 個工作要完成, 每一個工作都需要第 [l[i],r[i]] 的機器各生產出 w[i] 單位資料 可任意調換 n 台機器的順序, 目標是使得這 m 個工作做完的總時間要最小,輸出最小的總時間 #### 題解 #### 區間加值 : - 方法一 : 暴力 for 迴圈填入 單次複雜度為 O(N) 可通過最小 30% 的測資 - 方法二 : 應用差分陣列 差分陣列第 i 項表示原陣列第 i 項與第 i - 1 項的差。 ex : 原陣列 : 1 2 5 -2 7 差分陣列 : 0 1 1 3 -7 9 可發現原陣列即為差分陣列的前綴和。 那當我們要將一段連續區間 (l, r) 都 +k 時,差分陣列僅會在 l, r + 1 兩個位置產生改變 ex : 2~4 +3 原陣列 : 0 3 3 3 0 差分陣列 : 0 0 3 0 0 -3 每次修改僅需更改差分陣列的兩個位置,最後再做前綴和即可得到每台機器需產出的資料量 單次複雜度為 O(1) 可得到此題滿分 #### 排序機器 : 直覺的 Greedy,需要資料量多的使用所需時間較少的機器,將機器所需時間從小到大排,所需資料由大到小排,一對一對應即可算出答案。 #### 需注意的點 - 注意 overflow,需使用 long long。 - 差分陣列通常使用 1-base,最後計算加總時須注意正確的位置對應。 #### 範例解答 ``` cpp= #include <bits/stdc++.h> using namespace std; int main() { ios_base::sync_with_stdio(0); cin.tie(0); int n, m; cin >> n >> m; vector<long long>pre(n + 5); for (int i = 0 ; i < m ; i++) { int l, r, w; cin >> l >> r >> w;; pre[l] += w; pre[r + 1] -= w; } for (int i = 1 ; i <= n ; i++) { pre[i] += pre[i - 1]; } vector<long long>num(pre.begin() + 1, pre.begin() + n + 1); sort(num.begin(), num.end()); vector<long long>t; for (int i = 1 ; i <= n ; i++) { long long x; cin >> x; t.push_back(x); } sort(t.rbegin(), t.rend()); long long ans = 0; for (int i = 0 ; i < n ; i++) { ans += t[i] * num[i]; } cout << ans << '\n'; } ``` ## P4 - [link](https://zerojudge.tw/ShowProblem?problemid=g598) #### 題意 有 n 個人員, 將這些人分成 A 和 B 兩組, 初始有 m 個 pair 表示每個 pair 兩人之間屬於不同組, 接著有 p 個調查員, 每個調查員都會回傳恰好 k 個 pair 的資料回來,表示這些 pair 之間屬於不同組 有些調查員回傳的資料是錯誤的,表示他回傳的 k 個 pair 和初始 m 個 pair 會產生矛盾, 將回傳錯誤結果的調查員找出來, 至少一個至多三個。 輸入保證若調查員的 k 個 pair 和初始 m 個 pair 不會產生矛盾, 這些資料一定和原本分組吻合 #### 題解 #### 二分圖 為此題的先備知識,二分圖即為一張無向圖,可將點分成兩堆,使得同堆點間沒有邊,此也為著名的著色問題,,判斷一圖是否為二分圖為 O(n + m),也就是可用 DFS, BFS 稍作修改求解,可參考以下程式碼。 ``` cpp= include <bits/stdc++.h> using namespace std; vector<int>g[200000]; int c[200000]; void dfs(int now, int cc) { for (auto &i : g[now]) { if (~c[i]) { if (c[i] == c[now]) { cout << "NO" << '\n'; exit(0); } } else { c[i] = cc ^ 1; bfs(i, c[i]); } } } int main() { ios_base::sync_with_stdio(0); cin.tie(0); int n, m; cin >> n >> m; memset(c, -1, sizeof(c)); for(int i= 0 ; i < m ; i++) { int a, b; cin >> a >> b; g[a].push_back(b); g[b].push_back(a); } c[1] = 0; dfs(1, 1); cout << "YES\n"; } ``` #### 做法 1 暴力對 k 個觀察員分別求解,將每個觀察員的邊一一加入原圖,判斷圖是否為二分圖。 複雜度為 $O((n + m) * p)$ 在有好的實作下,此做法有機會拿到滿分。 #### 做法 2 題目有兩個關鍵條件 - 輸入保證若調查員的 k 個 pair 和初始 m 個 pair 不會產生矛盾, 這些資料一定和原本分組吻合 - 錯誤的調查員至少一個至多三個 不會產生矛盾,表示若 x, y, z 為未發生矛盾的觀察員,將他們的邊同時加入也會為二分圖,只有當加入的邊有包含發生矛盾的觀察員,原圖才會變為非二分圖,可使用二分搜的技巧大量減少需要 DFS 判二分圖的次數,首次二分搜找到最小 index 的矛盾觀察員,找到一個矛盾觀察員僅需判二分圖 log n 次,參考以下 psuedo code。 複雜度為 $O((n + m) * \log n)$ ``` cpp= vector<int>ans; int L = 0; for (int i = 0 ; i < 3 ; i++) { if (check(L, k - 1)) { // 加入這些觀察員是否為二分圖 break; } int l = L - 1, r = k - 1; while (r - l > 1) { int mid = (l + r) >> 1; if (check(L, mid)) { l = mid; } else { r = mid; } } ans.push_back(r); L = r + 1; } ``` #### 做法 3 可還原 disjoint set #### 需注意的點 - 判二分圖須對每個連通塊皆做判斷 - 加入觀察員的邊後做完二分圖判斷需記得移除 - 此題二分搜邊界較為複雜,設定邊界時需注意 #### 範例解答 ``` cpp= #include <bits/stdc++.h> using namespace std; const int maxn = 20005; vector<int>g[maxn]; vector<pair<int,int>>v[maxn]; int n, m, p, k; int color[maxn]; bool dfs(int now) { int f = 1; for (auto &i : g[now]) { if (~color[i]) { if (color[i] == color[now]) { return 0; } continue; } color[i] = 1 - color[now]; f &= dfs(i); } return f; } bool check(int l, int r) { int f = 1; for (int i = l ; i <= r; i++) { for (auto &j : v[i]) { g[j.first].push_back(j.second); g[j.second].push_back(j.first); } } fill(color, color + n, -1); for (int i = 0 ; i < n ; i++) { // 判二分圖 if (color[i] == -1) { color[i] = 0; f &= dfs(i); if (!f) { break; } } } for (int i = l ; i <= r; i++) { for (auto &j : v[i]) { g[j.first].pop_back(); g[j.second].pop_back(); } } return f; } int main() { ios_base::sync_with_stdio(0); cin.tie(0); cin >> n >> m; for (int i = 0 ; i < m ; i++) { int x, y; cin >> x >> y; g[x].push_back(y); g[y].push_back(x); } cin >> p >> k; for (int i = 1 ; i <= p ; i++) { for (int j = 0 ; j < k ; j++) { int a, b; cin >> a >> b; v[i].push_back({a, b}); } } int L = 1; for (int i = 0 ; i < 3 ; i++) { if (check(L, p)) { // 加入這些觀察員是否為二分圖 break; } int l = L - 1, r = p ; while (r - l > 1) { int mid = (l + r) >> 1; if (check(L, mid)) { l = mid; } else { r = mid; } } cout << r << '\n'; L = r + 1; } } ``` ![](https://i.imgur.com/vDrjGMe.png =500x)