2024-11-17 12:47阅读: 440评论: 0推荐: 4

AtCoder Beginner Contest 380

省流版
  • A. 计数判断即可
  • B. 计数统计即可
  • C. 模拟即可
  • D. 注意到字符串是左右大小写镜像,从长度大往小依次考虑实际所处的位置,维护镜像次数集合
  • E. 并查集维护连通性,并尝试与左右俩格子合并即可
  • F. 博弈dp,状态数只有 5e5,直接记忆化搜索即可
  • G. 枚举打乱起始位置,全排列分成三部分,考虑逆序对来源的6个部分,注意到打乱部分的逆序对期望数量为定值,其余 5部分用权值树状数组或权值线段树维护即可

A - 123233 (abc380 A)

题目大意

给了6个数字,问是否

  • 1的数字出现 1
  • 2的数字出现 2
  • 3的数字出现 3

解题思路

统计每个数字出现的次数即可。

神奇的代码
#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 s;
cin >> s;
bool ok = true;
for (int i = 1; i <= 3; ++i)
ok &= count(s.begin(), s.end(), i + '0') == i;
if (ok)
cout << "Yes" << '\n';
else
cout << "No" << '\n';
return 0;
}


B - Hurdle Parsing (abc380 B)

题目大意

给定一个包含-|的字符串。统计每两个|之间有多少个-

解题思路

找到连续的两个|的下标,然后其差即为答案。Python可以一行。

神奇的代码
print(' '.join([str(len(s)) for s in input()[1:-1].split('|')]))


C - Move Segment (abc380 C)

题目大意

给定一个01子串。

连续的1视为一个块。

现将第k个块移动到第 k1个块前面。

解题思路

按照题意,找到第k个块的下标,然后复制即可。下述是Python代码,split('0')可以轻易找到第k个块,然后移动即可。

神奇的代码
n, k = map(int, input().split())
k -= 1
s = input().split('0')
one = [pos for pos, i in enumerate(s) if i]
kk = s[one[k]]
s.pop(one[k])
s[one[k - 1]] += kk + '0'
print('0'.join(s))


D - Strange Mirroring (abc380 D)

题目大意

给定一个字符串s,重复下述操作 无数次:

  • s的字母大小写反转成 t,加到 s后面

给定 q个询问,每个询问问第 k个字符是什么。

解题思路

每进行一次操作,字符串s的长度会翻倍。

容易发现其字符串形如s0s0¯s0s0¯¯s0s0¯s0s0¯¯¯s0s0¯s0s0¯¯s0s0¯s0s0¯¯¯¯

我们从大往小的看,左右两边长度一样,区别就是反转。我们找到第一个si,使得k在右半边,那我们就记录一次反转操作,然后递归的考虑右边横线下方的字符串,直到 k<|s|,我们就得到对应的字符和反转的次数,就得知最后的字符是多少了。

即先找到第一个 si=si1ti1,其中 k位于 ti1里,这里si1ti1只是大小写反了。此时就需要反转一次了,然后令k=k|si1|,递归的考虑重复考虑相同的情况即可。

神奇的代码
#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 s;
int q;
cin >> s >> q;
vector<LL> round{int(s.size())};
auto dfs = [&](auto dfs, LL k, int sign) -> char {
if (k < s.size()) {
if (sign) {
if (isupper(s[k])) {
return tolower(s[k]);
} else {
return toupper(s[k]);
}
} else {
return s[k];
}
}
auto pos = upper_bound(round.begin(), round.end(), k);
LL len = *pos / 2;
return dfs(dfs, k - len, sign ^ 1);
};
while (q--) {
LL k;
cin >> k;
--k;
while (round.back() <= k) {
round.push_back(round.back() * 2);
}
cout << dfs(dfs, k, 0) << ' ';
}
cout << '\n';
return 0;
}


E - 1D Bucket Tool (abc380 E)

题目大意

