AtCoder Beginner Contest 383
省流版
- A. 模拟加水漏水即可
- B. 枚举两个加湿器的位置,然后统计加湿的单元格数量即可
- C. 从每个加湿器进行 \(BFS\) 即可
- D. 考虑因子个数的计算,分情况枚举质因数即可
- E. 考虑\(f\)函数的求法,从小到大加边,考虑每条边对答案的贡献即可
- F. 对颜色排序,在\(01\)背包的基础上,新增一个不同颜色时的转移,维护颜色的最后一次出现的位置即可
A - Humidifier 1 (abc383 A)
题目大意
加湿器初始无水,但一旦有水,单位时间内水量就会减少 \(1\) 升。
现给定 \(N\) 次加水操作,第 \(i\) 次加水发生在时间 \(T_i\) ,加了 \(V_i\) 升水。问最后一次加完后加湿器中剩余的水量。
解题思路
时刻最多只有 \(100\) ,可以直接模拟。也可以直接按题意维护下次加水时的水量。
神奇的代码
#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;
int w = 0;
int la = 0;
while (n--) {
int t, x;
cin >> t >> x;
w = max(0, w - t + la);
la = t;
w += x;
}
cout << w << '\n';
return 0;
}
B - Humidifier 2 (abc383 B)
题目大意
给定包含.#
的二维网格,选择两个.
放置加湿器,使得加湿器的加湿的单元格数量最大。
当且仅当单元格与至少一个加湿器的曼哈顿距离 \(D\) 在内时,该单元格被加湿。 \((i,j)\) 和 \((i',j')\) 之间的曼哈顿距离定义为 \(|i - i'| + |j - j'|\) 。
解题思路
枚举两个加湿器的位置,然后统计加湿的单元格数量。时间复杂度为 \(O((HW)^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 h, w, d;
cin >> h >> w >> d;
vector<string> s(h);
for (auto& i : s)
cin >> i;
vector<array<int, 2>> pos;
for (int i = 0; i < h; i++) {
for (int j = 0; j < w; j++) {
if (s[i][j] == '.') {
pos.push_back({i, j});
}
}
}
auto inner = [&](int x, int y, int z) -> bool {
auto [x1, y1] = pos[z];
return abs(x1 - x) + abs(y1 - y) <= d;
};
auto solve = [&](int x, int y) -> int {
int cnt = 0;
for (int i = 0; i < h; i++) {
for (int j = 0; j < w; j++) {
cnt += s[i][j] == '.' && (inner(i, j, x) || inner(i, j, y));
}
}
return cnt;
};
int ans = 0;
for (int i = 0; i < pos.size(); i++) {
for (int j = i + 1; j < pos.size(); j++) {
ans = max(ans, solve(i, j));
}
}
cout << ans << '\n';
return 0;
}
C - Humidifier 3 (abc383 C)
题目大意
给定一个 \(H \times W\) 的网格,"#"为墙壁;"."为地板;"H"为放置了加湿器。
如果从至少一个加湿器单元格向上、向下、向左或向右移动最多 \(D\) 次而不经过墙壁,则该单元格被视为加湿单元格。
求加湿地板单元格的数量。
解题思路
上述移动方式,即从每个加湿器单元格进行 \(BFS\) ,直到移动次数到达 \(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 h, w, d;
cin >> h >> w >> d;
vector<string> s(h);
for (auto& i : s)
cin >> i;
queue<array<int, 2>> team;
int ans = 0;
vector<vector<int>> dis(h, vector<int>(w, h * w + 8));
for (int i = 0; i < h; i++) {
for (int j = 0; j < w; j++) {
if (s[i][j] == 'H') {
team.push({i, j});
dis[i][j] = 0;
++ans;
}
}
}
array<int, 2> dir[] = {{0, 1}, {0, -1}, {1, 0}, {-1, 0}};
while (!team.empty()) {
auto [x, y] = team.front();
team.pop();
for (auto [dx, dy] : dir) {
int nx = x + dx, ny = y + dy;
if (nx >= 0 && nx < h && ny >= 0 && ny < w && s[nx][ny] != '#' &&
dis[x][y] + 1 <= d && dis[nx][ny] > dis[x][y] + 1) {
dis[nx][ny] = dis[x][y] + 1;
team.push({nx, ny});
++ans;
}
}
}
cout << ans << '\n';
return 0;
}
D - 9 Divisors (abc383 D)
题目大意
求恰好有 \(9\) 个因子的不大于 \(N\) 的正整数的个数。
解题思路
将一个数质因数分解,设 \(n = p_1^{a_1} \times p_2^{a_2} \times \cdots \times p_k^{a_k}\) ,则 \(n\) 的因子个数为 \((a_1 + 1) \times (a_2 + 1) \times \cdots \times (a_k + 1)\) 。
因此,如果一个数的因子个数为 \(9\) ,则其质因数分解后,只能有两种情况:
- \(p_1^2 \times p_2^2\) ,此时 \(a_1 = a_2 = 2\) ;
- \(p_1^8\) ,此时 \(a_1 = 8\) 。
由于 \(N \leq 4e12\),因此对于情况一,\(p_1p_2 \leq 2e6\),因此可以枚举 \(p_1\) 和 \(p_2\) ,然后判断是否满足条件,其时间复杂度不会超过 \(O(2e6)\) 。
而情况二,同样直接枚举 \(p_1\) ,然后判断是否满足条件即可。
神奇的代码
#include <bits/stdc++.h>
using namespace std;
using LL = long long;
const LL p_max = 2E6 + 100;
LL pr[p_max], p_sz;
void get_prime() {
static bool vis[p_max];
for (int i = 2; i < p_max; i++) {
if (!vis[i])
pr[p_sz++] = i;
for (int j = 0; j < p_sz; j++) {
if (pr[j] * i >= p_max)
break;
vis[pr[j] * i] = 1;
if (i % pr[j] == 0)
break;
}
}
}
int main(void) {
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
get_prime();
LL n;
cin >> n;
LL half = ceil(sqrt(n));
LL ans = 0;
for (int i = 0; i < p_sz; i++) {
for (int j = i + 1; j < p_sz; j++) {
if (pr[i] * pr[j] > half)
break;
ans += (pr[i] * pr[i] * pr[j] * pr[j] <= n);
}
LL tmp = 1;
int up = 0;
for (; up < 8; ++up) {
tmp *= pr[i];
if (tmp > n)
break;
}
ans += (up == 8);
}
cout << ans << '\n';
return 0;
}
E - Sum of Max Matching (abc383 E)
题目大意
给定一张无向图,边有边权。给定两个长度为 \(K\) 的序列: \((A_1, A_2, \ldots, A_K)\) 和 \((B_1, B_2, \ldots, B_K)\) 。
将序列 \(B\) 自由排列,使 \(\displaystyle \sum_{i=1}^{K} f(A_i, B_i)\) 最小。其中 \(f(x, y)\) 为从顶点 \(x\) 到顶点 \(y\) 的路径的最小路径权重。
一条路径的权重定义为路径中一条边的最大权重。
解题思路
考虑 \(f(x, y)\) 的定义,因为一条路径的权重定义为路径中一条边的最大权重,而两点的路径有很多条,我们要找最小权重的那条。
怎么求 \(f(x,y)\)呢?我们可以考虑加边,从边权最小的边开始加,这样,当两个点突然位于同一个连通块时(即有路径连通时),就说明这个边权就是这两个点路径的必经边,而由于我们是从小到大加边,所以这个边权就是这两个点路径的最小权重。
you
因此,我们可以将边按权重从小到大排序,然后依次加边,对于每条边,如果将两个不同的连通块合并,那么这两个连通块里的 \(A_i\) 和 \(B_i\) 就可以连通,对应的连通点对 \((A_i, B_i)\) 数量就是该边权对答案的贡献。所有的边权对答案的贡献求和即为答案。
我们可以用并查集来维护连通性。
神奇的代码
#include <bits/stdc++.h>
using namespace std;
using LL = long long;
class dsu {
public:
vector<int> p;
vector<int> a;
vector<int> b;
int n;
dsu(int _n) : n(_n) {
p.resize(n);
a.resize(n);
b.resize(n);
iota(p.begin(), p.end(), 0);
fill(a.begin(), a.end(), 0);
fill(b.begin(), b.end(), 0);
}
inline int get(int x) { return (x == p[x] ? x : (p[x] = get(p[x]))); }
inline LL unite(int x, int y, int w) {
x = get(x);
y = get(y);
LL ret = 0;
if (x != y) {
int c = min(a[x], b[y]);
a[x] -= c;
b[y] -= c;
ret += 1ll * c * w;
c = min(a[y], b[x]);
a[y] -= c;
b[x] -= c;
ret += 1ll * c * w;
p[x] = y;
a[y] += a[x];
b[y] += b[x];
}
return ret;
}
};
int main(void) {
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
int n, m, k;
cin >> n >> m >> k;
vector<array<int, 3>> edge(m);
for (auto& [u, v, w] : edge) {
cin >> u >> v >> w;
u--;
v--;
}
dsu d(n);
for (int i = 0; i < k; i++) {
int x;
cin >> x;
d.a[x - 1]++;
}
for (int i = 0; i < k; i++) {
int x;
cin >> x;
d.b[x - 1]++;
}
vector<int> id(m);
iota(id.begin(), id.end(), 0);
sort(id.begin(), id.end(),
[&](int i, int j) { return edge[i][2] < edge[j][2]; });
LL ans = 0;
for (auto i : id) {
auto [u, v, w] = edge[i];
ans += d.unite(u, v, w);
}
cout << ans << '\n';
return 0;
}
F - Diversity (abc383 F)
题目大意
给定 \(N\) 种商品,每种商品有价格 \(P_i\) ,效用值 \(U_i\) ,颜色 \(C_i\) 。
选一些商品,使得总价不超过 \(X\) ,满意度最大化。满意度定义为 \(S + T \times K\) ,其中 \(S\) 是所选商品的效用总和, \(T\) 是所选商品中不同颜色的数量, \(K\) 是一个给定的常数。
解题思路
如果没有颜色,那么这是一个经典的 \(01\) 背包问题。设 \(dp[i][j]\) 表示前 \(i\) 种商品,总价不超过 \(j\) 的情况下的最大效用值。
而颜色在这里会多了额外贡献。维护已经选了什么颜色的复杂度是指数级别的。我们可以考虑对商品按照颜色进行排序。
假设商品颜色为 \(1,1,1,2,2,2,...\),比如我们在考虑第 \(5\) 个商品,它可以从 \(dp[4]\)转移过来,这和正常的 \(01\) 背包没有区别。同时,它也可以从\(dp[3]\)转移过来,但此时会有一个额外的 \(k\)的贡献,因为这是第一次选择颜色 \(2\)的商品。
因此对于当前的商品,我们额外维护一个位置,该位置是上一个颜色最后一次出现的位置\(last\),这样在原来的 \(01\) 背包的基础上,再加一个从 \(dp[last]\)转移过来的状态即可。
由于转移时第一维仅依赖上一次,所以代码里将第一维的维滚动压缩掉,同时用 \(dpc\) 数组来维护 \(dp[last]\)的值。时间复杂度为 \(O(NX)\) 。
神奇的代码
#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, x, k;
cin >> n >> x >> k;
vector<array<int, 3>> a(n);
for (auto& [p, u, c] : a)
cin >> p >> u >> c;
vector<int> id(n);
iota(id.begin(), id.end(), 0);
sort(id.begin(), id.end(), [&](int i, int j) { return a[i][2] < a[j][2]; });
vector<LL> dp(x + 1, 0);
vector<LL> dpc(x + 1, 0);
int last = 0;
for (auto i : id) {
auto [p, u, c] = a[i];
if (last != c)
dpc = dp;
vector<LL> dp2 = dp;
for (int j = 0; j <= x; j++) {
if (j >= p) {
dp2[j] = max(dp2[j], dp[j - p] + u);
dp2[j] = max(dp2[j], dpc[j - p] + u + k);
}
}
last = c;
dp.swap(dp2);
}
LL ans = *max_element(dp.begin(), dp.end());
cout << ans << '\n';
return 0;
}
G - Bar Cover (abc383 G)
题目大意
问题陈述
有一行 \(N\) 单元格。每个单元格包含一个整数 \(A_i\) 。
有\(\lfloor \frac{N}{K} \rfloor\) 块瓷砖,每块瓷砖可以覆盖 \(K\) 个连续的单元格。
对于每一个 \(i = 1, \ldots, \lfloor \frac{N}{K} \rfloor\) 解决下面的问题:
- 当恰好放置 \(i\) 个瓷砖,且相互不重叠,求被覆盖单元格中数字和的最大值。
解题思路
<++>
神奇的代码