AtCoder Grand Contest 044 简要题解
从这里开始
因为比赛的时候在路上,所以又成功错过下分和被神仙 jerome_wei 吊起来打(按在地上摩擦)的好机会。
Problem A Pay to Win
把这个过程倒过来。不难发现到下一次除之前,要么是加到 $\lfloor n/d \rfloor d$ 要么是 $\lceil n/d \rceil d$,或者直接减到 0.
直接用 map 记忆化的复杂度为 $O(T \log^3 N \log \log N)$。具体的来说,每个状态只可能是 $\lfloor \frac{n}{2^a3^b5^c}\rfloor$ 或者 $\lceil\frac{n}{2^a3^b5^c}\rceil$,对于 $ \left\lceil\frac{\lfloor \frac{n}{u}\rfloor }{v}\right\rceil $ 的情况有 $\left \lfloor\frac{n}{uv} \right \rfloor = \left\lfloor\frac{\lfloor \frac{n}{u}\rfloor }{v}\right\rfloor \leqslant \left\lceil\frac{\lfloor \frac{n}{u}\rfloor }{v}\right\rceil \leqslant \left\lceil\frac{\lceil\frac{n}{u}\rceil}{v}\right\rceil = \left\lceil\frac{n}{uv}\right\rceil$
Code
#include <bits/stdc++.h> using namespace std; #define ll long long #define pli pair<ll, int> template <typename T> bool vmin(T& a, T b) { return (a > b) ? (a = b, true) : false; } const ll llf = 1e18; ll n; int T, A, B, C, D; map<ll, ll> F; vector<pair<int, int>> tr; ll dfs(ll n) { if (n == 0) { return 0; } else if (n == 1) { return D; } else if (F.count(n)) { return F[n]; } ll ret = llf; if (n <= ret / D) { ret = n * D; } for (auto t : tr) { int d = t.first; int c = t.second; ll nn = n / d * d; vmin(ret, dfs(nn / d) + (n - nn) * D + c); nn = (n + d - 1) / d * d; vmin(ret, dfs(nn / d) + (nn - n) * D + c); } return F[n] = ret; } void solve() { cin >> n >> A >> B >> C >> D; F.clear(); tr = vector<pair<int, int>> {make_pair(2, A), make_pair(3, B), make_pair(5, C)}; cout << dfs(n) << '\n'; } int main() { cin >> T; while (T--) { solve(); } return 0; }
Problem B Joker
容易发现初始最短路之和为 $O(n^3)$。一个人离开后暴力更新会产生改变的位置,时间复杂度不会超过初始最短路之和。
Code
#include <bits/stdc++.h> using namespace std; typedef bool boolean; template <typename T> T smin(T x) { return x; } template <typename T, typename ...K> T smin(T a, const K &...args) { return min(a, smin(args...)); } const int N = 505; const int mov[4][2] = {{1, 0}, {0, 1}, {-1, 0}, {0, -1}}; int n; int ans = 0; int f[N][N]; int vis[N][N]; bool occupy[N][N]; queue<int> qx0, qy0, qx1, qy1; void update(int x, int y) { static int dfc = 0; ++dfc; qx0.push(x); qy0.push(y); while (!qx0.empty() || !qx1.empty()) { int ex = -1, ey = -1; if (!qx0.empty()) { ex = qx0.front(); ey = qy0.front(); qx0.pop(); qy0.pop(); } else { ex = qx1.front(); ey = qy1.front(); qx1.pop(); qy1.pop(); } if (vis[ex][ey] == dfc) { continue; } vis[ex][ey] = dfc; int w = f[ex][ey] + occupy[ex][ey]; for (int i = 0; i < 4; i++) { int nx = ex + mov[i][0]; int ny = ey + mov[i][1]; if (nx >= 1 && nx <= n && ny >= 1 && ny <= n && w < f[nx][ny]) { f[nx][ny] = w; if (!occupy[ex][ey]) { qx0.push(nx); qy0.push(ny); } else { qx1.push(nx); qy1.push(ny); } } } } } int main() { scanf("%d", &n); for (int i = 1; i <= n; i++) { for (int j = 1; j <= n; j++) { f[i][j] = smin(i - 1, j - 1, n - i, n - j); occupy[i][j] = true; } } for (int i = 1, _ = n * n, t, x, y; i <= _; i++) { scanf("%d", &t); x = (t - 1) / n + 1; y = t - (x - 1) * n; ans += f[x][y]; occupy[x][y] = false; update(x, y); } printf("%d\n", ans); return 0; }
Problem C Strange Dance
建一个 Trie 树,维护位置 $i$ 上的是谁。
对于 S 操作就是一个全局交换 2,3 子树。
对于 R 操作做一次循环位移,然后暴力递归有进位的子树。
Code
#include <bits/stdc++.h> using namespace std; int ans[540000]; typedef class TrieNode { public: TrieNode *ch[3]; bool swp12; int id; void swap12() { swp12 ^= 1; swap(ch[1], ch[2]); } bool leaf() { return !ch[0]; } void push_down() { if (swp12) { ch[0]->swap12(); ch[1]->swap12(); ch[2]->swap12(); swp12 = false; } } void get_ans(int base, int v) { if (leaf()) { ans[id] = v; return; } push_down(); ch[0]->get_ans(base * 3, v); ch[1]->get_ans(base * 3, v += base); ch[2]->get_ans(base * 3, v += base); } } TrieNode; TrieNode pool[1000000]; TrieNode *_top = pool; TrieNode *newnode() { return _top++; } typedef class Trie { public: TrieNode* rt; void build(TrieNode*& p, int r, int base, int v) { p = newnode(); if (r == 0) { p->id = v; return; } build(p->ch[0], r - 1, base * 3, v); build(p->ch[1], r - 1, base * 3, v += base); build(p->ch[2], r - 1, base * 3, v += base); } void build(int n) { build(rt, n, 1, 0); } void swap12() { rt->swap12(); } void update(TrieNode*& p) { if (p->leaf()) { return; } p->push_down(); swap(p->ch[0], p->ch[1]); swap(p->ch[0], p->ch[2]); update(p->ch[0]); } void update() { update(rt); } void get_ans() { rt->get_ans(1, 0); } } Trie; int n; char T[200005]; Trie tr; int main() { scanf("%d", &n); scanf("%s", T + 1); int m = strlen(T + 1); tr.build(n); for (int i = 1; i <= m; i++) { char c = T[i]; if (c == 'S') { tr.swap12(); } else { tr.update(); } } tr.get_ans(); int all = 1; for (int i = 1; i <= n; i++) { all *= 3; } for (int i = 0; i < all; i++) { printf("%d ", ans[i]); } return 0; }
Problem D Guess the Password
考虑询问 62 次长度为 128 的全 a 串,全 b 串.....然后可以知道每种字符有多少个。
注意到如果有一个长度为 $l$ 的串是原串的子序列,那么如果在这个串中插入一个字符 $c$ 使得询问的结果减少 1,这意味着插入后仍然是原串的子序列。
考虑如果我们已经知道两个字符集不相交的串 $s, t$ 分别是原串的子序列,我们怎么把它们合并。考虑在 $s$ 的每个位置依次插入 $t$ 的下一个未确定字符,然后判断它是否是原串的子序列。
然后做一个简单归并就好了。
Code
#include <bits/stdc++.h> using namespace std; int query(string s) { cout << "? " << s << '\n'; cout.flush(); int dis; cin >> dis; return dis; } int L; string charset; vector<string> strs; string merge(int l, int r) { if (l == r) { return strs[l]; } int mid = (l + r) >> 1; string sl = merge(l, mid); string sr = merge(mid + 1, r); string cur = ""; int should = L - sl.length(); int pr = 0, _pr = (signed) sr.size(); for (int i = 0; i < (signed) sl.size(); i++) { while (pr < _pr && query(cur + sr[pr] + sl.substr(i)) == should - 1) { cur += sr[pr++]; should--; } cur += sl[i]; } while (pr < _pr) { cur += sr[pr++]; } return cur; } int main() { for (int i = 0; i < 26; i++) { charset += (char) ('a' + i); } for (int i = 0; i < 26; i++) { charset += (char) ('A' + i); } for (int i = 0; i < 10; i++) { charset += (char) ('0' + i); } L = 0; for (int i = 0; i < 62; i++) { char c = charset[i]; string s; int cnt; s.assign(128, c); L += (cnt = 128 - query(s)); strs.push_back(s.substr(0, cnt)); } string ans = merge(0, (signed) strs.size() - 1); cout << "! " << ans << '\n'; cout.flush(); return 0; }
Problem E Random Pawn
首先假设第一个和最后一个都是 $A$ 中最大的,如果不是这样的话可以通过旋转,然后再在后面增加一个来实现。
因为到 $A$ 最大的地方一定会停止,因此现在问题变成了链上。
设 $E_i$ 表示从 $i$ 出发的最大期望收益,显然有 $E_i = \max\{\frac{E_{i - 1} + E_{i + 1}}{2} - B_i, A_i\}$。
考虑有 $B_i$ 非常地难处理,考虑把它搞掉。
设 $F_i = E_i - C_i$,那么有 $F_i = \max\{\frac{E_{i - 1} - C_{i - 1} + E_{i + 1} - C_{i + 1}}{2}+ \frac{C_{i - 1} + C_{i + 1}}{2} - C_i - B_i, A_i - C_i\}$ 。
因此 $C_i$ 满足 $ \frac{C_{i - 1} + C_{i + 1}}{2} - C_i - B_i = 0$。钦定 $C_0 = C_1 = 0$,然后可以简单构造出来。
注意到 $C_{i + 1} - C_i - 2B_i = C_i - C_{i - 1}$ 因此 $C_i$ 大概是 $2B_i$ 做二次前缀和,因此范围大概是 $10^{12}$ 左右。
考虑最终的序列中是硬点若干位置 $p$,使得 $F_p = A_p$。接下来假定 $A_i$ 都已经减去了 $C_i$。
考虑知道一段最左端为 $l$,最右端为 $r$ 时怎么计算中间的贡献。不难发现中间的 $F$ 满足 $F_{i} - F_{i - 1} = F_{i + 1} - F_i$,因此有 $F_i = \frac{(i - l) A_r + (r - i) A_l}{r - l}$。
然后求一个和有 $\sum_{l <i < r} F_i = \frac{1}{2}(A_l + A_r) (r - l - 1)$。
如果答案被计算两次,每一段各计算一次首尾的贡献,最终再计算一次开头和结尾的贡献。
那么此时一段的贡献为 $(A_l + A_r) (r - l)$,不难发现这个是某个梯形面积的两倍。
不难发现取 $\{(i, A_i)\}$ 的上凸壳的点的时候,这个面积能达到最大值。(因为显然这个时候 $F_i$ 达到了最大值)
Code
#include <bits/stdc++.h> using namespace std; #define ll long long typedef class Point { public: ll x, y; Point() { } Point(ll x, ll y) : x(x), y(y) { } } Point; Point operator - (Point a, Point b) { return Point(a.x - b.x, a.y - b.y); } ll cross(Point a, Point b) { return a.x * b.y - a.y * b.x; } int n; int main() { scanf("%d", &n); vector<ll> A (n), B (n); for (auto& x : A) { scanf("%lld", &x); } for (auto& x : B) { scanf("%lld", &x); } int id = 0; for (int i = 1; i < n; i++) { if (A[i] > A[id]) { id = i; } } rotate(A.begin(), A.begin() + id, A.end()); rotate(B.begin(), B.begin() + id, B.end()); A.push_back(A[0]); B.push_back(B[0]); vector<ll> C (n + 1, 0); for (int i = 2; i <= n; i++) { C[i] = 2 * (C[i - 1] + B[i - 1]) - C[i - 2]; } int tp = 0; vector<Point> stk (n + 3); for (int i = 0; i <= n; i++) { Point P (i, A[i] - C[i]); while (tp >= 2 && cross(stk[tp] - stk[tp - 1], P - stk[tp]) >= 0) tp--; stk[++tp] = P; } ll res = 0; for (int i = 2; i <= tp; i++) { res += (stk[i - 1].y + stk[i].y) * (stk[i].x - stk[i - 1].x); } res += stk[1].y + stk[tp].y; for (int i = 0; i <= n; i++) { res += 2 * C[i]; } res -= A[0] * 2; double ans = 1.0 * res / (2 * n); printf("%.12lf\n", ans); return 0; }
Problem F Name-Preserving Clubs
假设有 $k$ 个集合,那么考虑建一个 $k \times n$ 的矩阵,每一位填 0 或者 1,表示这个集合中是否包含这个元素。
首先考虑任意两个集合都不同的情况。
如果称一个上述矩阵是好的,那么当且仅当任意打乱它的列(不能和原来相同),不存在一种方式使得打乱行和最初的矩阵相同。不难发现,这和题目中的一个 name-preserving configuration 一一对应。
不难证明一个好的矩阵任意两列都不同,因为如果存在两列相同,我们交换这两列, 它和原来一模一样,这和定义矛盾。
假设一个矩阵 $A$ 是好的,可以注意到下面两个性质:
- $A$ 的转置 $A^{T}$ 是好的。
- 考虑 $2^k$ 种不同的列,由其中所有不存在于 $A$ 的列构成的矩阵 $A^{C}$ 也是好的。
前者如果不成立,那么对应的行列操作可以应用到 $A$ 上使得操作后和它自己相同。因为没有任意一行或者一列是相同的,所以行列都至少操作 1 次,因此这是满足定义的。
注意到行列操作是独立的,因此先打乱行,再打乱列是等价的。
对于后者,考虑如果不成立,那么打乱行后,使得和原来的列集合相同,可以推出,做这些打乱行操作,可以使得 $A$ 不是好的。
推论 设 $c(k, n)$ 表示本质不同的 $k \times n$ 的好的矩阵的数量,那么有 $c(k, n) = c(n, k), c(k, n) = c(k, 2^{k} - n)$
证明由上述讨论易得。
设 $g(n)$ 表示最小的 $k$ 使得 $c(k, n) > 0$,那么有:
性质1 $2^{g(n)} - n \geqslant g(g(n))$
证明 不断应用推论可得 $c(g(n), n) = c(g(n), 2^{g(n)} - n) = c(2^{g(n)} - n, g(n))$,然后由定义可得。
设函数 $G(n)$ 满足 $G(1) = 0$,$G(n)$ 是最小的 $k$ 满足 $2^{k} - n \geqslant G(k)$。
引理1 对于 $n > 1$,那么有 $0 \leqslant G(i) - G(i - 1) \leqslant 1$。
证明 首先不难用归纳法证明 $G(i) < i$。然后 $G(i) \geqslant G(i - 1)$ 比较显然,这里略去证明。
考虑用归纳法,当 $n = 2,3$ 的时候显然。
考虑 $n = i(i > 3)$ 的情形。因为 $2^{G(i - 1)} - (i - 1) \geqslant G(G(i - 1))$,$2^{G(i - 1)} \geqslant 2$,所以有 $2^{G(i - 1) + 1} - i \geqslant 2^{G(i - 1)} + 1 - (i - 1) \geqslant G(G(i - 1)) + 1 \geqslant G(G(i - 1) + 1)$ 。
引理2 对于 $k \geqslant G(n)$,那么都有 $2^k - n \geqslant G(k)$
证明 考虑用归纳法,当 $k = G(n)$ 显然成立。
当 $k > G(n)$ 的时候因为有 $2^{k - 1} \geqslant 1$,所以有 $2^{k} - n \geqslant 2^{k -1} + 1 - n \geqslant G(k - 1) + 1 \geqslant G(k)$。
引理3 $c(k, n) > 0$ 当且仅当 $G(n) \leqslant k \leqslant 2^n - G(n)$。
证明 必要性显然,考虑充分性。
考虑使用归纳法,当 $n = 1$ 的时候显然成立。下面当 $n > 1$ 的时候
如果 $G(n) \leqslant k < n$,那么可以用推论使得变为 $k > n$ 的情形。现在我们来证明它满足条件,因为 $k \geqslant G(n)$,所以有 $2^k - n \geqslant G(k)$,所以有 $n \leqslant 2^n - G(k)$。因为 $k \leqslant 2^n - G(n)$,所以 $2^n - k \geqslant G(n)$,因此 $n \geqslant G(k)$,因此有 $G(k) \leqslant n \leqslant 2^n - G(k)$。
如果 $2^{n - 1} < k$,那么可以用推论使得变为 $k < 2^{n - 1}$ 的情形。我们还是来证明它满足条件,因为 $2^n - k < k$,所以只用证明 $G(n) \leqslant 2^n - k$。因为 $2^n - G(n) \geqslant k$,移项可得它成立。
现在考虑 $n \leqslant k \leqslant 2^{n - 1}$。
考虑构造一个集合 $\{\{1\}, \{1, 2\}, \{2, 3\}, \{3, 4\}, \cdots, \{n - 1, n\}\}$。然后对于剩下 $k - n$ 个填任意大小大于等于 $3$ 的集合。
可以手动验证当 $n = 2, n = 3$ 可行,当 $n \geqslant 4$ 的时候,满足大小大于等于 $3$ 的集合至少占了一半,因此一定可行。
引理4 当 $6 \leqslant k \leqslant n \leqslant 2^{k - 1}$ 时, $c(k, n) > 1000$
证明的思路大概是考虑先构造一个大小为 $k - 2$ 的集合,包含 $\{1, 2\}, \{2, 3\}, \cdots, \{k - 2, k -1\}$,然后将其中一个或者其中 $k - 3$ 个取补集。剩下的 $n - k + 2$ 个集合随便塞大小不为 2 或者 $k - 2$ 的集合。
然后考虑两个集合可能相同的情况。由于 yyf 非常菜,目前还不会,所以咕咕咕咕。结论是 $n = 4$ 的时候答案加上 1,$n = 7$ 的时候答案加上 2.
对于剩下 $k \leqslant 5, n \leqslant 2^{k - 1}$ 的情况写一个爆搜就行了。如果您的爆搜比较慢,请打表。
Code
#include <bits/stdc++.h> using namespace std; /* #include <bits/stdc++.h> using namespace std; const int Nmx = 5; const int fac[6] = {1, 1, 2, 6, 24, 120}; int shuf[Nmx + 1][120][1 << Nmx]; void prepare() { auto arrange = [&] (int v, int l, const vector<int>& p) { int rt = 0; for (auto x : p) { if (v & 1) { rt |= 1 << x; } v >>= 1; } return rt; }; for (int l = 1; l <= Nmx; l++) { vector<int> p (l); for (int i = 0; i < l; i++) { p[i] = i; } for (int id = 0; next_permutation(p.begin(), p.end()); id++) { for (int i = 0; i < (1 << l); i++) { shuf[l][id][i] = arrange(i, l, p); } } } } bool check(int n, const vector<int>& tb) { static int vis[1 << Nmx], dfc = 0; ++dfc; for (auto x : tb) { vis[x] = dfc; } for (int i = 0; i < fac[n] - 1; i++) { bool flag = true; for (int j = 0; j < (signed) tb.size() && flag; j++) { flag = vis[shuf[n][i][tb[j]]] == dfc; } if (flag) { return false; } } return true; } int ans = 0; vector<int> stk; void dfs(int l, int d, int k, int ls) { if (d == k) { ans += check(l, stk); if (ans > 1000 * fac[l]) { throw 1; } return; } for (int s = ls + 1; s < (1 << l); s++) { stk[d] = s; dfs(l, d + 1, k, s); } } int n, k; int table[6][20]; int main() { prepare(); for (int k = 1; k <= 5; k++) { for (int n = k; n <= (1 << (k - 1)); n++) { ::k = k; ::n = n; ::ans = 0; stk.resize(n); try { dfs(k, 0, n, -1); } catch(int) { ans = 1001 * fac[k]; } cout << k << " " << n << '\n'; ans /= fac[k]; table[k][n] = ans; } } cout << "{{}"; for (int i = 1; i <= 5; i++) { cout << ",\n {"; cout << table[i][0]; for (int j = 1; j <= 16; j++) { cout << ", " << table[i][j]; } cout << "}"; } cout << "};\n"; return 0; } */ const int table[6][20] = {{}, {1, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 4, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 36, 108, 220, 334, 384, 0, 0, 0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 976, 1001, 1001, 1001, 1001, 1001, 1001, 1001, 1001, 1001, 1001, 1001}}; #define ll long long vector<int> G (105); void prepare() { G[1] = 0; for (int n = 2; n <= 100; n++) { for (int k = 1; k < n; k++) { if ((1 << k) >= n && ((1 << k) - n) >= G[k]) { G[n] = k; break; } } } } int g(ll n) { if (n <= 100) { return G[n]; } for (int k = 1; ; k++) { if ((1ll << k) >= n && ((1ll << k) - n) >= g(k)) { return k; } } assert(false); return -1; } int solve(ll n, ll k) { if (n < k) { swap(n, k); } if (k < 63 && (1ll << k) - n < n) { return solve((1ll << k) - n, k); } if (k > 5) { return 1001; } return k == 0 ? 1 : table[k][n]; } int main() { ll n; prepare(); scanf("%lld", &n); int ans = solve(n, g(n)); ans += (n == 4); ans += (n == 7) * 2; printf("%d\n", (ans > 1000) ? -1 : ans); return 0; }