AtCoder Beginner Contest 443 ABCDEF 题目解析

A - Append s

  • 预估难度:入门

题意

给定一个仅由小写英文字母组成的字符串 \(S\)

在字符串 \(S\) 的末尾添加一个字符 s,然后输出这个字符串。

代码

void solve()
{
    string s;
    cin >> s;
    cout << s << "s";
}

B - Setsubun

  • 预估难度:入门
  • 标签:枚举

题意

在一年一度的“节分祭”上,人们要吃与自己年龄相同数量的豆子。已知高桥君在其他任何时候都不吃豆子。

他在今年“节分祭”时的年龄是 \(N\) 岁(今年当作第 \(0\) 年),并且在今年之前他从来都没有吃过豆子。

问从今年开始(包括今年),最早在多少年之后,他才能总共吃到 \(K\) 粒或更多的豆子?

因为高桥是不死之身,所以答案一定存在。

思路

数据范围较小,并且等差数列项数与总和呈平方关系,因此可以直接暴力枚举天数,时间复杂度 \(O(\sqrt K)\)

如果 \(K\) 较大,也可以借助二分 + 等差数列求和公式来在 \(O(\log K)\) 的复杂度内求解。

代码

void solve()
{
    int n, k;
    cin >> n >> k;
    
    int sum = n; // 总共吃到的豆子数量
    int ans = 0;
    
    while(sum < k)
    {
        ans++;
        n++;      // 过了一年
        sum += n;
    }
    
    cout << ans;
}

C - Chokutter Addiction

  • 预估难度:普及-
  • 标签:模拟

题意

AtCoder 公司的员工从时刻 \(0\) 开始工作,到时刻 \(T\) 结束工作。时刻 \(t\) 与时刻 \(t+1\) 之间的间隔为 \(1\) 秒。

在工作时间里,高桥会按照以下规则去玩手机:

  • 他在工作开始的同时打开手机。
  • 如果在青木经过高桥的办公桌时,手机处于打开状态,那么高桥会立即将其关闭。
  • 如果高桥在时刻 \(t\) 关闭了手机,那么他会在时刻 \(t+100\) 再次打开它。

已知从工作开始到结束,青木总共会经过高桥办公桌 \(N\) 次,其中第 \(i\) 次发生在时刻 \(A_i\)

请问在整个过程中,高桥一共玩了多少秒钟的手机?

保证青木绝对不会在高桥刚打开手机时经过他的办公桌。

思路

可以用一个变量来维护高桥上一次打开手机的时刻,一开始这个时刻为 \(0\)

如果在青木经过时,手机处于打开状态,则计算当前时刻与上一次打开的时刻之间的差值,加入答案。然后下一次打开的时刻记作当前时刻 \(+100\) 即可。

注意最后一次打开手机直到下班的这段时间,如果存在的话也要计入。

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

代码

void solve()
{
    int N, T;
    cin >> N >> T;
    
    int sum = 0; // 打开的总时间
    int open = 0; // 上一次打开的时刻
    
    for(int i = 1; i <= N; i++)
    {
        int t;
        cin >> t;
        if(t >= open) // 目前处于打开状态,需要关闭
        {
            sum += t - open;
            open = t + 100; // 100 秒后再打开
        }
    }
    
    if(T >= open) // 最后一次打开在下班前,也计入答案
        sum += T - open;
    
    cout << sum;
}

D - Pawn Line

  • 预估难度:普及/提高-
  • 标签:贪心、数学

题意

有一个 \(N \times N\) 的网格,其中每一列都恰好有一个棋子。一开始,第 \(i\) 列的棋子被放在第 \(R_i\) 行。

您可以执行以下操作零次或多次:

  • 选择一个不在第 \(1\) 行的棋子,将其移动到它正上方的那个格子里(即行 \(-1\))。

求满足以下条件所需的最少操作次数:

  • 任意相邻两列的棋子,其行号差值不超过 \(1\)

即给定一个整数数组,每次可以挑任意一个不为 \(1\) 的整数令其 \(-1\),问至少操作多少次可以让整个数组任意两个相邻整数的差值均不超过 \(1\)

思路

重点抓住“让整个数组任意两个相邻整数的差值均不超过 \(1\)”这个条件,即 \(|a_i - a_{i+1}| \le 1\)

如果我们从左往右看,为了让 \(a_i\)\(a_{i+1}\) 满足 \(|a_i - a_{i+1}| \le 1\) 这一条件,在已知 \(a_i\) 的情况下,\(a_{i+1}\) 的数值最多只能是 \(a_{i} + 1\)

但由于本题我们只能减少数字,所以如果 \(a_{i+1}\) 原本就比 \(a_i\) 要大,那么就必须得将其降低到 \(a_{i} + 1\) 或更低。又因为要让总操作数最少,因此我们应当贪心地尽可能保留大的数字,所以如果降低到 \(a_{i} + 1\) 已经足够,就不需要降得更低。

反过来,从右往左看也一样,\(a_{i}\) 最多也只能是 \(a_{i+1} + 1\)

