# APCS實作題2020年10月第4題:低地距離 > 日期:2024年9月8日 > 作者:王一哲 > [ZeroJudge 題目連結](https://zerojudge.tw/ShowProblem?problemid=f315) ## 題目 ### 問題描述 輸入一個長度為 $2n$ 的陣列,其中 $1$ ~ $n$ 的每個數字都剛好各 $2$ 次。 $i$ 的低窪值的定義是兩個數值為 $i$ 的位置中間,有幾個小於 $i$ 的的數字。 以 $[3, 1, 2, 1, 3, 2]$ 為例,$1$ 的低窪值為 $0$,$2$ 的低窪值值為 $1$,$3$ 的低窪值為 $3$。 請對於每個 $1$ ~ $n$ 的數字都求其低窪值(兩個相同的數字之間有幾個數字比它小),輸出低窪值的總和,答案可能會超過 C++ int 的上限。<br /> ### 輸入說明 第一行有一個正整數 $n$ 第二行有 $2n$ 個正整數,以空格分隔,保證 $1$ ~ $n$ 每個數字都恰好出現兩次。 配分 - 20%:$1 \leq n \leq 1000$ - 40%:$1 \leq n \leq 40000$ - 40%:$1 \leq n \leq 100000$ <br /> ### 輸出說明 輸出 $1$ ~ $n$ 每個數字的低窪值總和。 <br /> ### 範例輸入 ``` 3 3 1 2 1 3 2 ``` ### 範例輸出 ``` 4 ``` <br /> ## Python 程式碼 ### 寫法1,使用 set 及遞迴 費時最久約為 1.8 s,使用記憶體最多約為 38.3 MB,通過測試。 ```python= """ 分治法,輸入串列 arr,下限 low,上限 high """ def sol(arr, low, high): if len(arr) <= 2: return 0 # 遞迴出口,長度小於等於2,不能再切 mid = (high - low) // 2 + low # 取中間值 d = 0 # 答案 small = [] # 小於等於 mid 的資料 large = [] # 大於等於 mid 的資料 # 找某個數字位於多少大數對之間,大數 x 第一次出現時加入 between;x 第二次出現時已離開大數對範圍,從 between 移除。 between = set() for e in arr: # 由 arr 依序取出資料 e if e <= mid: # e 小於等於 mid d += len(between) # 更新 d,加上 between 的長度 small.append(e) # e 加入 small else: large.append(e) # e 加入 large if e in between: between.remove(e) # 如果 e 在 between 之中則移除 else: between.add(e) # 否則將 e 加入 between # 結束 for 迴圈 d += sol(small, low, mid) # 遞迴,找小於等於 x 的部分 d += sol(large, mid+1, high) # 遞迴,找大於等於 x 的部分 return d # 回傳 d """ 結束自訂函式 """ n = int(input()) a = list(map(int, input().split())) d = sol(a, 1, n) print(d) ``` <br /><br /> ### 寫法2,找同一個數字之前小於此數的數字數量再相減 為了判斷某個數字 x 是第一次或第二次出現,需要修改原來的資料,例如將第一個 x 改為 2x,第二個 x 改為 2x-1,則數字範圍由 1 到 n 變為 1 到 2n。費時最久約為 2.5 s,使用記憶體最多約為 31.9 MB,通過測試。 ```python= """ 計算同一個數字出現的位置之有幾個較小的數字再相減 """ def sol(arr): global total if len(arr) < 2: return 0 # 遞迴出口,長度小於等於2,不能再切 mid = len(arr) // 2 # 串列長度的一半 left = arr[:mid] # 左半部 right = arr[mid:] # 右半部 sol(left) # 遞迴,解左半部 sol(right) # 遞迴,解右半部 # 合併左半部及右半部 i, li = 0, 0 # arr 的索引值 i,left 的索引值 li for x in right: # 從右半部依序取出數字 x while li < len(left) and left[li] <= x: # 如果 li 還沒有出界,而且 left[li] 小於等於 x arr[i] = left[li] i += 1; li += 1 if x&1: total += li # 如果 x 第二次出現,total 加上 li else: total -= li # 如果 x 第二次出現,total 減去 li arr[i] = x i += 1 arr[i:] = left[li:] """ 結束自訂函式 """ n = int(input()) # 讀取數子上限 n a = list(map(int, input().split())) # 讀取資料 a t = [0]*(n+1) # 1 ~ n 各數字出現次數 for i in range(len(a)): # 依序産生 0 ~ n-1 y = a[i] # 正在處理的數值 y a[i] = y*2 - t[y] # 將 a[i] 修改成 2*y(第一次出現)或 2*y - 1(第二次出現) t[y] = 1 # y 出現次數加 1 total = 0 # 答案,預設為 0 sol(a) # 呼叫 sol,代入 a 求答案 print(total) # 印出答案 ``` <br /><br /> ### 寫法3,使用二元索引樹 (binary index tree, BIT) <img height="100%" width="100%" src="https://upload.wikimedia.org/wikipedia/commons/7/70/16-node_Fenwick_tree.svg" style="display: block; margin-left: auto; margin-right: auto;"/> <div style="text-align:center">BIT 結構示意圖</div> <br /><br /> 費時最久約為 2 s,使用記憶體最多約為 41.8 MB,通過測試。 ```python= def update(bit, idx, val): # 從 idx 開始向下更新節點 i = idx # 索引值 i while i <= len(bit): # 如果 i 還沒有出界繼續執行 bit[i] += val # 更新 bit[i],加上 val i += i&(-i) # 移到子節點 def psum(bit, n): # 計算前 n 項的和 result = 0 # 加總 i = n # 索引值預設為 i while i: # 當 i > 0 時繼續執行 result += bit[i] # result 加上 bit[i] i -= i&(-i) # 移到父節點 return result # 回傳加總 n = int(input()) # 讀取高度最大值 n line = list(map(int, input().split())) # 讀取每個位置的高度 arr = [[0, 0] for _ in range(n+1)] # 數字 1 ~ n 兩次出現的位置,開頭多了一組 [0, 0] for i, x in enumerate(line, start=1): # 依序由 line 取出位置 i、數字 x if arr[x][0] == 0: arr[x][0] = i # 如果是第1個 x,更新 arr[x][0] else: arr[x][1] = i # 如果是第2個 x,更新 arr[x][1] bit = [0]*(2*n+1) # binary index tree,共有 2*n+1 個節點,節點 0 沒有資料 ans = 0 # 答案 for i in range(1, n+1): # 依照 i = 1 ~ n 出現的位置,修改 bit 中相關結點儲存的數量 ans += psum(bit, arr[i][1]-1) - psum(bit, arr[i][0]) # ans 加上 psum[right-1] ~ psum[start] update(bit, arr[i][0], 1) # 呼叫 update,更新 bit 中的 arr[i][0],數量加 1 update(bit, arr[i][1], 1) # 呼叫 update,更新 bit 中的 arr[i][1],數量加 1 print(ans) # 印出答案 ``` <br /><br /> ### 寫法4,使用線段樹 第3筆測資就超時,但是程式碼邏輯是對的,改寫成 C++ 版本就能通過測試。 ```python= """ 自訂函式,更新線段樹,輸入儲存樹用的串列 tree,根節點編號 node, 節點編號 start ~ end,要修改的索引值 idx,要加上或減去的差值 val """ def update(tree, node, start, end, idx, val): if start == end: # 遞迴出口 tree[node] += val # tree[node] 的加上 val return mid = (end - start) // 2 + start # 中間值 leftNode = 2*node + 1 # 左子樹節點編號 rightNode = 2*node + 2 # 右子樹節點編號 if idx >= start and idx <= mid: # idx 在左子樹 update(tree, leftNode, start, mid, idx, val) else: # idx 在右子樹 update(tree, rightNode, mid+1, end, idx, val) tree[node] = tree[leftNode] + tree[rightNode] # node 的值為左、右子樹節點相加 """ 自訂函式,找 [L, R] 區間加總 """ def query(tree, node, start, end, L, R): # 計算前 [L, R] 區間和 if R < start or L > end: return 0 # 遞迴出口,[L, R] 和 [start, end] 沒有重疊 # 遞迴出口,要找的範圍只剩一個點或者 [start, end] 在 [L, R] 之間 if start == end or (L <= start and R >= end): return tree[node] mid = (end - start) // 2 + start # 中間值 leftNode = 2*node + 1 # 左子樹節點編號 rightNode = 2*node + 2 # 右子樹節點編號 sumLeft = query(tree, leftNode, start, mid, L, R) # 遞迴,找左子樹 sumRight = query(tree, rightNode, mid+1, end, L, R) # 遞迴,找右子樹 return sumLeft + sumRight # 回傳左、右子樹加總 """ 解題過程 """ n = int(input()) # 讀取高度最大值 n line = list(map(int, input().split())) # 讀取每個位置的高度 arr = [0]*(2*n) # 數字 1 ~ n 兩次出現的位置,2個一組 cnt = [0]*(n+1) # 數字 1 ~ n 兩次出現的次數,開頭多1個0 for i, x in enumerate(line): # 依序由 line 取出位置 i、數字 x if cnt[x] == 0: # 如果 x 還沒有出現過 cnt[x] += 1 # cnt[x] 加 1 arr[2*x-2] = i # 如果是第1個 x,更新 arr[2*x-2] else: arr[2*x-1] = i # 如果是第2個 x,更新 arr[2*x-1] tree = [0]*(4*2*n) # 線段樹,陣列長度是資料點數量 2*n 的4倍 ans = 0 # 答案 for i in range(1, n+1): # 依照 i = 1 ~ n 出現的位置,修改 tree 中相關結點儲存的數量 ans += query(tree, 0, 0, 2*n-1, arr[2*i-2]+1, arr[2*i-1]-1) # ans 加上 i 兩個位置之間已填入的資料數量 update(tree, 0, 0, 2*n-1, arr[2*i-2], 1) # 呼叫 update,更新 tree 中的 arr[2*i-2],數量加 1 update(tree, 0, 0, 2*n-1, arr[2*i-1], 1) # 呼叫 update,更新 tree 中的 arr[2*i-1],數量加 1 print(ans) # 印出答案 ``` <br /><br /> ## C++ 程式碼 ### 寫法1,使用 set 及遞迴 用 int 會溢位,要使用 long。費時最久約為 0.4 s,使用記憶體最多約為 8.5 MB,通過測試。 ```cpp= #include <iostream> #include <vector> #include <set> using namespace std; /* 分治法,輸入串列 arr,下限 low,上限 high */ long sol(vector<long>& arr, long low, long high) { if (arr.size() <= 2) return 0; // 遞迴出口,長度小於等於2,不能再切 long d = 0, mid = (high - low) / 2 + low; // 答案 d,中間值 mid vector<long> large, small; // 大於等於 mid 的資料,小於等於 mid 的資料 set<long> between; // 找某個數字位於多少大數對之間 for(long e : arr) { // 由 arr 依序取出資料 e if (e <= mid) { // e 小於等於 mid d += between.size(); // 更新 d,加上 between 的長度 small.push_back(e); // e 加入 small } else { large.push_back(e); // e 加入 large if (between.count(e) == 1) between.erase(e); // 如果 e 在 between 之中則移除 else between.insert(e); // 否則將 e 加入 between } } d += sol(small, low, mid); // 遞迴,找小於等於 x 的部分 d += sol(large, mid+1, high); // 遞迴,找大於等於 x 的部分 return d; // 回傳 d } int main() { long n; cin >> n; vector<long> a (2*n, 0); for(long i=0; i<2*n; i++) cin >> a[i]; cout << sol(a, 1, n) << "\n"; return 0; } ``` <br /><br /> ### 寫法2,使用二元索引樹 (binary index tree, BIT) 用 int 會溢位,要使用 long。費時最久約為 37 ms,使用記憶體最多約為 2.6 MB,通過測試。 ```cpp= #include <iostream> #include <utility> #include <vector> using namespace std; void update(vector<long>& bit, int idx, int val) { for(int i=idx; i<=(int)bit.size(); i+=i&(-i)) bit[i] += (long)val; } long psum(vector<long>& bit, int n) { long result = 0; for(int i=n; i>0; i-=i&(-i)) result += bit[i]; return result; } int main() { ios::sync_with_stdio(0); cin.tie(0); int n; cin >> n; vector<pair<int, int>> arr (n+1, make_pair(0, 0)); for(int i=1; i<=2*n; i++) { int x; cin >> x; if (arr[x].first == 0) arr[x].first = i; else arr[x].second = i; } vector<long> bit (2*n+1, 0); long ans = 0; for(int i=1; i<=n; i++) { ans += psum(bit, arr[i].second - 1) - psum(bit, arr[i].first); update(bit, arr[i].first, 1); update(bit, arr[i].second, 1); } cout << ans << "\n"; return 0; } ``` <br /><br /> ### 寫法3,使用線段樹 用 int 會溢位,要使用 long。費時最久約為 0.1 s,使用記憶體最多約為 7.6 MB,通過測試。 ```cpp= #include <iostream> #include <vector> using namespace std; /* 自訂函式,更新線段樹,輸入儲存樹用的串列 tree,根節點編號 node, 節點編號 start ~ end,要修改的索引值 idx,要加上或減去的差值 val */ void update(vector<long>& tree, int node, int start, int end, int idx, long val) { if (start == end) { // 遞迴出口 tree[node] += val; // tree[node] 的加上 val return; } int mid = (end - start) / 2 + start; // 中間值 int leftNode = 2*node + 1; // 左子樹節點編號 int rightNode = 2*node + 2; // 右子樹節點編號 if (idx >= start && idx <= mid) // idx 在左子樹 update(tree, leftNode, start, mid, idx, val); else // idx 在右子樹 update(tree, rightNode, mid+1, end, idx, val); tree[node] = tree[leftNode] + tree[rightNode]; // node 的值為左、右子樹節點相加 } /* 自訂函式,找 [L, R] 區間加總 */ long query(vector<long>& tree, int node, int start, int end, int L, int R) { if (R < start || L > end) return 0; // 遞迴出口,[L, R] 和 [start, end] 沒有重疊 // 遞迴出口,要找的範圍只剩一個點或者 [start, end] 在 [L, R] 之間 if (start == end || (L <= start and R >= end)) return tree[node]; int mid = (end - start) / 2 + start; // 中間值 int leftNode = 2*node + 1; // 左子樹節點編號 int rightNode = 2*node + 2; // 右子樹節點編號 long sumLeft = query(tree, leftNode, start, mid, L, R); // 遞迴,找左子樹 long sumRight = query(tree, rightNode, mid+1, end, L, R); // 遞迴,找右子樹 return sumLeft + sumRight; // 回傳左、右子樹加總 } int main() { ios::sync_with_stdio(0); cin.tie(0); int n; cin >> n; // 讀取高度最大值 n vector<int> arr (2*n, 0); // 數字 1 ~ n 兩次出現的位置,2個一組 vector<int> cnt (n+1, 0); // 數字 1 ~ n 兩次出現的次數,開頭多1個0 for(int i=0; i<2*n; i++) { // 讀取 2*n 個高度 int x; cin >> x; if (cnt[x] == 0) { // 如果 x 還沒有出現過 cnt[x]++; // cnt[x] 加 1 arr[2*x-2] = i; // 如果是第1個 x,更新 arr[2*x-2] } else arr[2*x-1] = i; // 如果是第2個 x,更新 arr[2*x-1] } vector<long> tree (4*2*n, 0); // 線段樹,陣列長度是資料點數量 2*n 的4倍 long ans = 0; // 答案 for(int i=1; i<=n; i++) { // 依照 i = 1 ~ n 出現的位置,修改 tree 中相關結點儲存的數量 ans += query(tree, 0, 0, 2*n-1, arr[2*i-2]+1, arr[2*i-1]-1); // ans 加上 i 兩個位置之間已填入的資料數量 update(tree, 0, 0, 2*n-1, arr[2*i-2], 1); // 呼叫 update,更新 tree 中的 arr[2*i-2],數量加 1 update(tree, 0, 0, 2*n-1, arr[2*i-1], 1); // 呼叫 update,更新 tree 中的 arr[2*i-1],數量加 1 } cout << ans << "\n"; // 印出答案 return 0; } ``` <br /><br /> ## 參考資料 1. 吳邦一教授的 APCS 解題影片〈[PyAp45 Python APCS 題解:低地距離(AP202010_4), ZeroJudge f315](https://youtu.be/IkZADcf3lcE?si=UMibzXb50j6xbt2r)〉 2. [【資料結構】二元索引樹 | 樹狀數組 | Fenwick Tree | Binary Indexed Tree](https://youtu.be/ErmFGGw7f-s?si=iu-HsqIJgKInuH8K) 3. [樹狀數組](https://youtu.be/v2Q4ZjPeFuc?si=RWpbwjH6l4b4-_Fq) 4. [线段树 (segment tree)](https://youtu.be/e_bK-dgPvfM?si=xO1anO0X13PdFd7k) --- ###### tags:`APCS`、`Python`、`C++`