2005 字
5 分钟
Atcoder_Beginner_Contest_470(A~G题)
2026-08-10
浏览量 77 · 访客 12

壁纸链接
题目链接:https://atcoder.jp/contests/abc470/tasks

A - Fizz#

直接遍历11NN。如果imod3=0i\bmod 3=0,说明ii33的倍数,输出FizzFizz;否则输出ii本身。

void solved(){
int n; cin >> n;
for(int i = 1; i <= n;i++){
if(i % 3 == 0) cout << "Fizz" << endl;
else cout << i << endl;
}
}

B - Monocolor#

假设最终统一为颜色cc,那么原本颜色为cc的球不需要修改,其余NcntcN-cnt_c个球都需要修改。为了让操作次数最少,应当保留出现次数最多的颜色。
我们用cnt[c]cnt[c]统计每种颜色的出现次数,设最大频次为mama,最终答案就是NmaN-ma

void solve(){
int n; cin >> n;
vector<int> cnt(n + 1, 0);
for(int i = 1; i <= n;i++){
int x; cin >> x;
cnt[x]++;
}
int ma = 0;
for(int i = 1; i <= n;i++){
ma = max(ma, cnt[i]);
}
cout << n - ma << endl;
}

C - Inc, Dec, Xor#

我们用ans=A1A2ANans=A_1\oplus A_2\oplus\cdots\oplus A_N维护当前异或和。异或具有自反性,即xx=0x\oplus x=0,所以当某个元素从oldold变为newnew时,可以通过 ansansoldnewans\leftarrow ans\oplus old\oplus new 先消去旧贡献,再加入新贡献,不需要重新计算整个数组。
对于第一类操作,只有AxA_x发生变化,直接用上述公式将它从AxA_x更新为Ax+1A_x+1。对于第二类操作,只需要处理当前大于00的元素。代码用activeactive保存这些下标,每次将其中的元素减一,并把减完后仍大于00的下标压缩到数组前部;元素第一次从00变成11时才加入activeactive
虽然一次第二类操作可能遍历多个下标,但每次遍历都会让某个正数减少11,这个减少量一定来自此前的一次加一操作。因此所有第二类操作的总遍历次数不超过第一类操作的数量,整体复杂度为O(N+Q)O(N+Q)

void solve(){
int n, q;
cin >> n >> q;
int ans = 0;
vector<int> a(n + 1, 0), active;
while(q--){
int op; cin >> op;
if(op == 1){
int x; cin >> x;
if(a[x] == 0) active.push_back(x);
ans = ans ^ a[x] ^ (a[x] + 1);
a[x]++;
}
else{
int remain = 0;
for(auto x : active){
ans = ans ^ a[x] ^ (a[x] - 1);
a[x]--;
if(a[x] > 0) active[remain++] = x;
}
active.resize(remain);
}
cout << ans << endl;
}
}

D - Inverse and Swap#

