atcoder abc 458 题解
A - Chompers
题目描述
给定一个由小写英文字母组成的字符串 SSS 和一个正整数 NNN。SSS 的长度至少为 2N+12N+12N+1。
请找出从 SSS 的开头移除 NNN 个字符、从结尾移除 NNN 个字符后得到的字符串。
解题思路
我们直接计算在截取之后的字符串长度是多少,然后从要保留的字符串开始的地方开始,用 substr 求解即可。
代码
cpp
#include <bits/stdc++.h>
using namespace std;
int main()
{
string s;
int n;
cin >> s >> n;
int len = s.size();
len -= 2 * n; // 计算中间剩余字符的长度
cout << s.substr(n, len); // 从第n个字符开始截取len个字符
return 0;
}
B - Count Adjacent Cells
题目描述
有一个 HHH 行 WWW 列的网格。从上往下数第 iii 行、从左往右数第 jjj 列的单元格记为 (i,j)(i, j)(i,j)。
当 ∣x1−x2∣+∣y1−y2∣=1|x_1 - x_2| + |y_1 - y_2| = 1∣x1−x2∣+∣y1−y2∣=1 时,我们称单元格 (x1,y1)(x_1, y_1)(x1,y1) 和 (x2,y2)(x_2, y_2)(x2,y2) 是边相邻的。
对每个单元格,求出与它边相邻的单元格的数量。
解题思路
我们可以发现,中间的单元格最多有 4 个相邻的格子。对于每个格子,我们先假设答案是 4,然后检查它是否在边界上:如果在第一行或最后一行,就减 1;如果在第一列或最后一列,也减 1。这样就能得到每个格子的相邻数了。
代码
cpp
#include <bits/stdc++.h>
using namespace std;
int h, w;
int main()
{
cin >> h >> w;
for (int i = 1; i <= h; i++, cout << '\n') // 遍历每一行
for (int j = 1; j <= w; j++) // 遍历每一列
{
int ans = 4; // 初始假设最多4个相邻格子
if (i == 1)
ans--; // 如果在第一行,上面没有格子
if (j == 1)
ans--; // 如果在第一列,左边没有格子
if (i == h)
ans--; // 如果在最后一行,下面没有格子
if (j == w)
ans--; // 如果在最后一列,右边没有格子
cout << ans << ' ';
}
return 0;
}
C - C Stands for Center
题目描述
给定一个由大写英文字母组成的字符串 SSS。请找出满足以下所有条件的子串(连续子序列)的数量。
- 子串包含奇数个字符。
- 它的中间字符是
C。更正式地说,如果提取的子串有 lll 个字符,那么它的第 ((l+1)/2)((l+1)/2)((l+1)/2) 个字符是C。
即使两个子串作为字符串完全相同,但只要它们是从不同位置提取的,就需要分别计数。
解题思路
可以对每个 C 单独考虑,因为每个符合条件的子串都必须以某个 C 为中心。对于位置 i 上的 C,左边最多可以延伸 i 个字符,右边最多可以延伸 s.size() - i - 1 个字符。我们取左右两边较小的那个值,就是以这个 C 为中心的符合条件的子串数量。把所有 C 的情况加起来就是答案了。
代码
cpp
#include <bits/stdc++.h>
#define int long long
using namespace std;
string s;
signed main()
{
cin >> s;
int ans = 0;
for (int i = 0; i < s.size(); i++) // 遍历每个字符
if (s[i] == 'C') // 如果是'C'
ans += min((int)s.size() - i, i + 1); // 计算以它为中心的子串数量
cout << ans;
return 0;
}
D - Chalkboard Median
题目描述
黑板上写着一个整数 XXX。
给定 QQQ 个查询,按顺序处理它们。第 iii 个查询(1≤i≤Q1 \le i \le Q1≤i≤Q)如下:
给出两个整数 AiA_iAi 和 BiB_iBi。在黑板上写下这两个新整数 AiA_iAi 和 BiB_iBi。
然后,输出黑板上写的 2i+12i+12i+1 个整数的中位数。
解题思路
我们可以用两个堆来维护所有数字。一个最大堆用来存较小的那一半数,这样它的顶部就是这一半数里最大的;一个最小堆用来存较大的那一半数,它的顶部就是这一半数里最小的。这样安排的好处是,中位数要么是最小堆的顶部,要么是最大堆的顶部,很容易拿到。
对于总数是奇数的情况,我们让最小堆的大小比最大堆多 1,这样中位数就是最小堆的顶部元素。每次添加新数字时,我们先看它和最大堆顶部的大小关系,如果比它大就放到最小堆,否则放到最大堆。
放完之后我们需要调整两个堆的大小平衡。如果最大堆的大小超过了最小堆,就把最大堆的顶部移到最小堆;如果最小堆的大小比最大堆多了不止 1,就把最小堆的顶部移到最大堆。这样每次调整完,中位数就一定是最小堆的顶部了。
代码
cpp
#include <bits/stdc++.h>
using namespace std;
int x, q;
int main()
{
priority_queue<int> q1; // 最大堆,存较小的一半
priority_queue<int, vector<int>, greater<int>> q2; // 最小堆,存较大的一半
cin >> x >> q;
q1.push(x); // 初始数字放入q1
while (q--)
{
int a, b;
cin >> a >> b;
// 将a放入合适的堆
if (a > q1.top())
q2.push(a);
else
q1.push(a);
// 将b放入合适的堆
if (b > q1.top())
q2.push(b);
else
q1.push(b);
// 调整堆的大小,保持q2.size() <= q1.size() + 1
while (q1.size() > q2.size())
q2.push(q1.top()), q1.pop();
while (q2.size() > q1.size() + 1)
q1.push(q2.top()), q2.pop();
cout << q2.top() << '\n'; // 中位数就是q2的顶部
}
return 0;
}
E - Count 123
题目描述
求满足以下所有条件的序列 A=(a1,⋯ ,aX1+X2+X3)A = (a_1, \cdots, a_{X_1 + X_2 + X_3})A=(a1,⋯,aX1+X2+X3) 的数量,对 998244353998244353998244353 取模。序列的长度为 X1+X2+X3X_1+X_2+X_3X1+X2+X3。
- AAA 中恰好包含 X1X_1X1 个 111、X2X_2X2 个 222 和 X3X_3X3 个 333。
- 相邻元素的绝对差不超过 111。即对于所有满足 1≤i≤X1+X2+X3−11 \leq i \leq X_1+X_2+X_3-11≤i≤X1+X2+X3−1 的整数 iii,都有 ∣ai+1−ai∣≤1|a_{i+1} - a_i| \leq 1∣ai+1−ai∣≤1。
解题思路
观察题目条件,相邻元素的差不能超过 1,这意味着 1 和 3 不能直接相邻。我们可以先考虑如何把 1 插入到 2 的里面,然后再放 3。
我们先枚举有 i 个位置插入 1,这 i 个位置要从 x2 + 1 个空位里选(x2 个 2 之间有 x2 + 1 个空位)。选好位置后,我们要把所有的 1 分到这 i 个位置里,用插板法,就是从 x1 - 1 个间隔里选 i - 1 个板子。
最后考虑放 3。因为我们用了 i 个位置放 1,还剩下 x2 - i 个位置可以插入 3。放 3 也可以用插板法,相当于把 x3 个 3 分到 x2 - i + 1 个位置里,也就是从 x3 + x2 - i 个位置里选 x2 - i 个板子。把这三个组合数乘起来,再枚举所有可能的 i 加起来就是答案了。
代码
cpp
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int mod = 998244353, maxn = 3e6 + 10;
int x1, x2, x3, fac[maxn]; // fac[i] 是 i! mod mod
int quick_pow(int a, int b) // 快速幂,计算 a^b mod mod
{
int ans = 1;
while (b)
{
if (b & 1)
(ans *= a) %= mod;
a = a * a % mod;
b >>= 1;
}
return ans;
}
int get_num(int a, int b) // 计算组合数 C(a, b)
{
if (a < b || a < 0 || b < 0)
return 0;
return fac[a] * quick_pow(fac[a - b], mod - 2) % mod * quick_pow(fac[b], mod - 2) % mod;
}
signed main()
{
cin >> x1 >> x2 >> x3;
fac[0] = 1;
for (int i = 1; i < maxn; i++) // 预处理阶乘
fac[i] = fac[i - 1] * i % mod;
int ans = 0;
for (int i = 1; i <= min(x1, x2 + 1); i++) // 枚举i个1被2隔开
{
int sum = 1;
(sum *= get_num(x2 + 1, i)) %= mod; // 选i个位置放分隔的2
(sum *= get_num(x1 - 1, i - 1)) %= mod; // 把x1个1分成i段
(sum *= get_num(x3 + x2 - i, x2 - i)) %= mod; // 放x3个3
ans += sum;
ans %= mod;
}
cout << ans;
return 0;
}
F - Critical Misread
题目描述
给定 KKK 个由小写英文字母组成的字符串 SiS_iSi。
求长度为 NNN 的、由小写英文字母组成的字符串中,不包含任何 S1,S2,...,SKS_1, S_2, \dots, S_KS1,S2,...,SK 作为子串(连续子序列)的字符串数量,对 998244353998244353998244353 取模。
解题思路
首先把所有禁止的字符串插到 trie 树上,然后建立 AC 自动机的 fail 指针。这样我们就能知道,当我们在某个状态时,添加一个新字符会转移到哪个状态。
我们可以用动态规划来计数。dp[i][j] 表示长度为 i 的字符串,最后在 AC 自动机的状态 j 的合法方案数。转移就是从状态 j 添加一个字符 c,转移到状态 k,如果 j 和 k 都不是禁止状态,就可以转移。
因为 N 可能很大,直接递推会超时,所以我们可以把转移关系表示成矩阵,然后用矩阵快速幂来加速。这样就能在 O((K*L)^3 * logN) 的时间内解决问题(应该没算错),其中 L 是禁止字符串的平均长度。
代码
cpp
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int MOD = 998244353;
const int maxn = 110;
int trie[maxn][26], fail[maxn], idx; // trie树、fail指针、节点编号
bool ban[maxn]; // 标记该状态是否包含禁止字符串
ll a[maxn][maxn], res[maxn][maxn]; // 转移矩阵和结果矩阵
void insert(char *s) // 把禁止字符串插入trie树
{
int x = 0;
for (int i = 0; s[i]; i++)
{
int c = s[i] - 'a';
if (!trie[x][c])
trie[x][c] = ++idx;
x = trie[x][c];
}
ban[x] = true; // 标记该节点是禁止的
}
void build() // 建立AC自动机的fail指针
{
queue<int> q;
for (int i = 0; i < 26; i++)
if (trie[0][i])
q.push(trie[0][i]);
while (!q.empty())
{
int u = q.front();
q.pop();
ban[u] |= ban[fail[u]]; // 如果fail指针指向的状态是禁止的,当前状态也是禁止的
for (int i = 0; i < 26; i++)
{
if (trie[u][i])
{
fail[trie[u][i]] = trie[fail[u]][i];
q.push(trie[u][i]);
}
else
{
trie[u][i] = trie[fail[u]][i]; // 路径压缩
}
}
}
}
void mul(ll x[][maxn], ll y[][maxn]) // 矩阵乘法
{
ll z[maxn][maxn] = {0};
for (int i = 0; i <= idx; i++)
for (int k = 0; k <= idx; k++)
for (int j = 0; j <= idx; j++)
z[i][j] = (z[i][j] + x[i][k] * y[k][j]) % MOD;
for (int i = 0; i <= idx; i++)
for (int j = 0; j <= idx; j++)
x[i][j] = z[i][j];
}
ll solve(int n) // 矩阵快速幂求解
{
memset(a, 0, sizeof(a));
memset(res, 0, sizeof(res));
for (int i = 0; i <= idx; i++)
res[i][i] = 1; // 单位矩阵
for (int i = 0; i <= idx; i++) // 构建转移矩阵
{
if (ban[i])
continue;
for (int j = 0; j < 26; j++)
{
if (!ban[trie[i][j]])
a[i][trie[i][j]]++; // 从i转移到trie[i][j]有1种方式
}
}
while (n) // 快速幂
{
if (n & 1)
mul(res, a);
mul(a, a);
n >>= 1;
}
ll ans = 0;
for (int i = 0; i <= idx; i++)
ans = (ans + res[0][i]) % MOD; // 从初始状态0出发,所有可能的终点
return ans;
}
int main()
{
int n, k;
char s[12];
scanf("%d%d", &n, &k);
while (k--)
{
scanf("%s", s);
insert(s);
}
build();
printf("%lld\n", solve(n));
return 0;
}
G - Children Yearn for the Evil Kindergarten
题目描述
游戏场地有 1010010^{100}10100 个孩子。初始时,没有孩子有任何奖牌。
一个孩子只有在被淘汰 或逃跑时才会离开场地。
游戏共 NNN 天。在第 iii 天(1≤i≤N1 \leq i \leq N1≤i≤N),按顺序执行以下操作。
- 收集场地中所有孩子持有的奖牌,设 sss 为收集到的奖牌总数。
- 将 s+Ais + A_is+Ai 枚奖牌自由分配给场地中的孩子(如果场地中没有孩子,则不做任何操作)。
- 在场地中的孩子里,奖牌少于 BiB_iBi 的被淘汰。奖牌不少于 BiB_iBi 的孩子各失去 BiB_iBi 枚奖牌。
- 在场地中的孩子里,奖牌不少于 CiC_iCi 的孩子可以选择此时逃跑或留在场地。
在第 NNN 天结束时仍留在场地的孩子会被淘汰。
求最终逃跑的孩子的最大可能数量。
给定 TTT 组测试数据,请分别求解。
解题思路
这道题可以二分答案。对于某个答案 m,我们可以检查是否存在一种方式让 m 个孩子最终逃跑。如果可以,我们就尝试更大的 m;如果不行,就尝试更小的 m。
关键在于如何高效地检查某个 m 是否可行。我们可以用一些线性函数来表示每个孩子的状态变化,并用凸包来维护这些函数。因为每天的操作都是线性变换,所以可以用斜率来表示状态的变化,用一个双端队列来维护凸包。
我们需要处理的操作包括:分配奖牌、淘汰奖牌不够的孩子、让奖牌够多的孩子逃跑。这些都可以通过维护凸包上的点来实现,确保我们只保留可能得到最优解的状态。最后根据能否在凸包上找到合适的状态来判断 m 是否可行。
代码
cpp
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef pair<ll, ll> pr;
const ll INF = 1LL << 60;
ll n;
vector<ll> a, b, c;
ll slope(pr l, pr r) // 计算两点之间的斜率
{
ll y = r.second - l.second;
ll x = r.first - l.first;
return y / x;
}
bool check(ll m) // 检查是否可以让m个孩子逃跑
{
if (m <= 0)
return true;
ll sa = 0, sb = 0; // 线性变换的参数
deque<pr> dots; // 双端队列维护凸包上的点
dots.push_back({m, 0});
auto evaldot = [&](pr p) -> ll { // 计算点p在当前变换下的值
return sa * p.first + sb + p.second;
};
for (ll di = 0; di < n; di++) // 处理每一天
{
sb += a[di]; // 分配奖牌的影响
sa -= b[di]; // 减去B_i的影响
// 从队尾移除不合法的点(奖牌不够会被淘汰)
while (dots.size())
{
pr p1 = dots.back();
ll y1 = evaldot(p1);
if (y1 >= 0)
break;
dots.pop_back();
if (dots.empty())
break;
pr p2 = dots.back();
ll y2 = evaldot(p2);
if (y2 < 0)
continue;
ll ta = slope(p2, p1);
ll xadd = y2 / (-(sa + ta));
if (xadd > 0)
dots.push_back({p2.first + xadd, p2.second + ta * xadd});
break;
}
// 从队头移除不合法的点
while (dots.size())
{
pr p1 = dots.front();
ll y1 = evaldot(p1);
if (y1 >= 0)
break;
dots.pop_front();
if (dots.empty())
break;
pr p2 = dots.front();
ll y2 = evaldot(p2);
if (y2 < 0)
continue;
ll ta = slope(p1, p2);
ll xadd = y2 / (sa + ta);
if (xadd > 0)
dots.push_front({p2.first - xadd, p2.second - ta * xadd});
break;
}
if (dots.empty()) // 如果没有合法的点,m不可行
return false;
// 维护凸包,处理逃跑的情况
while (dots.size() >= 2)
{
ll s = sa + slope(dots[0], dots[1]);
if (s >= c[di])
dots.pop_front();
else
break;
}
// 计算可以逃跑的孩子数量
ll xcadd = evaldot(dots[0]) / c[di];
if (xcadd >= dots[0].first)
return true; // 足够让m个孩子逃跑
else if (xcadd > 0)
dots.push_front({dots[0].first - xcadd, dots[0].second - (c[di] - sa) * xcadd});
}
return false;
}
void solve() // 二分答案
{
ll ok = 0, ng = max(a[0], 0LL) + 1;
while (ok + 1 < ng)
{
ll med = (ok + ng) / 2;
if (check(med))
ok = med;
else
ng = med;
}
printf("%lld\n", ok);
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(nullptr);
int T;
cin >> T;
while (T--)
{
cin >> n;
a.resize(n);
b.resize(n);
c.resize(n);
for (ll i = 0; i < n; i++)
cin >> a[i] >> b[i] >> c[i];
solve();
}
return 0;
}