--- tags: Programming Contest --- # AtCoder Beginner Contest 179 題解 [題目連結](https://atcoder.jp/contests/abc179) ## 心得 沒趕上比賽,只好賽後寫。 ## A, B (o´罒'o) ## C. A x B + C 差點就把質因數分解刻下去,後來想到用解不等式的方法即可在 $O(N)$ 時間解出。 我們可以枚舉 A 也枚舉 B,然後計算出 C,但這樣不夠快。 一個改進是:只枚舉 A,然後找出 B 的上下界,即 B 的合法範圍。 $$ 0 < A \cdot B = N - C < N \implies 0 < B < N / A $$ 加總所有的 A 對應的 B 的範圍即為答案。 ```python import math N = int(input()) ans = 0 for A in range(1, N): lb = 1 ub = math.ceil(N / A) - 1 ans += ub - lb + 1 print(ans) ``` ## D. Leaping Tak 大家應該都能想出這樣的 dp: ``` dp[i] = number of way to go from cell 0 to cell i dp[0] = 1 for i in range(1, N): dp[i] = sum(dp[i - d] for d in [seg.l, seg.r] for seg in segs) ``` 這題的問題是有太多 `d` 了,我們又需要加總的操作,於是就想到用線段樹或 BIT 來存 dp 陣列。 ```python mod = 998244353 class BIT: ''' All methods use 0-based index while the internal use 1-based index ''' def __init__(self, N, val=0): self.data = [val] * (N + 1) self.N = N def add_at(self, idx, val): i = idx + 1 while i <= self.N: self.data[i] += val self.data[i] %= mod i += i & (-i) def sum_to(self, idx): res = 0 i = idx + 1 while i > 0: res += self.data[i] res %= mod i -= i & (-i) return res def range_sum(self, l, r): # [l, r] return ((self.sum_to(r) - self.sum_to(l - 1)) + mod) % mod def __str__(self): data = [self.range_sum(i, i) for i in range(self.N)] return '[ ' + ' '.join(map(str, data)) + ' ]' N, K = map(int, input().split()) segs = [list(map(int, input().split())) for _ in range(K)] bit = BIT(N, 0) bit.add_at(0, 1) for i in range(1, N): for (l, r) in segs: lb = max(i - r, 0) ub = i - l if ub >= 0: val = bit.range_sum(lb, ub) bit.add_at(i, val) print(bit.range_sum(N - 1, N - 1)) ``` ## E. Sequence Sum 這題有兩種解法,一種是找循環,另一種是倍增法。 ### 找循環 假設 A 的長度為 M + 1,我們可以宣稱裡面必有重覆的值,因為餘數只有 M 種可能。 有重覆的值代表序列會循環。於是我們可以將長度為 N 的 A 可以分成 3 個部份: 1. 進入循環前的部份,讓我們稱之 `prefix` 2. 不斷循環的部份,讓我們將重覆的部份稱為 `cycle`,其完整的循環次數為 `n_full_period`。 3. 剩下的部份,稱之為 `suffix`,會是 `cycle` 的前綴。 例如 `N, X, M = 7, 2, 1001` 時,`A = [2, 4, 16, 256, 471, 620, 16]`,可以分成: 1. `prefix = [2, 4]` 2. `cycle = [16, 256, 471, 620]`,`n_full_period = 1` 3. `suffix = [16]`。 讓我們找出這三個部份,再算各自會貢獻多少到答案中。 實作時,記得特判 `N < len(prefix)` 的情況,即還沒出現循環,序列 A 就結束了。 ```python import math def solve(): N, X, M = map(int, input().split()) A = [X] vis = [-1] * M vis[X] = 0 for i in range(1, M + 1): X = X ** 2 % M if vis[X] != -1: prefix = A[:vis[X]] cycle = A[vis[X]:] break A.append(X) vis[X] = i if N < len(prefix): return sum(prefix[:N]) else: n_full_period = (N - len(prefix)) // len(cycle) suffix_length = N - len(prefix) - n_full_period * len(cycle) ans = 0 ans += sum(prefix) ans += sum(cycle) * n_full_period ans += sum(cycle[:suffix_length]) return ans print(solve()) ``` ### 倍增法 能透過找循環解的題目八成都可以用倍增法來解,這題也不例外。 讓我們這樣定義: ``` nxt[i][r] = the value after 2 ** i squaring starting from r dp[i][r] = the sum of sequence of length 2 ** i starting from r ``` 其中 r 的範圍是 [0, M - 1]。這兩個陣列都能透過標準的倍增法 dp 在 $O(lgN M)$ 時間求出。有了這兩陣列,那我們就能透過對 N 進行二進位拆解,算出序列 A 的和。具體而言是將序列 A 拆成一些不重疊的部份,這些部份的長度正好是 N 的二進制拆解。例如: ``` N = 11 = 1 + 2 + 8 A = A[0:1], A[1:3], A[3:11] Sum of each parts are: sum(A[0:1]) = dp[0][A[0] % M] sum(A[1:3]) = dp[1][A[1] % M] sum(A[3:11]) = dp[3][A[3] % M] ``` 其中我們知道 `A[0]` 是 `X`,`A[1]` 是 `nxt[1][A[0]]`,`A[3]` 是 `nxt[2][A[1]]`,以此類推。 ```python import math N, X, M = map(int, input().split()) H = math.ceil(math.log2(N)) + 1 nxt = [[0] * M for _ in range(H)] dp = [[0] * M for _ in range(H)] nxt[0] = [r ** 2 % M for r in range(M)] dp[0] = list(range(M)) for i in range(1, H): for r in range(M): nxt[i][r] = nxt[i - 1][nxt[i - 1][r]] dp[i][r] = dp[i - 1][r] + dp[i - 1][nxt[i - 1][r]] ans = 0 r = X % M for i in range(H): if N & (1 << i): ans += dp[i][r] r = nxt[i][r] print(ans) ``` ## F. Simplified Reversi 假設我們維護了兩種東西(0-based index): 1. 每個 row 從左往右第一個遇到的白色棋子的 col,叫 `min_col_of_row`。 2. 每個 col 從上往下第一個遇到的白色棋子的 row,叫 `min_row_of_col`。 那每個 query 都可以高速地處理了。 當 query 為 `1 col`,此時共翻面 `min_row_of_col[col] - 1` 個黑色棋子。同時,row `1` ~ row `min_row_of_col[col]` 的 `min_col_of_row` 都變成 `col`(如果值比 col 大的話)。同理當 query 為 `2 row` 時,會有 `min_col_of_row[row] - 1` 個黑色棋子被翻面,然後影響到 col `1` ~ col `min_col_of_row[row]` 的 `min_row_of_col`。 於是我們想到使用二個 LazySegTree 來存這二種東西,這二個線段樹的更新不是一般的將某個區間設為某個值,而是將某個區間用某個值 'change min' 看看。這個方法的時間為 $O(NlgN + Q)$。 實作上,我使用之前不久 release 的 [atcoder library](https://github.com/atcoder/ac-library)。記得 submit 時,先用 `python expander.py main.cpp` 將 atcoder library 的 hpp 注入到 source code 中,要不然會 CE。 第一次使用 ACL,[document](https://atcoder.github.io/ac-library/document_en/lazysegtree.html) 中的 op, e, mapping, composition, and id 實在讓人搞不懂,我猜測了一下各自的作用,並將他們重新命名:我認為 S 指的就是 data 型態,F 是 lazy 的型態,5 個要求的函式分別為: 1. `S op(S a, S b)`:如何 gather。 2. `S default_data()`:data 的預設值。 3. `S apply_lazy(F lazy, S data)`:如何將 lazy 應用到 data 上。 4. `F push_lazy(F root, F child)`:如何將 root 的 lazy 推到 child 的 lazy,回傳 child 的 lazy。 5. `F default_lazy()`:lazy 的預設值。 希望沒有理解錯。最後抱怨一下 ACL 的線段樹不能丟 functor,只能丟 function pointer。 ```cpp #include <atcoder/lazysegtree> #include <iostream> #include <vector> using namespace std; using namespace atcoder; using ll = long long; using S = long long; using F = long long; S op(S a, S b) { return min(a, b); } S default_data() { return S(1e10); } S apply_lazy(F lazy, S data) { return min(lazy, data); } F push_lazy(F root_lazy, F child_lazy) { return min(root_lazy, child_lazy); } F default_lazy() { return F(1e10); } using SegTree = lazy_segtree<S, op, default_data, F, apply_lazy, push_lazy, default_lazy>; int main() { ios::sync_with_stdio(false); cin.tie(nullptr); ll N, Q; cin >> N >> Q; auto min_col_of_row = SegTree(N); auto min_row_of_col = SegTree(N); min_col_of_row.apply(1, N, N - 1); min_row_of_col.apply(1, N, N - 1); ll ans = (N - 2) * (N - 2); while (Q--) { int cmd, x; cin >> cmd >> x; x--; if (cmd == 1) { auto row = min_row_of_col.get(x); ans -= row - 1; min_col_of_row.apply(1, row + 1, x); } else { auto col = min_col_of_row.get(x); ans -= col - 1; min_row_of_col.apply(1, col + 1, x); } } cout << ans << endl; return 0; } ``` :::success [< 回到所有題解](https://hackmd.io/@amoshyc/HkWWepaPv/) :::