AtCoder Beginner Contest 415 ABCDEF 题目解析

A - Unsupported Type

题意

给定一个正整数数组以及一个正整数 \(X\),判断正整数 \(X\) 是否包含在数组内。

思路

语言基础一维数组题,因为 \(X\) 是后输入的,所以一定要先用数组存储输入的每个数字。

但因为数字大小也只有最大 \(100\),所以也可以用计数思想。

代码一

int a[105];

void solve()
{
    int n;
    cin >> n;
    for(int i = 1; i <= n; i++)
        cin >> a[i];
    int x;
    cin >> x;
    for(int i = 1; i <= n; i++)
    {
        if(a[i] == x)
        {
            cout << "Yes\n";
            return;
        }
    }
    cout << "No\n";
}

代码二

bool a[105]; // a[i] 表示 i 是否出现过

void solve()
{
    int n;
    cin >> n;
    for(int i = 1; i <= n; i++)
    {
        int d;
        cin >> d;
        a[d] = true;
    }
    int x;
    cin >> x;
    if(a[x])
        cout << "Yes\n";
    else
        cout << "No\n";
}

B - Pick Two

题意

有一个仓库,共有 \(|S|\) 个分区,分别编号为 \(1, 2, \dots, |S|\),每个分区可能有包裹,也可能没有。

给定一个字符串 \(S\),仅由 .# 组成,且 # 的数量为偶数个。

当字符串第 \(i\) 个字符 \(S_i\)# 时,表示第 \(i\) 个分区存在一个包裹。

现有一个机器人,会重复执行以下操作,直到仓库中不存在任何一个包裹为止:

  • 从有包裹存在的分区中找出两个编号最小的分区,从中取出包裹,并输出这次所取的两个分区的编号。

思路

按顺序遍历整个字符串,可以借助容器先把所有 # 所在位置全部找出来,然后两两相邻位置分组一起输出。

或者可以直接借助一个变量 pre 存储每组靠前的那个有包裹的位置编号,记 pre = 0 表示还没找到包裹。

每当遇到一个 #,如果 pre = 0,就把当前位置存在 pre 变量内;如果 pre != 0,则将 pre 与当前位置一起输出即可,表示一组两个位置都找到了,记得输出完成后清空 pre = 0

代码

char s[1005];

void solve()
{
    cin >> (s + 1);
    int n = strlen(s + 1);
    
    int pre = 0; // 上一个找到的 # 位置
    for(int i = 1; i <= n; i++)
    {
        if(s[i] == '#')
        {
            if(pre == 0) // 此前还没找到
                pre = i;
            else
            {
                cout << pre << "," << i << "\n";
                pre = 0; // 清空 pre
            }
        }
    }
}

C - Mixture

题意

\(N\) 种化学物质,分别编号为 \(1, 2, \dots, N\)。你的目标是把所有种类的化学物质全部混合到一起。

一开始你只有一个空瓶子,然后每次你可以任意选择一种化学物质,将其添加进瓶子内。

我们用一个整数 \(i\) 的二进制来描述当前瓶子中存在的化学物质的状态。当 \(i\) 的二进制从右往左数第 \(k\) 位为 \(1\) 时,则说明当前瓶子内存在第 \(k\) 种化学物质。

  • 例如,\((13)_{10} = (1101)_2\),二进制从右往左数的第 \(1, 3, 4\) 位为 \(1\),因此 \(13\) 这个数字可以用来表示同时存在第 \(1, 3, 4\) 这三种化学物质时的瓶子状态。

给定一个长度为 \(2^N-1\) 的字符串 \(S\),其中第 \(i\) 个字符 \(S_i\) 表示整数 \(i\) 所代表的瓶子状态是否安全。如果 \(S_i = 1\) 则说明整数 \(i\) 代表的状态是危险的,我们在操作过程中一定要避免在某一时刻出现整数 \(i\) 所代表的这一状态。

现在请问是否存在一种方案,使得我们能够在保证安全的前提下,将所有化学物质混合到一起?

思路

无妨先将化学物质的编号改为 \(0 \sim N-1\),将二进制最低位表示为 \(2^0\) 位,最高位即 \(2^{N-1}\) 位。

当我们考虑某一种状态 \(i\) 是否能够安全得到时,只需要去在意最后一种被放进瓶子的化学物质是哪一种即可。也就是说我们可以一一考虑状态 \(i\) 中所包含的每一种化学物质,然后把这种化学物质先去掉,检查下此前的状态是否能够安全得到。只要存在一种方案能够让我们安全得到,那么再往此前这个状态中加入对应的被去除的物质,当前状态 \(i\) 也就可以安全得到了。

