AtCoder Beginner Contest 428 ABCDE 题目解析

A - Grandma's Footsteps

题意

下课铃响起后,高桥会立即开始重复执行以下动作:

  • 以每秒 \(S\) 米的速度跑 \(A\) 秒,然后保持静止 \(B\) 秒。

请问在下课铃响后 \(X\) 秒时,他总共跑了多少米?

思路

\(A+B\) 秒为一个完整周期,共 \(\lfloor\dfrac{X}{A+B}\rfloor\) 个完整周期,对总距离的贡献为 \(\lfloor\dfrac{X}{A+B}\rfloor \times (S \times A)\)

最后讨论剩余的不完整周期(共 \(X \bmod (A+B)\) 秒)是否对总距离有贡献即可。

代码

void solve()
{
    int s, a, b, x;
    cin >> s >> a >> b >> x;
    int ans = (x / (a + b)) * (s * a);
    x %= a + b;
    if(x <= a) // 最后一个周期不足 a 秒,按剩余秒数计算
        ans += s * x;
    else // 超过 a 秒时按 a 秒计算
        ans += s * a;
    cout << ans;
}

B - Most Frequent Substrings

题意

给定一个长度为 \(N\) 的仅由小写英文字母组成的字符串 \(S\) 以及一个整数 \(K\)

对于一个长度为 \(K\) 的字符串 \(t\),定义其出现次数为满足以下条件的整数 \(i\) 的个数:

  • \(1 \leq i \leq N-K+1\)
  • \(S\) 字符串的第 \(i\) 个字符到第 \(i+K-1\) 个字符所形成的子串,与字符串 \(t\) 相同

求在所有长度为 \(K\) 的字符串中的出现次数最大值 \(x\),同时按照字典序升序输出所有长度为 \(K\) 且出现次数为 \(x\) 的字符串。

思路

可以借助 string 容器进行快速的子串截取,借助 map 容器进行自动排序以及次数统计,方便地完成本题。

代码

void solve()
{
    int n, k;
    string s;
    map<string, int> cnt;
    
    cin >> n >> k;
    cin >> s;
    
    int maxx = 0;
    
    for(int i = 0; i + k - 1 < n; i++)
    {
        string t = s.substr(i, k); // [i, i+k-1] 形成的子串
        maxx = max(maxx, ++cnt[t]);
    }
    
    cout << maxx << "\n";
    for(auto it : cnt)
        if(it.second == maxx) // 当前字符串出现次数 = 最大出现次数
            cout << it.first << " ";
}

C - Brackets Stack Query

题意

一个字符串 \(T\) 被称为一个“好的括号序列”,当且仅当满足以下条件:

  • \(T\) 可以通过重复下面的操作 0 次或多次变成一个空字符串:

    • 每次在 \(T\) 中选择 () 这一连续子串,并将其删除。

有一个字符串 \(S\)。最初,\(S\) 是个空字符串。

请按照给出的顺序处理 \(Q\) 次操作。在每次操作后,判断当前的字符串 \(S\) 是否是一个好的括号序列。

操作有两种类型:

  • 1 c:给出一个字符 \(c\),保证 \(c\)() 中的一种。将 \(c\) 添加到字符串 \(S\) 的末尾。
  • 2:删除 \(S\) 的最后一个字符。保证此时 \(S\) 不是空字符串。

思路

栈的模拟题。

假如本题仅包含添加操作而没有删除操作,为了判断字符串是否是括号匹配的序列,只需要用一个栈,在每次添加左括号时将其进栈,每次添加右括号时判断栈内是否存在左括号能与其匹配即可。只要在最后栈是空的,且过程中每次出现右括号均有对应左括号能匹配,那么就可以判断当前字符串就是括号匹配的。

因为本题仅出现小括号这一种括号,所以每次只要栈内有左括号就可以直接与其相匹配,所以这一步我们可以直接优化成用单个变量 \(x\) 来模拟表示栈内的括号情况。每次加一个左括号则 \(x := x+1\),每次加一个右括号则 \(x:=x-1\)

括号匹配的条件即最后栈为空,且过程中每次从出现右括号都有对应左括号与其匹配,因此需要保证最后 \(x = 0\) 且过程中 \(x \ge 0\) 均成立。所以这一步我们只需要再来个变量 \(mi\) 存储 \(x\) 在过程中出现的最小值,保证最后 \(x = 0\)\(mi=0\) 即可。

然后考虑本题的删除操作。删除操作无非就是撤销上一次添加操作。所以我们还需要知道每一步添加操作完成之后,\(x\)\(mi\) 两个变量的值分别是多少。可以用两个栈来分别维护 \(x\)\(mi\) 这两个变量的变化,添加操作就往栈顶加新数字,删除操作则弹出栈顶即可。

代码

