--- tags: 進階班 --- # 10th 上學期進階班期中考題解 ## A. 果然我的高一平凡生活搞錯了。完 #### NA 30% 我們可以知道:$Hank$ 若經歷第 $i$ 個事件,他前一個事件必定是第 $(i - n - 1)$ ~ $(i - 1)$ 個事件。 而且他必經歷第 $0$ 個事件$\implies dp[0] = 1$ 因此: $\displaystyle \begin{cases} dp[0] = 1\\dp[i] = dp[i - 1] + dp[i - 2] + ... + dp[1] + dp[0]\quad\quad if\quad 1\leq i\leq n + 1\\dp[i] = dp[i - 1] + dp[i - 2] + ... + dp[i - n - 1]\quad\quad if\quad i > n + 1\\ \end{cases}$ 然後用複雜度很高的遞迴寫(不包含$DP$ ) :::spoiler `code` ```cpp= #include<bits/stdc++.h> #define LL long long using namespace std; constexpr int mod = 1e9 + 7; LL fib(int n, int k) { if (n == 1) { if (k < 0) return 0; else if (k <= 1) return 1; return (fib(n, k - 1) + fib(n, k - 2)) % mod; } else if (n == 2) { if (k < 0) return 0; else if (k <= 1) return 1; else if (k == 2) return 2; return (fib(n, k - 1) + fib(n, k - 2) + fib(n, k - 3)) % mod; } return 0; } int main() { //cin.tie(nullptr), ios_base::sync_with_stdio(false); int t, n, k; cin >> t; while (t--) { cin >> n >> k; cout << fib(n, k) << '\n'; } } ``` ::: #### NA 60% 在 $dp[i] = dp[i - 1] + dp[i - 2] + ... + dp[1] + dp[0]\quad\quad if\quad 1\leq i\leq n + 1$ 時 我們可以發現: $dp[0] = 1 = 2^0,dp[1] = 1 = 2^0, dp[2] = dp[0] + dp[1] = 2 = 2^1\implies dp[i] = 2^{i - 1}$ 然後這次用迴圈做: :::spoiler `code` ```cpp= #include<bits/stdc++.h> #define LL long long using namespace std; constexpr int mod = 1e9 + 7; LL arr[100001]; LL sum(int l, int r) { LL ret = 0; for (; l <= r; l++) ret = (ret + arr[l]) % mod; return ret; } int main() { //cin.tie(nullptr), ios_base::sync_with_stdio(false); int T, n, k; cin >> T; while (T--) { cin >> n >> k; for (int x = 0; x <= n; x++) arr[x] = pow(2, x); for (int x = n + 1; x < k; x++) arr[x] = sum(x - n - 1, x - 1); cout << arr[k - 1] << '\n'; } } ``` ::: #### NA 90 % $i>n+1$ 時 $dp[i] = dp[i - 1] + dp[i - 2] + .............. + dp[i - n - 1]$ $dp[i-1] = \quad\quad\quad\; dp[i - 2] + dp[i - 3] + ... + dp[i-n-1]+dp[i - n - 2]$ 兩式相扣 $\implies dp[i] - dp[i - 1] = dp[i - 1] - dp[i - n - 2]$ $\implies dp[i] = dp[i - 1]\times 2 - dp[i - n - 2]$ 實作可以兩種: 1. 把空間滾動,只開長度為 $n + 2$ 的陣列 2. 開一個長度 $10^7$ 的陣列,不過注意資料型態要是 $int\implies$每次運算都要 $mod\;10^9 + 7$ 注意 $(dp[i - 1]\times 2 - dp[i - n - 2]) \% mod$ 可能是負的。(因為 $dp[i - 1]$ 取完餘數後可能很小) :::spoiler `code_1` ```cpp= #include<bits/stdc++.h> #define LL long long using namespace std; constexpr int mod = 1e9 + 7; int main() { //cin.tie(nullptr), ios_base::sync_with_stdio(false); int T, n, k; cin >> T; while (T--) { cin >> n >> k; LL arr[n + 2]; for (int x = 0; x <= n; x++) arr[x] = pow(2, x); arr[n + 1] = 1; for (int x = n + 1; x < k; x++) { arr[x % (n + 2)] -= arr[(x - 1) % (n + 2)] * 2; arr[x % (n + 2)] *= -1; arr[x % (n + 2)] = (arr[x % (n + 2)] + mod * 2) % mod; } cout << arr[(k - 1) % (n + 2)] << '\n'; } } ``` ::: :::spoiler `code_2` ```cpp= #include<bits/stdc++.h> #define LL long long using namespace std; constexpr int mod = 1e9 + 7; int arr[10000001]; int main() { //cin.tie(nullptr), ios_base::sync_with_stdio(false); int T, n, k; cin >> T; while (T--) { cin >> n >> k; arr[0] = 1; for (int x = 1; x <= n + 1; x++) arr[x] = pow(2, x - 1); for (int x = n + 2; x <= k; x++) arr[x] = ((2 * arr[x - 1] - arr[x - n - 2]) % mod + mod) % mod; cout << arr[k] << '\n'; } } ``` ::: #### AC 把前面幾個轉移式的條件整合一下: $M = \left [ \begin{array}{ccc} 2^0 & 2^1 & 2^2 & ... & 2^n\end{array}\right]\left [ \begin{array}{ccc} 0 & 0 & 0 & 0 & ... &0& 1\\1 & 0 & 0 & 0 & ... & 0&1\\0 & 1 & 0 & 0 & ... & 0&1\\ 0&0&1&0&... & 0&1\\⋮\\0 & 0& 0& 0& ... & 1&1\end{array}\right]^{k - 1}$ (右陣列是一個大小為 $(n + 1)\times (n + 1)$ 的陣列) 然後用矩陣的方式運算,答案為 $M[0]$ :::spoiler `code_進階班` ```cpp= #include<bits/stdc++.h> #define LL long long using namespace std; constexpr LL m = 1e9 + 7; LL n; vector<vector<LL>> I(21, vector<LL> (21, 0)); vector<vector<LL>> mat(const vector<vector<LL>> &x, const vector<vector<LL>> &y) { vector<vector<LL>> ret(n + 1, vector<LL> (n + 1, 0)); for (int i = 0; i <= n; i++) { for (int j = 0; j <= n; j++) { for (int k = 0; k <= n; k++) ret[i][j] = (ret[i][j] + x[i][k] * y[k][j]) % m; } } return ret; } vector<vector<LL>> mpow(vector<vector<LL>> x, LL y) { vector<vector<LL>> ret = I; for (; y; y >>= 1, x = mat(x, x)) { if (y & 1) ret = mat(ret, x); } return ret; } int main() { cin.tie(nullptr), ios_base::sync_with_stdio(false); LL T, k; cin >> T; while (T--) { cin >> n >> k; vector<vector<LL>> A(n + 1, vector<LL> (n + 1, 0)); vector<vector<LL>> B(n + 1, vector<LL> (n + 1, 0)); for (int x = 0; x <= n; x++) { A[0][x] = pow(2, x), I[x][x] = 1; if (x < n) B[x + 1][x] = 1; B[x][n] = 1; } A = mat(A, mpow(B, k - 1)); cout << A[0][0] << '\n'; } return 0; } ``` ::: :::spoiler `code_初階班` ```cpp= #include<bits/stdc++.h> #define LL long long using namespace std; const int m = 1000000007; int main() { LL T, n, k; cin >> T; while (T--) { cin >> n >> k; k--; LL A[21][21], B[21][21], I[21][21], tmp[21][21]; for (int i = 0; i <= n; i++) { for (int j = 0; j <= n; j++) A[i][j] = B[i][j] = I[i][j] = 0; } for (int x = 0; x <= n; x++) { A[0][x] = pow(2, x), I[x][x] = 1; if (x < n) B[x + 1][x] = 1; B[x][n] = 1; } while (k != 0) { if (k % 2 == 1) { for (int i = 0; i <= n; i++) { for (int j = 0; j <= n; j++) tmp[i][j] = 0; } for (int i = 0; i <= n; i++) { for (int j = 0; j <= n; j++) { for (int k = 0; k <= n; k++) tmp[i][j] = (tmp[i][j] + I[i][k] * B[k][j]) % m; } } for (int i = 0; i <= n; i++) { for (int j = 0; j <= n; j++) I[i][j] = tmp[i][j]; } } for (int i = 0; i <= n; i++) { for (int j = 0; j <= n; j++) tmp[i][j] = 0; } for (int i = 0; i <= n; i++) { for (int j = 0; j <= n; j++) { for (int k = 0; k <= n; k++) tmp[i][j] = (tmp[i][j] + B[i][k] * B[k][j]) % m; } } for (int i = 0; i <= n; i++) { for (int j = 0; j <= n; j++) B[i][j] = tmp[i][j]; } k /= 2; } for (int i = 0; i <= n; i++) { for (int j = 0; j <= n; j++) tmp[i][j] = 0; } for (int i = 0; i <= n; i++) { for (int j = 0; j <= n; j++) { for (int k = 0; k <= n; k++) tmp[i][j] = (tmp[i][j] + A[i][k] * I[k][j]) % m; } } cout << tmp[0][0] << '\n'; } return 0; } ``` ::: ## B. 從零開始的程設班生活 這題根據提示,可以類推成:$\cfrac{(str.size())!}{n('a')!\;n('b')!\;n('c')!\;\;...}$ 也就是字串長度階乘除以各個字元的重複次數階乘。 由於答案需要 $mod\;10^9 + 7$,且 $10^9 + 7\in prime$,因此可以使用費馬小定理求得模逆元。 值得注意的是,由於計算階乘的複雜度是 $O(N)$,且本題會常常用到,因此建表可以壓低複雜度 ($O(TN)\Rightarrow O(N)$)。 而階乘的模逆元表可以不用建,因為複雜度為 $O(lgN)$,比 $O(N)$ 快非常多, 直接求與建表的時間差距不會太大。 得分依據: #### NA 20% 不用 $mod$ #### NA 40% 只算分子且有 $mod\; 10^9 + 7$ #### NA 70% 沒建階乘表 #### AC 有建階乘表且使用 $IO$ 優化 :::spoiler `code` ```cpp= #include<bits/stdc++.h> #define LL long long #define mem(x) memset(x, 0, sizeof(x)) using namespace std; constexpr int m = 1e9 + 7; const int maxn = 1e5; LL inv[maxn + 1], fac[maxn + 1]; LL fpow(LL x, int y = m - 2) { LL ret = 1; for (; y; y >>= 1, x = x * x % m) { if (y & 1) ret = ret * x % m; } return ret; } int main() { cin.tie(nullptr), ios_base::sync_with_stdio(false); int T; string str; cin >> T; fac[0] = inv[0] = 1; for (int x = 1; x <= maxn; x++) fac[x] = fac[x - 1] * x % m, inv[x] = fpow(fac[x]); while (T--) { cin >> str; LL ans = fac[str.size()]; int tmp[123]; mem(tmp); for (auto & i : str) tmp[i]++; for (auto & i : tmp) ans = ans * inv[i] % m; cout << ans << '\n'; } } ``` ::: ## C. 小青蛙的夢想 跟[無限階梯](http://203.64.191.163/ShowProblem?problemid=a650)很類似。 前進$a, b$階的方法數相加就好了。 ### 轉移式 #### NA 20% 另$dp[i][j]\implies$ 在第`i`次抽卡且執行結束後在長度為$j$的方法數 {$$\displaystyle \begin{cases} dp[0][0] = 1\\dp[i][j] \equiv dp[i - 1][j -a ] + dp[i - 1][j - b]\;\;mod (10^9 + 7)\end{cases}$$ } 時間複雜度$O(NM)$ 空間複雜度$O(NM)$ #### AC 空間複雜度在$N, M$都為$10^4$空間複雜度會爆掉。 所以需要把它改成滾動的。 ::: spoiler `code` ```cpp= #include <bits/stdc++.h> using namespace std; #define V vector #define ll long long #define pii pair<int,int> const int mod = 1e9 + 7; int main () { ios::sync_with_stdio(0); cin.tie(0); cout.tie(0); int N, M; cin >> N >> M; V<V<int>> dp(2, V<int> (N, 0)); dp[1][0] = 1; //dp[~0&1][0] = 1; for(int i = 0, a, b; i < M; i++) { cin >> a >> b; a %= N; b %= N; for(int j = 0; j < N; j++) { int cur_pos_a = (j - a + N) % N; int cur_pos_b = (j - b + N) % N; dp[i&1][j] = (dp[~i&1][cur_pos_a] + dp[~i&1][cur_pos_b]); dp[i&1][j] %= mod; } } cout << dp[~M&1][0] << endl; } ``` ::: ## D. 追番科高校的劣等生 這題的重點在於**溫馨提示**, 他說「既然是追番,那當然不能跳集看對吧?」, 這時波司淺雪一定是從第一集、第二集,到最後一集這樣看。 因此 **淺雪得到的分數 = 看到第幾集$\times 10$**。 這時就會有 **NA**$50\%$ 的解法, 就每次都遍歷所有的集數的時數並加總, 直到大於等於淺雪所擁有的時間為止。 `Time Complexity:` $O(QN)$ :::spoiler `code` ```cpp= #include <bits/stdc++.h> using namespace std; int main() { cin.tie(0), ios_base::sync_with_stdio(0); int n, q; cin >> n; vector<int> vt(n); for(auto& i : vt) cin >> i; cin >> q; while(q--) { int m, ans = 0, sum = 0; cin >> m; for(int i = 0; i < n; ++i) { sum += vt[i]; if(sum <= m) ++ans; else break; } cout << 10*ans << '\n'; } } ``` ::: 如果要拿到 **AC** 的話, 只要把線性搜尋改成二分搜尋就好, 因為遍歷加總其實就是前綴和, 而在 $T_i\ge 1$ 的情況下, 前綴和必呈現嚴格遞增, 所以可以用二分搜尋解。 `Time Complexity:` $O(Q\log N)$ :::spoiler `code` ```cpp= #include <bits/stdc++.h> using namespace std; int main() { cin.tie(0), ios_base::sync_with_stdio(0); int n, q; cin >> n; vector<int> t(n); for(auto& i : t) cin >> i; for(int i = 1; i < n; ++i) t[i] += t[i-1]; cin >> q; while(q--) { int m; cin >> m; auto it = lower_bound(t.begin(), t.end(), m); if(*it == m) cout << 10*(it-t.begin()+1) << '\n'; else cout << 10*(it-t.begin()) << '\n'; } } ``` ::: ## E. 樹語國和旭誠國之間的愛恨情仇 這一題沒有安排部分分數, 基本上拿到部分分的就是剛好喇到:poop:。 這題的重點其實就是在凱能的位置, 題目說他正對著 $(0, 0)$, 而且能看到兩排, 所以很顯然的他的位置會是與最中間的斜角重和。 知道這點後轉移式就出來了 $$ dp_{i, j} = dp_{i-1, j} + dp_{i, j-1} + dp_{i-1, j-1} $$ 這裡的 $i$ 和 $j$ 的意義是要綁在一起看的, 意思是在第 $i$ 列、第 $j$ 行的**敵人的編號**, 而 $dp_{i, j}$ 代表的是**受到的傷害**。 但是這裡如果直接打成程式碼的話會錯, 因為除非使用大數運算, 不然沒有資料結構存的了受到的傷害 ($1 \leq D_i\leq 100$ 在經過轉移式的疊加後會超大)。 這時要用到一點的數論, 由於 $DP$ 裡面的每一格都是正整數, 所以相加只會愈來愈大, 也就是說如果這一格的傷害殺的掉 Hank, 那從這格連鎖的敵人所受到的傷害也一定殺的掉 Hank。 所以轉移式可以改成 $$ dp_{i, j} = min(H,\quad dp_{i-1, j} + dp_{i, j-1} + dp_{i-1, j-1}) $$ 或著是 $$ dp_{i, j} = min(10^9,\quad dp_{i-1, j} + dp_{i, j-1} + dp_{i-1, j-1}) $$ :::spoiler `code` ```cpp= #include <bits/stdc++.h> using namespace std; using ll = long long; const ll mxH = 1e9, mxN = 500, mxM = 500; ll dp[mxN][mxM]; void solve() { int m, n, e, h, qx, qy; cin >> n >> m >> e >> h >> qx >> qy, --qx, --qy; memset(dp, 0, sizeof(dp)); for(int i = 0; i < e; ++i) { ll x, y, d; cin >> x >> y >> d, --x, --y; dp[x][y] += d; } for(int i = 1; i < n; ++i) dp[i][0] = min(mxH, dp[i][0] + dp[i-1][0]); for(int j = 1; j < m; ++j) dp[0][j] = min(mxH, dp[0][j] + dp[0][j-1]); for(int i = 1; i < n; ++i) { for(int j = 1; j < m; ++j) { dp[i][j] = min(mxH, dp[i-1][j] + dp[i][j-1] + dp[i-1][j-1]); } } cout << (dp[qx][qy] >= h ? "Yes" : "No") << '\n'; } int main() { cin.tie(0), ios_base::sync_with_stdio(0); int T; cin >> T; while(T--) solve(); } ``` :::