当然,\(S_i = 1\) 时直接判定为不可能即可。

考虑答案的转移关系,明显上述方法在求解状态 \(i\) 的答案时,二进制位数比 \(i\) 要少的那些状态的答案应该要先得到,所以我们可以从小到大枚举一遍每一种状态,求出前面的答案后再去推导更后面的答案即可,即状态压缩DP。

当然,本题也可以采用递归/深搜求解,注意加上记忆化即可。

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

代码(状压DP)

int n;
char s[263000];
bool dp[263000]; // dp[i] 表示 i 这个状态是否能够安全获得

void solve()
{
    cin >> n;
    cin >> (s + 1);
    
    dp[0] = true; // 空瓶子可以安全获得
    for(int i = 1; i < (1 << n); i++)
    {
        dp[i] = false; // 假设当前状态不能安全获得
        if(s[i] == '1') // 危险状态,直接跳过
            continue;
        for(int j = 0; j < n; j++) // 枚举每一种化学物质
            if(i >> j & 1) // 如果 i 的 2^j 这一位上为 1,表示 i 中存在第 j 种物质
                dp[i] |= dp[i ^ (1 << j)]; // 如果在加入第 j 种物质之前的状态可以安全得到,那么当前状态 i 也就可以安全得到
    }
    
    if(dp[(1 << n) - 1] == true) // 检查所有物质均存在的状态是否能够安全得到
        cout << "Yes\n";
    else
        cout << "No\n";
}
int main()
{
    int T;
    cin >> T;
    while(T--)
        solve();
    return 0;
}

代码(记忆化搜索)

int n;
char s[263000];

bool vis[263000]; // vis[i] 表示 i 这个状态是否已经搜索过
bool ans[263000]; // ans[i] 表示 i 这个状态是否能够安全得到(vis[i] = true 时才有用)

bool flag = false; // 答案

// 检查 sta 所代表的状态是否能够安全得到
bool dfs(int sta)
{
    if(sta == 0) // 空瓶子可以得到
        return true;
    if(vis[sta] == true) // 记忆化
        return ans[sta];
    if(s[sta] == '1') // 危险状态
        return false;
    
    vis[sta] = true;
    for(int i = 0; i < n; i++) // 枚举每一种化学物质
        if(sta >> i & 1) // 表示存在 i 这种物质
        {
            if(dfs(sta ^ (1 << i))) // 如果去掉 i 这种物质后的状态也能安全得到
            {
                ans[sta] = true;
                return true;
            }
        }
    return false;
}

void solve()
{
    cin >> n;
    cin >> (s + 1);
    
    for(int i = 1; i < (1 << n); i++)
        vis[i] = ans[i] = false; // 清空
    
    if(dfs((1 << n) - 1))
        cout << "Yes\n";
    else
        cout << "No\n";
}
int main()
{
    int T;
    cin >> T;
    while(T--)
        solve();
    return 0;
}

D - Get Many Stickers

题意

一开始高桥拥有 \(N\) 瓶可乐。

有一个可乐商店,高桥可以在这里进行 \(M\) 种交易。第 \(i\) 种交易的内容是可以把 \(A_i\) 个空瓶子交给商店,然后商店会返还 \(B_i\) 瓶新可乐以及一张贴纸。

高桥可以随意喝可乐,喝完的可乐将会变成空瓶子,并且也可以随意在可乐商店内选择任意一种交易方法去进行交易。

高桥的任务换取最多数量的贴纸。

请问他最多可以换到多少张贴纸?

思路

首先发现新的可乐没什么用,不如让高桥只要有新可乐就直接喝掉,将其变成空瓶子。接下来我们就直接考虑高桥拥有的空瓶子数量即可。

换最多数量的贴纸也就意味着交易次数越多越好。为了保证交易次数最多,所以我们肯定是希望高桥拥有的可乐瓶数量减少地慢一些。

由于第 \(i\) 种交易是拿 \(A_i\) 个瓶子换 \(B_i\) 个瓶子加一张贴纸,也就是说完成第 \(i\) 种交易之后手上的可乐瓶数量会减少 \(A_i - B_i\) 个。

贪心可得,我们只需要按照所有交易的 \(A_i - B_i\) 的值从小到大排序,优先去选择那些减少数量较少的交易来进行即可。

然后就是模拟的过程,注意到数据范围,我们肯定不能一次一次直接模拟交易,应当借助数学方法直接处理某种交易最多可以进行几次。

