AtCoder Beginner Contest 477 ABCDE
A - Traffic Light
- 预估难度:
入门 - 标签:条件分支
题面
问题描述
某个交通信号灯的颜色按 B(蓝色)、Y(黄色)、R(红色)、B(蓝色)、Y(黄色)、R(红色)、\(\dots\) 的顺序周期性变化。
给定一个字符 \(c\),表示交通信号灯当前的颜色。输出交通信号灯的下一个颜色。
约束条件
- \(c\) 是
B、Y、R之一。
输入
输入从标准输入按以下格式给出:
\(c\)
输出
输出表示交通信号灯下一个颜色的字符。
代码
void solve()
{
char c;
cin >> c;
if(c == 'B')
cout << 'Y';
else if(c == 'Y')
cout << 'R';
else
cout << 'B';
}
B - Standing Outliers
- 预估难度:
入门 - 标签:暴力枚举、一维数组
题面
问题陈述
数轴上有 \(N\) 个人。这些人编号为 \(1\) 到 \(N\),第 \(i\) 个人位于坐标 \(X_i\)。多个人可能位于同一坐标。
如果某人与其他每个人的距离都至少为 \(D\),则称此人“离群”。
形式化地,如果对于所有 \(j \ (1 \leq j \leq N,\ j \neq i)\) 都有 \(|X_i-X_j| \geq D\),则称第 \(i\) 个人“离群”。
按升序输出所有“离群”的人的编号。
约束条件
- \(2 \le N \le 100\)
- \(1 \le D \le 10^9\)
- \(1 \le X_i \le 10^9\)
- 所有输入值均为整数。
输入
输入按以下格式从标准输入给出:
\(N\) \(D\)
\(X_1\) \(X_2\) \(\dots\) \(X_N\)
输出
设 \(K\) 为“离群”的人数,并设 \((P_1,P_2,\dots,P_K)\) 为他们的编号按升序排列得到的序列。按以下格式输出它们:
\(K\)
\(P_1\) \(P_2\) \(\dots\) \(P_K\)
如果 \(K=0\),第二行应为空。
代码
void solve()
{
int n, d, x[105];
cin >> n >> d;
for(int i = 1; i <= n; i++)
cin >> x[i];
vector<int> ans;
for(int i = 1; i <= n; i++) // 枚举每个人
{
bool flag = false; // 标记是否有人与 i 距离 < d
for(int j = 1; j <= n; j++) // 枚举其他人
{
if(j == i)
continue;
if(abs(x[j] - x[i]) < d) // 存在一个人与 i 的距离 < d
{
flag = true;
break;
}
}
if(!flag)
ans.push_back(i);
}
cout << ans.size() << "\n";
for(int i : ans)
cout << i << " ";
}
C - Range Search Query
- 预估难度:
普及- - 标签:字符串、前缀和
题面
问题陈述
给定字符串 \(S\) 和 \(T\)。
处理 \(Q\) 个询问。
每个询问给出 \(L\) 和 \(R\)。判断由 \(S\) 的第 \(L\) 个到第 \(R\) 个字符组成的子串是否包含 \(T\) 作为子串。
约束
- \(S\) 是长度在 \(1\) 到 \(4 \times 10^5\) 之间的字符串,由小写英文字母组成。
- \(T\) 是长度在 \(1\) 到 \(10\) 之间的字符串,由小写英文字母组成。
- \(1 \leq Q \leq 2 \times 10^5\)
- \(1 \leq L \leq R \leq |S|\)
- \(Q, L, R\) 是整数。
输入
输入按以下格式从标准输入给出:
\(Q\)
\(S\)
\(T\)
\(\mathrm{query}_1\)
\(\mathrm{query}_2\)
\(\vdots\)
\(\mathrm{query}_Q\)
每个询问 \(\mathrm{query} _ q\) 按以下格式给出:
\(L\) \(R\)
输出
输出 \(Q\) 行。对于第 \(q\) 个询问,如果由 \(S\) 的第 \(L\) 个到第 \(R\) 个字符组成的子串包含 \(T\) 作为子串,则第 \(q\) 行应输出 Yes,否则输出 No。
思路
考虑到 \(T\) 字符串固定不变,且长度较短(最大仅为 \(10\)),考虑预处理出字符串 \(S\) 中有哪些位置的子串与 \(T\) 完全对应即可,时间复杂度 \(O(|S|\cdot |T|)\)。
记 \(\text{ok}_i\) 表示以 \(i\) 为左端点时,子串 \(S_{[i, i+|T|-1]}\) 与字符串 \(T\) 是否相等,值取 true / false。
对于每次询问 \([L, R]\),考虑边界情况:
- 如果子串左端点取 \(L\),对应的匹配结果为 \(\text{ok}_L\)。
- 如果子串右端点取 \(R\),对应左端点即 \(R-|T|+1\),对应的匹配结果即 \(\text{ok}_{R-|T|+1}\)。
因此单次询问只需要在下标区间 \([L, R-|T|+1]\) 范围内,检查是否存在某个位置的 \(\text{ok}_i\) 为 true 即可。静态区间查询,可以借助前缀和思想优化为 \(O(1)\)。
总时间复杂度 \(O(|S|\cdot |T| + Q)\)。
代码
int pre[400005];
// 假设字符串下标从 1 开始
// pre[i] 表示在 1~i 范围内有多少个位置 x 可以作为起点
// 使得 s[x ... x+|t|-1] 与 t 相等
void solve()
{
int q;
cin >> q;
string s, t;
cin >> s >> t;
for(int i = 0; i + t.size() - 1 < s.size(); i++) // 枚举可行的 s[] 子串起点
{
pre[i + 1] = pre[i]; // 前缀和,下标从 1 开始
if(s.substr(i, t.size()) == t) // 判断 s[i ... i+|t|-1] 与 t 是否匹配
pre[i + 1]++;
}
while(q--)
{
int l, r;
cin >> l >> r;
if(r - l + 1 < t.size()) // 长度不够,不可能出现子串 t
cout << "No\n";
else
{
// 当长度为 |t| 的子串右端点为 r 时,对应左端点为 r-|t|+1
// 判断在 l ~ r-|t|+1 范围内是否存在某个位置 可以作为起点,使子串与 t 相等
if(pre[r - t.size() + 1] - pre[l - 1] > 0)
cout << "Yes\n";
else
cout << "No\n";
}
}
}
D - Masking Tape
- 预估难度:
普及 - 标签:模拟、计数思想
题面
问题陈述
有 \(N\) 个方格排成一行,从左到右编号为方格 \(1\) 到方格 \(N\)。颜色用小写英文字母表示。
初始时,每个方格的颜色都是 a,且任何方格上都没有放置瓷砖。
给定 \(Q\) 个查询;请按给定顺序处理它们。每个查询为以下两种类型之一:
-
1 X:如果方格 \(X\) 上没有放置瓷砖,则在其上放置一块瓷砖;如果其上已放置瓷砖,则移除该瓷砖。 -
2 C:将所有未放置瓷砖的方格的颜色改为 \(C\)。
求处理完所有查询后每个方格的颜色(无论其上是否放置有瓷砖)。
约束条件
- \(1 \le N,Q \le 3\times 10^5\)
- 每个查询都是类型 \(1\) 或类型 \(2\)。
- 在类型 \(1\) 查询中,\(X\) 是满足 \(1 \le X \le N\) 的整数。
- 在类型 \(2\) 查询中,\(C\) 是单个小写英文字母。
输入
输入从标准输入按以下格式给出:
\(N\) \(Q\)
\(\mathrm{query}_1\)
\(\vdots\)
\(\mathrm{query}_Q\)
每个查询 \(\mathrm{query} _ i \ (1 \le i \le Q)\) 按以下形式给出:
\(1\) \(X\)
或
\(2\) \(C\)
输出
输出各方格的最终颜色,作为一个长度为 \(N\) 的字符串,从左到右拼接。
思路
下文的“全局颜色”指代上一次操作 2 为所有未放置瓷砖的方格所涂的颜色,初始为 a。
如果正向模拟每次类型 2 操作都要把 \(O(N)\) 个格子刷一遍颜色,直接做是 \(O(NQ)\),肯定不行,需要把"整段染色"这个操作想办法离线削减掉。
注意到某个方格最终的颜色,只取决于它最后一次被瓷砖覆盖的那一段时间里,全局颜色是什么。因为一旦瓷砖被取走、且之后又执行了类型 2,它就会被重新刷成新的全局颜色;反之如果取走后没有再出现类型 2,它保留的就是最后一次覆盖时被定住的那个颜色。
因此完全没必要真的去刷颜色,只需要维护两个"全局"变量:
T记录最后一次类型 2 操作发生的时间(用操作编号表示);C记录最后一次类型 2 操作设置的颜色。
再为每个方格开三个数组:
cover[i]:第 \(i\) 个格子当前是否放有瓷砖;color[i]:第 \(i\) 个格子最后一次被放置瓷砖时的全局颜色;tim[i]:第 \(i\) 个格子最后一次被取消瓷砖覆盖的时间(若从未被覆盖过则为 \(0\))。
处理类型 1 时按题意翻转 cover[x]:放置瓷砖时顺手把当前全局颜色 C 记进 color[x];取消时则把当前操作编号 \(i\) 记进 tim[x]。
但要注意一个细节:放置瓷砖的那一刻格子颜色未必是 C。如果这个格子在此前被覆盖过、又已经取消,且取消之后没有再出现过类型 2 操作(即 tim[x] != 0 && T <= tim[x]),说明它的颜色从上次记录起就没变过,此时再去记 C 反而是错的。代码里用 tim[x] == 0 || T > tim[x] 正是为了排除这种情况——只有从未被覆盖过,或取消覆盖之后又发生过类型 2,才需要更新 color[x]。
最后输出时逐个方格判断:
cover[i]为真:瓷砖还压着,直接输出color[i];- 否则看是否在取消覆盖之后发生过类型 2(
tim[i] == 0表示从未覆盖过,也可视作成立):若tim[i] == 0或tim[i] < T,说明它现在等于全局颜色,输出C; - 剩余情况说明取消覆盖后颜色没再被全局改过,输出
color[i]。
时间复杂度 \(O(N + Q)\)。
代码
int T = 0;
char C = 'a';
// T 记录 最后一次执行操作 2 的时间(按操作编号)
// C 记录 最后一次执行操作 2 时设置的颜色
bool cover[300005];
char color[300005];
int tim[300005];
// cover[i] 记录第 i 个方格 目前是否放置了瓷砖
// color[i] 记录第 i 个方格 最后一次 放置瓷砖 时的方格颜色
// tim[i] 记录第 i 个方格 最后一次 取消放置瓷砖 的时间
void solve()
{
int n, q;
cin >> n >> q;
for(int i = 1; i <= q; i++)
{
int op;
cin >> op;
if(op == 1)
{
int x;
cin >> x;
if(!cover[x])
{
cover[x] = true;
if(tim[x] == 0 || tim[x] != 0 && T > tim[x])
color[x] = C; // 记录当前的全局颜色
}
else
{
cover[x] = false;
tim[x] = i;
}
}
else // 操作 2,更新 T 和 C 变量
{
cin >> C;
T = i;
}
}
for(int i = 1; i <= n; i++)
{
if(cover[i]) // 如果有瓷砖覆盖,则直接输出最后一次覆盖时记录的颜色
cout << color[i];
else
{
if(tim[i] == 0) // 如果从来没有覆盖过瓷砖,直接输出全局颜色
cout << C;
else if(tim[i] < T) // 如果覆盖过瓷砖,且取消覆盖后执行过操作 2,依然输出全局颜色
cout << C;
else // 如果覆盖过瓷砖,但取消覆盖后没有执行过操作 2,输出最后一次覆盖时记录的颜色
cout << color[i];
}
}
}
E - Wheel Distance
- 预估难度:
普及+/提高- - 标签:图论、最短路、前缀和
题面
题目描述
给定一个带权无向图,有 \(N+1\) 个顶点,编号为 \(1\) 到 \(N+1\)。
对于每个满足 \(1 \leq i \leq N\) 的整数 \(i\),存在一条权值为 \(A _ i\) 的边连接顶点 \(i\) 和顶点 \((i \bmod N) + 1\)。
另外,对于每个满足 \(1 \leq i \leq N\) 的整数 \(i\),存在一条权值为 \(B _ i\) 的边连接顶点 \(i\) 和顶点 \(N+1\)。不存在其他边。
处理 \(Q\) 个询问。在每个询问中,给定 \(S\) 和 \(T\);求从顶点 \(S\) 到顶点 \(T\) 的最短路径长度。
约束条件
- \(3 \leq N \leq 2 \times 10^5\)
- \(1 \leq Q \leq 2 \times 10^5\)
- \(1 \leq A _ i, B _ i \leq 10^9\)
- \(1 \leq S \lt T \leq N+1\)
- 所有输入值均为整数。
输入
输入按以下格式从标准输入给出:
\(N\) \(Q\)
\(A_1\) \(A_2\) \(\dots\) \(A_N\)
\(B_1\) \(B_2\) \(\dots\) \(B_N\)
\(\mathrm{query}_1\)
\(\mathrm{query}_2\)
\(\vdots\)
\(\mathrm{query}_Q\)
每个询问 \(\mathrm{query} _ q\) 按以下格式给出:
\(S\) \(T\)
输出
输出 \(Q\) 行。第 \(q\) 行应包含第 \(q\) 个询问的答案。
思路
注意到整张图里只有 \(N+1\) 一个“特殊点”,任意一条从 \(S\) 到 \(T\) 的路径只可能有两种形态:
- 完全不走环外的点 \(N+1\):此时路径被限制在环上,而从 \(S\) 到 \(T\) 只有两条环上的走法只有两种:顺着编号增大直接走过去,或者绕过 \(1\) 与 \(N\) 之间的那条边反着走。记
sa[i]为 \(A_1 + \dots + A_i\),那么两种走法取最小值即可:- 正向走 \(S \to T\) 的距离是
sa[T-1] - sa[S-1]。 - 反向绕行 \(S \to 1 \to N \to T\) 的距离则是
sa[S-1] + (sa[n] - sa[T-1])。
- 正向走 \(S \to T\) 的距离是
- 中途经过 \(N+1\):也就是先从 \(S\) 走到 \(N+1\),再从 \(N+1\) 走到 \(T\)。由于最短路中途重复经过某个点一定不会更优,因此可以直接记
dis[i]表示 \(N+1\) 到 \(i\) 的最短路,然后放心地把这条路径直接拆成dis[S] + dis[T]。
sa 数组可以方便地借助前缀和求出,dis 数组则可以在 \(N+1\) 这个点上跑一遍单源最短路 Dijkstra 预处理得到。
最后注意一种特殊情况:\(T = N+1\),此时第二种形态已退化为 dis[S] 本身,直接输出即可。
预处理 Dijkstra 复杂度 \(O((N+Q)\log N)\),每个询问 \(O(1)\),总时间复杂度 \(O((N + Q)\log N)\)。
代码
typedef long long ll;
typedef pair<ll, int> pli;
struct edge
{
int v, w;
};
int n, q, a[200005], b[200005];
ll sa[200005];
// sa[i] 记录 a[1 .. i] 的前缀和,表示从 1 到 i+1 的正向环上总长度
vector<edge> G[200005];
ll dis[200005];
// dis[i] 记录 n+1 -> i 的最短路长度
void dijkstra(int s)
{
dis[s] = 0;
priority_queue<pli> q;
q.push(pli(0, s));
while(!q.empty())
{
pli p = q.top();
q.pop();
int u = p.second;
if(-p.first != dis[u])
continue;
for(edge e : G[u])
{
int v = e.v, w = e.w;
if(dis[v] > dis[u] + w)
{
dis[v] = dis[u] + w;
q.push(pli(-dis[v], v));
}
}
}
}
void addedge(int u, int v, int w)
{
G[u].push_back({v, w});
G[v].push_back({u, w});
}
void solve()
{
cin >> n >> q;
for(int i = 1; i <= n; i++)
{
cin >> a[i];
sa[i] = sa[i - 1] + a[i];
addedge(i, i % n + 1, a[i]);
}
for(int i = 1; i <= n; i++)
{
cin >> b[i];
addedge(i, n + 1, b[i]);
}
memset(dis, 0x3f, sizeof dis);
dijkstra(n + 1);
while(q--)
{
int s, t;
cin >> s >> t;
if(t == n + 1)
{
cout << dis[s] << "\n";
continue;
}
// s -> t 记 sa[t - 1] - sa[s - 1]
ll x = sa[t - 1] - sa[s - 1];
// t -> n -> 1 记 sa[n] - sa[t - 1]
// 1 -> s 记 sa[s - 1]
ll y = sa[s - 1] + (sa[n] - sa[t - 1]);
cout << min(min(x, y), dis[s] + dis[t]) << "\n";
}
}

浙公网安备 33010602011771号