AtCoder Beginner Contest 431 ABCDEF 题目解析

A - Robot Balance

题意

一个机器人由一个头部零件与一个身体零件组成。如果头部零件的重量大于身体零件的重量,机器人就会摔倒。

目前,高桥有一个头部零件和一个身体零件,头部零件的重量为 \(H\) 克,身体零件的重量为 \(B\) 克。

他想让身体零件更重一些,这样机器人就不会摔倒。

为使机器人不摔倒,身体零件需要再加重多少克?

代码

void solve()
{
    int h, b;
    cin >> h >> b;
    cout << max(0, h - b);
}

B - Robot Weight

题意

有一个初始重量为 \(X\) 的机器人。

该机器人有 \(N\) 种零件可以安装,每种零件的种类分别编号为 \(1, 2, \dots, N\),第 \(i\ (1\le i\le N)\) 种零件的重量为 \(W _ i\)

最初,机器人上没有安装任何零件。

请依次处理以下 \(Q\) 个询问。第 \(i\) 次询问 \((1\le i\le Q)\) 由一个整数 \(P _ i\) 表示:

  • 如果种类为 \(P _ i\) 的零件当前未安装到机器人上,则将其安装上;如果已安装,则将其移除。然后,输出机器人当前的重量。

思路

计数数组维护每种零件当前是否已安装即可。

代码

int w[105];
bool used[105];
// used[i] 表示零件 i 当前是否已安装

void solve()
{
    int x, n;
    cin >> x >> n;
    for(int i = 1; i <= n; i++)
        cin >> w[i];
    int q;
    cin >> q;
    while(q--)
    {
        int p;
        cin >> p;
        if(used[p] == false)
        {
            used[p] = true;
            x += w[p];
        }
        else
        {
            used[p] = false;
            x -= w[p];
        }
        cout << x << "\n";
    }
}

C - Robot Factory

题意

一个机器人由一个头部零件一个身体零件组成。如果头部零件的重量大于身体零件的重量,机器人就会摔倒。

目前,高桥有 \(N\) 个头部零件和 \(M\) 个身体零件,第 \(i\) 个头部零件的重量为 \(H_i\) 克,第 \(i\) 个身体零件的重量为 \(B_i\) 克。

他希望通过适当组合他所拥有的部件,制造出总共 \(K\) 个不会摔倒的机器人。

请判断他能否通过合理组合零件来实现该目标。

思路

为了制造出 \(K\) 个不会摔倒的机器人,每个机器人都应该让头部零件重量小于等于身体零件的重量。

贪心可得,头部零件重量越大越不可能被选择,身体零件重量越小越不可能被选择,所以我们不如直接考虑\(K\) 小的头部零件以及\(K\) 大的身体零件,然后对选出来的两种零件分别进行排序。

接下来,把每个头部零件和每个身体零件按重量从小到大一一对应,判断是否存在某个组合不符合条件即可。

代码

int n, m, k;
int h[200005], b[200005];

void solve()
{
    cin >> n >> m >> k;
    for(int i = 1; i <= n; i++)
        cin >> h[i];
    for(int i = 1; i <= m; i++)
        cin >> b[i];
    sort(h + 1, h + n + 1);
    sort(b + 1, b + m + 1);
    // h[1 ... k] 一一匹配 b[m-k+1 ... m]
    for(int i = 1; i <= k; i++)
    {
        // h[i] 匹配 b[m-k+i]
        if(h[i] > b[m-k+i])
        {
            cout << "No";
            return;
        }
    }
    cout << "Yes";
}

D - Robot Customize

题意

现在有一个由头部和身体所组成的机器人。

该机器人有 \(N\) 种零件可以安装,每种零件的种类分别编号为 \(1, 2, \dots, N\),第 \(i\ (1\le i\le N)\) 种零件的重量为 \(W _ i\)

每种零件可以选择安装到头部或者安装到身体,并且安装到头部和安装到身体所产生的幸福感是不同的。将第 \(i\) 种零件安装到头部时可以获得 \(H_i\) 的幸福感,而安装到身体时可以获得 \(B_i\) 的幸福感。

如果头部的重量大于身体的重量,机器人就会倒下。这里,头部的重量指的是选择安装在头部上的零件重量之和,身体的重量指的是选择安装在身体上的零件重量之和。

高桥希望在机器人上安装所有 \(N\) 种零件,每种各一个。在保证机器人不倒下的前提下,所有零件安装所能获得的幸福感之和的最大值是多少?

