AtCoder Beginner Contest 453 ABCDE 题目解析

A - Trimo

  • 预估难度:入门
  • 标签:字符串

题意

给定一个字符串 \(S\),请将其开头的所有 o 全部删除后再输出。

代码

void solve()
{
    int n;
    string s;
    cin >> n >> s;
    for(int i = 0; i < n; i++)
        if(s[i] != 'o')
        {
            cout << s.substr(i); // 从第一个不是 o 的字符开始的 后面所有字符
            break;
        }
}

B - Sensor Data Logging

  • 预估难度:入门
  • 标签:模拟

题意

在某次测量中,传感器在时刻 \(0,1,\dots,T\) 的读数按照以下规则进行记录。

  • 在时刻 \(0\),保存此时的读数。
  • 在时刻 \(1,2,\dots,T\),当且仅当 当前的读数 与 最近一次保存的读数 的差值绝对值至少为 \(X\) 时,才会保存读数。

时刻 \(i=0,1,\dots,T\) 的传感器读数为 \(A_i\)

请按时间升序输出保存读数的时刻以及对应的保存值。

思路

使用一个变量记录上一次保存的读数,时刻 \(0\) 默认保存。

此后每输入一个读数,判断差值绝对值是否大于等于 \(X\) 即可,如果是则改变变量为新读数。

代码

void solve()
{
    int T, X;
    cin >> T >> X;
    
    int last; // 上一次保存的读数
    cin >> last; // A0 直接作为保存的读数
    cout << "0 " << last << "\n";
    
    for(int i = 1; i <= T; i++)
    {
        int v;
        cin >> v;
        if(abs(v - last) >= X)
        {
            last = v;
            cout << i << " " << v << "\n";
        }
    }
}

C - Sneaking Glances

  • 预估难度:普及/提高-
  • 标签:深度优先搜索 / 二进制枚举

题意

高桥位于数轴上的坐标 \(0.5\) 处。

他将进行 \(N\) 次移动。
在第 \(i\) 次移动中,他可以任意选择向左或者向右,并向该方向移动 \(L_i\) 的距离。

请问他最多可以经过坐标 \(0\) 多少次?
在本题的约束条件下,不会有任何一次移动会结束在坐标 \(0\) 处。

思路

注意到 \(N \le 20\),考虑借助深搜或者二进制枚举的方法,将所有可能的方案全部找出来。在已知每一步的方向之后,模拟计算经过坐标 \(0\) 的次数,取最大值作为最终答案即可。

时间复杂度 \(O(2^N\cdot N)\)

代码一

int n, l[25];
int ans = 0;

// i 表示正打算走第几步
// p 表示目前坐标
// cnt 表示在此之前已经经过坐标 0 多少次
void dfs(int i, double p, int cnt)
{
    if(i > n) // 已全部走完
    {
        ans = max(ans, cnt);
        return;
    }
    
    // 向左
    if(p > 0 && p - l[i] < 0)
        dfs(i + 1, p - l[i], cnt + 1);
    else
        dfs(i + 1, p - l[i], cnt);
    
    // 向右
    if(p < 0 && p + l[i] > 0)
        dfs(i + 1, p + l[i], cnt + 1);
    else
        dfs(i + 1, p + l[i], cnt);
}

void solve()
{
    cin >> n;
    for(int i = 1; i <= n; i++)
        cin >> l[i];
    
    dfs(1, 0.5, 0);
    
    cout << ans;
}

代码二

int n, l[25];

void solve()
{
    cin >> n;
    for(int i = 0; i < n; i++)
        cin >> l[i];
    
    int ans = 0;
    // 二进制枚举,以 0 表示向左,1 表示向右
    for(int i = 0; i < (1 << n); i++)
    {
        double p = 0.5;
        int cnt = 0; // 这种方案下经过坐标 0 的次数
        for(int j = 0; j < n; j++)
        {
            if(i >> j & 1) // 向右
            {
                if(p < 0 && p + l[j] > 0)
                    cnt++;
                p += l[j];
            }
            else // 向左
            {
                if(p > 0 && p - l[j] < 0)
                    cnt++;
                p -= l[j];
            }
        }
        ans = max(ans, cnt);
    }
    
    cout << ans;
}

