AtCoder Beginner Contest 417 ABCDEF 题目解析

A - A Substring

题意

给定一个长度为 \(N\) 的字符串 \(S\),去掉其中 \(A\) 个前面的字符和 \(B\) 个后面的字符,输出剩余的字符串。

代码

int n, a, b;
char s[105];

int main()
{
    cin >> n >> a >> b;
    cin >> s;
    
    for(int i = a; i < n - b; i++)
        cout << s[i];
    
    return 0;
}

B - Search and Delete

题意

给定一个长度为 \(N\) 的非递减序列 \(A\)

\(M\) 次操作,第 \(i\) 次操作请从序列 \(A\) 中删除一个 \(B_i\)。如果不存在,则跳过这一次操作。

问做完 \(M\) 次操作之后的序列 \(A\)

思路一

因为序列 \(A\) 是非递减的,所以删除数字的顺序可以不用管,我们只需要知道每种数字分别被删除了多少次即可。

可以采用双指针的方法找出每种数字 \(X\) 原本出现了多少次,然后再去 \(B\) 数组里数出现了多少次,数量相减后,按照剩余数量输出该数字 \(X\) 即可。

代码一

int n, m;
int a[105], b[105];

int main()
{
    cin >> n >> m;
    for(int i = 1; i <= n; i++)
        cin >> a[i];
    for(int i = 1; i <= m; i++)
        cin >> b[i];
    
    for(int i = 1; i <= n; i++)
    {
        int j = i; // j 表示与 a[i] 相同的最后一个数字所在位置
        while(j + 1 <= n && a[j + 1] == a[i])
            j++;
        
        int cnt = j - i + 1; // a[i] 数字出现了 j-i+1 次
        i = j; // 直接移动 i,表示跳过 a[i] 这一种数字
        
        for(int k = 1; k <= m; k++)
            if(b[k] == a[i]) // 需要删除一次 a[i]
                cnt--;
        
        for(int k = 1; k <= cnt; k++) // 根据剩余数量输出 a[i] 即可
            cout << a[i] << " ";
        
    }
    
    return 0;
}

思路二

直接采用 STL 的 map 容器统计每种数字剩余出现次数即可。

代码二

int main()
{
    int n, m;
    cin >> n >> m;
    
    map<int, int> mp;
    for(int i = 1; i <= n; i++)
    {
        int x;
        cin >> x;
        mp[x]++;
    }
    for(int i = 1; i <= m; i++)
    {
        int x;
        cin >> x;
        mp[x]--;
    }
    
    for(auto it : mp)
    {
        for(int i = 1; i <= it.second; i++)
            cout << it.first << " ";
    }
    
    return 0;
}

C - Distance Indicators

题意

给定一个长度为 \(N\) 的整数序列 \(A\)

问有多少对 \((i, j)\) 满足 \(1 \le i \lt j \le N\)\(j-i = A_i + A_j\)

思路

\[\begin{aligned} j - i &= A_i + A_j \\ j - A_j &= i + A_i \end{aligned} \]

即对于每个位置 \(j\),计算下标与值的差值 \(j - A_j\),然后找在此之前有多少个位置 \(i\) 满足 \(i + A_i\) 与该差值相等即可。

由于 \(A_i\) 较小,\(i+A_i \le 4\times 10^5\),可以采用计数数组统计。

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

代码

int n, a[200005];
int cnt[400005]; // 统计 i+a[i] 出现的次数
long long ans = 0;

int main()
{
    cin >> n;
    for(int i = 1; i <= n; i++)
    {
        cin >> a[i];
        if(i - a[i] > 0) // 以当前 i 当作式子内的 j
            ans += cnt[i - a[i]];
        cnt[i + a[i]]++;
    }
    cout << ans;
    
    return 0;
}

D - Takahashi's Expectation

题意

高桥会按顺序收到 \(N\) 件礼物,每件礼物有三个属性 \(P_i, A_i, B_i\)

当他收到第 \(i\) 件礼物时,如果他当前的心情 \(\ge P_i\),那么他的心情会增加 \(A_i\);如果他当前的心情 \(\lt P_i\),那么他的心情会减少 \(B_i\)(如果减少后心情 \(\lt 0\),则心情会变成 \(0\))。

\(Q\) 个问题,每个问题给定一个 \(X_i\),表示当高桥一开始的心情为 \(X_i\) 时,最终心情是多少?

思路

直接模拟的复杂度是 \(O(Q\cdot N)\),会超时。

但注意到 \(1 \le P_i, A_i, B_i \le 500\) 这一条件,也就是说如果心情 \(\gt 500\),那么心情一定是会一直减少的。

