AtCoder Beginner Contest 465 ABCDE
A - Supermajority
- 预估难度:
入门 - 标签:条件分支
题意
给定两个正整数 \(A\) 和 \(B\)。
如果 \(\displaystyle A > B \times \frac{2}{3}\),请输出 Yes;否则,请输出 No。
数据范围
- \(1 \leq A, B \leq 1000\)
代码
void solve()
{
int a, b;
cin >> a >> b;
if(a * 3 > b * 2)
cout << "Yes";
else
cout << "No";
}
B - Parking 2
- 预估难度:
入门 - 标签:循环语句、条件分支
题意
有一个停车场。该停车场的收费标准如下:
- 在 \(L\) 点整到 \(R\) 点整的时段内,停车每小时收费 \(X\)。
- 在上述时段之外的时间,停车每小时收费 \(Y\)。
如果一辆车从 \(A\) 点整停到 \(B\) 点整,且期间不跨越午夜,那么停车费是多少?
数据范围
- \(1 \leq X, Y \leq 1000\)
- \(1 \leq L < R \leq 23\)
- \(1 \leq A < B \leq 23\)
思路
由于时间均是以小时作为单位,范围很小,所以我们可以通过枚举停车经过的每个小时,然后判断当前这个小时的收费标准属于哪一项。
但需要注意的是,题目给定的 \(L, R, A, B\) 指的均是时刻,而不是一段时间。
所以接下来用整数 \(i\) 描述 \(i\) 点整到 \(i+1\) 点整这一个小时。
一辆车从 \(A\) 点整停到 \(B\) 点整,第一个小时是 \(A \sim A+1\),最后一个小时是 \(B-1 \sim B\),所以可以用整数 \(A, A+1, \ldots, B-1\) 用来描述每一段停车的时间。
判断某个小时是否在 \(L\) 点整到 \(R\) 点整的时段内,只要判断开始时间 \(\ge L\) 且结束时间 \(\le R\) 即可。对于整数 \(i\) 而言,需要满足:
- \(i \ge L\)
- \(i + 1 \le R\)
- 或是 \(i \lt R\)
枚举过程中判断即可。
代码
void solve()
{
int X, Y, L, R, A, B, ans = 0;
cin >> X >> Y >> L >> R >> A >> B;
for(int i = A; i < B; i++)
{
if(i >= L && i < R)
ans += X;
else
ans += Y;
}
cout << ans;
}
C - Reverse Permutation
- 预估难度:
普及- - 标签:模拟、双端队列 / 数组 / vector
题意
给定一个整数 \(N\) 和一个长度为 \(N\) 的字符串 \(S\),字符串仅由字符 o 和 x 组成。
现有一个长度为 \(N\) 的整数序列 \(A=(A_1,A_2,\ldots,A_N)\)。
初始时,\(A=(1,2,\ldots,N)\)。
有 \(N\) 次操作,第 \(k\) 次操作的类型由字符串 \(S\) 的第 \(k\) 个字符 \(S_k\) 描述:
- 如果 \(S_k=\)
o,则将 \(A\) 的前 \(k\) 项进行反转。- 具体来说,将 \(A\) 替换为 \((A_k,A_{k-1},\ldots,A_1,A_{k+1},A_{k+2},\ldots,A_N)\)。
- 如果 \(S_k=\)
x,则不执行任何操作。
请输出所有操作执行完毕后的序列 \(A\)。
数据范围
- \(2\le N\le 5\times 10^5\)
- 字符串 \(S\) 的长度为 \(N\),且仅包含
o和x
思路
由于第 \(i\) 次操作只会影响前 \(i\) 个数字的顺序,不会影响到 \(i+1 \sim N\) 这些数字的顺序。
那我们可以假设数字是根据操作一个一个加入到 \(A\) 序列的,第 \(i\) 次操作将数字 \(i\) 加到序列末尾,而不是一开始就在 \(A\) 序列里。可以发现这并不影响答案。
那我们就可以用一个容器来模拟 \(A\) 序列,只要能够实现每次加一个数字在末尾即可。
当我们把数字 \(i\) 加入序列之后,如果 $S_i = $ x,无事发生;而当 $S_i = $ o 时,说明目前整个序列需要首尾翻转。
由于首尾反转的时间复杂度是 \(O(N)\) 的,如果整个 \(S\) 字符串均为 o,整体时间复杂度将会到达 \(O(N^2)\),不可接受。
但实际上我们不用真的去反转目前的整个序列。
思考下如果我们真的把序列前后反转了,那么后面还没加入序列的数字会加入到序列的什么位置。明显会加到反转之前的第一个数字前面。
所以我们只需要来一个标记变量,用来控制当前序列是正方向还是反方向即可。
如果序列当前是正方向,那么下一个数字就加到序列的最后面;而如果当前序列是反方向,那么下一个数字就加到序列的最前面即可。而每次遇到字符 o 时,则只需要让标记变量取反一次。
那么最后的问题就是,我们需要让用来模拟 \(A\) 序列的容器能实现快速地在序列的前面以及后面加一个数字了。
思路一
考虑数组模拟或是直接使用 STL 容器 deque,实现双端队列的功能。
时间复杂度 \(O(N)\)。
代码一
void solve()
{
int n;
string s;
cin >> n >> s;
deque<int> dq;
bool f = false;
// f 表示当前方向,false 为正,true 为负
for(int i = 1; i <= n; i++)
{
if(!f)
dq.push_back(i);
else
dq.push_front(i);
if(s[i - 1] == 'o') // 表示当前数字加入后,前 i 个数需要首尾反转
f = !f;
}
if(!f) // 目前队列的方向是正的
{
for(int i = 0; i < n; i++)
cout << dq[i] << " ";
}
else // 目前队列的方向是反的
{
for(int i = n - 1; i >= 0; i--)
cout << dq[i] << " ";
}
}
思路二
考虑借助两个数组或 STL 容器 vector,模拟正方向加数字和反方向加数字。最后输出时将处理反方向的容器倒着输出,再把处理正方向的容器正着输出即可。
时间复杂度 \(O(N)\)。
代码二
void solve()
{
int n;
string s;
cin >> n >> s;
vector<int> Z, F;
// Z 用于表示序列正方向的部分
// F 用来表示序列反方向的部分
bool f = false;
// f 表示当前方向,false 为正,true 为负
for(int i = 1; i <= n; i++)
{
if(!f) // 正方向
Z.push_back(i);
else // 反方向
F.push_back(i);
if(s[i - 1] == 'o') // 表示当前数字加入后,前 i 个数需要首尾反转
f = !f;
}
if(!f) // 最终方向为正方向
{
// 先把反方向的部分倒序输出
for(int i = (int)F.size() - 1; i >= 0; i--)
cout << F[i] << " ";
// 再把正方向的部分正序输出
for(int i = 0; i < Z.size(); i++)
cout << Z[i] << " ";
}
else // 最终方向为反方向,同上,但倒着输出
{
for(int i = (int)Z.size() - 1; i >= 0; i--)
cout << Z[i] << " ";
for(int i = 0; i < F.size(); i++)
cout << F[i] << " ";
}
}
D - X to Y
- 预估难度:
普及 - 标签:数学、树、最近公共祖先
题意
给定整数 \(X\)、\(Y\),以及一个不小于 \(2\) 的整数 \(K\)。
现有一个变量 \(x\),其初始值为 \(X\)。你可以对该变量执行零次或多次以下操作:
- 选择一个满足 \(\displaystyle \left\lfloor \frac xK \right\rfloor=y\) 或 \(\displaystyle \left\lfloor \frac yK \right\rfloor=x\) 的整数 \(y\),并将 \(x\) 的值替换为 \(y\)。
请计算使 \(x\) 变为 \(Y\) 所需的最少操作次数。
在给定的数据范围内,可以证明总能在有限次操作内使 \(x=Y\)。
\(T\) 组数据。
数据范围
- \(1\le T\le 2\times 10^5\)
- \(0\le X,Y\le 10^{18}\)
- \(2\le K\le 10^{18}\)
思路
先理解操作的意义:
- 如果选择 \(\displaystyle \left\lfloor \frac xK \right\rfloor=y\) 并让 \(x\) 改为 \(y\),这个操作相当于让 \(x\) 缩小 \(K\) 倍并向下取整。
- 也就是让 \(x\) 变为 \(\displaystyle \left\lfloor \frac xK \right\rfloor\)。
- 如果选择 \(\displaystyle \left\lfloor \frac yK \right\rfloor=x\) 并让 \(x\) 改为 \(y\),满足条件的最小的 \(y\) 发生在恰好整除时,为 \(xK\);最大的 \(y\) 发生在除法余数最大时,为 \(xK + K-1\)。换句话说,这个操作相当于让 \(x\) 放大 \(K\) 倍及以上,但不会到达 \(K+1\) 倍。
- 也就是让 \(x\) 变为 \(xK \sim xK + K - 1\) 范围内的任意整数。
考虑转换题意,考虑二叉树的常规编号规则。
假设目前有一棵根结点编号为 \(0\) 并按层序遍历顺序编号的 \(K\) 叉树,可以发现对于树上编号为 \(x\) 的结点而言,其父结点编号即 \(\displaystyle \left\lfloor \frac xK \right\rfloor\),其 \(K\) 个子结点的编号分别为 \(xK \sim xK + K - 1\)。(除根结点 \(0\) 以外,根结点的最小儿子需从 \(1\) 开始。)
所以这个问题相当于就是在一颗 \(K\) 叉树上,不断通过结点间的移动,问从点 \(X\) 移动到点 \(Y\) 的最小步数。
为求出树上两点间的距离,可以先求出两个点的最近公共祖先,转为两点分别到最近公共祖先的距离。
由于本题的点编号特别大,无法建树进行处理。但又由于 \(K\) 叉树上 \(N\) 号结点的深度大致为 \(\log_K N\),所以最近公共祖先部分可以直接让点暴力向根结点跳跃来求解。
单组数据时间复杂度 \(O(\log_KN)\)。
代码(单组数据)
void solve()
{
ll X, Y, K;
cin >> X >> Y >> K;
int ans = 0;
while(X != Y)
{
if(X > Y)
X /= K;
else
Y /= K;
ans++;
}
cout << ans << "\n";
}
E - Digit Circus
- 预估难度:
提高 - 标签:状压DP、数位DP
题意
求满足以下三个条件中恰好一个条件的整数 \(x\)(\(1 \leq x \leq N\))的数量,结果对 \(998244353\) 取模。
- \(x\) 是 \(3\) 的倍数。
- \(x\) 的十进制表示中包含字符
3。 - \(x\) 的十进制表示中恰好使用了三种不同的数字。
注意:整数的十进制表示中不应包含不必要的前导 0。
数据范围
- \(1 \leq N < 10^{500}\)
思路
首先发现数据范围很大,不可能进行暴力枚举。但注意到条件涉及数位相关的判断,因此可以考虑采用数位 DP 进行求解。
观察条件,可以发现需要在过程中注意三个要点:
- 当前选择的数字是否是 \(3\) 的倍数。
- 判断数字是否是 \(3\) 的倍数可以转为判断数位和是否是 \(3\) 的倍数。
- 当前选择的数字中是否包含 \(3\)。
- 当前选择的数字种类数。
对于第一个条件,可以借助一个状态 \(m\) 记录目前高位已确定的数位和 $ \bmod 3$ 的余数,后续拼接一个新数位可以直接加进去。
对于后两者,我们可以借助状态压缩的方式,用一个 \(10\) 位二进制数 \(sta\) 表示 \(0 \sim 9\) 每种数字目前是否出现过。是否包含 \(3\) 可以借助位运算判断,而数字种类数则可以直接求二进制上 \(1\) 的数量。
除此之外,数位 DP 过程中每一位的取值范围是否自由(即 \(0 \sim 9\) 都可以取)主要取决于高位数字是否和上界 \(N\) 紧贴着取。所以过程中还需要一个标记变量 \(flag\) 用于判断。
综上,可设计出搜索函数 dfs(p, m, sta, flag):
p表示当前搜到了第几位(高位为 \(0\))m表示已选择的数位总和 $ \bmod 3$sta表示已选择的数位集合flag表示已选择的高位数字是否紧贴着边界 \(N\) 取
借助搜索函数再开一个 dp[p][m][sta][flag] 数组用于记忆化即可。
对于搜索过程,重点要记住单独讨论高位全取 \(0\) 的情况。
时间复杂度 \(O(L\cdot M \cdot 2^D)\),其中 \(L = \log_{10}N\) 表示 \(N\) 的长度,\(M = 3\),\(D = 10\) 表示数位种类数。
代码
string N;
ll dp[505][3][1 << 10][2];
// dp[i][m][sta][flag] 用于辅助记忆化搜索
// 当前搜到第 p 位过
// 前面选择的数位总和 %3 == m
// 出现过的数字集合为 sta
// 且(flag)表示前 i 位是否贴着 N 的上限在取
ll dfs(int p, int m, int sta, bool flag)
{
// 边界,已搜完
if(p == N.size())
{
if(sta == 0) // 选的是数字 0,不符合题意
return 0;
int cnt = 0;
cnt += (m == 0); // 前面选择的数字是 3 的倍数
cnt += (sta & (1 << 3)) != 0; // 前面选的数字数位上包含 3
cnt += (__builtin_popcount(sta) == 3); // 前面选的数字恰好用了三种数位
return cnt == 1 ? 1 : 0; // 恰好满足一个条件
}
// 记忆化
if(dp[p][m][sta][flag] != -1)
return dp[p][m][sta][flag];
ll result = 0;
// 当前这一位能填的最大数字
// 如果前面都是贴着 N 填的,那么这一位最大也只能填 N 的这一位
// 否则直接填 9
int maxx = flag ? (N[p] - '0') : 9;
// 枚举当前这一位要填的数 i
for(int i = 0; i <= maxx; i++)
{
if(i == 0 && sta == 0) // 说明此时前面全部填 0,还没有选真正的第一个数字
result += dfs(p + 1, 0, 0, flag && (i == maxx));
else
result += dfs(p + 1, // 接着搜下一位
(m + i) % 3, // 加上 i 这一位除以 3 取余数
sta | (1 << i), // 把 i 加入用过的数字集合
flag && (i == maxx)); // 前面贴着 N 取,这一位也贴着 N 取时
}
result %= mod;
dp[p][m][sta][flag] = result;
return result;
}
void solve()
{
cin >> N;
memset(dp, -1, sizeof dp);
cout << dfs(0, 0, 0, true);
}

浙公网安备 33010602011771号