D - Go Straight

  • 预估难度:普及/提高-
  • 标签:宽度优先搜索

题意

有一个 \(H\)\(W\) 列的网格,高桥在此网格中进行上下左右的移动。
\(i\) 行(从上往下数)第 \(j\) 列(从左往右数)的单元格状态由字符 \(S_{i,j}\) 表示。
\(S_{i,j}\)#.oxSG 中的一种。

  • 如果 \(S_{i,j}=\)#:此单元格不可进入。
  • 如果 \(S_{i,j}=\).:此单元格可以自由进出。即,进入此单元格后,高桥可以向上、下、左、右方向移动到任意相邻的单元格(如果存在)。
  • 如果 \(S_{i,j}=\)o:在此单元格中,高桥必须沿与上一次移动相同的方向移动。即,进入此单元格后,他必须不改变方向地移动到下一个单元格。
  • 如果 \(S_{i,j}=\)x:在此单元格中,高桥不能沿与上一次移动相同的方向移动。即,进入此单元格后,他必须改变方向才能移动到下一个单元格。掉头 \(180\) 度返回上一个单元格也被视为改变方向。
  • 如果 \(S_{i,j}=\)S:此单元格是高桥的起始位置。此单元格可以自由进出。
  • 如果 \(S_{i,j}=\)G:此单元格是高桥的目的地。此单元格可以自由进出。

保证 SG 各只出现一次。

高桥希望从起始位置出发,通过重复向上、下、左、右移动到相邻单元格,最终到达目的地。
判断这是否可能,如果可能,则输出一个有效的移动序列,该序列包含的相邻单元格移动次数最多为 \(5\times 10^6\) 次。
可以证明,如果在题目条件下存在有效的移动序列,那么一定存在一个移动次数不超过 \(5\times 10^6\) 的序列。
你不需要最小化移动次数。

思路

比较模板的带方向宽搜题,明显对于最大 \(1000 \times 1000\) 且只能往四个方向走的一张网格图,结合坐标与方向所能出现的不同状态数量最多只有 \(4 \times 1000 \times 1000\) 种。因此只要存在方案,步数肯定不会超过总状态的数量。

由于是网格图里的搜索,每走一步步数均只 \(+1\),因此第一次搜到某一状态的方案一定是步数最少的方案。

本题在宽搜基础上加了个输出方案,这只需要我们在搜索过程中,对于每个状态去记录上一步的状态是什么。最后输出方案只需要从终点状态出发,一步步向前回溯到起点,再将倒着得出的方案再次翻转后输出即可。

时间复杂度 \(O(N\times M \times D)\),其中 \(D = 4\) 表示方向数量。

代码

const int INF = 0x3f3f3f3f;
const int dx[4] = {-1, 1, 0, 0};
const int dy[4] = {0, 0, -1, 1}; // 四个方向分别为 上 下 左 右
const string ds = "UDLR"; // 四个方向对应字符

int n, m;
char mp[1005][1005];

struct point
{
    int x, y, d; // 坐标 (x, y) 方向 d
};
point s, t; // 起点 与 终点
queue<point> q;

bool vis[1005][1005][4];
// vis[i][j][k] 表示
//     到达单元格 (i, j) 且进入该单元格的方向为 k 的这一状态
// 是否已经搜索过
point pre[1005][1005][4];
// pre[i][j][k] 表示在取到
//     到达单元格 (i, j) 且进入该单元格的方向为 k 的这一状态所需要的最少步数
// 时,上一步的状态

int flag = -1;
// 是否存在方案走到终点
// 如果存在,则表示到达终点时最后一步的朝向

// 尝试从 u 对应的状态出发,朝着 d 方向走一格
// 判断下一个状态是否能接着搜索
// 如果能,则下一状态进队
void move(point &u, int d)
{
    point v; // 下一状态
    v.x = u.x + dx[d];
    v.y = u.y + dy[d];
    v.d = d;
    
    if(v.x < 1 || v.y < 1 || v.x > n || v.y > m) // 越界
        return;
    if(mp[v.x][v.y] == '#') // 不能走
        return;
    if(vis[v.x][v.y][v.d]) // 已搜过该状态
        return;
    
    vis[v.x][v.y][v.d] = true;
    pre[v.x][v.y][v.d] = u; // 记录上一步的状态
    q.push(v);
    
    if(v.x == t.x && v.y == t.y) // 搜到终点
        flag = d;
}