综上,我们可以把条件 \(|a_i - a_{i+1}| \le 1\) 分为两部分:

  • \(a_{i+1} \le a_i + 1\)
  • \(a_i \le a_{i+1} + 1\)

于是便能够分开进行两次循环,用于分别处理这两个条件。

对于第一个条件 \(a_{i+1} \le a_i + 1\),从左往右正着循环,如果下一个数字超过 \(a_i + 1\),则将其降为 \(a_i + 1\),并记录降低所需要的操作次数。

第一遍循环完成后,第一个条件便已能全部满足。第二个条件只需要再倒着进行一遍相同的循环与讨论即可。

单组数据时间复杂度 \(O(N)\)

代码

int a[300005];

void solve()
{
    int n;
    cin >> n;
    for(int i = 1; i <= n; i++)
        cin >> a[i];
    
    long long ans = 0; // 答案
    
    int to = 1e9; // 存储当前允许出现的最大值
    for(int i = 1; i <= n; i++)
    {
        if(a[i] >= to) // 超过了允许的最大值
        {
            ans += a[i] - to;
            a[i] = to; // 将 a[i] 改为允许的最大值
        }
        to = a[i] + 1; // 下一个位置的数字 允许的最大值 = 当前数字 +1
    }
    
    to = 1e9;
    for(int i = n; i >= 1; i--) // 倒着再做一遍
    {
        if(a[i] >= to)
        {
            ans += a[i] - to;
            a[i] = to;
        }
        to = a[i] + 1;
    }
    
    cout << ans << "\n";
}

int main()
{
    int T;
    cin >> T;
    while(T--)
        solve();
    return 0;
}

E - Climbing Silver

  • 预估难度:普及/提高-
  • 标签:动态规划

题意

有一个 \(N \times N\) 的网格。其中第 \(i\) 行第 \(j\) 列的单元格被称作 \((i,j)\)

网格由 \(N\) 个长度均为 \(C\) 的字符串 \(S_1,S_2,\dots,S_N\) 描述。
如果 \(S_i\) 的第 \(j\) 个字符是 .,则单元格 \((i,j)\) 是一片空地;如果是 #,则 \((i,j)\) 是一面墙。

一开始,高桥位于空地 \((N,C)\) 处,接下来他会重复进行以下操作 \(N-1\) 次:

  • 如果他当前位于 \((r,c)\) 处,则接下来会指定 \((r-1,c-1),(r-1,c),(r-1,c+1)\) 中的一个为目的地。如果单元格不存在,不能作为目的地。(即选择上一行的左、中、右三个位置之一作为目的地。)
  • 假设目的地为 \((a,b)\),如果 \((a, b)\) 是一面墙,则会出现以下情况:
    • 如果 \((a,b)\) 下方的所有单元格均为空地,那么他将摧毁位于 \((a,b)\) 的墙,并移动到该处。
    • 否则,他将停止移动,即使他没有进行完 \(N-1\) 次操作。
  • 如果目的地 \((a,b)\) 是一个空格,那么他便可以直接移动到 \((a,b)\)

请输出一个长度为 \(N\) 的字符串 \(R\),其中如果 \((1, i)\) 能够作为高桥的终点(能够到达该点),则第 \(i\) 个字符为 \(1\),否则为 \(0\)

思路

对于 \((i, j)\) 这个位置,它可以作为下一行的左、中、右这三个位置的“目的地”(即上一个位置可以是 \((i+1, j-1), (i+1, j), (i+1, j+1)\) 三者之一)。

如果当前位置是一片空地,那么只要此前存在某种方案能够到达下一行三个位置中的某个位置,那么当前位置便也能够到达。

当前位置的可行性可以从下一行的可行性中讨论转移而来,因此考虑动态规划,直接记 vis[i][j] 表示是否存在某种移动方案能够到达 \((i, j)\) 这个位置。

空地的状态转移上面已讨论过,而如果当前位置是一面墙,根据题意,只有当这面墙下方没有其它墙,或者当前这面墙正下方的所有墙均已被摧毁时,我们才能够走到当前位置并打破该面墙。

这个条件看似需要记录正下方所有墙的状态,但实际上我们只需要看当前位置正下方的第一面墙即可。

  • 如果从 \((i, j)\) 开始往下走所能遇到的第一面墙可以到达,那就说明这面墙能被摧毁,而摧毁它的前提是更下面的墙均已被摧毁。因此只要第一面墙能够到达,则当前位置这面墙便也能够到达。
  • 而如果第一面墙无法到达,由于动态规划是从下往上依次求解的,那就说明这面墙注定无法被摧毁,则当前位置这面墙也不能被摧毁,无法到达。

至于如何对于每面墙,快速求出正下方的下一面墙在什么位置,可以一开始对于每列都从下往上预处理一遍,或者在过程中直接再来一层循环暴力向下查找即可。由于每次找到下一面墙就可以停止查找,所以对于整个动态规划的过程来说,暴力查找这部分的总复杂度只会是 \(O(N^2)\),独立于动态规划的 \(O(N^2)\) 之外。

单组数据时间复杂度 \(O(N^2)\)

代码

