AtCoder Beginner Contest 465 ABCDE

A - Supermajority

  • 预估难度:入门
  • 标签:条件分支

题意

给定两个正整数 \(A\)\(B\)

如果 \(\displaystyle A > B \times \frac{2}{3}\),请输出 Yes;否则,请输出 No

数据范围

  • \(1 \leq A, B \leq 1000\)

代码

void solve()
{
    int a, b;
    cin >> a >> b;
    if(a * 3 > b * 2)
        cout << "Yes";
    else
        cout << "No";
}

B - Parking 2

  • 预估难度:入门
  • 标签:循环语句、条件分支

题意

有一个停车场。该停车场的收费标准如下:

  • \(L\) 点整到 \(R\) 点整的时段内,停车每小时收费 \(X\)
  • 在上述时段之外的时间,停车每小时收费 \(Y\)

如果一辆车从 \(A\) 点整停到 \(B\) 点整,且期间不跨越午夜,那么停车费是多少?

数据范围

  • \(1 \leq X, Y \leq 1000\)
  • \(1 \leq L < R \leq 23\)
  • \(1 \leq A < B \leq 23\)

思路

由于时间均是以小时作为单位,范围很小,所以我们可以通过枚举停车经过的每个小时,然后判断当前这个小时的收费标准属于哪一项。

但需要注意的是,题目给定的 \(L, R, A, B\) 指的均是时刻,而不是一段时间。

所以接下来用整数 \(i\) 描述 \(i\) 点整到 \(i+1\) 点整这一个小时。

一辆车从 \(A\) 点整停到 \(B\) 点整,第一个小时是 \(A \sim A+1\),最后一个小时是 \(B-1 \sim B\),所以可以用整数 \(A, A+1, \ldots, B-1\) 用来描述每一段停车的时间。

判断某个小时是否在 \(L\) 点整到 \(R\) 点整的时段内,只要判断开始时间 \(\ge L\) 且结束时间 \(\le R\) 即可。对于整数 \(i\) 而言,需要满足:

  • \(i \ge L\)
  • \(i + 1 \le R\)
    • 或是 \(i \lt R\)

枚举过程中判断即可。

代码

void solve()
{
    int X, Y, L, R, A, B, ans = 0;
    cin >> X >> Y >> L >> R >> A >> B;
    
    for(int i = A; i < B; i++)
    {
        if(i >= L && i < R)
            ans += X;
        else
            ans += Y;
    }
    
    cout << ans;
}

C - Reverse Permutation

  • 预估难度:普及-
  • 标签:模拟、双端队列 / 数组 / vector

题意

给定一个整数 \(N\) 和一个长度为 \(N\) 的字符串 \(S\),字符串仅由字符 ox 组成。

现有一个长度为 \(N\) 的整数序列 \(A=(A_1,A_2,\ldots,A_N)\)
初始时,\(A=(1,2,\ldots,N)\)

\(N\) 次操作,第 \(k\) 次操作的类型由字符串 \(S\) 的第 \(k\) 个字符 \(S_k\) 描述:

  • 如果 \(S_k=\) o,则将 \(A\) 的前 \(k\) 项进行反转。
    • 具体来说,将 \(A\) 替换为 \((A_k,A_{k-1},\ldots,A_1,A_{k+1},A_{k+2},\ldots,A_N)\)
  • 如果 \(S_k=\) x,则不执行任何操作。

请输出所有操作执行完毕后的序列 \(A\)

数据范围

  • \(2\le N\le 5\times 10^5\)
  • 字符串 \(S\) 的长度为 \(N\),且仅包含 ox

思路

由于第 \(i\) 次操作只会影响前 \(i\) 个数字的顺序,不会影响\(i+1 \sim N\) 这些数字的顺序。

那我们可以假设数字是根据操作一个一个加入到 \(A\) 序列的,第 \(i\) 次操作将数字 \(i\) 加到序列末尾,而不是一开始就在 \(A\) 序列里。可以发现这并不影响答案。

那我们就可以用一个容器来模拟 \(A\) 序列,只要能够实现每次加一个数字在末尾即可。

当我们把数字 \(i\) 加入序列之后,如果 $S_i = $ x,无事发生;而当 $S_i = $ o 时,说明目前整个序列需要首尾翻转。

由于首尾反转的时间复杂度是 \(O(N)\) 的,如果整个 \(S\) 字符串均为 o,整体时间复杂度将会到达 \(O(N^2)\),不可接受。

但实际上我们不用真的去反转目前的整个序列。

思考下如果我们真的把序列前后反转了,那么后面还没加入序列的数字会加入到序列的什么位置。明显会加到反转之前的第一个数字前面。

所以我们只需要来一个标记变量,用来控制当前序列是正方向还是反方向即可。

如果序列当前是正方向,那么下一个数字就加到序列的最后面;而如果当前序列是反方向,那么下一个数字就加到序列的最前面即可。而每次遇到字符 o 时,则只需要让标记变量取反一次。

那么最后的问题就是,我们需要让用来模拟 \(A\) 序列的容器能实现快速地在序列的前面以及后面加一个数字了。