void solve()
{
    cin >> n >> m;
    for(int i = 1; i <= n; i++)
        for(int j = 1; j <= m; j++)
        {
            cin >> mp[i][j];
            if(mp[i][j] == 'S')
            {
                s.x = i;
                s.y = j;
            }
            else if(mp[i][j] == 'G')
            {
                t.x = i;
                t.y = j;
            }
        }
    
    for(int i = 0; i < 4; i++) // 枚举起点方向
    {
        s.d = i;
        vis[s.x][s.y][s.d] = true;
        q.push(s);
    }
    
    while(!q.empty())
    {
        point u = q.front();
        q.pop();
        
        if(mp[u.x][u.y] == '.' || mp[u.x][u.y] == 'S') // 这一步可以往任意方向移动
        {
            for(int i = 0; i < 4; i++)
                move(u, i);
        }
        else if(mp[u.x][u.y] == 'o') // 这一步的方向必须与上一步相同
        {
            move(u, u.d);
        }
        else if(mp[u.x][u.y] == 'x') // 这一步的方向必须与上一步不同
        {
            for(int i = 0; i < 4; i++)
                if(i != u.d)
                    move(u, i);
        }
        
        if(flag != -1) // 只要存在一种方案能走到终点,即可结束搜索
            break;
    }
    
    if(flag == -1)
    {
        cout << "No";
        return;
    }
    cout << "Yes\n";
    
    stack<char> sk; // 用栈存每一步的走法,方便倒着输出
    
    point p = {t.x, t.y, flag}; // 最后一步的状态
    while(p.x != s.x || p.y != s.y) // 只要还没走回起点,就继续向前回溯
    {
        sk.push(ds[p.d]);
        p = pre[p.x][p.y][p.d];
    }
    
    while(!sk.empty())
    {
        cout << sk.top();
        sk.pop();
    }
}

E - Team Division

  • 预估难度:普及+/提高
  • 标签:枚举、差分、前缀和、组合数学

题意

\(N\) 名玩家(玩家 \(1\)、玩家 \(2\)\(\ldots\)、玩家 \(N\))分成两个(可区分的)队伍 \(A\)\(B\),且满足以下所有条件。

  • 每个队伍至少包含一名玩家。
  • 每名玩家要么属于队伍 \(A\),要么属于队伍 \(B\),但不能同时属于两个队伍。
  • 玩家 \(i\) 所属队伍的玩家人数至少为 \(L_i\),至多为 \(R_i\)

求满足条件的分组方法数,并输出该数量对 \(998244353\) 取模后的结果。
如果存在一名玩家在两种分组方案中所属的队伍不同,则认为这两种分组方案是不同的。

思路

我们可以考虑枚举其中一队的分配人数,然后计算有多少种符合条件的方案。

假设枚举 A 队的最终总人数为 \(i\) 人,那么 B 队就要有 \(N-i\) 人。

当每个队伍的总人数确定后,我们可以再对于所有玩家,统计分别有多少名可以分配到此时的 A 队与 B 队内,分别记作 \(cnt_A\)\(cnt_B\)。同时,如果有玩家既能分配到 A 队,也能分配到 B 队,这些玩家就是可以自由分配的,人数记作 \(cnt_C\)

但需要注意,如果存在某名玩家无法分配到任意一队,则说明不存在任何分配方案。

由于 \(cnt_A\)\(cnt_B\) 中包括了能自由分配的 \(cnt_C\) 名玩家,因此还可以发现共有 \(cnt_A - cnt_C\) 名玩家是只能分配到 A 队里的,有 \(cnt_B - cnt_C\) 名玩家是只能分配到 B 队里的。

受这部分无法自由分配的玩家的限制,此时如果 A 队总人数满足 \(i \lt cnt_A - cnt_C\),或是 B 队总人数满足 \(N-i \lt cnt_B - cnt_C\) 的话,已有的人数已超过需要的人数,此时也是不存在任何分配方案的。

排除非法情况之后,我们让必须分配到 A 队和必须分配到 B 队的人先进入对应组,然后考虑每组还需要的人数。

