AtCoder Grand Contest 044 简要题解
从这里开始
因为比赛的时候在路上,所以又成功错过下分和被神仙 jerome_wei 吊起来打(按在地上摩擦)的好机会。
Problem A Pay to Win
把这个过程倒过来。不难发现到下一次除之前,要么是加到 要么是 ,或者直接减到 0.
直接用 map 记忆化的复杂度为 。具体的来说,每个状态只可能是 或者 ,对于 的情况有
Code
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 | #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
容易发现初始最短路之和为 。一个人离开后暴力更新会产生改变的位置,时间复杂度不会超过初始最短路之和。
Code
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 | #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 树,维护位置 上的是谁。
对于 S 操作就是一个全局交换 2,3 子树。
对于 R 操作做一次循环位移,然后暴力递归有进位的子树。
Code
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 | #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 串.....然后可以知道每种字符有多少个。
注意到如果有一个长度为 的串是原串的子序列,那么如果在这个串中插入一个字符 使得询问的结果减少 1,这意味着插入后仍然是原串的子序列。
考虑如果我们已经知道两个字符集不相交的串 分别是原串的子序列,我们怎么把它们合并。考虑在 的每个位置依次插入 的下一个未确定字符,然后判断它是否是原串的子序列。
然后做一个简单归并就好了。
Code
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 | #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
首先假设第一个和最后一个都是 中最大的,如果不是这样的话可以通过旋转,然后再在后面增加一个来实现。
因为到 最大的地方一定会停止,因此现在问题变成了链上。
设 表示从 出发的最大期望收益,显然有 。
考虑有 非常地难处理,考虑把它搞掉。
设 ,那么有 。
因此 满足 。钦定 ,然后可以简单构造出来。
注意到 因此 大概是 做二次前缀和,因此范围大概是 左右。
考虑最终的序列中是硬点若干位置 ,使得 。接下来假定 都已经减去了 。
考虑知道一段最左端为 ,最右端为 时怎么计算中间的贡献。不难发现中间的 满足 ,因此有 。
然后求一个和有 。
如果答案被计算两次,每一段各计算一次首尾的贡献,最终再计算一次开头和结尾的贡献。
那么此时一段的贡献为 ,不难发现这个是某个梯形面积的两倍。
不难发现取 的上凸壳的点的时候,这个面积能达到最大值。(因为显然这个时候 达到了最大值)
Code
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 | #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
假设有 个集合,那么考虑建一个 的矩阵,每一位填 0 或者 1,表示这个集合中是否包含这个元素。
首先考虑任意两个集合都不同的情况。
如果称一个上述矩阵是好的,那么当且仅当任意打乱它的列(不能和原来相同),不存在一种方式使得打乱行和最初的矩阵相同。不难发现,这和题目中的一个 name-preserving configuration 一一对应。
不难证明一个好的矩阵任意两列都不同,因为如果存在两列相同,我们交换这两列, 它和原来一模一样,这和定义矛盾。
假设一个矩阵 是好的,可以注意到下面两个性质:
- 的转置 是好的。
- 考虑 种不同的列,由其中所有不存在于 的列构成的矩阵 也是好的。
前者如果不成立,那么对应的行列操作可以应用到 上使得操作后和它自己相同。因为没有任意一行或者一列是相同的,所以行列都至少操作 1 次,因此这是满足定义的。
注意到行列操作是独立的,因此先打乱行,再打乱列是等价的。
对于后者,考虑如果不成立,那么打乱行后,使得和原来的列集合相同,可以推出,做这些打乱行操作,可以使得 不是好的。
推论 设 表示本质不同的 的好的矩阵的数量,那么有
证明由上述讨论易得。
设 表示最小的 使得 ,那么有:
性质1
证明 不断应用推论可得 ,然后由定义可得。
设函数 满足 , 是最小的 满足 。
引理1 对于 ,那么有 。
证明 首先不难用归纳法证明 。然后 比较显然,这里略去证明。
考虑用归纳法,当 的时候显然。
考虑 的情形。因为 ,,所以有 。
引理2 对于 ,那么都有
证明 考虑用归纳法,当 显然成立。
当 的时候因为有 ,所以有 。
引理3 当且仅当 。
证明 必要性显然,考虑充分性。
考虑使用归纳法,当 的时候显然成立。下面当 的时候
如果 ,那么可以用推论使得变为 的情形。现在我们来证明它满足条件,因为 ,所以有 ,所以有 。因为 ,所以 ,因此 ,因此有 。
如果 ,那么可以用推论使得变为 的情形。我们还是来证明它满足条件,因为 ,所以只用证明 。因为 ,移项可得它成立。
现在考虑 。
考虑构造一个集合 。然后对于剩下 个填任意大小大于等于 的集合。
可以手动验证当 可行,当 的时候,满足大小大于等于 的集合至少占了一半,因此一定可行。
引理4 当 时,
证明的思路大概是考虑先构造一个大小为 的集合,包含 ,然后将其中一个或者其中 个取补集。剩下的 个集合随便塞大小不为 2 或者 的集合。
然后考虑两个集合可能相同的情况。由于 yyf 非常菜,目前还不会,所以咕咕咕咕。结论是 的时候答案加上 1, 的时候答案加上 2.
对于剩下 的情况写一个爆搜就行了。如果您的爆搜比较慢,请打表。
Code
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 | #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; } |
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列:基于图像分类模型对图像进行分类
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 25岁的心里话
· 按钮权限的设计及实现