思路一

考虑数组模拟或是直接使用 STL 容器 deque,实现双端队列的功能。

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

代码一

void solve()
{
    int n;
    string s;
    cin >> n >> s;
    
    deque<int> dq;
    bool f = false;
    // f 表示当前方向,false 为正,true 为负
    
    for(int i = 1; i <= n; i++)
    {
        if(!f)
            dq.push_back(i);
        else
            dq.push_front(i);
        
        if(s[i - 1] == 'o') // 表示当前数字加入后,前 i 个数需要首尾反转
            f = !f;
    }
    
    if(!f) // 目前队列的方向是正的
    {
        for(int i = 0; i < n; i++)
            cout << dq[i] << " ";
    }
    else   // 目前队列的方向是反的
    {
        for(int i = n - 1; i >= 0; i--)
            cout << dq[i] << " ";
    }
}

思路二

考虑借助两个数组或 STL 容器 vector,模拟正方向加数字和反方向加数字。最后输出时将处理反方向的容器倒着输出,再把处理正方向的容器正着输出即可。

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

代码二

void solve()
{
    int n;
    string s;
    cin >> n >> s;
    
    vector<int> Z, F;
    // Z 用于表示序列正方向的部分
    // F 用来表示序列反方向的部分
    bool f = false;
    // f 表示当前方向,false 为正,true 为负
    
    for(int i = 1; i <= n; i++)
    {
        if(!f) // 正方向
            Z.push_back(i);
        else   // 反方向
            F.push_back(i);
        
        if(s[i - 1] == 'o') // 表示当前数字加入后,前 i 个数需要首尾反转
            f = !f;
    }
    
    if(!f) // 最终方向为正方向
    {
        // 先把反方向的部分倒序输出
        for(int i = (int)F.size() - 1; i >= 0; i--)
            cout << F[i] << " ";
        // 再把正方向的部分正序输出
        for(int i = 0; i < Z.size(); i++)
            cout << Z[i] << " ";
    }
    else  // 最终方向为反方向,同上,但倒着输出
    {
        for(int i = (int)Z.size() - 1; i >= 0; i--)
            cout << Z[i] << " ";
        for(int i = 0; i < F.size(); i++)
            cout << F[i] << " ";
    }
}

D - X to Y

  • 预估难度:普及
  • 标签:数学、树、最近公共祖先

题意

给定整数 \(X\)\(Y\),以及一个不小于 \(2\) 的整数 \(K\)

现有一个变量 \(x\),其初始值为 \(X\)。你可以对该变量执行零次或多次以下操作:

  • 选择一个满足 \(\displaystyle \left\lfloor \frac xK \right\rfloor=y\)\(\displaystyle \left\lfloor \frac yK \right\rfloor=x\) 的整数 \(y\),并将 \(x\) 的值替换为 \(y\)

请计算使 \(x\) 变为 \(Y\) 所需的最少操作次数。

在给定的数据范围内,可以证明总能在有限次操作内使 \(x=Y\)

\(T\) 组数据。

数据范围

  • \(1\le T\le 2\times 10^5\)
  • \(0\le X,Y\le 10^{18}\)
  • \(2\le K\le 10^{18}\)

思路

先理解操作的意义:

  • 如果选择 \(\displaystyle \left\lfloor \frac xK \right\rfloor=y\) 并让 \(x\) 改为 \(y\),这个操作相当于让 \(x\) 缩小 \(K\) 倍并向下取整。
    • 也就是让 \(x\) 变为 \(\displaystyle \left\lfloor \frac xK \right\rfloor\)
  • 如果选择 \(\displaystyle \left\lfloor \frac yK \right\rfloor=x\) 并让 \(x\) 改为 \(y\),满足条件的最小的 \(y\) 发生在恰好整除时,为 \(xK\);最大的 \(y\) 发生在除法余数最大时,为 \(xK + K-1\)。换句话说,这个操作相当于让 \(x\) 放大 \(K\) 倍及以上,但不会到达 \(K+1\) 倍。
    • 也就是让 \(x\) 变为 \(xK \sim xK + K - 1\) 范围内的任意整数。

考虑转换题意,考虑二叉树的常规编号规则。

假设目前有一棵根结点编号为 \(0\) 并按层序遍历顺序编号的 \(K\) 叉树,可以发现对于树上编号为 \(x\) 的结点而言,其父结点编号即 \(\displaystyle \left\lfloor \frac xK \right\rfloor\),其 \(K\) 个子结点的编号分别为 \(xK \sim xK + K - 1\)。(除根结点 \(0\) 以外,根结点的最小儿子需从 \(1\) 开始。)

所以这个问题相当于就是在一颗 \(K\) 叉树上,不断通过结点间的移动,问从点 \(X\) 移动到点 \(Y\) 的最小步数。

为求出树上两点间的距离,可以先求出两个点的最近公共祖先,转为两点分别到最近公共祖先的距离。

由于本题的点编号特别大,无法建树进行处理。但又由于 \(K\) 叉树上 \(N\) 号结点的深度大致为 \(\log_K N\),所以最近公共祖先部分可以直接让点暴力向根结点跳跃来求解。

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