思路

由于头部重量不能超过身体重量,所以我们可以先假设把所有零件全部安装在身体上。

\(\text{sumw}\) 表示所有零件的总重量。考虑转移一部分零件到头部以获得更多幸福感。由于头部零件总重不能超过身体零件总重,可以计算出头部零件总重最大为 \(\dfrac{\text{sumw}}2\)

对于零件 \(i\),如果将其转移到头部,相比于安装在身体上,可以多获得 \(H_i - B_i\) 点幸福感。

可以将第 \(i\) 种零件当作是一件需要花费 \(W_i\) 的代价来获得 \(H_i - B_i\) 价值的物品,允许的最大总代价为 \(\dfrac{\text{sumw}}2\),作为背包最大容量,考虑借助 0/1 背包求解最大价值即可。

时间复杂度 \(O(N\cdot \sum W_i)\)

代码

typedef long long ll;

int w[505], h[505], b[505];
ll dp[125005];
// dp[i] 表示取总重量为 i 的零件安装在头上
// 相比于安装在身体上能够 多获得 最多 多少幸福感

void solve()
{
    int n;
    cin >> n;
    
    ll ans = 0; // 零件全部安装在身体上的幸福感
    ll sumw = 0; // 零件总重量
    
    for(int i = 1; i <= n; i++)
    {
        cin >> w[i] >> h[i] >> b[i];
        // 先假设全部安装在身体上
        ans += b[i];
        sumw += w[i];
    }
    
    // 取总重的一半作为背包容量,表示移动到头部的零件总重不超过一半
    sumw /= 2;
    
    for(int i = 1; i <= n; i++)
    {
        if(h[i] <= b[i]) // 安装在身体上的幸福感更大,可以直接不考虑
            continue;
        // 如果转移到头上,需要花费 w[i] 的代价,相比于安装在身体上可以多获得 h[i]-b[i] 的幸福感
        for(int j = sumw; j >= w[i]; j--)
            dp[j] = max(dp[j], dp[j - w[i]] + (h[i] - b[i]));
    }
    
    cout << ans + dp[sumw];
}

E - Reflection on Grid

题意

有一个 \(H\times W\) 的网格,每个单元格上最多有一面镜子。

高桥站在 \((1,1)\) 单元格的左侧,青木站在 \((H,W)\) 单元格的右侧。高桥拿着手电筒,从 \((1,1)\) 单元格的左侧向右侧照射。在这里,假设手电筒的光线是一条沿着直线传播的光线。

高桥的目标是通过网格中的镜子将手电筒的光线传递给青木。

镜子的放置位置有三种。当光线照射到镜子上时,光线的传播方向会根据镜子的放置位置而发生变化。对于每种镜子的放置位置,入射方向的出射方向的关系如下图所示。

  • 类型 A(未放置反射镜)

  • B 型(在连接单元格左上角和右下角的对角线上放置一面镜子)

  • C 型(在连接单元格右上角和左下角的对角线上放置一面镜子)

网格上镜子的位置由 \(H\) 个长度为 \(W\) 的字符串表示:\(S_1,S_2,\ldots,S_H\)。字符共 ABC 三种,每种表示上述的一种镜子放置类型。第 \(i\) 个字符串 \(S_i\) 的第 \(j\) 个字符表示单元格 \((i, j)\) 内部的镜子放置类型。

高桥可以执行以下任意次数的操作,将光线传送给青木:

  • 选择一个单元格,并将该单元格的镜子位置改为其它类型。

求将光传递给青木所需的最少操作次数。

思路

首先可以讨论证明如果某个单元格必须发生改变,那么这个单元格只会被经过一次

  • 首先排除原路往回走的情况。
  • 如果某个单元格经过某次修改变成了 A 类型,那么不可能被垂直经过一次再水平经过一次,因为这样的路径交叉不如一开始就让光线在这个单元格内拐弯。
  • 反之亦然,如果某个单元格经过某次修改变成了 B / C 类型,假设接下来该单元格被光线经过了两次,那么经过的肯定是该镜面的两侧,这样的路线交叉明显也不如一开始就建成另一种类型的反射镜面,能够直接让光线拐到原本经过两次才能到达的方向上。

经过上述讨论,既然控制了每个位置最多只会改变一次,那就可以放心地把修改产生的影响直接计入搜索状态,而不用担心会影响原本的网格图信息。

