
壁纸链接
题目链接:https://atcoder.jp/contests/abc470/tasks
A - Fizz
直接遍历到。如果,说明是的倍数,输出;否则输出本身。
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
假设最终统一为颜色,那么原本颜色为的球不需要修改,其余个球都需要修改。为了让操作次数最少,应当保留出现次数最多的颜色。
我们用统计每种颜色的出现次数,设最大频次为,最终答案就是。
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
我们用维护当前异或和。异或具有自反性,即,所以当某个元素从变为时,可以通过
先消去旧贡献,再加入新贡献,不需要重新计算整个数组。
对于第一类操作,只有发生变化,直接用上述公式将它从更新为。对于第二类操作,只需要处理当前大于的元素。代码用保存这些下标,每次将其中的元素减一,并把减完后仍大于的下标压缩到数组前部;元素第一次从变成时才加入。
虽然一次第二类操作可能遍历多个下标,但每次遍历都会让某个正数减少,这个减少量一定来自此前的一次加一操作。因此所有第二类操作的总遍历次数不超过第一类操作的数量,整体复杂度为。
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
第二类操作的条件说明,也就是把当前排列替换为它的逆排列。由于,连续执行两次求逆会回到原排列,所以只需要用布尔变量记录当前应当把哪一个数组视为答案。
代码同时维护和。当时,第一类操作交换当前排列中的两个位置,并同步更新这两个值在中的位置;当时,当前排列是,于是交换,再对其逆排列做对应更新。第二类操作不需要实际重建数组,只需要翻转。
这样每次查询都能在时间内完成,最后根据输出或即可。总时间复杂度为。
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
这题需要使用概率。在最优策略下,已经知道位置的一对相同卡片一定会被直接消除,因此状态只需要记录尚未完全确定的卡片。定义表示还剩点生命、存在种两张位置都未知的卡片、种已经知道一张位置但另一张仍未知的卡片时,最终能够消除的卡片对数的期望。此时未知位置的卡片总数为。
每轮先翻开一张未知卡片。如果它属于已有的个单张信息之一,概率为,可以立即选择已知的另一张完成配对,转移到。否则它来自个完全未知的卡片对,概率为,再翻开一张未知卡片时有三种情况:
第一种是恰好翻到同一对中的另一张,概率为,成功配对后转移到。
第二种是翻到已有个单张信息之一的对应卡片,概率为。本轮失配并损失一点生命,但在生命仍大于时,原来已经记住的那张卡可以保证形成一对;同时本轮第一张卡成为新的单张信息,所以转移到。
第三种是翻到其他完全未知卡片对中的一张,概率为。本轮失配后新增两个单张信息,转移到。当时,后两种失配会使游戏立即结束,因此后续贡献为。
成功转移依赖同一生命值下规模更小的状态,失配转移依赖上一层生命值,所以代码按生命值从小到大计算,并用两个二维数组滚动。由于所有卡片对在随机排列中完全对称,每一对最终被消除的概率都等于。因此最终得分期望为
。
时间复杂度为。
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
把字符串位置看作图上的点,每个允许交换的位置对看作一条边。沿连通块内的边进行交换可以生成该连通块中任意位置置换,因此不同连通块相互独立。使用并查集求出所有连通块,并统计每个连通块内种字符的出现次数。
对于大小为的连通块,如果字符出现次,那么忽略交换次数奇偶性时,该连通块可以形成的不同字符串数量为多重集合排列数
。
将所有连通块的方案数相乘即可得到总排列数,阶乘与逆阶乘可以预处理。
本题要求恰好执行次交换,这是一个偶数,所以最终的位置置换必须是偶置换。连通块内的任意偶置换都可以通过边上的交换实现,并且可以把同一条边连续交换两次来增加次操作,因此这个足够大的操作次数只会限制置换的奇偶性。如果某个连通块内存在重复字符,那么交换两个相同字符不会改变最终字符串,却可以改变置换的奇偶性。因此每个可形成的字符串都同时存在奇、偶两种生成方式,所有多重集合排列都合法。
如果所有连通块内部都没有重复字符,那么最终字符串能够唯一确定各连通块中的位置置换,所有方案中恰好一半对应偶置换,所以答案需要乘以的模逆元。并查集部分的时间复杂度为,频次统计与组合数计算为,空间复杂度为。
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
利用恒等式
,
可以把答案转化为:依次枚举,统计同时包含到的子数组数量并累加。如果某个在原数组中没有出现,那么更大的也不会产生贡献,可以直接结束枚举。
使用从开始的下标。对于一个左端点,记为位置及其右侧第一个值为的位置,不存在时记为。处理完到后,令
。
那么以为左端点的子数组要包含到,右端点至少需要到达,合法右端点数量为。
对于固定的,相邻两次出现位置之间的所有左端点拥有相同的。因此预处理每个值的出现位置后,可以对每一段左端点区间执行,问题就转化为了若干次区间以及维护全局。
代码使用保存的极长连续段,每个键表示这一段上的函数值相同。更新前先用切出左右边界,再删除其中所有小于新值的连续段并合并,同时维护各段长度乘函数值的总和。由于单调不降,需要修改的部分一定是更新区间的一个前缀。
额外加入的哨兵并令后,当前同时包含到的子数组数量可以写成。每个出现位置只会产生常数次区间切分,旧区间被删除后不会再次出现,因此总时间复杂度为,空间复杂度为。
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 998244353typedef 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;}