假如说当前高桥拥有 \(N\) 个瓶子,由于交易的前提就是 \(N \ge A_i\),所以我们只能考虑多出 \(A_i\) 的这一部分瓶子,即 \(N - A_i\) 个。

只要 \(N \ge A_i\) 就一定能至少进行一次交易,并且每多 \(A_i - B_i\) 个瓶子就可以再交易一次,因此高桥在某种交易中最多可以进行 \(\lfloor \dfrac{N - A_i}{A_i - B_i}\rfloor + 1\) 次。

按顺序处理每种交易,直接计算交易的最大进行次数以及交易完成后的剩余瓶子数量即可。

时间复杂度 \(O(M\log M)\)

代码

typedef long long ll;

ll n;
int m;
struct cola
{
    ll a, b;
    bool operator < (const cola &c) const
    {
        return a - b < c.a - c.b; // 按照交换后减少的可乐瓶数 从小到大排序
    }
};
cola r[200005];

void solve()
{
    cin >> n >> m;
    for(int i = 1; i <= m; i++)
        cin >> r[i].a >> r[i].b;
    sort(r + 1, r + m + 1);
    ll ans = 0;
    for(int i = 1; i <= m; i++)
    {
        if(n < r[i].a) // 不足以交换一次
            continue;
        ll d = (n - r[i].a) / (r[i].a - r[i].b) + 1; // 当前方案的最大交换次数
        ans += d;
        n -= d * (r[i].a - r[i].b);
    }
    cout << ans << "\n";
}

E - Hungry Takahashi

题意

有一个 \(H \times W\) 的网格,每个网格都有一定数量的硬币,坐标为 \((i, j)\) 的网格的硬币数量为 \(A_{i, j}\)

高桥一开始位于 \((1, 1)\) 的位置,并且拥有 \(x\) 枚硬币。在接下来 \(H+W-1\) 天内,每天会发生一些事件。第 \(k\) 天,以下事件会依次发生:

  • 高桥会捡起他所在的那个网格内的所有硬币。
  • 高桥很饿,他必须花费 \(P_k\) 枚硬币买食物。但如果当前他手头上拥有的硬币数量不足 \(P_k\) 枚,他会饿得直接倒下。
  • 如果 \(k \lt H+W-1\),高桥会选择往右或者往下走一步(如果那个方向的相邻网格存在的话),否则将无事发生。

请问高桥一开始应该至少拥有多少枚硬币,才能够至少存在一种方案,使其不会在接下来的 \(H+W-1\) 天内饿倒?换言之,请求出上文中 \(x\) 的最小值。

思路一

明显当一开始的硬币足够多时,他一定有办法不会饿到,而如果硬币数量少于某个数字,他将有可能在过程中的某一天出现买不起食物的情况。

也就是说,一开始的硬币数量满足二分性质,我们可以直接考虑二分答案,然后检查是否满足题意即可。

检查过程只需要按顺序从左上往右下考虑每个格子,记 dp[i][j] 表示走到 \((i, j)\) 所能剩余的最多硬币数量,如果无法到达则用 \(-1\) 表示。

每到一个格子,先捡起该格子内对应的硬币数量,然后根据格子坐标计算出走到这个格子的天数,判断是否买得起这一天的食物。

如果买得起,就将当前硬币数量减去花费,然后将剩余硬币数量转移给下一步的格子即可。如果买不起,直接跳过。

时间复杂度 \(O(H \cdot W\log V)\),其中 \(V\) 表示答案的最大值。

代码一(二分+DP)

int n, m;
vector<vector<int> > A;
vector<vector<ll> > dp; // dp[i][j] 表示走到 (i, j) 时能够剩余的最大硬币数量
int P[200005];

// 检查初始硬币数量为 coin 时是否存在合法方案
bool check(ll coin)
{
    dp = vector<vector<ll>>(n + 1, vector<ll>(m + 1, -1)); // 清空 dp 数组并置 -1
    
    dp[1][1] = coin;
    
    for(int i = 1; i <= n; i++)
    {
        for(int j = 1; j <= m; j++)
        {
            if(dp[i][j] == -1)
                continue;
            dp[i][j] += A[i][j]; // 捡起
            int k = i + j - 1; // 计算走到 (i, j) 应该是哪一天
            if(dp[i][j] < P[k]) // 买不起当天的食物
            {
                dp[i][j] = -1; // 置 -1 便于判断最后一个格子是否满足条件
                continue;
            }
            dp[i][j] -= P[k];
            // 向后转移当前剩余的最大硬币数量
            if(i + 1 <= n)
                dp[i + 1][j] = max(dp[i + 1][j], dp[i][j]);
            if(j + 1 <= m)
                dp[i][j + 1] = max(dp[i][j + 1], dp[i][j]);
        }
    }
    
    return dp[n][m] != -1;
}