考虑带方向的最短路,记距离表示从起点开始搜到当前状态总共修改了多少个网格。下文采用堆优化后的 Dijkstra。

由于起点与终点均在网格外部,下文记录在队列里的状态表示的是即将离开当前点时的状态。每次从某个状态出发向外松弛时,先朝着当前方向移动一格,然后枚举讨论是否改变移动到的下一个点的网格信息。

至于方向与镜面类型之间的关系,可以写个函数辅助转换。观察可知除了镜面类型为 A 不影响传递方向以外,当镜面类型为 B 且光线方向水平,或镜面类型为 C 且光线方向垂直时,接下来的光线方向会右转 \(90^{\circ}\);反之则左转 \(90^{\circ}\)

单组数据时间复杂度 \(O(HW\log {HW})\)

代码

// 0,1,2,3 分别表示 右下左上
const int dx[4] = {0, 1, 0, -1};
const int dy[4] = {1, 0, -1, 0};

int n, m;

struct point
{
    int x, y, d;
    // (x, y) 表示当前行列,d 表示当前朝向
    int step;
    bool operator < (const point &p) const
    {
        return step > p.step;
    }
};

// 光线朝着 d 方向传递到类型为 c 的网格后,会转为什么方向
int change(int d, char c)
{
    // 方向不变
    if(c == 'A')
        return d;
    // 当类型为 B 且方向水平时,或是当类型为 C 且方向垂直时,接下来右转 90 度
    if(c == 'B' && (d == 0 || d == 2) || c == 'C' && (d == 1 || d == 3))
        return (d + 1) % 4;
    // 左转 90 度
    return (d + 3) % 4;
}

void solve()
{
    cin >> n >> m;
    
    // mp[i][j] 表示地图
    vector<vector<char>> mp(n + 1, vector<char>(m + 1));
    // dis[i][j][k] 表示光线准备离开 (i, j) 单元格,且当前传播方向为 k 时,在此之前需要至少更改多少个单元格
    // 初始设置极大值
    vector<vector<vector<int>>> dis(n + 1, vector<vector<int>>(m + 1, vector<int>(4, 1e9)));
    
    for(int i = 1; i <= n; i++)
        for(int j = 1; j <= m; j++)
            cin >> mp[i][j];
    
    priority_queue<point> q;
    q.push(point{1, 0, 0, 0}); // 在 (1, 0) 位置,初始方向朝右,初始步数 = 0
    dis[1][0][0] = 0;
    
    int ans = 1e9; // 答案
    
    while(!q.empty())
    {
        point u = q.top();
        q.pop();
        
        if(dis[u.x][u.y][u.d] != u.step) // 堆内无效状态
            continue;
        
        // 向前移动一步
        u.x += dx[u.d];
        u.y += dy[u.d];
        
        if(u.x == n && u.y == m + 1) // 判断终点
        {
            ans = min(ans, u.step);
            continue;
        }
        
        if(u.x < 1 || u.y < 1 || u.x > n || u.y > m) // 超出网格图范围
            continue;
        
        for(char c = 'A'; c <= 'C'; c++) // 枚举要把下一步到达的单元格改成什么类型
        {
            point v = u;
            
            if(mp[v.x][v.y] != c) // 单元格类型发生了改变
                v.step++;
            
            v.d = change(v.d, c); // 根据单元格类型改变光线方向
            
            if(dis[v.x][v.y][v.d] <= v.step) // 当前状态在此之前已搜索到更优解
                continue;
            
            q.push(v);
            dis[v.x][v.y][v.d] = v.step;
        }
    }
    
    cout << ans << "\n";
}
signed main()
{
    ios::sync_with_stdio(0);
    cin.tie(0); cout.tie(0);
    int T;
    cin >> T;
    while(T--)
    {
        solve();
    }
    return 0;
}

F - Almost Sorted 2

题意

给定一个长度为 \(N\) 的整数序列 \(A=(A_1,A_2,\ldots,A_N)\) 和一个正整数 \(D\)

问有多少种整数序列 \(B=(B_1, B_2, \ldots, B_N)\),能够通过重新排列序列 \(A\) 得到,且满足以下条件:

  • \(B_{i+1}\geq B_i-D\) 对所有 \(i\ (1\leq i\leq N-1)\) 都成立。

思路

考虑将 \(A\) 序列的元素一个一个取出,然后不断插入到一个新序列中,最终构造出序列 \(B\)