此时 A 队还需要 \(i - (cnt_A - cnt_C)\) 人,B 队还需要 \((N-i) - (cnt_B - cnt_C)\) 人,能自由分配的人数共 \(cnt_C\) 人,因此可行的方案总数为:

\[C_{cnt_C}^{i - (cnt_A - cnt_C)} = C_{cnt_C}^{(N-i) - (cnt_B - cnt_C)} \]

但上面的方法需要 \(O(N)\) 枚举两组人数,再 \(O(N)\) 统计符合条件的玩家数量,这样的时间复杂度是 \(O(N^2)\) 的,不可接受。

考虑将要统计的三个值改为数组:

  • \(cnt_A[i]\) 记录当 A 队人数为 \(i\) 时,能分配到 A 队里的人数。
  • \(cnt_B[i]\) 记录当 A 队人数为 \(i\) 时,能分配到 B 队里的人数。
  • \(cnt_C[i]\) 记录当 A 队人数为 \(i\) 时,能任意分配的人数。

然后考虑每名玩家对这三个数组的贡献。

当一名玩家所在队伍允许人数在 \([L_i, R_i]\) 范围内时:

  • 如果要将其分配到 A 队内,此时 A 队的人数范围就是 \([L_i, R_i]\),可以让 \(cnt_A\) 数组内的这段区间均 \(+1\),表示该玩家符合条件。
  • 如果要将其分配到 B 队内,此时 B 队的人数范围就是 \([L_i, R_i]\),对应的 A 队的人数范围应是 \([N-R_i, N-L_i]\),同样让 \(cnt_B\) 数组的这段区间均 \(+1\)
  • 如果该玩家可以任意分配,那么 A 队的人数应当是上述两种情况的合法区间取交集,即 \([\max(L_i, N-R_i), \min(R_i, N-L_i)]\),让 \(cnt_C\) 数组的这段区间均 \(+1\),需注意区间不存在的情况。

既然每位玩家对三个数组的贡献位置均是连续区间,那么区间修改就可以借助差分+前缀和来快速维护。

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

代码

typedef long long ll;

const ll mod = 998244353;

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;
}

ll fac[200005], inv[200005];

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;
}

ll getC(int n, int m)
{
    return fac[n] * inv[m] % mod * inv[n - m] % mod;
}

int n, a[200005], b[200005], c[200005];
// a[i] 表示当 A 队人数为 i 时,可以分配到 A 队的人数
// b[i] 表示当 A 队人数为 i 时,可以分配到 B 队的人数
// c[i] 表示当 A 队人数为 i 时,可以任意分配的人数

void solve()
{
    cin >> n;
    init(n);
    
    for(int i = 1; i <= n; i++)
    {
        int l, r;
        cin >> l >> r;
        
        // A 队人数在 [l, r] 范围内时,可以分配到 A 队
        a[l]++;
        a[r + 1]--;
        
        // A 队人数在 [n-r, n-l] 范围内时,可以分配到 B 队
        b[n - r]++;
        b[n - l + 1]--;
        
        // 取上面两段区间交集,此时可以任意分配
        int L = max(l, n - r);
        int R = min(r, n - l);
        if(L > R)
            continue;
        c[L]++;
        c[R + 1]--;
    }
    
    // 求前缀和
    for(int i = 1; i <= n; i++)
    {
        a[i] += a[i - 1];
        b[i] += b[i - 1];
        c[i] += c[i - 1];
    }
    
    ll ans = 0;
    for(int i = 1; i < n; i++)
    {
        // 假设 A 队需要 i 人,B 队则需要 n-i 人
        
        int aa = a[i] - c[i]; // 必须在 A 队的人数
        int bb = b[i] - c[i]; // 必须在 B 队的人数
        
        if(n - aa - bb - c[i] != 0) // 说明有人无法分配
            continue;
        if(aa > i || bb > n - i) // 必须分配的人数超出某组限制
            continue;
        
        int need = i - aa; // 需要让多少可任意分配的人来 A 队
        ans = (ans + getC(c[i], need)) % mod;
    }
    cout << ans;
}
posted @ 2026-04-11 22:08  StelaYuri  阅读(77)  评论(0)    收藏  举报