从左到右n 个格子,第 1 个格子颜色为i

维护 q 个查询,分两种:

  • 1 x c:将第x个格子连同周围与其同色的格子涂成颜色c
  • 2 c:问颜色c的格子数

解题思路

如果一个格子颜色与周围相同,则它们就会连通,成为一个整体,我们可以用并查集维护这个连通性。

对于操作1,从并查集就很方便的修改一个整体的颜色,进而维护每种颜色的格子数。但修改颜色后,需要看看这个整体与左右两个格子的颜色是否相同,能否合并成新的整体。

为了能找到这个整体的左右相邻格子,并查集需要维护该整体的最左和最右的格子,然后根据颜色决定是否合并。

神奇的代码
#include <bits/stdc++.h>
using namespace std;
using LL = long long;
class dsu {
public:
vector<int> p;
vector<int> l;
vector<int> r;
vector<int> sz;
vector<int> col;
int n;
dsu(int _n) : n(_n) {
p.resize(n);
col.resize(n);
sz.resize(n);
l.resize(n);
r.resize(n);
iota(l.begin(), l.end(), 0);
iota(r.begin(), r.end(), 0);
iota(p.begin(), p.end(), 0);
iota(col.begin(), col.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];
l[y] = min(l[y], l[x]);
r[y] = max(r[y], r[x]);
return true;
}
return false;
}
};
int main(void) {
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
int n, q;
cin >> n >> q;
dsu d(n);
vector<int> cnt(n, 1);
while (q--) {
int op;
cin >> op;
if (op == 1) {
int x, c;
cin >> x >> c;
x--;
--c;
int fx = d.get(x);
cnt[d.col[fx]] -= d.sz[fx];
d.col[fx] = c;
cnt[d.col[fx]] += d.sz[fx];
for (auto& nei : {d.l[fx] - 1, d.r[fx] + 1}) {
if (nei >= 0 && nei < n) {
int fnei = d.get(nei);
if (fnei != fx && d.col[fnei] == c) {
d.unite(fx, fnei);
}
}
}
} else {
int c;
cin >> c;
--c;
cout << cnt[c] << '\n';
}
}
return 0;
}


F - Exchange Game (abc380 F)

题目大意

高桥和青木手里各有n,m张牌,桌子上有 l张牌。牌上写了数字。

高桥和青木轮流操作,直到无法操作的人输。

操作为,将手里的一张牌 a放到桌子上,如果桌上有数字小于 a的牌,他可以拿一张到自己手上,当然也可以选择不拿。

高桥先手,问最优策略下谁赢。

解题思路

博弈dp,从卡牌在谁手中的角度思考状态数,只有 O(3n+m+l)=O(312)=5e5,因此直接搜索即可。

即设 dp[i][j]=1/0表示当前 i先手,局面是 j(即一个描述n+m+l张牌在谁手中的状态,可以是一个数组,0/1/2分别表示在高桥、青木或者桌子上,也可以是一个三进制的压缩状态),此时先手必赢/必输。

转移则枚举当前手的策略,即先花O(n+m+l)枚举将手里的哪张牌放到桌子上,再花 O(n+m+l)枚举 拿桌子上的哪张牌,然后更新局面状态,转移到下一个局面即可。

而当前局是先手必赢/必输,取决于下一个局面(注意下一个局面的先手是当前局面的后手)。因为是最优策略,因此下一个局面如果有(当前局面的后手)必输局面,那先手必赢,否则如果下一个局面全是(当前局面的后手)必赢局面,则先手必输。

因此求解是一个递归的过程。总的时间复杂度是O((n+m+l)23n+m+l)