void solve()
{
    cin >> n >> m;
    
    A = vector<vector<int>>(n + 1, vector<int>(m + 1));
    
    for(int i = 1; i <= n; i++)
        for(int j = 1; j <= m; j++)
            cin >> A[i][j];
    
    for(int i = 1; i <= n + m - 1; i++)
        cin >> P[i];
    
    ll l = 0, r = 2e14;
    while(l <= r)
    {
        ll mid = l + r >> 1;
        if(check(mid))
            r = mid - 1;
        else
            l = mid + 1;
    }
    cout << l << "\n";
}

思路二

既然想要求出一开始硬币数量的最小值,明显最后一天剩余的硬币数量越少越好,因此可以考虑从最后一天开始倒推回第一天。

首先,在走到最后一个格子时,我们至少需要保留买得起第 \(H+W-1\) 天的食物的硬币数量,又因为每个网格内都有一些硬币可以捡,可以计算出走到最后一个格子时我们至少需要携带的硬币数量为 max(0, P[H+W-1] - A[H][W])

然后从最后一个格子往前推。对于坐标 \((i, j)\) 的格子,我们目前知道的是下一天它所能移动到的两个格子所需要携带的硬币最小数量,明显我们肯定会挑要求携带硬币数量更少的那个格子走。对于当前格子,可知我们是在 \(i+j-1\) 这一天走到 \((i, j)\) 这个格子的,因此在此之前我们还需要 P[i+j-1] 个硬币用于购买当天的食物。但当前格子内又存在 A[i][j] 枚硬币可以捡,可以用于抵消携带的硬币数量。

因此可以得到状态转移方程为 dp[i][j] = P[i+j-1] - A[i][j] + 后一步要求携带的硬币数量最小值

注意硬币数量不能是负数,最后要和 \(0\) 取个最大值。

时间复杂度 \(O(H\cdot W)\)

代码二(倒推,DP)

int n, m;
vector<vector<int> > A;
vector<vector<ll> > dp; // dp[i][j] 表示走到 (i, j) 时至少应该携带的硬币数量
int P[200005];

void solve()
{
    cin >> n >> m;
    
    A = vector<vector<int>>(n + 1, vector<int>(m + 1));
    
    for(int i = 1; i <= n; i++)
        for(int j = 1; j <= m; j++)
            cin >> A[i][j];
    
    for(int i = 1; i <= n + m - 1; i++)
        cin >> P[i];
    
    dp = vector<vector<ll>>(n + 1, vector<ll>(m + 1, 0)); // 清空 dp 数组
    
    for(int i = n; i >= 1; i--)
        for(int j = m; j >= 1; j--)
            if(i == n && j == m)
                dp[i][j] = max(0, P[n+m-1] - A[n][m]);
            else
            {
                dp[i][j] = 1e18; // 取后一步要求的硬币数量较小值
                if(i + 1 <= n)
                    dp[i][j] = min(dp[i][j], dp[i+1][j]);
                if(j + 1 <= m)
                    dp[i][j] = min(dp[i][j], dp[i][j+1]);
                dp[i][j] = max(0LL, dp[i][j] + P[i+j-1] - A[i][j]); // 加上当前格子的贡献
            }
    
    cout << dp[1][1] << "\n";
}

F - Max Combo

题意

假如 \(t\) 是一个字符串,记 \(f(t)\) 表示 \(t\) 中出现的连续相同字符最大长度

现在有一个长度为 \(N\) 的字符串 \(S\),仅由小写英文字母组成,请按照以下顺序处理 \(Q\) 次询问:

  • 类型 \(1\):输入一个整数 \(i\) 和一个字符 \(x\),然后将字符串 \(S\) 的第 \(i\) 个字符改为 \(x\)
  • 类型 \(2\):输入两个整数 \(l, r\),记 \(t\) 表示 \(S\) 字符串的下标在 \([l, r]\) 范围内的子串,即 \(t = S_{[l, r]}\),并输出 \(f(t)\) 的值。

思路

这是一道有关线段树的经典类型题目。