因此当初始心情 \(\gt 500\) 时,我们可以考虑借助对 \(B\) 数组进行前缀和+二分来快速找出从哪一轮开始会让心情变为 \(\le 500\),然后再从那一轮开始往后推即可。

但每次都往后推到第 \(N\) 项显然是不现实的,所以可以考虑预处理。

f[i][j] 表示在收到第 \(i\) 件礼物之前,且高桥的心情为 \(j\) 时,最终他的心情是多少。

考虑转移:

  • 如果 p[i] >= j,此时收到礼物后高桥会高兴,心情变为 j + a[i],最终答案等同于 f[i + 1][j + a[i]]
  • 如果 p[i] < j,此时收到礼物后高桥会不高兴,心情变为 max(0, j - b[i]),最终答案等同于 f[i + 1][max(0, j - b[i])]

所以我们只需要考虑倒推每一轮的结果即可。

考虑初始条件,即超出第 \(n\) 轮后不会再导致心情发生变动,因此 dp[n + 1][j] = j

然后过程中 j + a[i] 这一步可能会超出 \(500\) 的范围,所以第二维推荐开 \(1000\) 进行处理。

总时间复杂度 \(O(N \cdot D + Q \log N)\),其中 \(D = 1000\)

代码

int n;
int p[10005], a[10005], b[10005];
int s[10005]; // s[i] 表示如果前 i 件物品全导致心情下降,下降幅度是多少

int f[10005][1005];
// f[i][j] 表示从第 i 轮开始,且第 i 轮的心情为 j 时,最终心情是多少

int main()
{
    cin >> n;
    for(int i = 1; i <= n; i++)
    {
        cin >> p[i] >> a[i] >> b[i];
        s[i] = s[i - 1] + b[i];
    }
    
    for(int j = 0; j <= 1000; j++)
        f[n + 1][j] = j; // 超出 n 轮则心情固定
    
    for(int i = n; i >= 1; i--)
        for(int j = 0; j <= 1000; j++)
        {
            if(p[i] >= j)
                f[i][j] = f[i + 1][j + a[i]];
            else
                f[i][j] = f[i + 1][max(0, j - b[i])];
        }
    
    int q;
    cin >> q;
    while(q--)
    {
        int x;
        cin >> x;
        if(x <= 1000)
            cout << f[1][x] << "\n";
        else
        {
            // 二分找从哪一轮开始可以让心情 <= 1000
            int l = 1, r = n, pos = n+1;
            while(l <= r)
            {
                int mid = (l + r) / 2;
                if(x - s[mid - 1] <= 1000)
                {
                    pos = mid;
                    r = mid - 1;
                }
                else
                    l = mid + 1;
            }
            if(pos > n) // 如果 n 轮结束后心情仍然超过 1000
                cout << x - s[n] << "\n"; // 直接减去 n 轮的 b 数组总和
            else // 否则,从第 pos 轮开始,初始心情减去前 pos-1 轮的 b 数组总和即可
                cout << f[pos][x - s[pos - 1]] << "\n";
        }
    }
    
    return 0;
}

E - A Path in A Dictionary

题意

给定一张连通的简单无向图 \(G\),包含 \(N\) 个点与 \(M\) 条边。

请找出图中字典序最小的一条从 \(X\)\(Y\)简单路径

思路

我们在使用深搜求解排列/组合问题时,可以根据每一步枚举的顺序来控制找出的答案的字典序,为了让最终答案字典序最小,只需要在过程中每一步都从小到大枚举即可。

本题可以借助深度优先搜索的性质,在存储完邻接表之后,为每个点的邻接表从小到大排个顺序,保证从某个点出发,一定是先搜索编号更小的相邻点,再去搜索编号较大的点,即可保证答案的字典序。

至于搜索过程,由于图是保证连通的,因此只要一个点此前已经被搜索过,但若没找到答案的话,那么这个点接下来便不会再被搜到,开个计数数组记录每个点的搜索情况即可。

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

代码

int n, m, x, y;

vector<int> G[1005];
bool vis[1005];
int ans[1005];

bool dfs(int u, int step)
{
    ans[step] = u;
    vis[u] = true;
    
    if(u == y)
    {
        for(int i = 1; i <= step; i++)
            cout << ans[i] << " ";
        cout << "\n";
        return true;
    }
    
    for(int &v : G[u])
    {
        if(vis[v])
            continue;
        if(dfs(v, step + 1))
            return true;
    }
    return false;
}

