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,则该天的记录为「雨」。

求高桥君连续记录为「雨」的最大天数。
若高桥君从未记录为「雨」,则答案为 \(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\):

\[R_i \leftarrow \min(R_i, L_i + \min_{j=1}^{i-1}{R_j}) \]

更新过程只需要一个循环,在加一个变量 minn 用于维护 \(1 \sim i-1\) 的前缀最小值即可。

对称地,每个点(除首尾)的 \(L_i\) 更新方式类似:

\[L_i \leftarrow \min(L_i, R_i + \min_{j=i+1}^{N}{L_j}) \]

在预处理结束后,根据每次问题起点与终点的左右关系分类讨论。如果 \(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] << " ";
}
posted @ 2026-10-11 22:01  StelaYuri  阅读(29)  评论(0)    收藏  举报