AtCoder Beginner Contest 479 ABCDE
A - Odd Index Sum
- 预估难度:
入门 - 标签:循环语句、条件分支
题面
问题陈述
给定一个长度为 \(N\) 的整数序列 \(A=(A_1,A_2,\dots,A_N)\)。
求 \(A\) 中奇数下标项之和。
约束
- 输入的所有值均为整数。
- \(1 \le N \le 100\)
- \(1 \le A_i \le 100\)
输入
输入从标准输入按以下格式给出:
\(N\)
\(A_1\) \(A_2\) \(\dots\) \(A_N\)
输出
输出答案。
代码
void solve()
{
int n, a, sum = 0;
cin >> n;
for(int i = 1; i <= n; i++)
{
cin >> a;
if(i % 2 == 1)
sum += a;
}
cout << sum;
}
B - Longest Rainy Streak
- 预估难度:
入门 - 标签:循环语句 / 循环嵌套
题面
问题陈述
高桥君连续 \(N\) 天记录了天气,将每天的天气记录为「晴」或「雨」。
他的记录以如下形式给出:一个长度为 \(N\) 的字符串 \(S\)。
- \(S\) 的第 \(i\) 个字符(\(1 \le i \le N\))表示从记录开始算起第 \(i\) 天的天气。
- 若 \(S\) 的第 \(i\) 个字符是
S,则该天的记录为「晴」。 - 若 \(S\) 的第 \(i\) 个字符是
R,则该天的记录为「雨」。
- 若 \(S\) 的第 \(i\) 个字符是
求高桥君连续记录为「雨」的最大天数。
若高桥君从未记录为「雨」,则答案为 \(0\)。
约束
- \(N\) 是满足 \(1 \le N \le 100\) 的整数。
- \(S\) 是由
S和R构成的长度为 \(N\) 的字符串。
输入
输入从标准输入按以下格式给出:
\(N\)
\(S\)
输出
输出答案。
思路
相当于在字符串中找长度最长的一段全 R 子串的长度。
考虑到 \(N \le 100\),可以直接 \(O(N^3)\) 暴力枚举左右端点后再检查区间内是否全为 R。
或者把检查区间内是否全为 R 的过程与枚举右端点两个循环合并,优化为 \(O(N^2)\)。
或者借助变量在遍历字符串的同时记录当前雨天连续的天数,遇到晴天则清零,在过程中取最大值亦可优化为 \(O(N)\)。
代码 \(O(N^2)\)
void solve()
{
int n;
string s;
cin >> n >> s;
int ans = 0;
for(int i = 0; i < n; i++)
for(int j = i; j < n; j++) // 从第 i 天开始判断
{
if(s[j] == 'S') // 是晴天,结束循环
break;
ans = max(ans, j - i + 1);
}
cout << ans;
}
代码 \(O(N)\)
void solve()
{
int n;
string s;
cin >> n >> s;
int ans = 0, cnt = 0;
// cnt 表示目前连续雨天的天数
for(int i = 0; i < n; i++)
{
if(s[i] == 'R')
cnt++;
else
cnt = 0;
ans = max(ans, cnt);
}
cout << ans;
}
C - Bento Packing
- 预估难度:
普及- - 标签:深度优先搜索
题面
问题陈述
有 \(N\) 道配菜。
第 \(i\) 道配菜的美味度为 \(A_i\),体积为 \(B_i\)。
你需要选出若干道配菜装进午餐盒中,使得所选配菜的总体积不超过 \(W\)。
求所选配菜的总美味度的最大值。
约束
- \(1 \leq N \leq 20\)
- \(1 \leq W \leq 10^9\)
- \(1 \leq A_i, B_i \leq 10^9\)
- 输入的所有值均为整数。
输入
输入从标准输入按以下格式给出:
\(N\) \(W\)
\(A_1\) \(B_1\)
\(A_2\) \(B_2\)
\(\vdots\)
\(A_N\) \(B_N\)
输出
输出答案。
思路
考虑到 \(N \le 20\),直接借助 DFS 或者二进制枚举,将所有 \(2^N\) 种选择方案全部找出来,一一判断总体积是否符合条件,然后在符合条件的所有方案里找出最大美味度即可。
时间复杂度 \(O(2^N)\),注意数据范围。
代码
int n, w, a[25], b[25];
long long ans = 0; // 答案
// 当前搜到了第 p 道菜
// 前面打算选择的所有菜的总美味度为 suma,总体积为 sumb
void dfs(int p, long long suma, long long sumb)
{
if(p > n) // 搜完所有菜
{
if(sumb <= w) // 当前方案总体积符合条件
ans = max(ans, suma);
return;
}
// 第 p 道菜不选
dfs(p + 1, suma, sumb);
// 第 p 道菜要选
dfs(p + 1, suma + a[p], sumb + b[p]);
}
void solve()
{
cin >> n >> w;
for(int i = 1; i <= n; i++)
cin >> a[i] >> b[i];
dfs(1, 0, 0);
cout << ans;
}
D - Back and Forth Distance
- 预估难度:
普及 - 标签:贪心、前缀最值
题面
问题陈述
有 \(N\) 个方块排成一行,从左到右编号为 \(1, 2, \ldots, N\)。
当你位于方块 \(i\) 上时,可以进行以下移动之一。
-
选择一个满足 \(1 \leq j \lt i\) 的整数 \(j\),移动到方块 \(j\)。该移动的代价为 \(L_i\)。
-
选择一个满足 \(i \lt j \leq N\) 的整数 \(j\),移动到方块 \(j\)。该移动的代价为 \(R_i\)。
给定 \(Q\) 个询问。请回答每个询问。每个询问的格式如下。
- 给定整数 \(s\) 和 \(t\)。求从方块 \(s\) 出发,通过反复进行移动到达方块 \(t\) 所需的最小总代价。
约束
- \(2 \leq N \leq 3 \times 10^5\)
- \(1 \leq Q \leq 3 \times 10^5\)
- \(1 \leq L_i \leq 10^9\) \((2 \leq i \leq N)\)
- \(1 \leq R_i \leq 10^9\) \((1 \leq i \leq N - 1)\)
- 每个询问中 \(1 \leq s, t \leq N\)。
- 每个询问中 \(s \neq t\)。
- 输入的所有值均为整数。
输入
输入从标准输入按以下格式给出:
\(N\) \(Q\)
\(L_2\) \(L_3\) \(\ldots\) \(L_N\)
\(R_1\) \(R_2\) \(\ldots\) \(R_{N - 1}\)
\(\text{query}_1\)
\(\text{query}_2\)
\(\vdots\)
\(\text{query}_Q\)
这里,\(\text{query}_i\) 是第 \(i\) 个询问,按以下格式给出:
\(s\) \(t\)
输出
输出 \(Q\) 行。
第 \(i\) 行应包含对第 \(i\) 个询问的答案。
思路
首先注意到一个关键性质:每次移动的代价只由出发点决定,与目的地无关。
我们以起点在终点的左边这一情况为例:
- 如果第一步打算向右跳,明显直接跳到终点就是最优解,花费即 \(R_i\)。
- 因为不可能在已经花费 \(R_i\) 的前提下继续花费一些代价进行移动。
- 如果第一步打算向左跳,先花费 \(L_i\)。然后如果第二步要继续向左跳,肯定不如直接第一步就跳到对应点上更优,因此第二步一定是向右跳。而只要向右跳,那就是直接跳到终点最优。此时总花费即 \(L_i + R_{\text{第一步移动到的点}}\)。
第一种情况答案固定,而对于第二种情况,明显我们在第一步先直接跳到 \(1 \le j \lt i\) 范围内 \(R_j\) 最小的点上,能够让最终总花费取到最小。
因此,我们可以根据第二种情况来更新每个点(除首尾)的 \(R_i\):
更新过程只需要一个循环,在加一个变量 minn 用于维护 \(1 \sim i-1\) 的前缀最小值即可。
对称地,每个点(除首尾)的 \(L_i\) 更新方式类似:
在预处理结束后,根据每次问题起点与终点的左右关系分类讨论。如果 \(s \lt t\),则取从起点向右跳跃的最小代价 \(R_s\) 即可;如果 \(s \gt t\),则取从起点向左跳跃的最小代价 \(L_s\) 即可。
时间复杂度 \(O(N+Q)\)。
代码
int l[300005], r[300005];
void solve()
{
int n, q;
cin >> n >> q;
for(int i = 2; i <= n; i++)
cin >> l[i];
for(int i = 1; i < n; i++)
cin >> r[i];
int minn = r[1]; // 维护 1~i-1 范围内的 r[] 最小值
for(int i = 2; i < n; i++)
{
// 对于 i,向右跳的最小代价,要么就是初始的 r[i]
// 要么先花费 l[i] 跳到 1~i-1 范围内 r[] 最小的位置,然后再向右跳
r[i] = min(r[i], l[i] + minn);
minn = min(minn, r[i]);
}
minn = l[n]; // 维护 i+1~n 范围内的 l[] 最小值
for(int i = n - 1; i >= 2; i--)
{
// 对于 i,向左跳的最小代价,要么就是初始的 l[i]
// 要么先花费 r[i] 跳到 i+1~n 范围内 l[] 最小的位置,然后再向左跳
l[i] = min(l[i], r[i] + minn);
minn = min(minn, l[i]);
}
while(q--)
{
int s, t;
cin >> s >> t;
if(s < t)
cout << r[s] << "\n";
else
cout << l[s] << "\n";
}
}
E - Cut and Add
- 预估难度:
普及+/提高- - 标签:图论、子树和、树上前缀和
题面
问题陈述
有一棵包含 \(N\) 个顶点、编号为 \(1,2,\dots,N\) 的树。其中 \(N\) 为奇数。
边同样编号为 \(1,2,\dots,N-1\),第 \(i\) 条边连接顶点 \(A_i\) 和 \(B_i\)。
每个顶点 \(i\) 有一个变量 \(x_i\),初始时对于每个顶点 \(i\) 都有 \(x_i=0\)。
接下来,按 \(i=1,2,\dots,N-1\) 的顺序依次进行以下操作。
- 首先,移除第 \(i\) 条边。此时,树被分割为两个连通分量。
- 对含顶点数更多的那个连通分量中的每个顶点 \(v\),将 \(i\) 加到 \(x_v\) 上。
- 由于 \(N\) 为奇数,含顶点数更多的连通分量是唯一确定的。
- 最后,恢复第 \(i\) 条边。
求所有操作结束后各顶点的变量 \(x_1,x_2,\dots,x_N\) 的值。
约束
- 输入的所有值均为整数。
- \(N\) 是满足 \(3 \le N \le 2 \times 10^5 - 1\) 的奇数。
- \(1 \le A_i < B_i \le N\)
- 给定的图是一棵树。
输入
输入从标准输入按以下格式给出:
\(N\)
\(A_1\) \(B_1\)
\(A_2\) \(B_2\)
\(\vdots\)
\(A_{N-1}\) \(B_{N-1}\)
输出
按以下格式输出变量最终的值:
\(x_1\) \(x_2\) \(\dots\) \(x_N\)
思路
考虑每条边被删除时,需要给哪一部分顶点整体加上它的编号。
先假定根结点为 \(1\),然后借助一遍 DFS 求出每棵子树内的结点个数,记作 siz[]。
假定 \(u\) 是 \(v\) 的父结点,对于边 \(i:(u,v)\),删掉它之后,得到的两个连通分量之一就是 \(v\) 的子树(含 siz[v] 个点),另一个连通分量就是剩余的所有点(含 n - siz[v] 个点)。
由于 \(n\) 是奇数,两边不可能一样大,因此必然是其中一侧严格更大,直接比较两部分点数大小即可。
接下来考虑为较大的那部分连通分量内的所有点进行 \(+i\) 的操作:
- 如果
siz[v] > n - siz[v],此时只需要对 \(v\) 的子树内所有点进行 \(+i\)。这里可以考虑借助树上前缀和的做法。- 为每个点开一个
delta[]数组,然后把整棵子树所有点的变化量先存储在对应的子树根结点上,在这里也就是delta[v] += i。在所有操作全部处理完后,最后再执行一次 DFS 把每个点的标记通过树上前缀和传递给子树内的所有点即可。
- 为每个点开一个
- 如果
siz[v] < n - siz[v],此时需要对不在 \(v\) 的子树内的所有点进行 \(+i\)。这个操作直接做是不好做的,但我们依然可以借助前一种情况的做法来完成。- 这里可以先理解为我们要对整棵树所有 \(N\) 个点都进行一次 \(+i\) 的操作,但由于 \(v\) 的子树内所有点并不需要 \(+i\),所以再单独对 \(v\) 的子树内的所有点再执行一次 \(-i\) 的操作即可。
- 也就是可以开一个全局变量
tot用于记录整棵树所有点被加的总权值,在这步操作就可以直接tot += i,然后再为 \(v\) 点打上delta[v] -= i的标记即可。
时间复杂度 \(O(N)\)。
代码
typedef long long ll;
struct edge
{
int v, id; // 下一个点、这条边的编号
};
vector<edge> G[200005];
int n;
int siz[200005];
// siz[i] 记录以 i 为根的子树 结点个数
ll delta[200005];
// delta[i] 记录以 i 为根的子树 内部所有点需要加的值
ll tot = 0;
// tot 记录整棵树所有点要加的值
void dfs(int u, int fa)
{
siz[u] = 1;
for(edge e : G[u])
{
int v = e.v, id = e.id;
if(v == fa)
continue;
dfs(v, u);
siz[u] += siz[v];
// 对于 u - v 这条边,如果删除
if(siz[v] < n - siz[v])
{
// 此时点更多的连通分量包含所有 不在v的子树内的点
tot += id;
delta[v] -= id;
}
else
{
// 此时点更多的连通分量包含所有 v的子树内的点
delta[v] += id;
}
}
}
// 将每棵子树根结点位置的标记下传,做树上前缀和
void dfs2(int u, int fa)
{
for(edge e : G[u])
{
int v = e.v;
if(v == fa)
continue;
delta[v] += delta[u];
dfs2(v, u);
}
}
void solve()
{
cin >> n;
for(int i = 1; i < n; i++)
{
int a, b;
cin >> a >> b;
G[a].push_back({b, i});
G[b].push_back({a, i});
}
dfs(1, 0);
dfs2(1, 0);
for(int i = 1; i <= n; i++)
cout << tot + delta[i] << " ";
}

浙公网安备 33010602011771号