操作涉及单点修改以及区间查询,考虑线段树,由于我们需要知道的是区间内部连续相同字符的最大长度,但我们不知道取到最大长度时,这些字符在区间内的位置是怎么样的,所以线段树需要重点维护三个值:

  • ansl,表示包含区间左端点字符的连续相同字符最长长度;
  • ansm,表示区间内部连续相同字符最长长度;
  • ansr,表示包含区间右端点字符的连续相同字符最长长度。

考虑答案上传过程,对于结点 \(root\),记 \(a, b\) 分别为该结点的左右儿子,首先可以发现以下转移方法:

  • root.ansm = max(a.ansm, b.ansm),表示父结点区间内部的连续相同字符最长长度可以从两个儿子的子区间内部取最大值得到。
  • root.ansl = a.ansl,因为此时父结点的左端点与左儿子的左端点相同,因此包含左端点字符的答案可以直接从左儿子得到。
  • root.ansr = b.ansr,同上。

考虑特殊情况,当左儿子的右端点字符与右儿子的左端点字符相同时,此时连续相同字符是可以跨过父结点的区间中点的,所以这种情况下还能得到以下转移方法:

  • root.ansm = max(root.ansm, a.ansr + b.ansl),表示父结点区间内部的连续相同字符最长长度还可以由包含左儿子右端点以及右儿子左端点的两端连续相同字符长度相加得到。
  • root.ansl = a.ansl + b.ansl,但需要先判断左儿子内部字符是否完全相同,如果是,则包含左端点字符的连续相同字符最长长度可以直接采用 左儿子区间长度 + 右儿子的 ansl 来得到。
  • root.ansr = a.ansr + b.ansr,但需要先判断右儿子内部字符是否完全相同,原理同上。

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

代码

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

struct node
{
    // 记录结点代表的区间
    int l, r;
    // 分别记录当前区间内:
    // 包含左端点字符的连续相同字符最长长度
    // 内部连续相同字符最长长度
    // 包含右端点字符的连续相同字符最长长度
    int ansl, ansm, ansr;
};

node tr[500005 << 2];
int n, q;
char s[500005];

node push_up(node a, node b) // 根据左右儿子答案 更新父结点答案
{
    node root;
    root.l = a.l;
    root.r = b.r;
    root.ansm = max(a.ansm, b.ansm);
    root.ansl = a.ansl;
    root.ansr = b.ansr;
    if(s[a.r] == s[b.l]) // 如果左儿子右端点与右儿子左端点字符相同,可以跨区间更新
    {
        root.ansm = max(root.ansm, a.ansr + b.ansl);
        if(a.ansl == a.r - a.l + 1) // 左儿子内部字符完全相同
            root.ansl = a.ansl + b.ansl;
        if(b.ansr == b.r - b.l + 1) // 右儿子内部字符完全相同
            root.ansr = a.ansr + b.ansr;
    }
    return root;
}

// 建树
void build(int l, int r, int p = 1)
{
    tr[p].l = l;
    tr[p].r = r;
    if(l == r)
    {
        tr[p].ansl = tr[p].ansm = tr[p].ansr = 1;
        return;
    }
    int mid = l + r >> 1;
    build(l, mid, ls);
    build(mid + 1, r, rs);
    tr[p] = push_up(tr[ls], tr[rs]); // 更新当前结点答案
}

// 单点更新
void update(int pos, int p = 1)
{
    if(tr[p].l == tr[p].r)
        return;
    if(pos <= tr[ls].r) // 更新位置在左儿子内部
        update(pos, ls);
    if(pos >= tr[rs].l) // 更新位置在右儿子内部
        update(pos, rs);
    tr[p] = push_up(tr[ls], tr[rs]); // 更新当前结点答案
}

node query(int l, int r, int p = 1)
{
    if(l <= tr[p].l && tr[p].r <= r)
        return tr[p];
    if(r <= tr[ls].r) // 整个询问都在左儿子内部
        return query(l, r, ls);
    if(l >= tr[rs].l) // 整个询问都在右儿子内部
        return query(l, r, rs);
    // 否则,询问既包含左儿子又包含右儿子
    node a = query(l, r, ls);
    node b = query(l, r, rs);
    return push_up(a, b);
}

void solve()
{
    cin >> n >> q;
    cin >> (s + 1);
    build(1, n);
    while(q--)
    {
        int op;
        cin >> op;
        if(op == 1)
        {
            int i;
            char c;
            cin >> i >> c;
            s[i] = c;
            update(i);
        }
        else
        {
            int l, r;
            cin >> l >> r;
            cout << query(l, r).ansm << "\n";
        }
    }
}
posted @ 2025-07-19 22:44  StelaYuri  阅读(131)  评论(0)    收藏  举报