AtCoder Beginner Contest 416 ABCDEF 题目解析
A - Vacation Validation
题意
给定一个字符串 \(S\) 以及两个整数 \(L, R\),问字符串第 \(L\) 个字符到第 \(R\) 个字符是否都是 o?
代码
char s[105];
void solve()
{
int n, l, r;
cin >> n >> l >> r;
cin >> (s + 1);
for(int i = l; i <= r; i++)
{
if(s[i] != 'o')
{
cout << "No";
return;
}
}
cout << "Yes";
}
B - 1D Akari
题意
给定一个仅由 . 和 # 组成的字符串 \(S\)。
你需要尽可能多地把字符串 \(S\) 中的 . 改成 o,但需要满足以下条件:
- 任意两个
o之间至少存在一个#
答案不唯一,输出任意一种即可。
思路
题目要求任意两个 o 之间至少存在一个 #
我们可以把 # 作为分隔符,把 \(S\) 分成多段均由 . 组成的字符串
为了让字符 o 尽可能多,肯定是每段都选择把一个 . 改成 o 最好
代码方面,我们可以从前往后扫一遍字符串,只要遇到一个 #,就说明接下来是新的一段由 . 组成的字符串,我们就可以马上把接下来遇到的第一个 . 改成 o,改完后继续等待下一个 # 出现即可,如此反复
过程中用一个标记变量 flag 来记录“自从上一次遇到 # 开始,是否已经遇到过一个 . 并将其改为 o 了”,以此判断接下来是否需要更改即可
代码
char s[105];
void solve()
{
cin >> (s + 1);
int n = strlen(s + 1);
bool flag = false; // 表示自从上一次遇到 # 开始,是否已经把某个 . 改为 o
for(int i = 1; i <= n; i++)
{
if(s[i] == '#')
flag = false;
else // 遇到的是 .
{
if(flag == false) // 如果还没改过,就直接改
{
s[i] = 'o';
flag = true;
}
}
}
cout << (s + 1);
}
C - Concat (X-th)
题意
给定 \(N\) 个长度不超过 \(10\) 的字符串 \(S_1, S_2, \dots, S_N\)。
对于一个长度为 \(K\) 且各个元素均在 \([1, N]\) 之间的序列 \((A_1, A_2, \dots, A_K)\),我们定义 \(f(A_1, A_2, \dots, A_K)\) 表示将 \(S_{A_1}, S_{A_2}, \dots, S_{A_K}\) 这些字符串进行首尾拼接后得到的字符串。
对于所有 \(N^K\) 种可能的序列所组成的最终字符串 \(f(A_1, A_2, \dots, A_K)\),在按照字典序对所有字符串排序后,问排在第 \(x\) 个位置的字符串是什么?
思路
注意到数据范围均比较小,可以考虑模拟法,预处理出所有可能的字符串,排序后输出第 \(x\) 小的。
对于所有可能的序列 \((A_1, A_2, \dots, A_K)\),可以根据 \(k\) 的大小直接写 for 循环嵌套,但最好还是借助深搜来完成对序列的搜索。
对于搜索过程,可以先搜出所有 \(N^K\) 种可能的序列,然后把序列每一项当作字符串数组 \(S\) 的下标,把每个对应的字符串在搜索的最后一一拼接起来得到最终字符串;或者直接将当前搜索得到的字符串直接当作参数传递,每搜一个字符串就拼接上去,搜到最后一层自然就得到最终字符串了。
最后可以用数组或是 vector 容器把所有可能的最终字符串存起来,排序后输出指定位置即可。
时间复杂度 \(O(|S| \cdot N^K \log N^K)\),其中 \(|S|\) 可以当作最终字符串的平均长度,表示字符串比较对时间复杂度的常数产生的影响。
代码
int n, k, x;
string s[15];
vector<string> V; // 存储所有可能的最终字符串
// 正在搜索序列第 p 个字符串
// 此时前面已选择的字符串拼接后得到 s
void dfs(int p, string t)
{
if(p > k) // 找到一个长度为 k 的序列所拼出的字符串后
{
V.push_back(t);
return;
}
for(int i = 1; i <= n; i++)
dfs(p + 1, t + s[i]); // 第 p 个字符串选择 S[i],然后继续向后搜
}
void solve()
{
cin >> n >> k >> x;
for(int i = 1; i <= n; i++)
cin >> s[i];
dfs(1, "");
sort(V.begin(), V.end());
cout << V[x - 1] << "\n"; // vector 下标从 0 开始
}
D - Match, Mod, Minimize 2
题意
给定两个长度为 \(N\) 的非负整数序列 \(A\) 和 \(B\) 以及一个正整数 \(M\)。
你可以任意排列序列 \(A\) 中的元素位置,并使得以下公式取到最小答案,输出这个最小值:
思路
根据公式,题目希望我们任意排列 \(A\) 数组内的数字,然后让每个数字与 \(B\) 数组内每个位置的数字一一对应,最后将对应的两个数字 \(A_i, B_i\) 相加后除以 \(M\) 取余数,将这个余数加入到答案当中。
首先考虑任意排列这一步,排列的目的是可以任意打乱 \(A\) 和 \(B\) 两序列内各个数字的对应关系,因此只能任意排列 \(A\) 序列和可以任意排列两个序列对于本题而言结果是相同的。因此 \(B\) 数组的数字顺序我们也不用过于在意。
然后考虑每两个数字统计进答案的这一部分,因为是相加后除以 \(M\) 取余数,所以如果每个数字一开始就是 \(\ge M\) 的,那么取其各自先对 \(M\) 取余后的结果,再拿来相加后再取一次余数,得到的最终答案是一样的。也就是说答案也可以写成以下公式:
所以我们可以在输入 \(A, B\) 两序列每个元素时,直接先对 \(M\) 取余,然后剩下的每个数字就应该都在 \([0, M-1]\) 的范围内了。
接下来就是讨论,两个范围在 \([0, M-1]\) 内的数字 \(A_i, B_i\) 在相加后再对 \(M\) 取余的结果会有什么情况:
- 如果 \(A_i + B_i \lt M\),此时最后一次取余做和不做的结果都一样,这两个数字的当前值会直接原封不动地加到答案里。
- 如果 \(A_i + B_i \ge M\),由于两个数字最大也只有 \(M-1\),所以这个条件还可以写成 \(M \le A_i + B_i \lt 2M\)。此时如果我们取 \((A_i + B_i) \bmod M\) 作为答案,实际上和直接计算 \(A_i + B_i - M\) 是一样的。
也就是说,现在对于每一对 \(A_i, B_i\),他们对答案的影响要么就是 \(A_i + B_i\),要么就是 \(A_i + B_i - M\)。
也就是说,现在的最终答案可以看作是:
其中 \(k\) 表示的是有多少对 \(A_i, B_i\) 在相加后 \(\ge M\)。
为了让最终答案最小,很明显我们应该让 \(k\) 最大。也就是尽可能地去配对两数组中的数字,使得 \(A_i + B_i \ge M\) 的数量越多越好。
这里可以采用贪心的策略,先对 \(A, B\) 两序列进行排序,然后采用双指针算法对配对过程进行优化。
我们的目的是让 \(A_i + B_j \ge M\),如果我们枚举的是 \(A_i\),随着 \(A_i\) 慢慢变大,满足条件的 \(B_j\) 一定会跟着慢慢变小,所以可以 \(i = 1 \rightarrow N\) 枚举,让 \(j = N \rightarrow 1\) 跟随着变化即可。
单组数据时间复杂度 \(O(N\log N)\)。
代码
int a[300005], b[300005];
void solve()
{
int n, m;
cin >> n >> m;
long long sum = 0; // 当前总和
for(int i = 1; i <= n; i++)
{
cin >> a[i];
a[i] %= m; // 最后统计答案时要两数相加再 %m,所以只有 %m 的部分会对答案有贡献,下同
sum += a[i];
}
for(int i = 1; i <= n; i++)
{
cin >> b[i];
b[i] %= m;
sum += b[i];
}
// 任意排列 a 然后再一一对应,与 a b 两数组均任意排列后再一一对应 意义相同
sort(a + 1, a + n + 1);
sort(b + 1, b + n + 1);
// 双指针,i 表示现在正在看 a[i] 能否和某个 b[j] 匹配,使得总和 >= m
// j 则表示目前 b 数组还没用过的最大数字 b[j] 所在的位置
for(int i = 1, j = n; i <= n; i++)
{
if(a[i] + b[j] >= m) // 只要相加 >=m,那么只有 %m 的部分对答案有贡献,总和会减少 m
{
j--; // 这个 b[j] 就和 a[i] 匹配,表示用过了
sum -= m;
}
}
cout << sum << "\n";
}
int main()
{
int T;
cin >> T;
while(T--)
solve();
return 0;
}
E - Development
题意
有一个国家共有 \(N\) 座城市,一开始有 \(M\) 条道路和 \(K\) 个机场。
第 \(i\) 条道路连接 \(A_i, B_i\) 两座城市,道路是双向的,通行需要花费 \(C_i\) 个小时。
机场会建在城市的内部,只要两座城市都有机场,那么就可以花费固定的 \(T\) 小时在这两座城市之间通过飞机进行通行。
有 \(Q\) 个操作,按顺序进行,共三种类型:
1 x y t:表示 \(x, y\) 两座城市之间要新建一条双向道路,通行时间为 \(t\) 小时。2 d:表示 \(d\) 这座城市要建机场。3:询问目前任意两座城市之间的最短通行时间的总和,如果无法到达则以 \(0\) 替代。
思路
本题的解题关键在于 \(N \le 500, Q \le 1000\)。
如果不带修改操作,仅处理全源最短路,直接采取 Floyd 算法即可。在建图过程中需要注意以下几点:
- 道路可以直接当作双向边。
- 由于有机场的城市之间的通行时间固定为 \(T\),直接做最短路不大好处理,不如可以建立一个虚点 \(N+1\),然后我们假设任意两座城市之间如果使用飞机进行通行,会先花 \(\dfrac T 2\) 小时的时间来到虚点 \(N+1\) 的位置,然后再花 \(\dfrac T 2\) 小时的时间前往目的地,两趟时间总和恰好为 \(T\)。这样我们就可以跑共 \(N+1\) 个点的全源最短路了。
- 但是,如果真的像上面这样建边的话,除法操作可能会出现小数,为了避免小数出现,我们可以让每条边的距离扩大一倍(时间全部 \(\times 2\)),以此保证时间都是整数,最后输出答案时再把答案除以 \(2\) 再输出即可。
然后我们考虑建图动态建图,也就是本题的 1、2 两种类型的操作。根据上面的建图方法,可以发现这两种操作无非都是往图中再新增一条边。
假设现在新增的这条边是连接 \(a, b\) 两个点,距离(时间)为 \(c\),那么对于整张图的任意两点 \(i, j\),其最短距离 dis[i][j] 与新增的这条边的关系可以是以下三种:
- 与新边无关,即
dis[i][j] = dis[i][j]。 - 与新边有关,也就是 \(i \rightarrow j\) 的最短路需要经过 \(a \leftrightarrow b\) 这条边,继续分为两种情况讨论:
- 以 \(a \rightarrow b\) 方向经过这条边,全程走法为 \(i\rightarrow a \rightarrow b\rightarrow j\),总距离为
dis[i][a] + c + dis[b][j] - 以 \(b \rightarrow a\) 方向经过这条边,全程走法为 \(i\rightarrow b \rightarrow a\rightarrow j\),总距离为
dis[i][b] + c + dis[a][j]
- 以 \(a \rightarrow b\) 方向经过这条边,全程走法为 \(i\rightarrow a \rightarrow b\rightarrow j\),总距离为
只需要在以上三种情况中取最小值当作最新的最短路长度即可。
时间复杂度 \(O(N^3 + Q\cdot N^2)\)。
代码
typedef long long ll;
const ll INF = 0x3f3f3f3f3f3f3f3f;
int n, m, k, t;
ll dis[505][505]; // dis[i][j] 记 i 到 j 的最短时间
ll ans = 0;
// 再加一条连接 a 和 b 且长度为 c 的边
void addedge(int a, int b, ll c)
{
for(int i = 1; i <= n+1; i++)
for(int j = 1; j <= n+1; j++)
{
ll tmp = min(dis[i][a] + dis[b][j], dis[i][b] + dis[a][j]) + c;
if(dis[i][j] > tmp)
{
if(i != n+1 && j != n+1) // 如果 i j 均和虚点 n+1 无关
{
if(dis[i][j] != INF) // 如果此前已经有最短路,需要把之前的答案先减去
ans -= dis[i][j];
ans += tmp; // 加上最新的最短路长度
}
dis[i][j] = tmp;
}
}
}
void solve()
{
cin >> n >> m;
for(int i = 1; i <= n+1; i++)
for(int j = 1; j <= n+1; j++)
if(i != j)
dis[i][j] = INF; // 初始化 任意两座不同城市之间均记作不可到达
for(int i = 1; i <= m; i++)
{
int a, b, c;
cin >> a >> b >> c;
// 时间扩大一倍,以防止下面的 t 出现除法
dis[a][b] = dis[b][a] = min(dis[a][b], (ll)c * 2);
}
cin >> k >> t;
for(int i = 1; i <= k; i++)
{
int d;
cin >> d;
// 假定有机场的城市会向虚拟点 n+1 连边,按照原本的题意,来回总共花费 t 小时时间
// 但这样单程就只会花费 t/2 小时,可能出现小数,所以不如直接让所有时间先 *2
dis[d][n + 1] = dis[n + 1][d] = min(dis[d][n + 1], (ll)t);
}
// Floyd
for(int k = 1; k <= n+1; k++)
for(int i = 1; i <= n+1; i++)
for(int j = 1; j <= n+1; j++)
dis[i][j] = min(dis[i][j], dis[i][k] + dis[k][j]);
// 先求出所有能够互相到达的点对组成的初始答案
for(int i = 1; i <= n; i++)
for(int j = 1; j <= n; j++)
if(dis[i][j] != INF)
ans += dis[i][j];
int q;
cin >> q;
while(q--)
{
int op;
cin >> op;
if(op == 1)
{
int a, b, c;
cin >> a >> b >> c;
addedge(a, b, c * 2);
}
else if(op == 2)
{
int d;
cin >> d;
addedge(d, n + 1, t);
}
else
cout << ans / 2 << "\n"; // 把建图时边长 *2 的影响去除
}
}
F - Paint Tree 2
题意
给定一棵包含 \(N\) 个点的树,每个点上都写有一个整数,第 \(i\) 个点的整数为 \(A_i\)。
一开始,树上的每个点都是白色的。
你可以进行 \(0\) 次或多次以下操作,但不超过 \(K\) 次:
- 选择树上的一条简单路径,需保证这条路径上每个点目前都是白色的,然后把路径上所有点都涂成黑色。
问进行完所有操作之后,所有黑色点上的数字总和最大是多少?
思路
假定 \(1\) 号点是整棵树的根结点,考虑树形DP。
对于以树上每个点作为根结点所形成的子树而言,我们需要重点讨论根结点是否被选中成为某条路径的一部分。情况可以有以下几种:
- 当前根结点没有用过
- 当前根结点是某条路径的一个端点
- 当前根结点是某条路径过程中的一个点(非端点)
所以定义 dp[i][j][k] 表示在以点 \(i\) 作为根结点的子树中,我们进行了 \(j\) 次操作,且 \(k = 0/1/2\) 表示根结点符合以上三种情况的哪一种。
对于树上的某个点 \(u\) 而言,先将其子结点的答案求出,然后考虑其子结点 \(v\) 的答案对他的影响:
- 如果子结点 \(v\) 中选择的路径与当前根结点 \(u\) 毫无相关,那就直接把两个点分开看。枚举在根结点 \(u\) 此前已搜索过的其它子结点中所使用的操作次数为 \(i\),枚举子结点 \(v\) 的子树所使用的操作次数为 \(j\),那么直接合并答案,总操作次数就是 \(i + j\)。该部分状态转移方程较为普通,我们不需要在意子结点 \(v\) 在它的子树的答案里是怎么样的一个情况,只需要专注于当前根结点 \(u\) 的情况即可。该部分状态转移方程可以写作:\(dp[u][i+j][p] \leftarrow \max\limits_{q \in [0,2]} (dp[u][i][p] + dp[v][j][q])\),具体参考下面代码中的”普通转移“。
- 如果子结点 \(v\) 中选择的路径可以与当前根结点 \(u\) 合并,总共存在两种情况:
- 一:当前根结点 \(u\) 此前并未被任何路径选中,而子结点 \(v\) 存在于一条以 \(v\) 为端点的路径中,此时我们可以将根结点 \(u\) 加入到这条路径当中,将其变成一条以 \(u\) 作为端点的路径,此时的状态转移方程为 \(dp[u][i+j][1] \leftarrow dp[u][i][0] + dp[v][j][1] + A[u]\)。
- 二:当前根结点 \(u\) 存在于一条以 \(u\) 作为端点的路径中,子结点 \(v\) 也存在于一条以 \(v\) 为端点的路径中,此时这两条路径是可以合并成一条路径,使操作次数少一次的,但合并后根结点就只是某条路径的中间过程点了。这部分的状态转移方程则为 \(dp[u][i+j-1][2] \leftarrow dp[u][i][1] + dp[v][j][1]\)。
转移过程如果全部在单个数组内完成,请注意转移顺序,否则可以每次转移都开一个新数组来保证不会重复转移。
最后考虑动态规划的初始状态,对于单个点而言,在处理其子树之前先考虑其本身是否需要选择成为某条新路径的一部分:
- 如果该点不选,则 \(dp[u][0][0] = 0\)
- 如果该点要选,则 \(dp[u][1][1] = dp[u][1][2] = A[u]\)
最终答案即处理完整棵树之后,根结点 \(1\) 位置处的 \(\max\limits_{i \in [0, K]} \max\limits_{j \in [0, 2]} (dp[1][i][j])\)。
总时间复杂度 \(O(NK^2)\)。
代码
int n, k;
int w[200005]; // 每个点的数字
vector<int> G[200005]; // 图
ll dp[200005][7][3];
// dp[i][j][k] 表示在以 i 这个点作为根结点的子树中 总共进行了 j 次操作,且
// k=0:根结点 i 不选
// k=1:根结点 i 是某条路径的一个端点
// k=2:根结点 i 是某条路径过程中的点(也可以是端点)
// 的情况下所能获得的最大总和
ll tmp[6][3]; // 暂存当前这一次合并所得到的新答案
// 将子结点 v 的答案合并到父结点 u 内
void merge(int u, int v)
{
memset(tmp, -0x3f, sizeof tmp);
for(int i = 0; i <= k; i++)
for(int j = 0; j <= k; j++)
{
// 已存在 u 内的答案中 其它子树进行了 i 次操作
// 子树 v 进行了 j 次操作
if(i + j <= k)
{
// 普通转移
tmp[i+j][0] = max(tmp[i+j][0], dp[u][i][0] + dp[v][j][0]);
tmp[i+j][0] = max(tmp[i+j][0], dp[u][i][0] + dp[v][j][1]);
tmp[i+j][0] = max(tmp[i+j][0], dp[u][i][0] + dp[v][j][2]);
tmp[i+j][1] = max(tmp[i+j][1], dp[u][i][1] + dp[v][j][0]);
tmp[i+j][1] = max(tmp[i+j][1], dp[u][i][1] + dp[v][j][1]);
tmp[i+j][1] = max(tmp[i+j][1], dp[u][i][1] + dp[v][j][2]);
tmp[i+j][2] = max(tmp[i+j][2], dp[u][i][2] + dp[v][j][0]);
tmp[i+j][2] = max(tmp[i+j][2], dp[u][i][2] + dp[v][j][1]);
tmp[i+j][2] = max(tmp[i+j][2], dp[u][i][2] + dp[v][j][2]);
// 单链向上延长一个点
tmp[i+j][1] = max(tmp[i+j][1], dp[u][i][0] + dp[v][j][1] + w[u]);
}
if(i + j - 1 >= 0 && i + j - 1 <= k)
{
// 两条单链合并成一条链
tmp[i+j-1][2] = max(tmp[i+j-1][2], dp[u][i][1] + dp[v][j][1]);
}
}
// 将新答案更新回 dp 数组
for(int i = 0; i <= k; i++)
for(int j = 0; j < 3; j++)
dp[u][i][j] = max(dp[u][i][j], tmp[i][j]);
}
void dfs(int u, int fa)
{
dp[u][0][0] = 0;
dp[u][1][1] = w[u];
dp[u][1][2] = w[u];
for(int &v : G[u])
{
if(v == fa)
continue;
dfs(v, u);
merge(u, v);
}
}
void solve()
{
cin >> n >> k;
for(int i = 1; i <= n; i++)
cin >> w[i];
for(int i = 1; i < n; i++)
{
int u, v;
cin >> u >> v;
G[u].push_back(v);
G[v].push_back(u);
}
memset(dp, -0x3f, sizeof dp);
dfs(1, 0);
ll ans = 0;
for(int i = 1; i <= k; i++)
for(int j = 0; j < 3; j++)
ans = max(ans, dp[1][i][j]);
cout << ans;
}

浙公网安备 33010602011771号