int n;
char mp[3005][3005];
bool vis[3005][3005]; // vis[i][j] 标记 (i, j) 能否到达

void solve()
{
    int c;
    cin >> n >> c;
    for(int i = 1; i <= n; i++)
        for(int j = 1; j <= n; j++)
        {
            cin >> mp[i][j];
            vis[i][j] = false; // 清空
        }
    
    vis[n][c] = true;
    for(int i = n - 1; i >= 1; i--)
        for(int j = 1; j <= n; j++)
        {
            // (i, j) 可从 (i+1, j-1) (i+1, j) (i+1, j+1) 三个点转移过来
            if(mp[i][j] == '.') // 可以直接到达
            {
                if(vis[i+1][j-1] || vis[i+1][j] || vis[i+1][j+1])
                    vis[i][j] = true;
            }
            else // 墙
            {
                // 先判断正下方是否有其它墙
                // 如果没有,则可以直接打破
                // 如果有,下方的墙必须也要能到达并打破,才可以打破这面墙
                bool flag = true;
                for(int k = i + 1; k <= n; k++)
                    if(mp[k][j] == '#') // 有墙
                    {
                        if(!vis[k][j]) // 但这面墙之前没法到达 并将其打破
                            flag = false;
                        break;
                    }
                if(flag)
                {
                    if(vis[i+1][j-1] || vis[i+1][j] || vis[i+1][j+1])
                        vis[i][j] = true;
                }
            }
        }
    
    for(int i = 1; i <= n; i++)
        cout << vis[1][i];
    cout << "\n";
}

int main()
{
    int T;
    cin >> T;
    while(T--)
        solve();
    return 0;
}

F - Non-Increasing Number

  • 预估难度:普及+/提高
  • 标签:宽度优先搜索、数学

题意

当且仅当一个正整数 \(X\) 满足以下条件时,它才被称为好整数

  • \(X\) 用十进制表示时,能够构成一个单调非递减的数字序列。

例如, \(112389\)\(1\)\(777\) 是好整数,但 \(443\)\(404\) 不是好整数。

给定一个正整数 \(N\),请判断 \(N\) 的倍数中是否存在好整数。如果存在,求最小的 \(N\) 的倍数。

思路

直接枚举 \(N\) 的倍数太麻烦,不如直接从构造一个非递减的数字序列入手,将问题转为构造答案。

也就是现在我们需要从一个空数字出发,不断地在当前数字后面再拼上一个新的数位,使该数除以 \(N\) 的余数为 \(0\)。根据“好整数”的性质,我们还需要保证新拼上的数位不能小于上一位。

而对于构造过程中出现的两个不同的正整数 \(a, b\) 而言,如果 \(a \equiv b \ (\bmod N)\),也就是在模 \(N\) 的意义下同余,那么接下来不论在这两个整数后面拼什么相同的数位,其除以 \(N\) 的余数将会永远保持不变。

因此,同一余数的答案我们只需要保留最小的一个。再加上需要控制下一个要拼的数位范围,如此,我们构造的整数最多不会超过 \(10N\) 个。

考虑宽度优先搜索,记录 vis[i][j] 表示模 \(N\) 余数为 \(i\) 且最后一个数位为 \(j\) 的整数是否已经搜索过。同时为了记录路径,我们可以借助类似于最短路算法求路径的原理,不断去记录搜索过程中每个状态的前驱状态,最后借助回溯来构造整个答案。

借助宽搜的性质,我们可以保证数位长度较短的状态会被先搜索到。当数位长度相同时,我们可以优先搜索下一个数位更小的状态,以此保证第一次搜到终点时一定是最小的数值。

时间复杂度 \(O(N\sigma)\),其中 \(\sigma = 10\)

代码

struct node
{
    int rmd; // 余数
    int last; // 最后一个数位
};

bool vis[3000005][10];
node pre[3000005][10];
// pre[i][j] 表示余数为 i 且最后一个数位为 j 这个状态 的前驱状态

void solve()
{
    int n;
    cin >> n;
    
    queue<node> q;
    q.push({0, 0});
    
    while(!q.empty())
    {
        node u = q.front(), v;
        q.pop();
        
        for(int i = max(u.last, 1); i <= 9; i++) // 枚举下一个要拼的数位
        {
            // 下一状态
            v.rmd = (u.rmd * 10 + i) % n;
            v.last = i;
            
            if(vis[v.rmd][v.last]) // 该状态已搜索过
                continue;
            
            q.push(v);
            vis[v.rmd][v.last] = true;
            pre[v.rmd][v.last] = u; // 记录上一状态
            
            if(v.rmd == 0) // 余数为 0,说明找到了最小的倍数,回溯输出答案
            {
                vector<int> ans;
                while(v.rmd != 0 || v.last != 0) // 0, 0 为初始状态
                {
                    ans.push_back(v.last);
                    v = pre[v.rmd][v.last];
                }
                for(int i = (int)ans.size() - 1; i >= 0; i--) // 倒序输出
                    cout << ans[i];
                return;
            }
        }
    }
    
    cout << -1;
}
posted @ 2026-01-31 22:08  StelaYuri  阅读(0)  评论(0)    收藏  举报