# 112-1 515507 演算法概論期末上機考題解 ## A 和 Homework 1 A 一樣的題目,只是少了要輸出解的步驟。可以使用 Divide & Conquer 或 DP 解決。 :::spoiler Sample solution ```cpp= #include <iostream> #include <vector> using namespace std; constexpr long long INF = 1'000'000'000'000'000'000LL; int main() { int n; cin >> n; vector<int> a(n); for (int &i : a) cin >> i; long long ans = -INF, sum = 0; int ansl, ansr, curl = 0; for (int i = 0; i < n; i++) { if (sum + a[i] > ans) { ans = sum + a[i]; ansl = curl + 1; ansr = i + 1; } sum += a[i]; if (sum <= 0) { sum = 0; curl = i + 1; } } cout << ans << '\n'; } ``` ::: ## B Homework 5 B 的類題。使用 Disjoint Set 維護每個 Connected Component,在每個 Connected Component 紀錄他的大小。 則可以發現第一個操作相當於 Union 兩個 set,而第二個操作相當於詢問某個 set 的大小,都是經典的 Disjoint set 操作。 :::spoiler Sample solution ```cpp= #include <iostream> #include <queue> #include <vector> using namespace std; struct DSU { int n; vector<int> p, size; void init(int n_) { n = n_; p.resize(n); size.resize(n); for (int i = 0; i < n; i++) { p[i] = i; size[i] = 1; } } int find(int u) { return p[u] == u ? u : p[u] = find(p[u]); } void add_edge(int u, int v) { u = find(u), v = find(v); if (u == v) return; if (size[u] > size[v]) swap(u, v); size[v] += size[u]; p[u] = v; } int get_size(int u) { return size[find(u)]; } } dsu; int main() { int n, q; cin >> n >> q; dsu.init(n); while (q--) { int type; cin >> type; if (type == 1) { // add edge int u, v; cin >> u >> v; u--, v--; dsu.add_edge(u, v); } else if (type == 2) { // get count int u; cin >> u; u--; cout << dsu.get_size(u) << '\n'; } } } ``` ::: ## C 手寫作業題。方法是首先先從一個節點開始找出距離他最遠的那個點,之後再從那個最遠的點找到距離他最遠的點,這個距離就是 Tree Diameter。 找最遠的距離使用 BFS 或 DFS 都可以。 :::spoiler Sample solution ```cpp= #include <algorithm> #include <iostream> #include <vector> using namespace std; void dfs(int u, int p, vector<int> &dist, vector<vector<int>> &G) { for (int v : G[u]) if (v != p) { dist[v] = dist[u] + 1; dfs(v, u, dist, G); } } int main() { int n; cin >> n; vector<vector<int>> G(n); for (int i = 0; i < n - 1; i++) { int u, v; cin >> u >> v; u--, v--; G[u].emplace_back(v); G[v].emplace_back(u); } vector<int> dist(n, -1); dist[0] = 0; dfs(0, -1, dist, G); int v = max_element(dist.begin(), dist.end()) - dist.begin(); dist = vector<int>(n, -1); dist[v] = 0; dfs(v, -1, dist, G); cout << *max_element(dist.begin(), dist.end()) << '\n'; } ``` ::: ## D 這題會需要用到在 Tree 上做 DP 的概念。 注意到有根樹的每個節點都還是有根樹,這就讓樹可以很好的做 DP 或 Divide and Conquer。 在這題中,我們在樹以任意節點為根以後,令 $dp[u][0/1]$ 代表說以 $u$ 這個節點為根的 subtree, 且 $u$ 不在(或在)independent 時,subtree 的 independent set 最大權重和可以是多少。 我們有以下遞迴式: $dp[u][0] = \sum_{v \in S_u} \max(dp[v][0], dp[v][1])$ $dp[u][1] = (\sum_{v \in S_u} dp[v][0]) + a_u$ 其中 $S_u$ 代表 $u$ 的所有 children 的集合。 邊界條件:當一個節點是 leaf 的時候,有 $dp[u][0] = 0, dp[u][1] = a_u$。 實作上可以在 DFS 遞迴的時候順便算出 DP 值,這是最方便的實作方法。 :::spoiler Sample solution ```cpp= #include <iostream> #include <vector> using namespace std; void solve(int u, int p, vector<vector<long long>> &dp, vector<int> &a, vector<vector<int>> &G) { long long sum0 = 0, sum1 = 0; for (int v : G[u]) if (v != p) { solve(v, u, dp, a, G); sum0 += max(dp[v][0], dp[v][1]); sum1 += dp[v][0]; } dp[u][0] = sum0; dp[u][1] = sum1 + a[u]; } int main() { int n; cin >> n; vector<int> a(n); vector<vector<long long>> dp(n, vector<long long>(2)); vector<vector<int>> G(n); for (int &i : a) cin >> i; for (int i = 0; i < n - 1; i++) { int u, v; cin >> u >> v; u--, v--; G[u].emplace_back(v); G[v].emplace_back(u); } solve(0, -1, dp, a, G); cout << max(dp[0][0], dp[0][1]) << '\n'; } ``` ::: ## E ### Type 1 直接使用 Dijkstra Algorithm 求出最短路徑長度即可。注意到需要把距離存在一個 min heap 裡面,才會讓整個演算法達到 $O((n + m) \log n)$ 的時間複雜度。 ### Type 2 在使用 Dijkstra Algorithm 求出距離以後,我們只保留那些滿足 $dist[u] + w(u, v) = dist(v)$ 的那些(有向)邊,這些邊是「至少在一條從 $1$ 出發的最短路上」的邊。把這些有向邊建起來以後,則可以發現他是一個有向無環圖 DAG(可以想想看為什麼)。在這個新的 DAG 上,任意一個從 $1$ 出發到 $2$ 的路徑都是一個最短路徑。 所以現在的問題就變成一個在 DAG 上數 path 數量的問題。這是手寫作業的題目,可以使用 Topological Sort + DP 解決。在實作中可以不需要真的做 Topological Sort;可以在做 Dijkstra Algorithm 的時候,順便找出那些邊做 DP。細節可以參考 code。 :::spoiler Sample solution ```cpp= #include <iostream> #include <queue> #include <vector> using namespace std; using ll = long long; using pii = pair<int, int>; using pli = pair<ll, int>; constexpr ll MOD = 1'000'000'007; constexpr ll MAXD = 1e18; int main() { int n, m, type; cin >> n >> m >> type; vector<vector<pii>> G(n); for (int i = 0; i < m; i++) { int u, v, w; cin >> u >> v >> w; u--, v--; G[u].emplace_back(v, w); G[v].emplace_back(u, w); } vector<ll> dist(n, MAXD), dp(n, 0); priority_queue<pli, vector<pli>, greater<>> pq; dist[0] = 0, dp[0] = 1; pq.emplace(dist[0], 0); while (!pq.empty()) { auto [distu, u] = pq.top(); pq.pop(); if (dist[u] != distu) { continue; } cerr << u << endl; for (auto [v, w] : G[u]) { if (dist[v] > dist[u] + w) { dist[v] = dist[u] + w; dp[v] = dp[u]; pq.emplace(dist[v], v); } else if (dist[v] == dist[u] + w) { dp[v] = (dp[v] + dp[u]) % MOD; } } } if (type == 1) cout << (dist[1] == MAXD ? -1 : dist[1]) << '\n'; else cout << (dist[1] == MAXD ? -1 : dp[1]) << '\n'; } ``` ::: ## F 這個問題又稱 [Maximum-weight Clousre Problem](https://en.wikipedia.org/wiki/Closure_problem),上課的投影片有,位於倒數幾頁可以去參考。 把所有的關係畫成一個有向圖,每條邊的邊權都是 $\infty$。新建一個源點 $s$ 和一個匯點 $t$。對於每個節點 $v$,如果 $a_v > 0$ 就從 $s$ 連到 $v$,邊權是 $a_v$。否則就從 $v$ 連到 $t$,邊權是 $-a_v$。之後求出從 $s$ 到 $t$ 的 min cut,也就是 max flow。之後答案就是原本所有的正的 $a_v$ 減去 min cut。 這個做法正確的原因,可以參考上課投影片或上面的 wiki 連結。 :::spoiler Sample solution ```cpp= #include <iostream> #include <queue> #include <vector> using namespace std; static constexpr int MAXF = 1e9; struct Dinic { struct edge { int to, cap, flow, rev; }; static constexpr int MAXN = 102; vector<edge> v[MAXN]; int top[MAXN], deep[MAXN], side[MAXN], s, t; void make_edge(int s, int t, int cap) { v[s].push_back({t, cap, 0, (int)v[t].size()}); v[t].push_back({s, 0, 0, (int)v[s].size() - 1}); } int dfs(int a, int flow) { if (a == t || !flow) return flow; for (int &i = top[a]; i < v[a].size(); i++) { edge &e = v[a][i]; if (deep[a] + 1 == deep[e.to] && e.cap - e.flow) { int x = dfs(e.to, min(e.cap - e.flow, flow)); if (x) { e.flow += x, v[e.to][e.rev].flow -= x; return x; } } } deep[a] = -1; return 0; } bool bfs() { queue<int> q; fill_n(deep, MAXN, 0); q.push(s), deep[s] = 1; int tmp; while (!q.empty()) { tmp = q.front(), q.pop(); for (edge e : v[tmp]) if (!deep[e.to] && e.cap != e.flow) deep[e.to] = deep[tmp] + 1, q.push(e.to); } return deep[t]; } int max_flow(int _s, int _t) { s = _s, t = _t; int flow = 0, tflow; while (bfs()) { fill_n(top, MAXN, 0); while ((tflow = dfs(s, MAXF))) flow += tflow; } return flow; } void reset() { fill_n(side, MAXN, 0); for (auto &i : v) i.clear(); } } flow; int main() { int n, m; cin >> n >> m; int sum = 0; vector<int> a(n); for (int i = 0; i < n; i++) { cin >> a[i]; if (a[i] > 0) { flow.make_edge(n, i, a[i]); sum += a[i]; } else { flow.make_edge(i, n + 1, -a[i]); } } for (int i = 0; i < m; i++) { int x, y; cin >> x >> y; x--, y--; flow.make_edge(x, y, MAXF); } cout << sum - flow.max_flow(n, n + 1) << '\n'; } ``` :::