AtCoder Beginner Contest 314
怎么好多陌生单词
审核怎么这么逆天,半小时还没审完
A - 3.14 (abc314 A)
题目大意
给定\(pi\)的值以及数 \(n\),要求保留 \(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);
string pi = "3."
"14159265358979323846264338327950288419716939937510582097494459"
"23078164062862089986280348253421170679";
int x;
cin >> x;
cout << pi.substr(0, x + 2) << '\n';
return 0;
}
B - Roulette (abc314 B)
题目大意
\(n\)个人玩轮盘赌游戏,简单说就是一个转盘有 \(37\)个数字以及一个箭头,箭头会等概率停在某个数字上。
给定每个人压了哪些数字,以及最终的结果数字 \(x\)。
问压中数字 \(x\)的,且压数字的数量的最少的那些人是谁。
解题思路
按照题意,依次找每个人是否压\(x\),压了的话再看压的数字的数量是否是最少的,储存下来。
由于范围不大,判断是否压可以用 vector
一个一个找,也可以用set
。
神奇的代码
#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<set<int>> a(n);
for (auto& i : a) {
int c;
cin >> c;
while (c--) {
int j;
cin >> j;
i.insert(j);
}
}
int x;
cin >> x;
vector<int> ans;
for (int i = 0; i < n; ++i) {
if (a[i].find(x) != a[i].end()) {
if (!ans.empty() && a[ans.front()].size() > a[i].size())
ans.clear();
if (ans.empty() || a[ans.front()].size() == a[i].size())
ans.push_back(i);
}
}
cout << ans.size() << '\n';
for (auto& i : ans)
cout << i + 1 << ' ';
cout << '\n';
return 0;
}
C - Rotate Colored Subsequence (abc314 C)
题目大意
给定一个字符串\(s\),每个字符都被涂了一个颜色。
要求将每种颜色的所有字符都往右循环移位一格,最右边的移动到第一位。
问移动后的字符串。
解题思路
每个颜色的字符都移动到同颜色的下一个字符。
因此记录下每个颜色的最后一次出现位置\(la\),当该颜色的新位置出现时,就知道该位置经过循环移位后的字母就是\(s[la]\)。
再特别处理下该颜色第一次出现的位置,然后直接输出就可以了。
神奇的代码
#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;
string s;
cin >> n >> m >> s;
vector<int> la(n, -1);
vector<int> last(n);
vector<int> col(n);
for (int i = 0; i < n; ++i) {
int c;
cin >> c;
--c;
last[i] = la[c];
col[i] = c;
la[c] = i;
}
for (int i = 0; i < n; ++i) {
if (last[i] == -1) {
last[i] = la[col[i]];
}
}
for (int i = 0; i < n; ++i)
cout << s[last[i]];
cout << '\n';
return 0;
}
D - LOWER (abc314 D)
题目大意
给定一个字符串\(s\),进行三种操作:
- 令\(s_x = c\)
- 字符串\(s\)全部变成小写
- 字符串\(s\)全部变成大写
经过\(q\)次操作后,输出 \(s\)。
解题思路
由于\(n,q\)有 \(2e5\),暴力处理第二、三操作的复杂度是\(O(n)\),会超时。
操作二和操作三的影响会相互覆盖,因此我们可以只记录最后一次进行的是操作二还是操作三即可。
剩下的问题就是操作二、三是否会影响到操作一。
如何判断是否会影响到,实际上我们可以比较一下某个位置进行操作一的时间戳和进行操作二、三的时间戳,如果前者小则会影响到,否则就影响不到。
因此我们就记录下每个位置执行操作一的时间戳,并且 \(s_x = 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);
int n;
string s;
int q;
cin >> n >> s >> q;
vector<int> time(n, 0);
int all = -1, cur = -1;
for (int i = 0; i < q; ++i) {
int t, x;
string c;
cin >> t >> x >> c;
if (t == 1) {
--x;
s[x] = c[0];
time[x] = i;
} else if (t == 2) {
all = i;
cur = 0; // low
} else if (t == 3) {
all = i;
cur = 1; // up
} else {
assert(0);
}
}
for (int i = 0; i < n; ++i) {
if (time[i] < all) {
if (cur == 0)
cout << (char)tolower(s[i]);
else if (cur == 1)
cout << (char)toupper(s[i]);
else
assert(0);
} else
cout << s[i];
}
cout << '\n';
return 0;
}
E - Roulettes (abc314 E)
题目大意
给定\(n\)个轮盘,每个轮盘有 \(p_i\)个数字,花费 \(c_i\)元去转它,等概率停在某个数字,你将获得对应数字的积分。
问达到 \(m\)积分的最小期望花费。
解题思路
期望题,一定记得从定义出发。
考虑每回合的操作,就是选择哪个轮盘去转,转了之后会有若干个后继状态,由于是概率,这里会有个期望。这里的后继状态会发现和当前考虑的状态是一样的。
问最小花费,那我们就是要选择最优的那个轮盘去转。
考虑\(dp[i]\)表示已经得到了积分\(i\),为了达到 \(m\)积分或以上还需要的最小花费。
然后就枚举转哪个轮盘,再枚举转这个轮盘到了哪个数字,对应了一个后继状态。根据期望定义,由于是等概率的\(dp[i]\)就是所有后继状态的平均值+当前转的花费。
举个例子,假设转的第\(j\)个轮盘有数字\(2,4,6,8\),则 \(dp[i] = c_j + \frac{1}{4} dp[i + 2] + \frac{1}{4} dp[i + 4] + \frac{1}{4} dp[i + 6] + \frac{1}{4} dp[i + 8]\)(这个式子就是期望的定义,某个状态的期望就是所有后继状态的概率加权平均)
注意有\(0\)的存在,事先要把 \(0\)去掉,对应的花费也会增加。转移式移项一下就知道系数了。
举个例子,假设转的第\(j\)个轮盘有数字\(0,4,6,8\),则 \(dp[i] = c_j + \frac{1}{4} dp[i] + \frac{1}{4} dp[i + 4] + \frac{1}{4} dp[i + 6] + \frac{1}{4} dp[i + 8]\),移动一下项就是\(dp[i] = \frac{4}{3}c_j + \frac{1}{3} dp[i + 4] + \frac{1}{3} dp[i + 6] + \frac{1}{3} dp[i + 8]\)
复杂度就是\(O(mnp)\)
神奇的代码
#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<double> c(n);
vector<vector<int>> a(n);
for (int i = 0; i < n; ++i) {
cin >> c[i];
int x;
cin >> x;
int zero = 0;
for (int j = 0; j < x; ++j) {
int p;
cin >> p;
if (p)
a[i].push_back(p);
else
++zero;
}
c[i] = 1. * c[i] * x / (x - zero);
}
vector<double> dp(m, 0);
for (int i = m - 1; i >= 0; --i) {
double ans = 1e18;
for (int j = 0; j < n; ++j) {
double tmp = 0;
for (auto& k : a[j]) {
tmp += (i + k >= m ? 0 : dp[i + k]) + c[j];
}
tmp /= a[j].size();
ans = min(ans, tmp);
}
dp[i] = ans;
}
cout << fixed << setprecision(10) << dp[0] << '\n';
return 0;
}
F - A Certain Game (abc314 F)
题目大意
\(n\)个人,一开始每个人自成一队。
进行 \(n-1\)场比赛,每场比赛第 \(i,j\)人所在的队伍决斗,假设第 \(i\)所在队伍人数为 \(x\),第 \(j\)所在队伍人数为 \(y\),则前者获胜概率为 \(\frac{x}{x + y}\),后者为 \(\frac{y}{x + y}\)。比完后两队合并。
问最后每个人获胜的期望次数。
解题思路
假设第\(i\)个人所在队伍进行了 \(j\)场比赛,那它的期望获胜次数就是这 \(j\)场比赛的获胜概率的和。
但注意到每次获胜需要对这个队伍里的所有人都要加上一个概率,最坏的时间复杂度是 \(O(n^2)\)。
优化的突破点肯定是想办法不让所有人都加,而是将它们视为一个整体(集合),让这个整体去加,最后再查每个人的概率和时再将这个效果加上,跟线段树的懒标记一个道理。
但注意到这个集合会变化的,比完赛后它们就会合并(线段树的就不会变化,它所维护的始终是那个区间的信息),这使得我们得保留这一时刻的集合状态。
想到这里时会发现我们要维护的东西跟Kruskal重构树非常类似,或者说刚刚好就可以用它来维护,Kruskal重构树就是一个维护最小生成树构建时每一步的信息的树。
两个集合合并后会生成一个新的点,这个新的点就是这一时刻的集合状态,然后新点和旧点的连边边权就是获胜的概率,这样既懒标记的给整体加上了概率,同时也保留了当前的集合状态,使得该懒标记只生效于这个集合。
最终从根节点开始dfs
,每个人的获胜概率就是根节点到该点的边权和。
神奇的代码
#include <bits/stdc++.h>
using namespace std;
using LL = long long;
constexpr LL 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;
}
long long inv(long long x) { return qpower(x, mo - 2); }
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;
cin >> n;
vector<vector<array<int, 2>>> edge(n + n - 1);
int id = n - 1;
dsu d(n + n - 1);
fill(d.sz.begin() + n, d.sz.end(), 0);
for (int i = 0; i < n - 1; ++i) {
int u, v;
cin >> u >> v;
--u, --v;
int fu = d.get(u), fv = d.get(v);
int szu = d.sz[fu], szv = d.sz[fv], invuv = inv(szu + szv);
++id;
edge[id].push_back({fu, int(1ll * szu * invuv % mo)});
edge[id].push_back({fv, int(1ll * szv * invuv % mo)});
assert(d.unite(u, id));
assert(d.unite(v, id));
}
vector<int> ans(n);
function<void(int, int)> dfs = [&](int u, int p) {
if (u < n) {
ans[u] = p;
return;
}
for (auto& [v, w] : edge[u]) {
dfs(v, (p + w) % mo);
}
};
dfs(id, 0);
for (int i = 0; i < n; ++i)
cout << ans[i] << " \n"[i == n - 1];
return 0;
}
G - Amulets (abc314 G)
题目大意
有\(n\)个怪兽和\(m\)种攻击类型,每种怪兽有一个攻击力和攻击类型。
你依次挑战每个怪兽,直到打完所有怪兽或生命值非正。
依次对每个 \(k\),求,当你可以选择免疫 \(k\)种攻击类型的怪兽伤害时,最多能打死的怪兽数量。
对于每只怪兽,你首先生命值扣除怪兽攻击力的数值,如果你还活着,则你打死怪兽。
解题思路
考虑每个\(k\),注意到我们可以二分打死了\(x\)只怪兽,然后判断是否可行。
如何判可行,关键在于如何选择免疫的攻击类型。
考虑前\(x\)只怪兽每种攻击类型的和,我们肯定是免疫和最大的那\(k\)个攻击类型,然后看剩下的攻击力是否小于生命值。
这样的复杂度是 \(O(m^2 \log n)\)
感觉是整体二分,即没必要对每个问题单独二分求,而是可以将这\(m\)个问题通过一次二分解决,但其中的处理复杂度貌似不太对,得继续细想一下。
TBD.
神奇的代码
Ex - Disk and Segments (abc314 Ex)
题目大意
<++>
解题思路
<++>
神奇的代码