AtCoder Beginner Contest 349
A - Zero Sum Game (abc349 A)
题目大意
个人游戏,每局有一人 分,有一人 分。
给定最后前 个人的分数,问第 个人的分数。
解题思路
零和游戏,所有人总分是 ,因此最后一个人的分数就是前 个人的分数和的相反数。
神奇的代码
n = input() print(-sum([int(i) for i in input().split()]))
B - Commencement (abc349 B)
题目大意
对于一个字符串,如果对于所有 ,都有恰好 或 个自负出现次,则该串是好串。
给定一个字符串,问它是不是好串。
解题思路
只有 ,统计每个字符的出现次数,再枚举 即可。
神奇的代码
#include <bits/stdc++.h> using namespace std; using LL = long long; int main(void) { ios::sync_with_stdio(false); cin.tie(0); cout.tie(0); string s; cin >> s; bool ok = true; map<char, int> cnt; for (auto c : s) cnt[c]++; auto check = [&](int c) { int cc = 0; for (auto& [k, v] : cnt) { cc += (v == c); } return cc == 0 || cc == 2; }; for (int i = 1; i <= s.size(); ++i) { ok &= check(i); } cout << (ok ? "Yes" : "No") << endl; return 0; }
C - Airport Code (abc349 C)
题目大意
给定一个字符串和字符串 ,问字符串 能否从字符串 得到。操作为:
- 从 挑三个字母,不改变顺序变成 。
- 从 挑两个字母,加上,不改变顺序变成 。
解题思路
就子序列匹配问题。就近匹配原则即可。
神奇的代码
#include <bits/stdc++.h> using namespace std; using LL = long long; int main(void) { ios::sync_with_stdio(false); cin.tie(0); cout.tie(0); string s, t; cin >> s >> t; if (t.back() == 'X') t.pop_back(); auto pos = s.find_first_of(tolower(t[0])); for (int i = 1; i < t.size() && pos < s.size(); ++i) { pos = s.find_first_of(tolower(t[i]), pos + 1); } if (pos < s.size()) cout << "Yes" << endl; else cout << "No" << endl; return 0; }
D - Divide Interval (abc349 D)
题目大意
给定序列,拆分成最少的序列个数,使得每个序列形如 。
给出拆分方案。
解题思路
拆分的序列个数最小,那肯定想让一个序列尽可能的长,而长的话,就是让尽可能大。
因此就贪心地让尽可能大,即 ,这里的是最大的 (这意味着 是奇数),并且 ,如果 ,那说明 太大了,就变成 来试试。
神奇的代码
#include <bits/stdc++.h> using namespace std; using LL = long long; int main(void) { ios::sync_with_stdio(false); cin.tie(0); cout.tie(0); LL l, r; cin >> l >> r; vector<array<LL, 2>> ans; while (l < r) { LL p2 = 1; LL bl = l; while (bl % 2 == 0 && l + p2 <= r) { bl >>= 1; p2 <<= 1; } while (l + p2 > r) { p2 >>= 1; bl <<= 1; } ans.push_back({l, l + p2}); l += p2; } cout << ans.size() << '\n'; for (auto& i : ans) { cout << i[0] << ' ' << i[1] << '\n'; } return 0; }
E - Weighted Tic-Tac-Toe (abc349 E)
题目大意
给定的网格,高桥和青木画 。
每个格子有分数。
若存在同行同列或同对角线,则对应方赢,否则全部画满后,所画格子的分数和较大者赢。
问最优策略下,谁赢。
解题思路
朴素的博弈,状态即为当前的棋盘样子,总状态数为 ,直接搜索即可。
即设 表示当前局面 的当前操作者(称为先手
)的必赢或必输 。
转移,则枚举当前操作者的行为,即选择哪个格子,进入后继状态。
如果所有后继状态都是(先手)必赢,那么当前状态则是(先手)必输,即如果所有, 是后继状态。
否则,如果有一个后继状态是先手必输,那么当前状态的先手就可以控制局面走向该状态,使得当前状态是必胜态,即如果存在一个。
转移显而易见是,总状态数只有 ,因此朴素搜索就可以通过了。
神奇的代码
#include <bits/stdc++.h> using namespace std; using LL = long long; int main(void) { ios::sync_with_stdio(false); cin.tie(0); cout.tie(0); typedef array<array<LL, 3>, 3> tu; tu a; for (auto& x : a) for (auto& y : x) cin >> y; map<tu, int> dp; auto check_end = [&](tu& pos) -> int { LL p1 = 0, p2 = 0; int left = 0; for (int i = 0; i < 3; ++i) { for (int j = 0; j < 3; ++j) { left += !pos[i][j]; p1 += (pos[i][j] == 1) * a[i][j]; p2 += (pos[i][j] == 2) * a[i][j]; } } if (left == 0) { if (p1 > p2) return 0; else return 1; } for (int i = 0; i < 3; ++i) { if (pos[i][0] == pos[i][1] && pos[i][1] == pos[i][2] && pos[i][0] != 0) { if (pos[i][0] == 1) return 0; else return 1; } if (pos[0][i] == pos[1][i] && pos[1][i] == pos[2][i] && pos[0][i] != 0) { if (pos[0][i] == 1) return 0; else return 1; } } if (pos[0][0] == pos[1][1] && pos[1][1] == pos[2][2] && pos[0][0] != 0) { if (pos[0][0] == 1) return 0; else return 1; } if (pos[0][2] == pos[1][1] && pos[1][1] == pos[2][0] && pos[0][2] != 0) { if (pos[0][2] == 1) return 0; else return 1; } return -1; }; auto dfs = [&](auto self, tu& pos, int role) -> bool { int status = check_end(pos); if (status != -1) { return dp[pos] = status == role; } if (dp.find(pos) != dp.end()) return dp[pos]; bool lose = false; for (auto& i : pos) for (auto& j : i) { if (j) continue; j = (role + 1); lose |= (!self(self, pos, role ^ 1)); j = 0; } return dp[pos] = lose ? 1 : 0; }; tu ini{}; bool win = dfs(dfs, ini, 0); cout << (win ? "Takahashi" : "Aoki") << endl; return 0; }
F - Subsequence LCM (abc349 F)
题目大意
给定一个序列和,问子序列的数量,使得其(最小公倍数)为 。
子序列之间的不同,当且仅当有元素在原位置不一样,即使它们的数字可能是一样的。
解题思路
我们可以依次考虑每个,选或不选,很显然是。
我们不能维护每个数选择的状态,但是要维护怎样的状态呢?是怎样的中间状态能够导出它们的最小公倍数呢。
一个简单的办法就是记录此时的最小公倍数,即表示考虑前 个数,选择若干后,最小公倍数是 的方案数。转移就考虑当前数选或不选, 如果选,则通过和 可以得到新的最小公倍数,转移到后继状态。
但是状态数有 ,太大了。
要简化状态数,一个比较明显的观察的是,并不是所有的都是必要的,只有 的因子的那些 才有可能转移到 ,其他的都不可能转移到 ,因此第二维状态数可以减小,但因子数的数量级大概是根号级别,也就是说还有 左右,还是不行。
如何继续简化呢,那得知道如何求解最小公倍数。两个数的最小公倍数是,但还有另一种求法,这种求法可以一次求解 个数的最小公倍数。
根据其定义,假设最小公倍数是,那就意味着 是每个数的倍数。
从质因数的角度来思考倍数关系,那就是 的每个质因子的幂 每个数对应质因子的幂。
为了让是最小的公倍数,那就让的每个质因子的幂就是这些数对应质因子幂的最大值,这样在保证是倍数的关系下,还能最小。(最大公因数对应的其实就是幂的最小值)
从上述求最小公倍数的角度来理解状态,其实就是记录了的各个质因子的幂的值。但稍微细想,结合转移的取幂的最大值
,会发现,这里的状态还是冗余的:我们无需记录各个质因子的幂
,而是记录是否达到对应的幂就可以了:还是能够转移,能够从最后得到正确答案。
由此就可以得到更加简化的状态:设 表示考虑前 个数,选择若干个后,其最小公倍数的各个质数幂的最大值是否达到
对应的质数幂的值的状态为 (对于每个 的质数,都有 未达到
或达到
这一 状态,因此是一个二进制压缩的状态)的选择方案数。初始条件是 。
转移就考虑,选择当前数后,状态 是否会变化。因此要事先预处理每个数选择后对这一状态的影响。即事先对质因数分解,再预处理每个 对转移的影响。
考虑时间复杂度, 为 ,至多有13个质数,总的复杂度是 ,粗略算也有 了。当前的 还过不了。考虑进一步优化。
神奇的代码
#include <bits/stdc++.h> using namespace std; using LL = long long; const int mo = 998244353; int main(void) { ios::sync_with_stdio(false); cin.tie(0); cout.tie(0); int n; LL m; cin >> n >> m; bool one = (m == 1); vector<pair<LL, int>> fac; int up = 1e8; for (int i = 2; i <= up; ++i) { if (m % i == 0) { fac.push_back({i, 0}); while (m % i == 0) { m /= i; fac.back().second++; } } } if (m != 1) { fac.push_back({m, 1}); } vector<int> a; for (int i = 0; i < n; ++i) { LL x; cin >> x; bool ok = true; int sign = 0; for (int j = 0; j < fac.size(); ++j) { auto& [p, v] = fac[j]; int cnt = 0; while (x % p == 0) { x /= p; ++cnt; } ok &= (cnt <= v); if (cnt == v) sign |= (1 << j); } ok &= (x == 1); if (ok) a.push_back(sign); } vector<int> dp(1 << fac.size(), 0); dp[0] = 1; for (int x : a) { vector<int> dp2 = dp; for (int i = 0; i < (1 << fac.size()); ++i) { dp2[i | x] = (dp2[i | x] + dp[i]); if (dp2[i | x] >= mo) dp2[i | x] -= mo; } dp.swap(dp2); } int ans = dp.back(); if (one) { ans = (ans - 1 + mo) % mo; } cout << ans << '\n'; return 0; }
欲知后事如何,且等作业写完后再写
如何继续优化呢,注意到个数,我们预处理它们对状态的转移的影响后,也即变成一堆标记后,容易意识到的一点是它们有很多重复的,比如很多数选择后它们对状态的影响是,因此可以把它们整合到一起考虑。
即处理出表示标记 出现的次数,标记种类的数量级同样是 ,然后对每个标记 考虑如何选(有种选法,然后标记影响,转移到后继状态即可。这样时间复杂度是 ,可以通过了。
最后要特判下的情况。
超时的1e9
#include <bits/stdc++.h> using namespace std; using LL = long long; const int mo = 998244353; long long qpower(long long a, long long b) { long long qwq = 1; while (b) { if (b & 1) qwq = qwq * a % mo; a = a * a % mo; b >>= 1; } return qwq; } int main(void) { ios::sync_with_stdio(false); cin.tie(0); cout.tie(0); int n; LL m; cin >> n >> m; bool one = (m == 1); vector<pair<LL, int>> fac; int up = 1e8; for (int i = 2; i <= up; ++i) { if (m % i == 0) { fac.push_back({i, 0}); while (m % i == 0) { m /= i; fac.back().second++; } } } if (m != 1) { fac.push_back({m, 1}); } vector<int> a; for (int i = 0; i < n; ++i) { LL x; cin >> x; bool ok = true; int sign = 0; for (int j = 0; j < fac.size(); ++j) { auto& [p, v] = fac[j]; int cnt = 0; while (x % p == 0) { x /= p; ++cnt; } ok &= (cnt <= v); if (cnt == v) sign |= (1 << j); } ok &= (x == 1); if (ok) a.push_back(sign); } vector<int> cnt(1 << fac.size(), 0); vector<int> dp(1 << fac.size(), 0); for (auto& i : a) cnt[i]++; dp[0] = 1; for (int x = 0; x < cnt.size(); ++x) { vector<int> dp2 = dp; int w = qpower(2, cnt[x]) - 1; if (w < 0) w += mo; for (int i = 0; i < (1 << fac.size()); ++i) { dp2[i | x] = dp2[i | x] + 1ll * dp[i] * w % mo; if (dp2[i | x] >= mo) dp2[i | x] -= mo; } dp.swap(dp2); } int ans = dp.back(); if (one) { ans = (ans - 1 + mo) % mo; } cout << ans << '\n'; return 0; }
看官方题解貌似可以通过zeta变换+莫比乌斯容斥降到
G - Palindrome Construction (abc349 G)
题目大意
<++>
解题思路
<++>
神奇的代码
本文作者:~Lanly~
本文链接:https://www.cnblogs.com/Lanly/p/18133534
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步