AtCoder Beginner Contest 450 ABCDEF 题目解析
A - 3,2,1,GO
- 预估难度:
入门
题意
依次输出 \(N, N-1, \dots, 1\),中间用逗号 , 分隔。
代码
void solve()
{
int n;
cin >> n;
for(int i = n; i >= 1; i--)
{
cout << i;
if(i != 1)
cout << ",";
}
}
B - Split Ticketing
- 预估难度:
入门 - 标签:枚举
题意
有 \(N\) 个车站 \(1, 2, \dots, N\),按顺序从西到东排成一条直线。
AtCoder 铁路列车从西向东经过这 \(N\) 个车站。
对于满足 \(1 \leq i \lt j \leq N\) 的任意两个整数 \(i, j\),在车站 \(i\) 上车并在车站 \(j\) 下车的费用为 \(C_{i,j}\)。
判断是否存在三个整数 \(a, b, c\),使得:
- \(1 \leq a \lt b \lt c \leq N\)
- 在车站 \(a\) 上车、在车站 \(b\) 下车,然后再次在车站 \(b\) 上车、在车站 \(c\) 下车的总费用,小于直接在车站 \(a\) 上车并在车站 \(c\) 下车的费用。
思路
\(O(N^3)\) 枚举对应的 \(a, b, c\) 车站,判断是否存在 \(C_{a, b} + C_{b, c} \lt C_{a, c}\) 即可。
代码
int N;
int C[105][105];
void solve()
{
cin >> N;
for(int i = 1; i < N; i++) // 上车车站
for(int j = i + 1; j <= N; j++) // 下车车站
cin >> C[i][j];
for(int a = 1; a <= N; a++)
for(int b = a + 1; b <= N; b++)
for(int c = b + 1; c <= N; c++)
if(C[a][b] + C[b][c] < C[a][c])
{
cout << "Yes";
return;
}
cout << "No";
}
C - Puddles
- 预估难度:
普及- - 标签:搜索
题意
有一个 \(H\) 行 \(W\) 列的网格。
如果 \(S_{i,j}\) 是 #,则第 \(i\) 行第 \(j\) 列的单元格被涂成黑色;如果 \(S_{i,j}\) 是 .,则被涂成白色。
请找出有多少个由白色单元格组成的四连通区域,其被黑色单元格所包围。
思路
如果一个白色连通块没有被黑色单元格包围,那么也就说明从它内部的单元格出发,一定能够走到地图边界。
反过来,我们可以从地图边界上的所有白色单元格出发,先把所有没被包围的连通块都标记掉。之后剩下来的没有被标记过的白色连通块即为答案,宽搜或深搜求解连通块数量即可。
时间复杂度 \(O(H \times W)\)。
代码
const int dx[4] = {-1, 1, 0, 0};
const int dy[4] = {0, 0, -1, 1};
int n, m;
char mp[1005][1005];
bool vis[1005][1005]; // 标记每个位置是否被搜过
// 搜索 (x, y) 所在的白色连通块
void dfs(int x, int y)
{
vis[x][y] = true;
for(int i = 0; i < 4; i++)
{
int px = x + dx[i], py = y + dy[i];
if(px < 1 || py < 1 || px > n || py > m) // 超出边界
continue;
if(mp[px][py] != '.') // 不是白色连通块
continue;
if(vis[px][py]) // 已搜过
continue;
dfs(px, py);
}
}
void solve()
{
cin >> n >> m;
for(int i = 1; i <= n; i++)
for(int j = 1; j <= m; j++)
cin >> mp[i][j];
// 在地图周围的白色网格所在连通块 没有被黑色包围
for(int i = 1; i <= n; i++)
for(int j = 1; j <= m; j++)
if(mp[i][j] == '.')
if(i == 1 || j == 1 || i == n || j == m)
dfs(i, j);
int ans = 0;
for(int i = 1; i <= n; i++)
for(int j = 1; j <= m; j++)
if(mp[i][j] == '.' && !vis[i][j]) // 没被搜过的白色连通块
{
dfs(i, j);
ans++;
}
cout << ans;
}
D - Minimize Range
- 预估难度:
普及- - 标签:枚举、贪心
题意
给定由 \(N\) 个正整数组成的序列 \(A = (A_1, A_2, \dots, A_N)\) 和一个正整数 \(K\)。
你可以对序列 \(A\) 执行以下操作任意次:
- 选择一个整数 \(i\)(\(1 \leq i \leq N\)),并将 \(A_i\) 加上 \(K\)。
求序列 \(A\) 中最大值与最小值之差可能的最小值。
思路
由于最终要求的答案为最值之差,所以我们只需要关注数字之间的差值而非实际大小。
可以考虑数字间的相对关系,明确“让小的数字变大等同于让大的数字变小”,因此操作也可以理解为每次任选数字减去 \(K\) 或加上 \(K\) 均可,这对最终答案来说没有影响。
既然操作不局限于加法了,不如直接让给定的所有 \(A_i\) 全部通过 \(-K\) 操作降低为最小的非负整数先,即 \(A_i := A_i \bmod K\),这样整个序列的数值范围就都变为 \([0, K-1]\) 之间了。
接下来便可以通过枚举去假设让每个数都成为最终序列中的最小值。假设 \(A_i\) 是序列的最小值,那么对于目前序列而言,我们应该让所有 \(\lt A_i\) 的数都进行一次 \(+K\) 操作,然后取序列中的最大值与 \(A_i\) 求差值即可得到答案。
对于序列最大值,易得以下分类讨论:
- 如果序列中没有任何 \(\lt A_i\) 的数字,那么 \(A_i\) 也就是原序列的最小值,此时最大值便是原序列的最大值。
- 如果序列中存在 \(\lt A_i\) 的数字,由于前面的处理中序列内每个整数均在 \([0, K-1]\) 范围内,只要某个数字 \(+K\) 就一定会超过序列中的其它所有数字。因此,此时最大值便是“\(\lt A_i\) 的数字中最大的数字 \(+K\)”。
为了快速地求出 \(\lt A_i\) 的数字中最大的数,可以对 \(A\) 序列进行排序,那么上述分类讨论可以更简单地变为:
- 假如 \(A_1\) 是最小值,那么最大值就是 \(A_N\),差值即为 \(A_N - A_1\)。
- 假如 \(A_i\) \((2 \le i \le N)\) 是最小值,那么最大值就是 \(A_{i-1} + K\),差值即为 \(A_{i-1} + K - A_i\)。
求最小差值即可,时间复杂度 \(O(N\log N)\)。
代码
int n, k;
int a[200005];
void solve()
{
cin >> n >> k;
for(int i = 1; i <= n; i++)
{
cin >> a[i];
a[i] %= k;
}
sort(a + 1, a + n + 1);
int ans = a[n] - a[1];
for(int i = 2; i <= n; i++)
ans = min(ans, (a[i - 1] + k) - a[i]);
cout << ans;
}
E - Fibonacci String
- 预估难度:
普及/提高- - 标签:深度优先搜索、前缀和
题意
给定字符串 \(X\) 和 \(Y\)。按如下方式定义字符串序列 \(S_1, S_2, \dots\):
- \(S_1 = X\)
- \(S_2 = Y\)
- 对于 \(i \geq 3\),\(S_i\) 是前后拼接 \(S_{i-1}\) 和 \(S_{i-2}\) 两个字符串所得到的字符串。
对于每个 \(i = 1, 2, \ldots, Q\),回答以下问题。
- 给定整数 \(L_i, R_i\) 和字符 \(C_i\),求字符 \(C_i\) 在字符串 \(S_{10^{18}}\) 的第 \(L_i\) 个到第 \(R_i\) 个字符中出现的次数。
思路
首先注意到,当 \(i \geq 3\) 时,\(S_i\) 是将 \(S_{i-1}\) 作为其前半部分,再在后面拼上 \(S_{i-2}\) 所得到的。换句话说, \(S_{i-1}\) 一定是是 \(S_i\) 的前缀。扩展出去,\(S_{i-1}\) 一定是后面所有字符串的前缀。
对于该字符串序列中每个字符串的长度,其增长速度类似于斐波那契数列,并且本题询问的下标区间 \(L_i, R_i\) 在 \(10^{18}\) 范围内。根据斐波那契数列 \(F_1 = F_2 = 1, F_i = F_{i-1} + F_{i-2}\) 可得,\(F_{88} \gt 10^{18}\),因此我们完全不需要在意题目中的 \(S_{10^{18}}\) 这一项说明,最多只需要看到字符串序列第 \(88\) 项即可。
如此,我们便可以借助深搜+剪枝来从长度最大的这一项开始倒着解决每个询问。定义数组 \(\text{len}[i]\) 表示第 \(i\) 个字符串的长度,明显:
- \(\text{len}[1] = |X|\)
- \(\text{len}[2] = |Y|\)
- \(\text{len}[i] = \text{len}[i-1] + \text{len}[i-2]\)
按顺序处理该数组,直到某一项 \(\text{len} \gt 10^{18}\) 为止,记最大这一项为第 \(n\) 项,之后每次询问就从这一项开始往前推。
我们记 \(f(n, l, r, c)\) 表示“\(S_n\) 的下标 \([l, r]\) 范围内有多少个字符 \(c\)”。由于 \(S_n = S_{n-1} + S_{n-2}\),因此我们可以将 \(S_n\) 的下标区间划分成 \(1 \sim \text{len}[n-1]\) 与 \(\text{len}[n-1]+1 \sim \text{len}[n]\) 这两段。分类讨论询问的区间 \([l, r]\) 与这两段下标区间的关系:
- 如果 \(r \le \text{len}[n-1]\),说明整个问题与后半段的 \(S_{n-2}\) 无关,那么该问题便等价于
- \(f(n-1, l, r, c)\)
- 如果 \(l \gt \text{len}[n-1]\),说明整个问题与前半段的 \(S_{n-1}\) 无关,那么该问题便等价于
- \(f(n-2, l-\text{len}[n-1], r-\text{len}[n-1], c)\)
- 否则,说明询问跨越了这两段下标区间,该问题与 \(S_{n-1}\) 与 \(S_{n-2}\) 均有关联,此时问题等价于以下两部分
- \(f(n-1, l, \text{len}[n-1], c)\)
- \(f(n-2, 1, r - \text{len}[n-1], c)\)
考虑搜索边界,当 \(n=1\) 时等价于直接询问字符串 \(X\) 内部字符出现个数,当 \(n=2\) 时等价于直接询问字符串 \(Y\),那么便可以对这两个字符串开两个前缀和数组,用于快速获取一段区间内某种字符的出现次数。
如果搜索只写到这里,时间复杂度等同于递归实现斐波那契数列求解,最坏情况下可能达到 \(O(2^n)\),这是因为过程中出现了大量的重复子问题。
考虑剪枝。首先发现问题只会在遇到上述分类讨论的第三种情况时,才会因为询问跨越两段区间,而导致单个问题变为多个子问题。但观察此时的询问区间,会发现其从原本的字符串内部,变成了询问两个字符串的前后缀,因此在之后的搜索过程中,一旦两个子问题再遇到跨越两段区间的情况,其中一定会有一个子问题包含整个字符串。
因此我们可以再开一个数组 \(\text{sum}\),用于求解每个字符串中某个字符的出现次数,来将上述包含整个字符串的询问直接解决。这个数组可以方便地跟着 \(\text{len}\) 数组一起递推求得。
这样,我们便可以保证单次询问的时间复杂度是 \(O(n)\) 的。
最终时间复杂度为 \(O((|X| + |Y| + n) \cdot \sigma + Q \cdot n)\),其中 \(\sigma = 26, n \le 88\)。
代码
string x, y;
ll len[100]; // len[i] 表示第 i 项的字符串长度
int xpre[10005][26]; // xpre[i][j] 表示字符串 x 的前 i 个位置里出现了多少次字符 j
int ypre[10005][26]; // ypre[i][j] 表示字符串 y 的前 i 个位置里出现了多少次字符 j
ll sum[100][26]; // sum[i][j] 表示第 i 个字符串中出现了多少次字符 j
// 求第 n 个字符串下标 [l, r] 范围内出现了多少次字符 c
ll dfs(int n, ll l, ll r, int c)
{
if(n == 1)
return xpre[r][c] - xpre[l-1][c];
if(n == 2)
return ypre[r][c] - ypre[l-1][c];
if(l == 1 && r == len[n]) // 询问包含整个字符串 S[n]
return sum[n][c];
// S[n] = S[n-1] + S[n-2]
ll mid = len[n - 1];
if(r <= mid)
return dfs(n - 1, l, r, c);
else if(l > len[n - 1])
return dfs(n - 2, l - mid, r - mid, c);
else
return dfs(n - 1, l, mid, c) + dfs(n - 2, 1, r - mid, c);
}
void solve()
{
cin >> x >> y;
// 处理两字符串各字符的前缀数量
for(int i = 0; i < x.size(); i++)
for(int j = 0; j < 26; j++)
xpre[i + 1][j] = xpre[i][j] + (x[i] - 'a' == j);
for(int i = 0; i < y.size(); i++)
for(int j = 0; j < 26; j++)
ypre[i + 1][j] = ypre[i][j] + (y[i] - 'a' == j);
for(int j = 0; j < 26; j++)
{
sum[1][j] = xpre[x.size()][j];
sum[2][j] = ypre[y.size()][j];
}
int n = 2; // 可能需要用到的字符串序列最大下标
len[1] = x.size();
len[2] = y.size();
while(true)
{
n++;
len[n] = len[n - 1] + len[n - 2];
for(int j = 0; j < 26; j++)
sum[n][j] = sum[n-1][j] + sum[n-2][j];
if(len[n] > 1e18) // 第 n 个字符串长度超过 1e18,即可结束
break;
}
int q;
cin >> q;
while(q--)
{
ll l, r;
char c;
cin >> l >> r >> c;
cout << dfs(n, l, r, c - 'a') << "\n";
}
}
F - Strongly Connected 2
- 预估难度:
提高+/省选- - 标签:动态规划、线段树
题意
有一个包含 \(N\) 个点和 \(M + N - 1\) 条边的有向图,边和点都有编号。
对于 \(1 \leq i \leq M\),第 \(i\) 条边是从点 \(X_i\) 指向点 \(Y_i\) 的有向边(\(X_i \lt Y_i\));
对于 \(1 \leq i \leq N-1\),第 \(M+i\) 条边是从顶点 \(i+1\) 指向顶点 \(i\) 的有向边(即存在一条从 \(N\) 到 \(1\) 的链)。
现在考虑从第 \(1 \sim M\) 条边中选择一些边并删除(也可以不选),共有 \(2^M\) 种选择方式。
请问有多少种选择方式,使得在删除所选边后,剩余的图是强连通的?输出对 \(998244353\) 取模后的结果。
思路
由于 \(N \rightarrow 1\) 这条链上的 \(N-1\) 条边不可被删除,且另外的 \(M\) 条边起点编号一定小于终点编号。为了保证强连通,就必须要保证剩余的图中有至少一种走法能从点 \(1\) 走到点 \(N\)。
首先改变一下题意,求删除边的方案数,等价于求保留边的方案数。
假设有四个整数 \(1 \le a \lt b \lt c \lt d \le N\),且 \(a \rightarrow c\) 与 \(b \rightarrow d\) 存在两条边,由于倒序链的存在,我们可以从编号较大的 \(c\) 走回点 \(b\),因此这样交叉的两条链也能够实现从 \(a \rightarrow d\) 的走法。因此,我们可以尝试去维护所有选择的边能够到达的最右侧顶点的编号。
考虑动态规划。既然需要维护选择的边能到达的最右侧顶点,可以先让每条边按起点 \(X_i\) 从小到大排序,然后按顺序考虑每条边。记 \(\text{dp}[i][j]\) 表示考虑前 \(i\) 条边,有多少种选择方案能够从点 \(1\) 出发,最大能到达点 \(j\)。
初始状态 \(\text{dp}[0][0] = 1\)。
对于 \(\text{dp}[i][j]\) 与正在看的第 \(i\) 条边 \(X_i \rightarrow Y_i\),讨论如下:
- 如果 \(j \lt X_i\),此时 \(j\) 到 \(X_i\) 之间存在未被覆盖的部分,我们无法从 \(j\) 直接到达 \(X_i\)。又因为边按起点 \(X_i\) 排序,因此后面的边也无法覆盖这一段,可以当作无解,无需转移。
- 如果 \(X_i \le j \lt Y_i\),此时选择这条边会导致最大能到达的点变为 \(Y_i\)。为了让最大能到达的点仍然为 \(j\),此时这条边只能不选。
- \(\text{dp}[i][j] = \text{dp}[i-1][j]\)
- 如果 \(j = Y_i\),此时我们可以将此前最大覆盖到 \(X_i \sim Y_i\) 之间的所有方案,通过选择这条边,来将其全部变为最大覆盖到 \(j\) 的方案。而如果这条边不选,那么答案只能取在此之前就覆盖到 \(Y_i\) 的方案。
- \(\text{dp}[i][j] = (\sum\limits_{k=X_i}^{Y_i}\text{dp}[i-1][k]) + \text{dp}[i-1][j]\)
- 如果 \(j \gt Y_i\),此时这条边选与不选均不影响最大覆盖的位置。
- \(\text{dp}[i][j] = 2\cdot \text{dp}[i-1][j]\)
上述动态规划时间复杂度为 \(O(N \cdot M)\),仍然较高,但我们可以考虑优化掉表示看到第几条线段过的第一维,只维护其中用于表示最大覆盖到的顶点编号的第二维。
那么,对于每次操作,可以简化为:
- 对下标 \(Y_i\) 位置的值加上 \(\sum\limits_{k=X_i}^{Y_i}\text{dp}[i-1][k]\)
- 对下标在区间 \([Y_i + 1, N]\) 范围内的值全部 \(\times 2\)
操作涉及单点修改、区间求和与区间修改,考虑带懒惰标记的线段树进行优化即可。
时间复杂度 \(O(M\log M + M\log N)\)。
代码
typedef long long ll;
struct edge
{
int x, y;
bool operator < (const edge &e) const
{
return x < e.x;
}
};
edge e[200005];
int n, m;
#define ls (p << 1)
#define rs (p << 1 | 1)
struct node
{
int l, r;
ll sum; // 区间总和
ll lazy; // 控制区间乘的懒惰标记
};
node tr[200005 << 2];
// 答案上传
void push_up(int p)
{
tr[p].sum = (tr[ls].sum + tr[rs].sum) % mod;
}
// 标记下传
void push_down(int p)
{
if(tr[p].lazy == 1)
return;
tr[ls].sum = tr[ls].sum * tr[p].lazy % mod;
tr[rs].sum = tr[rs].sum * tr[p].lazy % mod;
tr[ls].lazy = tr[ls].lazy * tr[p].lazy % mod;
tr[rs].lazy = tr[rs].lazy * tr[p].lazy % mod;
tr[p].lazy = 1;
}
void build(int l, int r, int p = 1)
{
tr[p].l = l;
tr[p].r = r;
tr[p].lazy = 1;
if(l == r)
return;
int mid = l + r >> 1;
build(l, mid, ls);
build(mid + 1, r, rs);
}
// 单点修改,让 pos 位置的值加 add
void updatePoint(int pos, ll add, int p = 1)
{
if(tr[p].l == tr[p].r) // 叶子结点
{
tr[p].sum = (tr[p].sum + add) % mod;
return;
}
push_down(p);
if(pos <= tr[ls].r)
updatePoint(pos, add, ls);
if(pos >= tr[rs].l)
updatePoint(pos, add, rs);
push_up(p);
}
// 区间修改,让区间 [l, r] 每个值均 *2
void updateSegment(int l, int r, int p = 1)
{
if(l <= tr[p].l && tr[p].r <= r)
{
tr[p].sum = tr[p].sum * 2 % mod;
tr[p].lazy = tr[p].lazy * 2 % mod;
return;
}
push_down(p);
if(l <= tr[ls].r)
updateSegment(l, r, ls);
if(r >= tr[rs].l)
updateSegment(l, r, rs);
push_up(p);
}
// 区间求和
ll query(int l, int r, int p = 1)
{
if(l <= tr[p].l && tr[p].r <= r)
return tr[p].sum;
push_down(p);
ll res = 0;
if(l <= tr[ls].r)
res += query(l, r, ls);
if(r >= tr[rs].l)
res += query(l, r, rs);
return res % mod;
}
void solve()
{
cin >> n >> m;
for(int i = 1; i <= m; i++)
cin >> e[i].x >> e[i].y;
sort(e + 1, e + m + 1);
build(1, n);
updatePoint(1, 1); // dp[1] = 1
for(int i = 1; i <= m; i++)
{
int &x = e[i].x, &y = e[i].y;
// 单点修改,y 位置加 [x, y] 的总和
updatePoint(y, query(x, y));
// 区间修改,[y+1, n] 全部 *2
if(y + 1 <= n)
updateSegment(y + 1, n);
}
cout << query(n, n);
}

浙公网安备 33010602011771号