代码(单组数据)

void solve()
{
    ll X, Y, K;
    cin >> X >> Y >> K;
    int ans = 0;
    while(X != Y)
    {
        if(X > Y)
            X /= K;
        else
            Y /= K;
        ans++;
    }
    cout << ans << "\n";
}

E - Digit Circus

  • 预估难度:提高
  • 标签:状压DP、数位DP

题意

求满足以下三个条件中恰好一个条件的整数 \(x\)\(1 \leq x \leq N\))的数量,结果对 \(998244353\) 取模。

  • \(x\)\(3\) 的倍数。
  • \(x\) 的十进制表示中包含字符 3
  • \(x\) 的十进制表示中恰好使用了三种不同的数字。

注意:整数的十进制表示中不应包含不必要的前导 0

数据范围

  • \(1 \leq N < 10^{500}\)

思路

首先发现数据范围很大,不可能进行暴力枚举。但注意到条件涉及数位相关的判断,因此可以考虑采用数位 DP 进行求解。

观察条件,可以发现需要在过程中注意三个要点:

  • 当前选择的数字是否是 \(3\) 的倍数。
    • 判断数字是否是 \(3\) 的倍数可以转为判断数位和是否是 \(3\) 的倍数。
  • 当前选择的数字中是否包含 \(3\)
  • 当前选择的数字种类数。

对于第一个条件,可以借助一个状态 \(m\) 记录目前高位已确定的数位和 $ \bmod 3$ 的余数,后续拼接一个新数位可以直接加进去。

对于后两者,我们可以借助状态压缩的方式,用一个 \(10\) 位二进制数 \(sta\) 表示 \(0 \sim 9\) 每种数字目前是否出现过。是否包含 \(3\) 可以借助位运算判断,而数字种类数则可以直接求二进制上 \(1\) 的数量。

除此之外,数位 DP 过程中每一位的取值范围是否自由(即 \(0 \sim 9\) 都可以取)主要取决于高位数字是否和上界 \(N\) 紧贴着取。所以过程中还需要一个标记变量 \(flag\) 用于判断。

综上,可设计出搜索函数 dfs(p, m, sta, flag)

  • p 表示当前搜到了第几位(高位为 \(0\)
  • m 表示已选择的数位总和 $ \bmod 3$
  • sta 表示已选择的数位集合
  • flag 表示已选择的高位数字是否紧贴着边界 \(N\)

借助搜索函数再开一个 dp[p][m][sta][flag] 数组用于记忆化即可。

对于搜索过程,重点要记住单独讨论高位全取 \(0\) 的情况。

时间复杂度 \(O(L\cdot M \cdot 2^D)\),其中 \(L = \log_{10}N\) 表示 \(N\) 的长度,\(M = 3\)\(D = 10\) 表示数位种类数。

代码

string N;

ll dp[505][3][1 << 10][2];
// dp[i][m][sta][flag] 用于辅助记忆化搜索

// 当前搜到第 p 位过
// 前面选择的数位总和 %3 == m
// 出现过的数字集合为 sta
// 且(flag)表示前 i 位是否贴着 N 的上限在取
ll dfs(int p, int m, int sta, bool flag)
{
    // 边界,已搜完
    if(p == N.size())
    {
        if(sta == 0) // 选的是数字 0,不符合题意
            return 0;
        int cnt = 0;
        cnt += (m == 0);                       // 前面选择的数字是 3 的倍数
        cnt += (sta & (1 << 3)) != 0;          // 前面选的数字数位上包含 3
        cnt += (__builtin_popcount(sta) == 3); // 前面选的数字恰好用了三种数位
        return cnt == 1 ? 1 : 0; // 恰好满足一个条件
    }
    
    // 记忆化
    if(dp[p][m][sta][flag] != -1)
        return dp[p][m][sta][flag];
    
    ll result = 0;
    // 当前这一位能填的最大数字
    // 如果前面都是贴着 N 填的,那么这一位最大也只能填 N 的这一位
    // 否则直接填 9
    int maxx = flag ? (N[p] - '0') : 9;
    // 枚举当前这一位要填的数 i
    for(int i = 0; i <= maxx; i++)
    {
        if(i == 0 && sta == 0) // 说明此时前面全部填 0,还没有选真正的第一个数字
            result += dfs(p + 1, 0, 0, flag && (i == maxx));
        else
            result += dfs(p + 1,                // 接着搜下一位
                          (m + i) % 3,          // 加上 i 这一位除以 3 取余数
                          sta | (1 << i),       // 把 i 加入用过的数字集合
                          flag && (i == maxx)); // 前面贴着 N 取,这一位也贴着 N 取时
    }
    result %= mod;
    dp[p][m][sta][flag] = result;
    return result;
}

void solve()
{
    cin >> N;
    memset(dp, -1, sizeof dp);
    cout << dfs(0, 0, 0, true);
}
posted @ 2026-07-04 21:58  StelaYuri  阅读(46)  评论(0)    收藏  举报