AtCoder Beginner Contest 356
A - Subsegment Reverse (abc356 A)
题目大意
给定一个 \(1,2,3,...,n\)的排列\(a\),给定两个数 \(l,r\),左右颠倒\(a[l..r]\)。输出。
解题思路
按照题意模拟即可。
神奇的代码
#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, a, b;
cin >> n >> a >> b;
--a;
vector<int> ans(n);
iota(ans.begin(), ans.end(), 1);
reverse(ans.begin() + a, ans.begin() + b);
for (auto x : ans)
cout << x << ' ';
cout << '\n';
return 0;
}
B - Nutrients (abc356 B)
题目大意
给定一天\(n\)种营养的摄入目标量。
给定\(m\)种食物的\(n\)种营养的含量。
问是否所有营养都达到目标摄入了。
解题思路
按照题意,对每种营养的摄入总量求和,与目标比较即可。
神奇的代码
#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, m;
cin >> n >> m;
vector<int> a(m);
for (auto& x : a)
cin >> x;
while (n--) {
for (auto& x : a) {
int s;
cin >> s;
x -= s;
}
}
bool ok = true;
for (auto x : a) {
ok &= x <= 0;
}
if (ok) {
cout << "Yes" << '\n';
} else {
cout << "No" << '\n';
}
return 0;
}
C - Keys (abc356 C)
题目大意
\(n\)把钥匙,有些真的,有些假的。一个门,可以拿一些钥匙打开它,若其中有 \(k\)个真的钥匙,则门打开。
给定了 \(m\)条记录,表示用了哪些钥匙,门是否打开。
对于这\(n\)把钥匙的真假,共 \(2^n\)种情况,问有多少种情况,不违反上述的记录。
解题思路
\(n\)只有 \(15\),直接 \(O(2^n)\)枚举所有情况,然后枚举每个记录,检测其钥匙能否打开门,与结果是否相符。
神奇的代码
#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, m, k;
cin >> n >> m >> k;
vector<pair<vector<int>, int>> a(m);
for (auto& [v, c] : a) {
int x;
cin >> x;
v.resize(x);
for (auto& x : v) {
cin >> x;
--x;
}
string s;
cin >> s;
c = s[0] == 'o';
}
int ans = 0;
int up = (1 << n);
for (int i = 0; i < up; ++i) {
bool ok = true;
for (auto& [v, c] : a) {
int cnt = 0;
for (auto x : v) {
if (i & (1 << x)) {
++cnt;
}
}
if ((cnt >= k) ^ c) {
ok = false;
break;
}
}
ans += ok;
}
cout << ans << '\n';
return 0;
}
D - Masked Popcount (abc356 D)
题目大意
给定\(n,m\),求 \(\sum_{k=0}^{n}popcount(k\&m)\)
\(popcount(x)\)表示 \(x\)二进制下 \(1\)的个数。
解题思路
\(n\)高达 \(10^{18}\),直接枚举会超时。
考虑贡献转换。
考虑到答案来自于二进制下\(1\)的个数。 由于\(\&\)运算的特性,这些实际都是来自于\(m\)的每一个\(1\)。 我们需要考虑\(m\)二进制下每一个 \(1\)对答案的贡献,即有多少个 \(k\),使得 \(k\&m\)后该位是 \(1\)。
假设\(m\)的二进制表示为 \(1...10...0\),考虑第 \(i\)位上的\(1\),思考有多少个 \(k\),使得 \(k\&m\)的第\(i\)位是 \(1\)。
考虑如何计算 \(k\)的数量,首先, \(k\)的第 \(i\)位一定是 \(1\),然后就剩下低位
和高位
的情况数。低位就是低于\(i\)位的那些位数的取值,高于 \(i\)位的就是高于 \(i\)位的位数取值。这个计数问题其实和数位\(dp\)差不多。
由于\(k\)有最大值 \(n\)的限制,低位
的情况数会依赖于高位
,为方便表述,设高位是\(up\),当前位是 \(middle\),低位是 \(down\),比如 \(n=110101\),当前第 \(i=2\)位(从 \(0\)开始),则 \(up=110, middle=1, down=01\)。然后情况数其实就分两种:
- 如果
高位
取值和\(n\)的高位
不一致,则低位
取值没有限制,因此低位
的情况数是\(2^i\) ,而高位
的情况数是\(up\),若此时 \(m\)的第 \(i\)位是 \(1\),则其贡献(出现的次数)为 \(2^i \times up\)。 - 如果
高位
取值和\(n\)的高位
一致,则当前位
和低位
的都会收到\(n\)的限制。如果此时 \(middle=0\),则说明该位不能取 \(1\),则 \(m\)的第 \(i\)位没有贡献。否则 \(middle=1\),低位
的情况数就有 \(down+1\)种情况,而高位
的 情况数只有\(1\)种,若此时 \(m\)的第 \(i\)位是 \(1\),则其贡献(出现的次数)为 \(down + 1\)。
综上,考虑第\(i\)位,其中 \(n\)的高位是 \(up\),当前位是 \(middle\),低位是 \(down\),若 \(m\)的第 \(i\)位是 \(1\),则其对答案的贡献(满足 \(k\&m\)的第 \(i\)位是 \(1\)的 \(k\)的个数)为 \(2^i \times up + middle \times (down + 1)\)。
神奇的代码
#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);
LL n, m;
cin >> n >> m;
LL ans = 0;
LL up = n >> 1, middle = n & 1, down = 0, cnt = 1;
for (int i = 0; i < 60; ++i) {
if ((m >> i) & 1) {
ans += 1ll * up * cnt % mo + middle * (down + 1);
ans %= mo;
}
down = down | (middle << i);
middle = up & 1;
up >>= 1;
cnt <<= 1;
}
cout << ans << '\n';
return 0;
}
E - Max/Min (abc356 E)
题目大意
给定一个数组\(a\),求 \(\sum_{i=1}^{n}\sum_{j=i+1}^{n} \lfloor \frac{\max(a_i, a_j)}{\min(a_i, a_j)} \rfloor\)。
解题思路
作除法始终是最大值除以最小值,因此可以先对\(a\)进行排序再计算,这不会影响答案。
对 \(a\)从小到大排序后,考虑枚举 \(j\),然后计算 \(i < j\)情况对答案的贡献,即枚举最大值。
一个比较明显的观察是,可能会有若干个 \(a_i\),使得 \(\lfloor \frac{a_j}{a_i} \rfloor\)是同样的值。那我们可以把这些值合并地来算,即值
\(\times\)个数
。
事实上可以通过数论分块来求解,\(\lfloor \frac{a_j}{a_i} \rfloor\)的值的可能数量和因子个数同一个数量级,即\(O(\sqrt{a_i})\)个,而造成该取整结果的\(a_i\)的取值就是数论分块里的两个边界 \(l,r\),可以在 \(a\)中二分得到对应位置,因而得到个数
。而这复杂度是\(O(n\log n\sqrt{a_i})\),会超时。
数论分块复杂度是根号级别,在这里用不了,只能另寻它路。
这次考虑枚举 \(i\),然后计算 \(i < j\)情况对答案的贡献,即枚举最小值。
同样考虑贡献转换,原本的想法是求\(\lfloor \frac{a_j}{a_i} \rfloor = val\)的\(a_j\)数量\(cnt\),然后累计\(val \times cnt\)。
我们将其\(val\)看成\(1+1+1...\),然后重组一下,变成求 \(\lfloor \frac{a_j}{a_i} \rfloor \geq val\)的\(a_j\)数量\(cnt\)。
即原本是求\(\lfloor \frac{a_j}{a_i} \rfloor = 1,2,3\)的\(a_j\)数量,现在求\(\lfloor \frac{a_j}{a_i} \rfloor \geq 1,2,3\)的\(a_j\)数量。
这里的贡献转换就是将\(\lfloor \frac{a_j}{a_i} \rfloor = 3\)对答案有\(3\)的贡献,拆成了 \(1+1+1\),即\(\lfloor \frac{a_j}{a_i} \rfloor \geq 1 + \lfloor \frac{a_j}{a_i} \rfloor \geq 2 + \lfloor \frac{a_j}{a_i} \rfloor \geq 3\),然后合并所有的\(\lfloor \frac{a_j}{a_i} \rfloor \geq 1\)(或\(2,3\)),求其个数。
现在我们的视角转换成求 \(\lfloor \frac{a_j}{a_i} \rfloor \geq v\)的\(a_j\)数量,即枚举\(v\),求\(a_j \geq va_i\)的\(j\)的数量。很明显通过在\(a\)数组二分\(va_i\),就能求得\(j\)的数量了。
由于\(max_a = 10^6\),那么这里枚举的\(v\)的数量就是\(O(\sum_{i=1}^{n} \frac{max_a}{a_i})\)。每次枚举都有一个二分的\(O(\log n)\),因此总的时间复杂度是\(O(\sum_{i=1}^{n} \frac{max_a}{a_i} \log n)\)。
如果所有的\(a_i\)都很小,比如都是 \(a_i=1\),那时间复杂度就是 \(O(nmax_a \log n)\),又炸了!
但看这个式子,非常像对数求和,如果\(a_i\)唯一,那复杂度就是\(O(\sum_{i=1}^{max_a} \frac{max_a}{i} \log n) = O(max_a \log max_a \log n)\),是可过的。
因此,如果\(a_i\)有重复的,那么我们就合并重复的数,并记录\(cnt_k\)表示 \(a_i=k\)的数量,然后考虑怎么修正一下答案的计算。
合并相同的数,并从小到大排序,然后枚举当前的 \(a_i\),再枚举 \(v\),求得第一个\(a_j > va_i\)的 \(a_j\),那么此时\(\lfloor \frac{a_j}{a_i} \rfloor \geq v\)的数量就是\(cnt_{a_i} \times \sum_{k \geq j} cnt_{a_k}\)。而后者\(\sum_{k=j}^{n} cnt_{a_j}\)就是一个关于\(cnt\)数组的后缀和,预处理一下就能\(O(1)\)得到。
然后对于相同数之间的贡献,则是\(\sum_{i} \frac{cnt_{a_i} \times (cnt_{a_i} - 1)}{2}\)。
最终的答案就是两者的和。
神奇的代码
#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> r(n);
for (auto& x : r)
cin >> x;
map<int, int> s;
for (auto& i : r)
s[i]++;
vector<int> a;
vector<int> sum;
for (auto& [x, y] : s) {
a.push_back(x);
sum.push_back(y);
}
vector<int> suf(sum.size());
partial_sum(sum.rbegin(), sum.rend(), suf.rbegin(), plus<int>());
LL ans = 0;
n = a.size();
for (int i = 0; i < n; ++i) {
int x = a[i];
int pos = i + 1;
ans += 1ll * sum[i] * (sum[i] - 1) / 2;
while (pos < n) {
pos = lower_bound(a.begin() + pos, a.end(), x) - a.begin();
if (pos < n)
ans += 1ll * sum[i] * suf[pos];
x += a[i];
}
}
cout << ans << '\n';
return 0;
}
F - Distance Component Size Query (abc356 F)
题目大意
<++>
解题思路
<++>
神奇的代码
G - Freestyle (abc356 G)
题目大意
<++>
解题思路
<++>
神奇的代码