AtCoder Beginner Contest 288
A - Many A+B Problems (abc288 a)
题目大意
给定, ,输出 。
解题思路
范围不会爆,直接做即可。
神奇的代码
#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); int t; cin >> t; while(t--){ int a, b; cin >> a >> b; cout << a + b << '\n'; } return 0; }
B - Qualification Contest (abc288 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); int n, k; cin >> n >> k; vector<string> s(n); for(auto &i : s) cin >> i; sort(s.begin(), s.begin() + k); for(int i = 0; i < k; ++ i) cout << s[i] << '\n'; return 0; }
C - Don’t be cycle (abc288 c)
题目大意
给定一张无向图,要求删除一些边,使得没有环。
解题思路
根据定义,无环就是一棵树或者森林。
对原图跑一遍最小生成树,不在该树的边都是要删去的。
故答案就是总边数
减去最小生成树的边数
。
神奇的代码
#include <bits/stdc++.h> using namespace std; using LL = long long; class dsu { public: vector<int> p; vector<int> sz; int n; dsu(int _n) : n(_n) { p.resize(n); sz.resize(n); iota(p.begin(), p.end(), 0); fill(sz.begin(), sz.end(), 1); } inline int get(int x) { return (x == p[x] ? x : (p[x] = get(p[x]))); } inline bool unite(int x, int y) { x = get(x); y = get(y); if (x != y) { p[x] = y; sz[y] += sz[x]; return true; } return false; } }; int main(void) { ios::sync_with_stdio(false); cin.tie(0); cout.tie(0); int n, m; cin >> n >> m; dsu d(n); int ans = 0; for(int i = 0; i < m; ++ i){ int u, v; cin >> u >> v; -- u; -- v; if (d.unite(u, v)) ++ ans; } cout << m - ans << '\n'; return 0; }
D - Range Add Query (abc288 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); int n, k; cin >> n >> k; vector<LL> a(n); for(int i = 0; i < n; ++ i){ cin >> a[i]; if (i >= k) a[i] += a[i - k]; } int q; cin >> q; while(q--){ int l, r; cin >> l >> r; -- l; -- r; vector<LL> tmp(k); for(int i = 0, pos = r; i < k; ++ i, pos --){ tmp[pos % k] = a[pos]; } for(int i = 0, pos = l - 1; i < k && pos >= 0; ++ i, pos --){ tmp[pos % k] -= a[pos]; } cout << (set<LL>(tmp.begin(), tmp.end()).size() == 1 ? "Yes" : "No") << '\n'; } return 0; }
E - Wish List (abc288 e)
题目大意
给定个商品的价格 ,标号到 。你要买个物品,分别是 。同时给定一个数组 。购买规则为:
- 购买序号为的商品,其标号是未买商品的第小,其购入价格为 。
你可以买不需要的物品。
问购买所需物品的最小花费。
解题思路
考虑暴力,发现不仅要确定购买哪些商品,还需要规定购买这些商品的顺序。不同顺序代价会不一样(购买同一间商品的可能因购买顺序而不同)
再考虑暴力搜索过程中,当确定购买一个物品的代价时,需要知道一个物品的标号是目前第几小的。知道这两个状态后发现可以切割子问题,因此考虑。
一开始考虑 表示前个物品,其第个物品的标号是第 时的最小花费,转移就考虑该物品买或不买,当然如果是必须要买的物品
就不能不买
。但这个状态有问题,就是它规定了购买的顺序一定是标号从小到大的。而这显然不对。
那就不能设第j小
这样的状态,但转移的话需要知道物品标号排名
,所以考虑另一个状态,即 表示前个物品,已经购买了 个物品的最小花费,因为知道了买了个物品,就知道下一个要买的物品的标号
是第几小的
。
这状态看似和之前一样,但转移有点不同:当我决定买第个物品时,已知状态是购买了个物品,但不一定第 个物品是购买的第 件(它可以是之前购买的,注意,标号大的物品先买不会影响到标号小的物品的选择,即后来的决策不会影响先前的结果),因此其附加代价的值可以是 ,选择不同的 值 就是规定其购买的顺序。为了最小代价,那肯定是取最小的那个。
因此转移式就是:
当然如果是该物品必须买的话,就没有后面这一项。
转移式涉及区间最小值,可以事先预处理或者转移时递增维护。
神奇的代码
#include <bits/stdc++.h> using namespace std; using LL = long long; const LL inf = 1e18; int main(void) { ios::sync_with_stdio(false); cin.tie(0); cout.tie(0); int n, m; cin >> n >> m; vector<LL> a(n), c(n); for(auto &i : a) cin >> i; for(auto &i : c) cin >> i; vector<int> must(n); for(int i = 0; i < m; ++ i){ int x; cin >> x; -- x; must[x] = 1; } vector<LL> dp(n + 1, inf); dp[0] = 0; for(int i = 0; i < n; ++ i){ vector<LL> tmp(n + 1, inf); LL minn = inf; for(int j = 0; j <= i; ++ j){ minn = min(minn, c[i - j]); tmp[j + 1] = min(tmp[j + 1], dp[j] + a[i] + minn); if (!must[i]) tmp[j] = min(tmp[j], dp[j]); } dp.swap(tmp); } LL ans = *min_element(dp.begin(), dp.end()); cout << ans << '\n'; return 0; }
F - Integer Division (abc288 f)
题目大意
给定一个位数。 其有 个切割点,一种切割方案包括若干个切割点,其代价是,切割后的所有数字的乘积。
问所有的 种切割 方案的代价和。
解题思路
经典切分数字题,从爆搜的角度发现问题可切割,考虑。
设 表示前 个数的的所有切割方案的代价和,转移就是枚举最后一个切割点位置。
其转移式为(这里假设下标从开始,):
转移是,总的复杂度是 。暂且过不了,考虑优化转移。
考虑 和的转移式,发现两者非常相似,只需一点改动就可以转移。
可以发现两者只有变成 的 ,相当于原来的转移和
,乘以,然后加上个,再补上多的一项 就变成的转移和
了。
因此转移可以优化成 ,最终的复杂度就是
神奇的代码
#include <bits/stdc++.h> using namespace std; using LL = long long; const LL mo = 998244353; int main(void) { ios::sync_with_stdio(false); cin.tie(0); cout.tie(0); int n; string s; cin >> n >> s; LL ans = 0; LL presum = 1; for(int i = 0; i < n; ++ i){ int val = s[i] - '0'; ans = (ans * 10 + presum * val) % mo; presum = (presum + ans) % mo; } cout << ans << '\n'; return 0; }
G - 3^N Minesweeper (abc288 g)
题目大意
众所周知,在著名的扫雷游戏里,格子上有个数字,表示该格子的邻居中有炸弹的数量。注意一个格子最多只有一个炸弹。
但这里有个格子,标号,且格子与格子 相邻,当且仅当:
- 标号和 在三进制表示下,每个数位的值的
差的绝对值
不超过。
现给定每个格子邻居的炸弹数量,求每个格子的炸弹数量。
解题思路
设的三进制表示为: ,则表示成。
由题定义的邻居关系,可得
我们已知左边的值,要求右边的每一项,这显然是个容斥。
但观察到求和条件是个差的绝对值
这一非常规条件,直接反演难度非常大似乎反不动。
但这是数位上的条件,各个数位是独立的,我们尝试迭代数位的方式进行容斥。
考虑最简单的情况,即三进制表示下只有一位,很显然根据容斥,容易得到:
这里其实忽略了高位,以第一个式子为例,可以看成:
但此时的还不是答案的 ,该项的意义是:最低位是 , 其余位是数组意义的炸弹数。
但我们以上述的 的结果,对 进行同样方法的容斥,就得到 是 其值的,其余位是 数组意义的值。由此迭代的方式容斥,就能得到答案数组。
以官方题解的说法,每个数位有六种情况,已知的是由前三种情况组成的值,通过迭代容斥,能逐步得到由后三种情况组成的值。
这其实非常类似于fast Zeta transformation
我们设表示:低项是精确的(就是该值),剩下的项是满足邻居条件
的那些格子的炸弹数。
那么可以由得到,针对第项进行容斥。
而fast Zeta transformation
又名子集和(SOS DP, sum over subset),为了不重复计算,通过额外的一个信息 ,将前 位定义为精确值,后面的位定义为其子集(题目意义)的值,然后通过迭代计算得出的结果。
观察上面的图,每个节点的红色部分就是精确的,黑色部分是模糊的(子集的),其代表的值(所有叶子)就是黑色部分的所有子集的和。
当然本题是已知所谓子集
,求每个精确项的值,是个逆过程,其实是一样的,只是每次迭代由相加
变成了容斥
。但不变的是以迭代的方式求解(其实这也是FZT
的核心,具体每次迭代怎么计算因题而异)。
总的复杂度就是
代码实现的话,简单一点就是考虑对的计算,已知的是(全部都不是精确)的值,最终要求的就是 (全部是精确)的值。
因为每次都是用到的都是,因此最后一维可以压缩掉(压缩掉的话注意转移时引用计算的是正确的数),以及前项通过三进制压缩程一个数。
神奇的代码
#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); int n; cin >> n; vector<int> p3(n + 1); p3[0] = 1; for(int i = 1; i <= n; ++ i) p3[i] = p3[i - 1] * 3; vector<int> a(p3[n]); for(auto &i : a) cin >> i; for(int i = 0; i < n; ++ i){ int p = p3[i]; for(int j = 0; j < p3[n]; ++ j){ if ((j / p) % 3 == 0){ int a1 = j, a2 = a1 + p, a3 = a2 + p; int v1 = a[a1], v2 = a[a2], v3 = a[a3]; a[a1] = v2 - v3; a[a2] = v1 + v3 - v2; a[a3] = v2 - v1; } } } for(int i = 0; i < p3[n]; ++ i) cout << a[i] << " \n"[i == p3[n] - 1]; return 0; }
Ex - A Nameless Counting Problem (abc288 h)
题目大意
<++>
解题思路
<++>
神奇的代码
本文作者:~Lanly~
本文链接:https://www.cnblogs.com/Lanly/p/17094101.html
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步