void solve()
{
    cin >> n >> m >> x >> y;
    
    for(int i = 1; i <= n; i++)
    {
        G[i].clear();
        vis[i] = false;
    }
    
    for(int i = 1; i <= m; i++)
    {
        int u, v;
        cin >> u >> v;
        G[u].push_back(v);
        G[v].push_back(u);
    }
    
    for(int i = 1; i <= n; i++)
        sort(G[i].begin(), G[i].end());
    
    dfs(x, 1);
}

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

F - Random Gathering

题意

\(N\) 个盘子排成一排,从左到右分别编号为 \(1, 2, \dots, N\)。一开始,编号为 \(i\) 的盘子内有 \(A_i\) 块石头。

\(M\) 次操作,第 \(i\) 次操作给定两个正整数 \(L_i, R_i\)

  • 移除编号在 \([L_i, R_i]\) 范围内的所有盘子中的石头。
  • 随机选择一个在 \([L_i, R_i]\) 范围内的整数 \(x\)
  • 将所有第一步中移除的石头全部放到编号为 \(x\) 的盘子内。

对于 \(i = 1, 2, \dots, N\) 的每一个整数 \(i\),请求出最终第 \(i\) 个盘子内的石头数量期望值,对 \(998244353\) 取模。

思路

考虑操作,由于位置 \(x\) 是随机选择的,所以相当于每次选定一个区间 \([L, R]\),然后这个区间内每个位置的数字有 \(\dfrac{R-L}{R-L+1}\) 的概率分不到任何一个石头,有 \(\dfrac {1} {R-L+1}\) 的概率能分到区间内所有石头的总和。

记总和为 \(S\),则区间内每个位置在操作后的期望值均为 \(\dfrac{R-L}{R-L+1} \times 0 + \dfrac{1}{R-L+1} \times S = \dfrac{S}{R-L+1}\)

所以需要有一个数据结构能够快速求出当前的区间总和,并快速将区间赋值为一个新值。考虑带懒惰标记的线段树即可。

时间复杂度 \(O(N\log 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;
}

#define ls (p << 1)
#define rs (p << 1 | 1)

struct node
{
    int l, r;
    ll sum;
    ll lazyVal;
    bool lazyTag;
};

node tr[200005 << 2];
int A[200005];

void push_up(int p)
{
    tr[p].sum = (tr[ls].sum + tr[rs].sum) % mod;
}

void push_down(int p)
{
    if(!tr[p].lazyTag)
        return;
    
    tr[ls].lazyTag = true;
    tr[ls].lazyVal = tr[p].lazyVal;
    tr[ls].sum = (tr[ls].r - tr[ls].l + 1) * tr[p].lazyVal % mod;
    
    tr[rs].lazyTag = true;
    tr[rs].lazyVal = tr[p].lazyVal;
    tr[rs].sum = (tr[rs].r - tr[rs].l + 1) * tr[p].lazyVal % mod;
    
    tr[p].lazyTag = false;
    tr[p].lazyVal = 0;
}

void build(int l, int r, int p = 1)
{
    tr[p].l = l;
    tr[p].r = r;
    tr[p].lazyVal = 0;
    tr[p].lazyTag = false;
    if(l == r)
    {
        tr[p].sum = A[l];
        return;
    }
    int mid = (l + r) / 2;
    build(l, mid, ls);
    build(mid+1, r, rs);
    push_up(p);
}

void update(int l, int r, ll val, int p = 1)
{
    if(l <= tr[p].l && tr[p].r <= r)
    {
        tr[p].lazyTag = true;
        tr[p].lazyVal = val;
        tr[p].sum = (tr[p].r - tr[p].l + 1) * val % mod;
        return;
    }
    push_down(p);
    if(l <= tr[ls].r)
        update(l, r, val, ls);
    if(r >= tr[rs].l)
        update(l, r, val, rs);
    push_up(p);
}

ll query(int l, int r, int p = 1)
{
    if(l <= tr[p].l && tr[p].r <= r)
        return tr[p].sum;
    push_down(p);
    ll sum = 0;
    if(l <= tr[ls].r)
        sum += query(l, r, ls);
    if(r >= tr[rs].l)
        sum += query(l, r, rs);
    return sum % mod;
}

int main()
{
    int n, m;
    cin >> n >> m;
    for(int i = 1; i <= n; i++)
        cin >> A[i];
    
    build(1, n);
    
    while(m--)
    {
        int l, r;
        cin >> l >> r;
        ll v = query(l, r) * qpow(r - l + 1, mod - 2) % mod;
        update(l, r, v);
    }
    
    for(int i = 1; i <= n; i++)
        cout << query(i, i) << " ";
    
    return 0;
}
posted @ 2025-08-02 22:15  StelaYuri  阅读(63)  评论(0)    收藏  举报