第二类操作的条件PPi=iP_{P'_i}=i说明P=P1P'=P^{-1},也就是把当前排列替换为它的逆排列。由于(P1)1=P(P^{-1})^{-1}=P,连续执行两次求逆会回到原排列,所以只需要用布尔变量rere记录当前应当把哪一个数组视为答案。
代码同时维护a[i]=Pia[i]=P_ipos[v]=Pv1pos[v]=P^{-1}_v。当re=falsere=false时,第一类操作交换当前排列中的两个位置a[x],a[y]a[x],a[y],并同步更新这两个值在pospos中的位置;当re=truere=true时,当前排列是pospos,于是交换pos[x],pos[y]pos[x],pos[y],再对其逆排列aa做对应更新。第二类操作不需要实际重建数组,只需要翻转rere
这样每次查询都能在O(1)O(1)时间内完成,最后根据rere输出aapospos即可。总时间复杂度为O(N+Q)O(N+Q)

void solve(){
int n, q; cin >> n >> q;
vector<int> a(n + 1, 0);
vector<int> pos(n + 1, 0);
for(int i = 1; i <= n;i++){
cin >> a[i];
pos[a[i]] = i;
}
bool re = false;
while(q--){
int op; cin >> op;
if(op == 1){
int x, y; cin >> x >> y;
if(!re){
int temp = a[x]; a[x] = a[y]; a[y] = temp;
temp = pos[a[x]]; pos[a[x]] = pos[a[y]];
pos[a[y]] = temp;
}
else{
int temp = pos[x]; pos[x] = pos[y]; pos[y] = temp;
temp = a[pos[x]]; a[pos[x]] = a[pos[y]];
a[pos[y]] = temp;
}
}
else re = !re;
}
if(!re) for(int i = 1; i <= n;i++)
cout << a[i] << " ";
else for(int i = 1; i <= n;i++)
cout << pos[i] << " ";
cout << endl;
}

E - Concentration#

这题需要使用概率dpdp。在最优策略下,已经知道位置的一对相同卡片一定会被直接消除,因此状态只需要记录尚未完全确定的卡片。定义Fi(j,k)F_i(j,k)表示还剩ii点生命、存在jj种两张位置都未知的卡片、kk种已经知道一张位置但另一张仍未知的卡片时,最终能够消除的卡片对数的期望。此时未知位置的卡片总数为U=2j+kU=2j+k
每轮先翻开一张未知卡片。如果它属于已有的kk个单张信息之一,概率为kU\frac{k}{U},可以立即选择已知的另一张完成配对,转移到1+Fi(j,k1)1+F_i(j,k-1)。否则它来自jj个完全未知的卡片对,概率为2jU\frac{2j}{U},再翻开一张未知卡片时有三种情况:
第一种是恰好翻到同一对中的另一张,概率为1U1\frac{1}{U-1},成功配对后转移到1+Fi(j1,k)1+F_i(j-1,k)
第二种是翻到已有kk个单张信息之一的对应卡片,概率为kU1\frac{k}{U-1}。本轮失配并损失一点生命,但在生命仍大于00时,原来已经记住的那张卡可以保证形成一对;同时本轮第一张卡成为新的单张信息,所以转移到1+Fi1(j1,k)1+F_{i-1}(j-1,k)
第三种是翻到其他完全未知卡片对中的一张,概率为2(j1)U1\frac{2(j-1)}{U-1}。本轮失配后新增两个单张信息,转移到Fi1(j2,k+2)F_{i-1}(j-2,k+2)。当i=1i=1时,后两种失配会使游戏立即结束,因此后续贡献为00
成功转移依赖同一生命值下规模更小的状态,失配转移依赖上一层生命值,所以代码按生命值从小到大计算,并用两个二维数组滚动。由于所有卡片对在随机排列中完全对称,每一对最终被消除的概率都等于FL(N,0)/NF_L(N,0)/N。因此最终得分期望为 FL(N,0)Ni=1NAi\frac{F_L(N,0)}{N}\sum_{i=1}^{N}A_i。 时间复杂度为O(LN2)O(LN^2)

void solve(){
int n, l; cin >> n >> l;
ll sum = 0;
for(int i = 1; i <= n;i++){
int x; cin >> x;
sum += x;
}
/*
dp[j][k]表示当前状态下已完成配对的个数。
j表示还未知的单个卡片个数,k表示目前已知一个卡片,还差另一个的个数。
ndp记录的是上一次dp的状态,因为dp在计算时会用到上一次dp的状态
*/
vector<vector<double>> ndp(n + 1, vector<double>(n + 1, 0.0));
for(int i = 1; i <= l;i++){
//当前状态下的dp。
vector<vector<double>> dp(n + 1, vector<double>(n + 1, 0.0));
for(int j = 0; j <= n;j++){
for(int k = 0; j + k <= n; k++){
if(j == 0 && k == 0) continue;
int unknown = 2 * j + k;
double ans = 0.0;
//第一张未知的卡和已知卡成对
if(k > 0)
ans += (double)k / unknown * (1.0 + dp[j][k - 1]);
//第一张未知的卡不和任何已知卡成对
if(j > 0){
double newone = 0.0;
//第二张卡是这张未知卡的配对
newone += (1.0 + dp[j - 1][k]) / (unknown - 1);
//剩下的两种情况都会损失生命值,需要判断是否大于1才能继续
if(i > 1){
//第二张卡与原来的k张卡可以配对
if(k > 0)
newone += (double)k / (unknown - 1) * (1.0 + ndp[j - 1][k]);
//第二张卡是其他的未知卡
if(j > 1)
newone += (double)(2 * (j - 1)) / (unknown - 1) * ndp[j - 2][k + 2];
}
ans += (double)(2 * j) / unknown * newone;
}
dp[j][k] = ans;
}
}
ndp.swap(dp);
}
//每一对牌获得的概率都为p = ndp[n][0] / n, 因此期望就是累加每个A元素乘p。
double res = ndp[n][0] / n * double(sum);
printf("%.15lf\n", res);
}

F - Googol Swaps#

把字符串位置看作图上的点,每个允许交换的位置对(Ai,Bi)(A_i,B_i)看作一条边。沿连通块内的边进行交换可以生成该连通块中任意位置置换,因此不同连通块相互独立。使用并查集求出所有连通块,并统计每个连通块内2626种字符的出现次数。
对于大小为ss的连通块,如果字符cc出现cntccnt_c次,那么忽略交换次数奇偶性时,该连通块可以形成的不同字符串数量为多重集合排列数 s!ccntc!\frac{s!}{\prod_c cnt_c!}。 将所有连通块的方案数相乘即可得到总排列数,阶乘与逆阶乘可以预处理。
本题要求恰好执行1010010^{100}次交换,这是一个偶数,所以最终的位置置换必须是偶置换。连通块内的任意偶置换都可以通过边上的交换实现,并且可以把同一条边连续交换两次来增加22次操作,因此这个足够大的操作次数只会限制置换的奇偶性。如果某个连通块内存在重复字符,那么交换两个相同字符不会改变最终字符串,却可以改变置换的奇偶性。因此每个可形成的字符串都同时存在奇、偶两种生成方式,所有多重集合排列都合法。
如果所有连通块内部都没有重复字符,那么最终字符串能够唯一确定各连通块中的位置置换,所有方案中恰好一半对应偶置换,所以答案需要乘以22的模逆元。并查集部分的时间复杂度为O((N+M)α(N))O((N+M)\alpha(N)),频次统计与组合数计算为O(26N)O(26N),空间复杂度为O(26N)O(26N)

void solve(){
int n, m; cin >> n >> m;
string s; cin >> s;
// 阶乘和逆阶乘预处理
vector<ll> f(n + 1, 1);
vector<ll> inverse_f(n + 1, 1);
for(int i = 1; i <= n;i++) f[i] = f[i - 1] * i % mod;
inverse_f[n] = fast_pow(f[n], mod - 2, mod);
for(int i = n;i >= 1;i--)
inverse_f[i - 1] = inverse_f[i] * i % mod;
// 连通块处理
vector<int> fa(n), sz(n, 1);
iota(fa.begin(), fa.end(), 0);
auto Find = [&](auto &&self, int x) -> int {
if(fa[x] == x) return x;
return fa[x] = self(self, fa[x]);
};
auto unite = [&](int x, int y){
int fx = Find(Find, x);
int fy = Find(Find, y);
if(fx == fy) return;
if(sz[fx] < sz[fy]) swap(fx, fy);
fa[fy] = fx;
sz[fx] += sz[fy];
};
// 构造连通块
for(int i = 0; i < m;i++){
int a, b; cin >> a >> b;
unite(a - 1, b - 1);
}
// 记录每个连通块内各字符出现的频率
vector<array<int, 26>> freq(n);
for(int i = 0; i < n;i++){
int u = Find(Find, i);
freq[u][s[i] - 'a']++;
}
ll ans = 1;
bool repeat = false;
for(int i = 0; i < n;i++){
// 跳过非根节点
if(Find(Find, i) != i) continue;
// 计算该连通块内不同字符串排列的数量
ans = ans * f[sz[i]] % mod;
for(int c = 0; c < 26;c++){
ans = ans * inverse_f[freq[i][c]] % mod;
// 重复字符可以在不改变字符串的情况下改变置换奇偶性
if(freq[i][c] >= 2) repeat = true;
}
}
// 没有重复字符时,只有一半的排列是偶置换
if(!repeat) cout << ans * ((mod + 1) / 2) % mod << endl;
else cout << ans << endl;
}

G - ΣШX#

利用恒等式 mex(B)=x0[0,1,,x均在B中出现]mex(B)=\sum_{x\geq 0}[0,1,\ldots,x\text{均在}B\text{中出现}], 可以把答案转化为:依次枚举x=0,1,x=0,1,\ldots,统计同时包含00xx的子数组数量并累加。如果某个xx在原数组中没有出现,那么更大的xx也不会产生贡献,可以直接结束枚举。
使用从00开始的下标。对于一个左端点ll,记nextx(l)next_x(l)为位置ll及其右侧第一个值为xx的位置,不存在时记为NN。处理完00xx后,令 f(l)=max(l,next0(l),next1(l),,nextx(l))f(l)=\max(l,next_0(l),next_1(l),\ldots,next_x(l))。 那么以ll为左端点的子数组要包含00xx,右端点至少需要到达f(l)f(l),合法右端点数量为Nf(l)N-f(l)
对于固定的xx,相邻两次出现位置之间的所有左端点拥有相同的nextx(l)next_x(l)。因此预处理每个值的出现位置后,可以对每一段左端点区间执行f(l)max(f(l),nextx(l))f(l)\leftarrow\max(f(l),next_x(l)),问题就转化为了若干次区间chmaxchmax以及维护全局f(l)\sum f(l)
代码使用mapmap保存ff的极长连续段,每个键[l,r)[l,r)表示这一段上的函数值相同。更新[l,r)[l,r)前先用splitsplit切出左右边界,再删除其中所有小于新值的连续段并合并,同时维护各段长度乘函数值的总和sumsum。由于f(l)f(l)单调不降,需要修改的部分一定是更新区间的一个前缀。
额外加入l=Nl=N的哨兵并令f(N)=Nf(N)=N后,当前同时包含00xx的子数组数量可以写成N(N+1)sumN(N+1)-sum。每个出现位置只会产生常数次区间切分,旧区间被删除后不会再次出现,因此总时间复杂度为O(NlogN)O(NlogN),空间复杂度为O(N)O(N)

void solve(){
int n; cin >> n;
vector<vector<int>> pos(n + 1);
for(int i = 0, x; i < n; i++) cin >> x, pos[x].push_back(i);
map<pii, int> mp;
ll sum = 0, ans = 0;
for(int i = 0; i <= n; i++) mp[{i, i + 1}] = i, sum += i;
auto split = [&](int x){
auto it = prev(mp.upper_bound({x, INT_MAX}));
auto [l, r] = it->first;
int val = it->second;
if(x == l || x == r) return;
mp.erase(it);
mp[{l, x}] = mp[{x, r}] = val;
};
for(int x = 0; x < n && !pos[x].empty(); x++){
pos[x].push_back(n);
for(int i = 0; i < (int)pos[x].size(); i++){
int l = (i ? pos[x][i - 1] + 1 : 0);
int r = pos[x][i] + 1, val = pos[x][i], end = l;
split(l), split(r);
auto it = mp.lower_bound({l, -1});
while(it != mp.end() && it->first.first < r && it->second < val){
sum -= 1LL * (it->first.second - it->first.first) * it->second;
end = it->first.second;
it = mp.erase(it);
}
if(l < end){
mp[{l, end}] = val;
sum += 1LL * (end - l) * val;
}
}
ans += 1LL * n * (n + 1) - sum;
}
cout << ans << '\n';
}

模板:

#include <bits/stdc++.h>
using namespace std;
using ll = long long;
using ull = unsigned long long;
#define mod 998244353
typedef pair<int, int> pii;
typedef pair<ll, ll> pll;
ll fast_pow(ll a, ll b, ll MOD){
ll ans = 1;
while(b){
if(b & 1) ans = (ans * a) % MOD;
b >>= 1;
a = (a * a) % MOD;
}
return ans;
}
vector<ll> get_inverse(ll n, ll MOD){
vector<ll> inverse(n + 1, 0);
inverse[1] = 1;
for(int i = 2; i <= n;i++){
inverse[i] = MOD - (MOD / i) * inverse[MOD % i] % MOD;
}
return inverse;
}
void solve(){
}
int main(){
ios::sync_with_stdio(false),
cin.tie(nullptr), cout.tie(nullptr);
int _ = 1;
//cin >> _;
while(_--){
solve();
}
return 0;
}
分享

如果这篇文章对你有帮助,欢迎分享给更多人!

Atcoder_Beginner_Contest_470(A~G题)
https://mkrari.cn/posts/abc_470/
作者
Mkrari
发布于
2026-08-10
许可协议
CC BY-NC-SA 4.0