AtCoder Beginner Contest 473 (ABC473) 解题报告
很简单的一场。
ABC473A
signed main(){
ios::sync_with_stdio(0), cin.tie(0);
int n, ans = 0;
cin >> n;
for(int i = 1; i <= n; i ++){
int x; cin >> x;
if(i > n / 2) ans += x;
}
cout << ans << "\n";
return 0;
}
ABC473B
统计所有出现奇数次的数的和。
int n, cnt[107], ans;
signed main(){
ios::sync_with_stdio(0), cin.tie(0);
cin >> n;
for(int i = 1; i <= n; i ++){
int x; cin >> x;
cnt[x] ++;
}
for(int i = 1; i <= 100; i ++){
ans += (cnt[i] & 1) * i;
}
cout << ans << "\n";
return 0;
}
ABC473C
只有出现次数最多,或者比最多少 \(1\) 的班级合法。
const int N = 2e5 + 7;
int n, k, cnt[N];
set<int> st;
int fst, snd;
signed main(){
ios::sync_with_stdio(0), cin.tie(0);
cin >> n >> k;
for(int i = 1; i <= n; i ++){
int x; cin >> x;
cnt[x] ++;
}
for(int i = 1; i <= n; i ++){
st.insert(cnt[i]);
}
fst = *st.rbegin();
int ans = 0;
for(int i = 1; i <= n; i ++){
ans += (cnt[i] == fst || cnt[i] == fst - 1);
}
cout << ans << "\n";
return 0;
}
ABC473D
暴力 DFS 加上对 \(x = n\) 时的 \(\mathcal{O}(1)\) 计算即可通过。
int n, k, a[17];
void dfs(int x, int rem){
if(x == n && (rem % n)) return;
if(x == n){
a[x] = rem / x;
for(int i = 1; i <= n; i ++) cout << a[i] << " ";
cout <<"\n"; return;
}
for(int i = 0; i * x <= rem; i ++){
a[x] = i, dfs(x + 1, rem - i * x);
}
}
调用 dfs(1, k)。
ABC473E
题目是想让你把一个数组分为若干段,一种方案的得分,是 \(k | sum\) 的段数,求最大得分。
这是个简单的 DP。首先肯定有 \(f_i\) 代表前 \(i\) 个的最大得分。但是这样是 \(\mathcal{O}(n^2)\)。题意等价于:找出尽可能多个互不相交的段,使得每段 \(k | sum\)。那么重新定义 \(f_i\) 代表前 \(i\) 个内的答案,显然有转移
\(\text{sum}(l, r)\) 代表区间和,对于后者,我们拿 std::map 维护每个前缀和下的最大 \(f\) 值即可,答案为 \(f_n\)。
const int N = 2e5 + 7;
int n, k, a[N], f[N];
map<int, int> mx;
signed main(){
ios::sync_with_stdio(0), cin.tie(0);
cin >> n >> k;
for(int i = 1; i <= n; i ++){
cin >> a[i];
a[i] += a[i - 1];
a[i] %= k;
}
mx[0] = 0;
for(int i = 1; i <= n; i ++){
f[i] = f[i - 1];
if(mx.count(a[i])) f[i] = max(f[i], mx[a[i]] + 1);
mx[a[i]] = max(mx[a[i]], f[i]);
}
cout << f[n] << "\n";
return 0;
}
ABC473F
题意是,字符串单点修改,查询区间字串是否能由一个空串反复插入 A,AB 得到。
你会发现满足条件的区间中,A 的个数一定不少于 B,并且 \([l, r]\) 的每个前缀都要满足这个条件。
如果你做过的题足够多,你会发现这是一个括号匹配。将 A,B 分别赋值为 \(1\),\(-1\),你会发现条件其实等价于 \([l, r]\) 的每个前缀和都要非负,所以 \([l, r]\) 中前缀和的最小值决定其是否合法。
那只需要维护整个数组的前缀和即可。使用一棵区间加,区间求最小值,求和的线段树即可。
struct sgt {
void add(int x, int l, int r, int ql, int qr, int k) {}
int qsum(int x, int l, int r, int ql, int qr) {}
int qmin(int x, int l, int r, int ql, int qr) {}
} SGT;
signed main(){
ios::sync_with_stdio(0), cin.tie(0);
int n; string s; cin >> n >> s;
s = "o" + s;
for(int i = 1; i <= n; i ++){
if(s[i] == 'A') SGT.add(1, 1, n, i, n, 1);
else SGT.add(1, 1, n, i, n, -1);
}
map<char, int> mp;
mp['A'] = 1, mp['B'] = -1;
int q; cin >> q;
while(q --){
int opt, x; cin >> opt >> x;
if(opt == 1){
char y; cin >> y;
SGT.add(1, 1, n, x, n, -mp[s[x]]);
s[x] = y;
SGT.add(1, 1, n, x, n, mp[s[x]]);
}
else{
int y; cin >> y;
int pre = (x > 1 ? SGT.qsum(1, 1, n, x - 1, x - 1) : 0);
int mn = SGT.qmin(1, 1, n, x, y) - pre; // [x, y] 上的最小前缀和
cout << (mn >= 0 ? "Yes\n" : "No\n");
}
}
return 0;
}
ABC473G
题意是,有 \(n\) 张正面朝下的牌,数字为 \([1, n]\),现在你不知道每张牌的具体数字。每次你可以选择翻一张牌。如果比当前牌小的牌全都被拿走,这张牌也可以被拿走。
特别地,第一张牌可以直接拿走。求在最优策略下,使用 \(v\) 步翻完所有牌的概率。
然后考虑一下这个所谓的最优策略是啥:显然是如果当前的牌符合条件就直接拿走,否则记住这个位置,下次能拿就拿。
那么肯定是,一个牌最多贡献两次操作数。
并且,我们还发现,最优策略下的操作数一定处于 \([n, 2n - 1]\) 之间。假设初始操作数为 \(n\) 的情况下。假设我们还有 \(s\) 张牌不知道是什么,那么不论现在你已经拿走了多少张牌,下一次操作时,你肯定有 \(\frac{s - 1}{s}\) 的概率使得操作数 \(+1\),有 \(\frac{1}{s}\) 的概率操作数不变。
这样,如果我们的目标操作数是 \(v = n + k\),那么在前 \(n\) 次操作中(其实前 \(n\) 次操作结束后我们肯定已经知道所有牌上的数了),必须有 \(k\) 次选到了 \(\frac{s - 1}{s}\),另外 \(n - k\) 次选到 \(\frac{1}{s}\)。我们让 \(h(i, 0) = \frac{1}{i}, h(i, 1) = \frac{i - 1}{i}\)。定义一个 \(01\) 数组 \(g\),\(\text{count(g)}\) 代表 \(g\) 里 \(1\) 的个数。我们其实是想求
然后推式子,定义 \(z(i, 0) = 1, z(i, 1) = \frac{h(i, 1)}{h(i, 0)} = i - 1\),则上述式子等于
进一步化简可得
其实到这一步就很明显了,显然 \(\prod_i h(i, 0)\) 和 \(g\) 数组无关,那么其实我们想要计算的其实是 \(z_{*, 1}\) 数组的所有 \(k\) 子集的乘积之和。
推到这一步之后应该很可做了但是我不会,问了一下 DeepSeek 说是可以使用 NTT 之类的科技来求,就可以达到一个较低的复杂度了。
提供一个思路,代码就不给了。

浙公网安备 33010602011771号