void solve()
{
    int q;
    cin >> q;
    
    stack<int> sk1; // 存储每一次操作后的 x 的值,栈顶即当前 x
    stack<int> sk2; // 存储 sk1 的前缀最小值 mi,栈顶即当前 mi
    sk1.push(0); // x 初始值为 0
    sk2.push(0);
    
    while(q--)
    {
        int op;
        cin >> op;
        if(op == 1)
        {
            char c;
            cin >> c;
            if(c == '(')
            {
                int x = sk1.top() + 1; // x = x + 1
                sk1.push(x);
                sk2.push(min(sk2.top(), x));
            }
            else
            {
                int x = sk1.top() - 1; // x = x - 1
                sk1.push(x);
                sk2.push(min(sk2.top(), x));
            }
        }
        else
        {
            sk1.pop();
            sk2.pop();
        }
        if(sk1.top() == 0 && sk2.top() == 0) // x == 0 && mi == 0
            cout << "Yes\n";
        else
            cout << "No\n";
    }
}

D - 183184

题意

对于正整数 \(x\)\(y\) ,定义 \(f(x,y)\) 如下:

  • \(x\)\(y\) 转为十进制字符串前后拼接后,再转回对应的十进制数值。

例如, \(f(3,14)=314,\ f(100,3)=1003\)

给定两个正整数 \(C\)\(D\) 。求满足以下条件的整数 \(x\) 的个数:

  • \(1 \leq x \leq D\)
  • \(f(C, C+x)\) 是一个完全平方数。

思路

我们考虑 \(C+x\) 这个数字的位数。假设位数为 \(d\),那么 \(f(C, C+x)\) 就可以通过 \(10^d \times C + (C+x)\) 来获得,相当于让 \(C\) 向高位移动 \(d\) 位后再让 \(C+x\) 拼在其后。

位数固定后,我们便可以计算相同位数的完全平方数个数。

已知由 \(d\) 位组成的整数的数值范围在 \(10^{d-1} \sim 10^d - 1\) 之间,可以定义出两个变量:

  • \(L = 10^d \times C + 10^{d-1}\)
  • \(R = 10^d \times C + (10^d - 1)\)

用于表示以 \(C\) 作为高位,且低位位数为 \(d\) 时的所有可能数值的范围。

但因为 \(x\) 受到 \(1 \le x \le D\) 的影响,所以这两个变量需要对其可行范围进行限制:

  • \(L = \max(L, 10^d \times C + (C+1))\)
  • \(R = \min(R, 10^d \times C + (C+D))\)

已知在 \(1 \sim T\) 的范围内,完全平方数的数量可以直接通过 \(\lfloor \sqrt T \rfloor\) 计算得出。因此为了计算 \(L \sim R\) 范围内的完全平方数,可以借助前缀和思想,通过 \(\lfloor \sqrt R \rfloor - \lfloor \sqrt {L-1} \rfloor\) 来获得。

单组数据时间复杂度 \(O(\log (C+D))\),注意取平方根在数值范围较大时不能直接用 sqrt 计算,会因为精度误差出错。推荐借助二分找平方根,或者使用 long double 类型的 sqrtl 函数。

代码

typedef long long ll;

// 根号 n 向下取整
// 推荐二分,或根号函数用 long double 的 sqrtl
ll sqt(ll n)
{
    return sqrtl(n);
}

void solve()
{
    ll C, D;
    cin >> C >> D;
    
    ll ans = 0;
    
    ll a = 1, b = 9, x = 10;
    // 假设位数 d 从 1 开始变大
    // 用 x 表示 10^d
    // [a, b] 表示位数为 d 时的最小最大值
    
    while(a <= C + D) // 低位最小值还没超过可行最大值 C+D 时
    {
        ll l = max(a, C + 1) + C * x; // 可行范围
        ll r = min(b, C + D) + C * x;
        
        if(l <= r)
            ans += sqt(r) - sqt(l - 1);
        
        a = a * 10; // 位数变大一位后 abx 的变化
        b = b * 10 + 9;
        x = x * 10;
    }
    cout << ans << "\n";
}

signed main()
{
    ll t;
    cin >> t;
    while(t--)
        solve();
	return 0;
}

E - Farthest Vertex

题意

有一棵共 \(N\) 个点的树,每个点分别编号为 \(1\)\(N\) 。 第 \(i\) 条边连接 \(A_i\)\(B_i\) 两点。

定义点 \(u\)\(v\) 之间的距离为 \(u\)\(v\) 的简单路径上的边数。

对于每个 \(v = 1, 2, \dots, N\),解决以下问题:

  • 在顶点 \(1, 2, \dots, N\) 中,输出与顶点 \(v\) 距离最大的顶点的编号。如果有多个顶点满足条件,则输出编号最大的顶点

思路

(本题可以借助树的直径的性质分类讨论来简单地获取答案),下面分析另一种借助换根DP的做法。

