【比賽題解】NOIP2021 題解
T1. 報數
可以先把十進位制表示下數位含有 \(7\) 的所有數都求出來,然後去列舉這些數的倍數,將其標記。
如果當前列舉到的數被標記過了則就不需要再列舉倍數了(因為列舉的倍數肯定也被標記過了)。
時間複雜度 \(\mathcal{O}(n \log \log n)\)。
#include <cstdio> #include <cstring> #include <algorithm> using namespace std; inline int read() { int x = 0, f = 1; char s = getchar(); while (s < '0' || s > '9') { if (s == '-') f = -f; s = getchar(); } while (s >= '0' && s <= '9') { x = x * 10 + s - '0'; s = getchar(); } return x * f; } const int SIZE = 1e7 + 500; bool ori[SIZE + 10]; bool f[SIZE + 10]; int go[SIZE + 10]; void prework(int N) { for (int i = 1; i <= N; i ++) ori[i] = ori[i / 10] | (i % 10 == 7); for (int i = 1; i <= N; i ++) { if (!ori[i]) continue; if (f[i]) continue; for (int j = i; j <= N; j += i) f[j] = 1; } for (int i = N; i >= 1; i --) if (f[i]) go[i] = go[i + 1]; else go[i] = i; } void work() { int x = read(); if (f[x]) puts("-1"); else printf("%d\n", go[x + 1]); } int main() { prework(SIZE); int T = read(); while (T --) work(); return 0; } // I hope changle_cyx can pray for me.
T2. 數列
注意到直接在序列後加入一個數會比較難維護,可以考慮從小到大向序列中加數。
設 \(f(i, j, h, S)\) 表示:已經選了 \(j\) 個數,選出的數的值域為 \([0, i]\),二進位制表示中的 \([0, i]\) 位中有 \(h\) 個 \(1\),\([i + 1, *]\) 中的二進位制表示為 \(S\) 時的方案數。
可以考慮列舉選了多少個 \(i\)。假設選了 \(x\) 個 \(i\),那麼就有轉移:
\[f(i, j, h + (S + x) \bmod 2, \left\lfloor\frac{S + x}{2}\right\rfloor) \gets_+ f(i - 1, j - x, h, S) \cdot v_i^x \cdot \dbinom{j}{x} \]最後答案即為:\(\sum\limits_{h + \text{count}(S) \leq k} f(m, n, h, S)\)
其中 \(\text{count}(S)\) 表示 \(S\) 二進位制表示下 \(1\) 的個數。
時間複雜度 \(\mathcal{O}(n^4m)\)。
#include <cstdio> #include <cstring> #include <algorithm> using namespace std; inline int read() { int x = 0, f = 1; char s = getchar(); while (s < '0' || s > '9') { if (s == '-') f = -f; s = getchar(); } while (s >= '0' && s <= '9') { x = x * 10 + s - '0'; s = getchar(); } return x * f; } int power(int a, int b, int p) { int ans = 1; for (; b; b >>= 1) { if (b & 1) ans = 1ll * ans * a % p; a = 1ll * a * a % p; } return ans; } const int N = 35, M = 110; const int mod = 998244353; int n, m, k; int val[M]; int fact[N], inv[N]; int C(int n, int m) { return 1ll * inv[n - m] * inv[m] % mod * fact[n] % mod; } int f[M][N][N][N]; void add(int &x, long long d) { x = (x + d) % mod; } int cnt[N]; int main() { n = read(), m = read(), k = read(); for (int i = 0; i <= m; i ++) val[i] = read(); fact[0] = 1; for (int i = 1; i <= n; i ++) fact[i] = 1ll * fact[i - 1] * i % mod; inv[n] = power(fact[n], mod - 2, mod); for (int i = n - 1; i >= 0; i --) inv[i] = 1ll * inv[i + 1] * (i + 1) % mod; for (int x = 0, cur = 1; x <= n; x ++, cur = 1ll * cur * val[0] % mod) f[0][x][x % 2][x / 2] = cur; for (int i = 1; i <= m; i ++) { for (int x = 0, cur = 1; x <= n; x ++, cur = 1ll * cur * val[i] % mod) { for (int j = x; j <= n; j ++) { for (int h = 0; h <= n; h ++) { for (int S = 0; S <= n; S ++) { if (!f[i - 1][j - x][h][S]) continue; int d = 1ll * f[i - 1][j - x][h][S] * C(j, x) % mod * cur % mod; add(f[i][j][h + (S + x) % 2][(S + x) / 2], d); } } } } } for (int S = 1; S <= n; S ++) cnt[S] = cnt[S ^ (S & -S)] + 1; int ans = 0; for (int h = 0; h <= n; h ++) for (int S = 0; S <= n; S ++) if (h + cnt[S] <= k) add(ans, f[m][n][h][S]); printf("%d\n", ans); return 0; }
T3. 方差
簡單處理可以把方差的 \(n^2\) 倍化為:
\[n\left(\sum\limits_{i = 1}^n a_i^2\right) - \left(\sum\limits_{i = 1}^n a_i\right)^2 \]那麼,只需要知道一個序列的數值和與數值平方和,就可以計算出該序列的方差了。
記差分陣列 \(b_i = a_i - a_{i - 1}\),那麼一次對 \(i\) 的操作就相當於交換 \(b_i, b_{i + 1}\)。
由於鄰項交換,並且可以進行任意多次操作。
那麼所有操作結束後的新差分陣列 \(\{b'\}\) 可以是原差分陣列 \(\{b\}\) 的任意一個排列。
一個結論:最優解的差分陣列一定是先遞減、後遞增的(也就是單谷的)。
根據這個性質,我們就可以先將所有的 \(n - 1\) 個差分值從小到大排序,然後從小到大加入差分值。由於方差關係的是資料之間的相對大小,我們不妨欽定一個基準點,它的值為 \(0\)。
設 \(\text{sum}_i\) 表示前 \(i\) 小的差分值之和是多少。
設 \(f(i, S)\) 表示:考慮到前 \(i\) 個差分值,當前的數值和為 \(S\) 時,數值平方和最小是多少。
若向當前序列的左端加數,則有轉移:
\[f(i, S + b_i \cdot i) \gets_\text{min} f(i - 1, S) + 2 \cdot S \cdot b_i + i \cdot b_i^2 \]若向當前序列的右端加數,則有轉移:
\[f(i, S + \text{sum}_i) \gets_{\min} f(i - 1, S) + \text{sum}_i^2 \]最後的答案即為 \(\min\{n \cdot f(n - 1, S) - S^2\}\)。
設值域為 \(m\),直接做顯然是 \(\mathcal{O}(n^2m)\) 的。
但是注意到整個序列 \(\{a\}\) 中不同的數總共就只有 \(\min(n, m)\) 個,也就是說差分陣列中不為 \(0\) 的有效差分值不超過 \(\min(n, m)\) 個,於是就可以優化到 \(\mathcal{O}(\min(n, m) \cdot nm)\)。
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
inline int read() {
int x = 0, f = 1; char s = getchar();
while (s < '0' || s > '9') { if (s == '-') f = -f; s = getchar(); }
while (s >= '0' && s <= '9') { x = x * 10 + s - '0'; s = getchar(); }
return x * f;
}
const int N = 10010, SZ = 500100;
const int inf = 0x3f3f3f3f;
int n, m, L, res;
int a[N], b[N];
int sum[N];
int p, q;
int f[2][SZ];
int main() {
n = read();
for (int i = 1; i <= n; i ++)
a[i] = read(), m = max(m, a[i]);
L = n * m;
for (int i = 1; i < n; i ++) {
b[i] = a[i + 1] - a[i];
if (!b[i]) res ++;
}
sort(b + 1, b + n);
for (int i = 1; i < n; i ++)
sum[i] = sum[i - 1] + b[i];
p = 0, q = 1;
f[p][0] = 0;
for (int S = 1; S <= L; S ++) f[p][S] = inf;
for (int i = res + 1; i < n; i ++) {
p ^= 1, q ^= 1;
for (int S = 0; S <= L; S ++) f[p][S] = inf;
for (int S = 0; S <= L; S ++) {
f[p][S + sum[i]] = min(f[p][S + sum[i]], f[q][S] + sum[i] * sum[i]);
f[p][S + b[i] * i] = min(f[p][S + b[i] * i], f[q][S] + 2 * S * b[i] + i * b[i] * b[i]);
}
}
long long ans = 1e18;
for (int S = 0; S <= L; S ++)
ans = min(ans, 1ll * n * f[p][S] - 1ll * S * S);
printf("%lld\n", ans);
return 0;
}
T4. 棋局
(待填 ...)
keep the love forever.