vector状态1700ms
#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, l;
cin >> n >> m >> l;
vector<int> c(n + m + l);
for (auto& x : c)
cin >> x;
array<map<vector<int>, int>, 2> dp;
vector<int> st(n + m + l);
fill(st.begin(), st.begin() + n, 0);
fill(st.begin() + n, st.begin() + n + m, 1);
fill(st.begin() + n + m, st.end(), 2);
auto dfs = [&](auto dfs, vector<int>& st, int p) -> bool {
if (count(st.begin(), st.end(), p) == 0) {
return dp[p][st] = 0;
}
if (dp[p].count(st))
return dp[p][st];
bool ok = true;
for (int i = 0; i < n + m + l; i++) {
if (st[i] == p) {
st[i] = 2;
for (int j = 0; j < n + m + l; j++) {
if (st[j] == 2 && c[j] < c[i]) {
st[j] = p;
ok &= dfs(dfs, st, p ^ 1);
st[j] = 2;
}
}
ok &= dfs(dfs, st, p ^ 1);
st[i] = p;
}
}
return dp[p][st] = (ok ^ 1);
};
if (dfs(dfs, st, 0))
cout << "Takahashi" << '\n';
else
cout << "Aoki" << '\n';
return 0;
}

三进制压缩状态200ms
#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, l;
cin >> n >> m >> l;
vector<int> c(n + m + l);
for (auto& x : c)
cin >> x;
vector<int> base(n + m + l, 1);
for (int i = 1; i < n + m + l; ++i) {
base[i] = base[i - 1] * 3;
}
array<vector<int>, 2> dp{vector<int>(base.back() * 3, -1),
vector<int>(base.back() * 3, -1)};
auto get_bit = [&](int x, int y) { return x / base[y] % 3; };
auto tr = [&](int& cur, int pos, int v) {
cur -= get_bit(cur, pos) * base[pos];
cur += v * base[pos];
};
auto final = [&](int st, int p) {
for (int i = 0; i < n + m + l; i++) {
if (get_bit(st, i) == p)
return false;
}
return true;
};
auto dfs = [&](auto dfs, int st, int p) -> bool {
if (final(st, p)) {
return dp[p][st] = 0;
}
if (dp[p][st] != -1)
return dp[p][st];
bool ok = true;
for (int i = 0; i < n + m + l; i++) {
if (get_bit(st, i) == p) {
tr(st, i, 2);
for (int j = 0; j < n + m + l; j++) {
if (get_bit(st, j) == 2 && c[j] < c[i]) {
tr(st, j, p);
ok &= dfs(dfs, st, p ^ 1);
tr(st, j, 2);
}
}
ok &= dfs(dfs, st, p ^ 1);
tr(st, i, p);
}
}
return dp[p][st] = (ok ^ 1);
};
int st = 0;
for (int i = 0; i < n + m + l; i++) {
tr(st, i, (i >= n) + (i >= n + m));
}
if (dfs(dfs, st, 0))
cout << "Takahashi" << '\n';
else
cout << "Aoki" << '\n';
return 0;
}

G - Another Shuffle Window (abc380 G)

题目大意

给定一个关于n的全排列p和一个数字 k。进行如下操作一次。

  • 第一步,从[1,nk+1]随机选一个数i
  • 第二部,将 pi,i+k1随机打乱

问进行操作后的逆序对的期望值。

解题思路

期望的求法就是该情况的逆序对数×发生该情况的概率,对所有情况求和。因此我们先考虑所有情况怎么来的。

首先第一步,枚举i,它数量只有 O(n),可以枚举。

枚举了 i后,排列 p就分成了三部分: l,m,r,其中 m就是将会随机打乱的k个数,然后l,r就是左右两部分的数。

再然后就是第二部,将中间部分m打乱,但这情况数有O(k!),显然不能枚举,我们考虑能否综合第二步的所有情况来求。

考虑逆序对(i,j)的来源,根据i,j的值,有这几部分:

  • ll部分
  • lr部分
  • lm部分
  • rr部分
  • mr部分
  • mm部分

除了最后一个,前5个的逆序对的数量不会随着第二步的操作而改变。当第一个的i变化时,我们可以实时维护前 5部分的逆序对数量的变化。