如果我们假定 \(1\) 号点作为整棵树的根结点,将其拎起,然后考虑每个结点 \(u\) 到树上与其距离最远的结点之间的距离以及结点编号如何获得。

很明显,距离 \(u\) 最远的点要么是当前点的子树内部的某个叶子结点,要么是在子树外部(即答案在父结点方向,此时可以看作将根结点换为 \(u\) 点)。

  • 如果答案在子树内,我们可以在深搜完所有的子结点之后,在所有上传的答案中取个最大值来获得。
  • 如果答案在子树外,那么我们可以在深搜 \(u\) 的时候,让其父结点下传一个不在 \(u\) 的子树内部的点到 \(u\) 的最远距离及其编号的信息作为深搜参数,以此把父结点当作 \(u\) 的某个子结点来传递答案,实现换根。

所以先考虑对于树上每个结点 \(u\),从它出发往每个子结点的方向走,能走的最远距离以及到达的结点最大编号,通过一遍深搜进行求解。将每个儿子返回的结果存储在一个 vector 容器 \(A[u]\) 内。

在所有结点往子树方向的答案全部得出后,进行第二次深搜进行最终答案的求解。

对于根结点 \(1\),其没有父结点方向的答案来源,答案只存在于子结点方向,取 \(A[1]\) 内的最大答案即可。

然后考虑往子结点搜索。假如当前站在 \(u\) 点上,考虑搜索 \(v\) 这个子结点的答案:

  • 如果当前 \(u\) 点的答案来源(距离 \(u\) 点最远的点)不在 \(v\) 的子树内部,那么对于 \(v\) 点而言,其父结点方向与其距离最远的点,可以直接继承自当前 \(u\) 点的答案。
  • 如果当前 \(u\) 点的答案来源在 \(v\) 的子树内部,这不符合上面对于这一步深搜传参的定义(要保证参数的答案不在子树内),所以这里需要重新取 \(u\) 点的次大答案及其父结点方向的答案中的较大值作为传参。

接下来对于不是根结点的每个结点,其只需要在子树内部答案最大值以及父结点方向传参之间再挑选一个最大值作为最终答案即可。

时间复杂度 \(O(N \log N)\)

代码

int n;
vector<int> G[500005]; // 邻接表存图

vector<pii> A[500005]; // A[i] 表示 i 到 每个子结点的 子树内部某一结点的 最远距离 及其编号
pii ret[500005]; // ret[i] 表示 dfs1 中 i 向父结点 return 的结果

pii ans[500005]; // first 表示到其它点的距离最大值,second 表示点的编号

// 返回 u 的子树内部 距离 u 最远的结点 与 u 之间的距离 及其编号
// first 表示距离,second 表示编号
pii dfs1(int u, int f)
{
    for(int &v : G[u])
    {
        if(v == f)
            continue;
        pii res = dfs1(v, u);
        res.first++; // 往上传递一层 最远距离+1
        A[u].push_back(res);
    }
    A[u].push_back(pii(0, u)); // 多放一个我到我自己的贡献,免去对叶子结点以及空数组的判断
    sort(A[u].begin(), A[u].end());
    return ret[u] = A[u].back(); // 返回 距离最大值及此时的结点编号
}

// first 表示距离,second 表示编号
// 判断两个答案,取距离较大者,距离相同取编号较大者
pii checkMax(pii a, pii b)
{
    if(a.first > b.first || a.first == b.first && a.second > b.second)
        return a;
    return b;
}

// 换根 dp,假设 u 为根
// ansf 表示不在 u 的子树内部的点到 u 的最远距离 及其编号
// first 表示距离,second 表示编号
void dfs2(int u, int f, pii ansf)
{
    ans[u] = checkMax(ansf, A[u].back());
    
    for(int &v : G[u])
    {
        if(v == f)
            continue;
        
        if(ret[v].second == ans[u].second) // v 方向是当前 ans[u] 的答案来源,不能直接传 ans[u]
        {
            // 要取 A[u] 的次大答案与 ansf 的较大值 作为新的 ansf 传递下去
            pii nxtf;
            nxtf = checkMax(ansf, A[u][A[u].size() - 2]);
            nxtf.first++; // 往下传递一层 最远距离+1
            dfs2(v, u, nxtf);
        }
        else
        {
            pii nxtf = ans[u];
            nxtf.first++; // 往下传递一层 最远距离+1
            dfs2(v, u, nxtf);
        }
    }
}

void solve()
{
    cin >> n;
    for(int i = 1; i < n; i++)
    {
        int a, b;
        cin >> a >> b;
        G[a].push_back(b);
        G[b].push_back(a);
    }
    dfs1(1, 0);
    dfs2(1, 0, pii(-1, 0));
    for(int i = 1; i <= n; i++)
        cout << ans[i].second << "\n";
}
posted @ 2025-10-18 23:25  StelaYuri  阅读(38)  评论(0)    收藏  举报