可以先对 \(A\) 序列进行排序,然后倒序\(A\) 序列中的每个元素,以此保证在看到 \(A_i\) 这个整数时,\(A_{i+1 \sim N}\) 这些整数均已在新序列内确定相对位置

考虑将 \(A_i\) 插入到新序列内,根据题意中 \(B_{i+1} \ge B_i - D\) 的限制(即“\(\text{前} - D \le \text{后}\)”),可以发现 \(A_i\) 能够插入到任意一个取值范围在 \([A_i, A_i + D]\) 的整数的后面;或者因为目前出现的数字数值均不小于 \(A_i\),因此也可以直接将 \(A_i\) 当作新序列的第一个元素,插入到序列开头。

\(A_{i+1 \sim N}\) 的范围内找取值范围在 \([A_i, A_i + D]\) 内的整数数量,可以借助二分或是双指针法求解。记符合条件的整数数量为 \(x\),那么这 \(x\) 个整数的后一个位置均能够插入 \(A_i\),加上序列开头的位置,故 \(A_i\) 共有 \(x+1\) 个可以插入的位置。

对于每个 \(A_i\),求出其可以插入的总位置数,累乘以获得不同的排列数量,暂记作 \(\text{Ans}\)

但本题问的是有多少种整数序列 \(B=(B_1, B_2, \ldots, B_N)\) 可以通过重排 \(A\) 得到,而不是问有多少种重排的方案可以使得到的序列满足条件(考虑的是值而不是具体方案)。因此我们需要将重复的方案去除。

发现重复方案只会出现在数字相同的情况下,即对于所有相同的数字,不管其内部如何重排,对最终答案的贡献只有一种。因此我们可以用桶维护出每种数值在序列内出现的次数,记 \(\text{cnt}[i]\) 表示数字 \(i\) 出现的次数,那么数字 \(i\) 的重排方案数即可通过 \(\text{A}_{\text{cnt}[i]}^{\text{cnt}[i]} = {\text{cnt}[i]}!\) 求得。

每种数值的内部重排方案仅保留一种,所以只需将每种数值的重排方案数从上面累乘所得到的答案中除去即可。

最终答案即 \(\dfrac{\text{Ans}}{\prod\limits_{i=1}^{\max A} {\text{cnt}[i]}!}\)

时间复杂度 \(O(N + \max A)\),若使用二分则为 \(O(N\log N + \max A)\)

代码

typedef long long ll;
const ll mod = 998244353;

int n, d, a[200005];
int cnt[1000005];

ll fac[200005], inv[200005];

ll qpow(ll a, ll n)
{
    ll r = 1;
    while(n)
    {
		if(n & 1)
            r = r * a % mod;
        a = a * a  % mod;
        n >>= 1;
    }
    return r;
}

void init(int n)
{
    fac[0] = 1;
    for(int i = 1; i <= n; i++)
        fac[i] = fac[i - 1] * i % mod;
    inv[n] = qpow(fac[n], mod - 2);
    for(int i = n-1; i >= 0; i--)
        inv[i] = inv[i + 1] * (i + 1) % mod;
}

void solve()
{
    cin >> n >> d;
    init(n);
    for(int i = 1; i <= n; i++)
    {
        cin >> a[i];
        cnt[a[i]]++;
    }
    sort(a + 1, a + n + 1);
    
    // 考虑倒序将每个整数取出,插入并形成一个新的符合条件的序列 B
    ll ans = 1;
    // 枚举每个整数 a[i]
    // 这里其实正序倒序枚举均可,反正只需要对每个 a[i] 求后面有多少个位置在 [a[i], a[i]+d] 内
    for(int i = 1, j = 1; i <= n; i++)
    {
        // 找到最大的位置 j,满足 a[i] 可以插入到 a[i+1 ~ j] 之间的任意整数的后一个位置
        while(j + 1 <= n && a[j + 1] - d <= a[i]) // 前 - d <= 后
            j++;
        // 共 j-i 个位置可供插入,但由于 a[i] 此时是最小值,插入到当前序列最前面也可行,所以总共有 j-i+1 个位置
        ans = ans * (j - i + 1) % mod;
    }
    // 每种数值的内部重排方案共 cnt[i]! 种,借助阶乘逆元将其除去
    for(int i = 1; i <= 1000000; i++)
        ans = ans * inv[cnt[i]] % mod;
    cout << ans;
}
posted @ 2025-11-08 22:32  StelaYuri  阅读(249)  评论(0)    收藏  举报