而第六部分的 mm,考虑里面的任意两个数 (i,j),综合所有的随机打乱情况 ,谁前谁后其实各占一半,也就是说,任意一对数只有一半的概率会产生逆序对,而k个数一共有 k(k1)2对数,每对形成逆序对的概率都是 12,因此第六部分的期望逆序对数量始终是 k(k1)212=k(k1)4

而前 5部分的维护,即 m最左边少一个数, l最右边多一个数, m 最右边多一个数,r最左边少一个数,依次计算这些数所产生的逆序对数量,然后更新即可。

而产生逆序对的数量,即对于数i ,我们想知道大于i或小于 i的数量,可以通过维护 l,m,r部分的桶(即cnti表示数字i出现的次数 ),加以树状数组或线段树的单点修改和区间求和,从而在 O(logn)的时间得到。即权值线段树或权值树状数组。

总的时间复杂度为O(nlogn)

神奇的代码
#include <bits/stdc++.h>
using namespace std;
using LL = long long;
// starting from 1
template <typename T> class fenwick {
public:
vector<T> fenw;
int n;
int tot;
fenwick(int _n) : n(_n) {
fenw.resize(n);
tot = 0;
}
inline int lowbit(int x) { return x & -x; }
void modify(int x, T v) {
tot += v;
for (int i = x; i < n; i += lowbit(i)) {
fenw[i] += v;
}
}
T get(int x) {
T v{};
for (int i = x; i > 0; i -= lowbit(i)) {
v += fenw[i];
}
return v;
}
};
const 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); }
int main(void) {
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
int n, k;
cin >> n >> k;
vector<int> p(n);
fenwick<int> l(n + 1), mid(n + 1), r(n + 1);
for (auto& x : p) {
cin >> x;
}
LL llc = 0, lmc = 0, mrc = 0, rrc = 0, lrc = 0;
LL mmc = 1ll * k * (k - 1) % mo * inv(4) % mo;
const int large = 1;
const int small = 0;
auto get_cnt = [&](fenwick<int>& f, int p, int large) {
if (!large)
return f.get(p - 1);
else
return f.tot - f.get(p);
};
auto update_l = [&](int num, int v) { // the right of l
l.modify(num, v);
llc += get_cnt(l, num, large) * v;
lmc += get_cnt(mid, num, small) * v;
lrc += get_cnt(r, num, small) * v;
};
auto update_m = [&](int num, int v) {
mid.modify(num, v);
lmc += get_cnt(l, num, large) * v;
mrc += get_cnt(r, num, small) * v;
};
auto update_r = [&](int num, int v) { // the left of r
r.modify(num, v);
lrc += get_cnt(l, num, large) * v;
mrc += get_cnt(mid, num, large) * v;
rrc += get_cnt(r, num, small) * v;
};
for (int i = 0; i < k; ++i)
update_m(p[i], 1);
for (int i = n - 1; i >= k; --i)
update_r(p[i], 1);
LL ans = (llc + lmc + mrc + rrc + lrc + mmc) % mo;
for (int i = 0; i < n - k; ++i) {
update_m(p[i], -1);
update_l(p[i], 1);
update_r(p[i + k], -1);
update_m(p[i + k], 1);
ans = (ans + llc + lmc + mrc + rrc + lrc + mmc) % mo;
}
ans = ans * inv(n - k + 1) % mo;
cout << ans << '\n';
return 0;
}


本文作者:~Lanly~

本文链接:https://www.cnblogs.com/Lanly/p/18550426

版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。

posted @   ~Lanly~  阅读(440)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!
点击右上角即可分享
微信分享提示
💬
评论
📌
收藏
💗
关注
👍
推荐
🚀
回顶
收起
  1. 1 404 not found REOL
404 not found - REOL
00:00 / 00:00
An audio error has occurred.