AtCoder Beginner Contest 406 ABCDEF 题目解析

A - Not Acceptable

题意

给定四个整数 \(A, B, C, D\)

\(C\)\(D\) 分是否在 \(A\)\(B\) 分之前。

代码

void solve()
{
    int a, b, c, d;
    cin >> a >> b >> c >> d;
    if(c < a || c == a && d < b)
        cout << "Yes";
    else
        cout << "No";
}

B - Product Calculator

题意

给定 \(N, K\) 以及 \(N\) 个正整数。

一开始有一个计算器,上面显示数字 \(1\)

按顺序将 \(K\) 个整数依次乘到计算器中。

如果过程中得到的数字位数超过了 \(K\) 位,那么计算器上的数字将会变成数字 \(1\),否则能够正常得到乘法的结果。

问在所有数字全部乘进去之后,计算器上最终会显示什么数字?

思路

直接模拟乘法即可,但有一个点需要注意:把新数字乘到答案里这一步,根据数据范围,可能会出现两个 \(10^{18}\) 范围的数字相乘,这个答案是会超出 long long 的范围的。

解法一:乘法过程使用 __int128 类型,最后输出答案时强制转回 long long 类型。

解法二:在乘法前先用除法判断,也就是把 a * b <= c 写作 a <= c / b,但要注意判断整除关系。

  • 整除关系可以采取模运算判断,或者转为浮点除法计算。如果采用的是浮点运算,注意浮点误差,可能需要使用 long double 类型才能通过。

代码一 __int128

typedef long long ll;

void solve()
{
    int n, k;
    cin >> n >> k;
    
    __int128 mx = 1;
    for(int i = 1; i <= k; i++)
        mx *= 10;
    
    __int128 ans = 1;
    for(int i = 1; i <= n; i++)
    {
        ll d;
        cin >> d;
        if(ans * d >= mx)
            ans = 1;
        else
            ans *= d;
    }
    cout << (ll)ans;
}

代码二 浮点判断

typedef long long ll;

void solve()
{
    int n, k;
    cin >> n >> k;
    
    ll mx = 1;
    for(int i = 1; i <= k; i++)
        mx *= 10;
    
    ll ans = 1;
    for(int i = 1; i <= n; i++)
    {
        ll d;
        cin >> d;
        if(ans >= (long double)1.0 * mx / d)
            ans = 1;
        else
            ans *= d;
    }
    cout << (ll)ans;
}

代码三 模运算判断

typedef long long ll;

void solve()
{
    int n, k;
    cin >> n >> k;
    
    ll mx = 1;
    for(int i = 1; i <= k; i++)
        mx *= 10;
    
    ll ans = 1;
    for(int i = 1; i <= n; i++)
    {
        ll d;
        cin >> d;
        if(mx % d == 0 && ans >= mx / d || mx % d != 0 && ans >= mx / d + 1)
            ans = 1;
        else
            ans *= d;
    }
    cout << (ll)ans;
}

C - ~

题意

给定一个 \(N\) 的排列 \(P = (P_1, P_2, \ldots, P_N)\),问其中有多少个不同的连续子序列 \(A = (A_1, A_2, \ldots, A_{|A|})\) 满足以下条件:

  • 子序列 \(A\) 长度至少是 \(4\)
  • \(A_1 \lt A_2\)
  • 存在且仅存在一个位置 \(i\) \((1 \lt i \lt |A|)\) 满足 \(A_{i-1} \lt A_i \gt A_{i+1}\)
  • 存在且仅存在一个位置 \(i\) \((1 \lt i \lt |A|)\) 满足 \(A_{i-1} \gt A_i \lt A_{i+1}\)

思路

我们可以先找出在 \(P\)\(2 \sim N-1\) 以内有多少个位置满足上面的条件三或条件四,我们接下来把这些位置叫做“特殊位置”,将这些特殊位置先存起来,放在同一个数组内。

根据条件,在答案的连续子序列中必须只能够分别存在一个满足条件三的位置和一个满足条件四的位置,于是我们可以扫描一遍上面的数组中被存起来的每一个特殊位置。

只有在前后两个相邻的特殊位置所满足的条件不同时,我们才需要去思考这两个特殊位置对答案的贡献。

例如,现在我们假设 \(x\)\(y\) \((x \lt y)\) 这两个位置分别满足条件三或条件四中的一种,且这两个特殊位置之间没有其它特殊位置:

  • 首先,为了让 \(x\)\(y\) 能够作为特殊位置,他们两边的数字也必须要被包含到连续子序列中来,此时区间 \([x-1, y+1]\) 所形成的连续子序列便是一个满足除条件二以外的子序列。
  • 然后考虑是否能够向两边扩张这个区间。
  • 对于区间左端点,由于条件二的存在,我们必须要保证左端点小于它右边的数字。分类讨论左端点的情况:
    • 如果在 \(x-1\) 左侧存在其它特殊位置,我们记最近的特殊位置为 \(t\)。因为特殊位置需要包含旁边的两个数字,因此只要不把 \(t-1\) 包含到区间內部就不会打破条件三/四的限制。因此,此时左端点可以选择的情况数量为区间 \([t, x - 1]\) 内部满足 \(P_i \lt P_{i+1}\) 的位置 \(i\) 数量
    • 如果在 \(x-1\) 左侧不存在其它特殊位置,那么左端点可以选择的情况数量可以直接记作区间 \([1, x - 1]\) 内部满足 \(P_i \lt P_{i+1}\) 的位置 \(i\) 数量
  • 对于右端点则没有条件限制,但还是同上需要分类讨论:
    • 如果在 \(y+1\) 右侧存在其它特殊位置,我们记最近的特殊位置为 \(t\)。同理,只要不要选到 \(t+1\) 这个位置就不会打破条件三/四的限制。因此右端点可以选择区间 \([y+1, t]\) 内的任意一个位置,方案数为 \(t - y\)
    • 如果在 \(y+1\) 右侧不存在其它特殊位置,那么右端点便可以选择区间 \([y+1, n]\) 内的任意一个位置,方案数为 \(n - y\)

最后,对于找一段区间内部有多少个位置满足 \(P_i \lt P_{i+1}\) 的这一步骤,可以采用数组套二分的方式,或者前缀和均可。

代码一 二分

typedef long long ll;

int n;
int p[300005];

struct node
{
    int pos, typ; // 位置、满足的条件类型 1/2
};
    
vector<node> V; // 存储所有满足条件三/四的位置
vector<int> G; // 存储所有 p[i] < p[i+1] 的位置 i

// 求区间 [l, r] 内部有多少个位置 i 满足 p[i] < p[i+1]
int cal(int l, int r)
{
    if(l > r)
        return 0;
    return upper_bound(G.begin(), G.end(), r) - lower_bound(G.begin(), G.end(), l);
}

void solve()
{
    cin >> n;
    for(int i = 1; i <= n; i++)
        cin >> p[i];
    
    for(int i = 2; i < n; i++)
    {
        if(p[i] > p[i - 1] && p[i] > p[i + 1]) // 满足条件三
            V.push_back(node{i, 1});
        if(p[i] < p[i - 1] && p[i] < p[i + 1]) // 满足条件四
            V.push_back(node{i, 2});
    }
    
    for(int i = 1; i < n; i++)
        if(p[i] < p[i + 1])
            G.push_back(i);
    
    ll ans = 0;
    for(int i = 1; i < V.size(); i++) // 枚举每一对相邻的特殊位置 V[i-1] 和 V[i]
    {
        if(V[i].typ != V[i - 1].typ) // 只要前后两个特殊位置类型不同
        {
            int a, b;
            // a b 分别表示左端点的情况数量和右端点的情况数量
            if(i - 2 >= 0) // 左侧还有其它特殊位置
                a = cal(V[i - 2].pos, V[i - 1].pos - 1);
            else
                a = cal(1, V[i - 1].pos - 1);
            if(i + 1 < V.size()) // 右侧还有其他特殊位置
                b = V[i + 1].pos - V[i].pos;
            else
                b = n - V[i].pos;
            ans += 1LL * a * b; // 此时的答案即左右两边方案数之乘积
        }
    }
    cout << ans;
}

代码二 前缀和

typedef long long ll;

int n;
int p[300005];

struct node
{
    int pos, typ; // 位置、满足的条件类型 1/2
};
    
vector<node> V; // 存储所有满足条件三/四的位置
int sum[300005];

// 求区间 [l, r] 内部有多少个位置 i 满足 p[i] < p[i+1]
int cal(int l, int r)
{
    if(l > r)
        return 0;
    return sum[r] - sum[l - 1];
}

void solve()
{
    cin >> n;
    for(int i = 1; i <= n; i++)
        cin >> p[i];
    
    for(int i = 2; i < n; i++)
    {
        if(p[i] > p[i - 1] && p[i] > p[i + 1]) // 满足条件三
            V.push_back(node{i, 1});
        if(p[i] < p[i - 1] && p[i] < p[i + 1]) // 满足条件四
            V.push_back(node{i, 2});
    }
    
    for(int i = 1; i < n; i++)
        if(p[i] < p[i + 1])
            sum[i] = sum[i - 1] + 1;
        else
            sum[i] = sum[i - 1];
    sum[n] = sum[n - 1];
    
    ll ans = 0;
    for(int i = 1; i < V.size(); i++) // 枚举每一对相邻的特殊位置 V[i-1] 和 V[i]
    {
        if(V[i].typ != V[i - 1].typ) // 只要前后两个特殊位置类型不同
        {
            int a, b;
            // a b 分别表示左端点的情况数量和右端点的情况数量
            if(i - 2 >= 0) // 左侧还有其它特殊位置
                a = cal(V[i - 2].pos, V[i - 1].pos - 1);
            else
                a = cal(1, V[i - 1].pos - 1);
            if(i + 1 < V.size()) // 右侧还有其他特殊位置
                b = V[i + 1].pos - V[i].pos;
            else
                b = n - V[i].pos;
            ans += 1LL * a * b; // 此时的答案即左右两边方案数之乘积
        }
    }
    cout << ans;
}

D - Garbage Removal

题意

有一个大小为 \(H \times W\) 的网格图,网格图内部有 \(N\) 个位置存在垃圾。

\(Q\) 次操作,每次操作为以下两种情况之一:

  • 1 x:请输出第 \(x\) 行的垃圾总数,然后将第 \(x\) 行的垃圾全部清扫掉。
  • 2 y:请输出第 \(y\) 列的垃圾总数,然后将第 \(y\) 列的垃圾全部清扫掉。

思路

\(H, W\) 都很大,直接模拟肯定不可行。

但我们发现垃圾的数量 \(N\) 也是 \(2\times 10^5\) 级别的,因此当地图很大时,实际上有垃圾的格子会分布得很稀疏。因此可以考虑从垃圾的角度入手。

每个垃圾只会受到某一行或者某一列这两个方向的清除操作影响,因此我们可以用一个容器来存每一行/每一列分别有哪些垃圾。这里可以采取 set 集合。

例如,每次如果我们想删除 \(x\) 这一行上的所有垃圾,首先输出 \(x\) 这一行当前的集合大小,然后对于 \(x\) 这一行上的每一个垃圾,获得他们对应的列 \(y\),然后跑到对应列的集合中将 \(x\) 行这个垃圾的信息删除即可。最后清空 \(x\) 这个集合。

如果删除的是某一列,同理。

代码

set<int> X[200005], Y[200005];

void solve()
{
    int h, w, n;
    cin >> h >> w >> n;
    
    for(int i = 1; i <= n; i++)
    {
        int x, y;
        cin >> x >> y;
        X[x].insert(y); // x 这一行多了个在 y 列的垃圾
        Y[y].insert(x); // y 这一列多了个在 x 行的垃圾
    }
    
    int q;
    cin >> q;
    while(q--)
    {
        int op, d;
        cin >> op >> d;
        if(op == 1)
        {
            cout << X[d].size() << "\n"; // 先输出 d 这一行的垃圾数量
            for(int y : X[d]) // 对于 d 这一行的每个垃圾所处列 y
                Y[y].erase(d); // 到 y 这一列的集合中把 d 这一行的信息删除
            X[d].clear(); // 最后清空 d 这一行
        }
        else // 删除列同理
        {
            cout << Y[d].size() << "\n";
            for(int x : Y[d])
                X[x].erase(d);
            Y[d].clear();
        }
    }
}

E - Popcount Sum 3

题意

给定两个正整数 \(N, K\),问在 \(1 \sim N\) 中有哪些数字,二进制上 \(1\) 的位数恰好是 \(K\)

输出所有满足条件的数字总和,对 \(998244353\) 取模。

思路

考虑数位DP,借助深搜从高位往低位考虑二进制的每一位,记当前已经确认放 \(1\) 的位置所组成的数字总和为 \(sum\),记当前还没考虑到的数位当中最高位为 \(2^p\) 位,最后记在 \(2^{0\sim p}\)\(p+1\) 位当中还可以选择多少位放 \(1\)

讨论不合法的搜索状态:

  • 当前已经确认放 \(1\) 的位置所组成的数字总和 \(sum\) 如果超过了 \(N\) 的限制,不存在答案。
  • 当前还需要放置的 \(1\) 的数量 \(cnt\) 大于剩余的位置数 \(p+1\),不存在答案。

特殊情况:当 \(cnt = 0\),此时剩余位置的放置方案只有一种,总和就是当前得到的数字 \(sum\)

搜索过程每次只需要讨论当前 \(2^p\) 这一位要放 \(0\) 还是放 \(1\) 即可。

考虑剪枝,当发现把当前剩余的 \(cnt\)\(1\) 全部放在剩余数位中最高的 \(cnt\) 位上,也不会超过 \(N\) 的最大值限制时,此时可以考虑借助组合数学直接求解答案。(当然这里也可以忽略 \(cnt\) 的限制,直接考虑剩余的所有数位全部置 \(1\) 也没问题)

于是最后的问题便变成了:

  • 当已经考虑过的高位上所有放 \(1\) 的位置总和为 \(sum\),且剩下的 \(cnt\)\(1\) 可以随意放在数字末尾的 \(p+1\) 个位置上时,所有情况下数字总和是多少。
    • 首先考虑高位对总和的贡献,由于放置方案有 \(\text{C}_{p+1}^{cnt}\) 种,因此高位的贡献为 \(sum \times \text{C}_{p+1}^{cnt}\)
    • 对于剩余的 \(p+1\) 位对答案的贡献,我们单独对每一位进行考虑。对于第 \(i\) \((i \in [0, p])\) 位数,只有在置 \(1\) 时才会对总和产生贡献,并且第 \(i\) 位数的实际数值为 \(2^{i}\)。当第 \(i\) 位置 \(1\),剩余的 \(cnt-1\)\(1\) 只能够分在其余的 \(p\) 个位置上,方案数量共有 \(\text{C}_{p}^{cnt-1}\)。因此当前这一位的贡献可以记作 \(2^i \times \text{C}_{p}^{cnt-1}\)。对于 \(i \in [0, p]\) 的每一个 \(i\),贡献计算方式都相同,因此低位的贡献即 \((2^0 + 2^1 + 2^2 + \dots + 2^{p})\times \text{C}_{p}^{cnt-1}\),也就是 \((2^{p+1}-1)\times \text{C}_{p}^{cnt-1}\)

总结,当剩余的 \(cnt\)\(1\) 可以随意放在数字末尾的 \(p+1\) 个位置上,且此时高位已经置 \(1\) 的位置组成的数字总和为 \(sum\) 时,答案可以借助以下公式计算:

\[sum \times \text{C}_{p+1}^{cnt} +(2^{p+1}-1)\times \text{C}_{p}^{cnt-1} \]

组合数学部分可以采取阶乘与逆元,或是杨辉三角求解。

至于时间复杂度,当搜索过程中某一位在 \(N\) 这个最大值上为 \(1\),可这一步我们没有置 \(1\),明显接下来就会跳入剪枝直接公式求解。如果某一位在 \(N\) 这个最大值上为 \(0\),可这一步我们置 \(1\) 了,明显直接判定为非法。其余两种情况,高位会贴合 \(N\) 的高位搜索,因此单组数据时间复杂度为 \(O(\log N)\)

代码

typedef long long ll;

ll C[65][65];

void init()
{
    C[0][0] = 1;
    for(int i = 1; i <= 60; i++)
    {
        C[i][0] = C[i][i] = 1;
        for(int j = 1; j < i; j++)
            C[i][j] = (C[i - 1][j - 1] + C[i - 1][j]) % mod;
    }
}

ll n;
int k;

ll dfs(int p, ll sum, int cnt)
{
    if(sum > n || cnt > p + 1)
        return 0;
    if(cnt == 0) // 如果已经没有 1 可以放了,方案只有一种,即当前总和 sum
        return sum % mod;
    if(sum + (1LL << (p + 1)) - 1 <= n) // 即使后面的数位全部置 1 也不会超过 n 的范围
    {
        // 剩余 cnt 个 1 可以放在 0~p 的任意位置
        ll t = ((1LL << (p + 1)) - 1) % mod * C[p][cnt - 1] % mod;
        t = t + C[p + 1][cnt] * (sum % mod) % mod;
        return t % mod;
    }
    return (dfs(p - 1, sum, cnt) + dfs(p - 1, sum + (1LL << p), cnt - 1)) % mod;
}

void solve()
{
    init();
    int T;
    cin >> T;
    while(T--)
    {
        cin >> n >> k;
        cout << dfs(59, 0, k) << "\n";
    }
}

F - Compare Tree Weights

题意

给定一棵树 \(T\),其中包含 \(N\) 个点和 \(N-1\) 条边。每个点都有一个 \(1, 2, \dots, N\) 的编号,每条边也有一个 \(1, 2, \dots, N-1\) 的编号。

\(i\) 条边连接 \(U_i, V_i\) 这两个点。

一开始树上每个点的权值都是 \(1\)

\(Q\) 次操作,每次操作为以下两种情况之一:

  • 1 x w:表示将 \(x\) 这个点的权值加上 \(w\)
  • 2 y假如我们将第 \(y\) 条边删去,这棵树将会变成两个连通块。每个连通块的权值可以记作连通块内部所有点的权值之和。问此时两个连通块的权值之差。

思路

建边,考虑先以任意一点作为根结点,借助深搜求出每个结点的 dfs 序以及每棵子树的 dfs 序区间,接下来便能将树上问题借助 dfs 序序列来转为区间问题。

对于操作一,原本是树上单点修改,此时我们可以找出 \(x\) 点在 dfs 序序列中出现的位置,进行序列单点修改即可。

对于操作二,我们可以在 \(U_i, V_i\) 两个点当中先找出深度更深的一个点(离 \(1\) 更远),那么这个点所在的连通块权值就是以这个点作为树根时的子树和,另一个连通块的权值便可以借助树上总权值与其相减来得出。这原本是在树上动态求子树和,但在已知每棵子树的 dfs 序区间后,便可以转化为序列区间求和问题。

总结,我们只需要完成序列的单点修改以及区间求和操作。这可以借助树状数组或者线段树轻松实现。

代码一 树状数组

int n;
int u[300005], v[300005];
vector<int> G[300005];

int dfs_clock = 0, st[300005], ed[300005];
int dep[300005];

// 深搜求出每个结点的深度以及 dfs 序区间
void dfs(int u, int fa)
{
    dep[u] = dep[fa] + 1;
    st[u] = ++dfs_clock;
    for(int &v : G[u])
    {
        if(v == fa)
            continue;
        dfs(v, u);
    }
    ed[u] = dfs_clock;
}

ll tree[300005];
int lowbit(int x)
{
    return x & -x;
}
void update(int p, int v)
{
    while(p <= n)
    {
        tree[p] += v;
        p += lowbit(p);
    }
}
ll query(int p)
{
    ll r = 0;
    while(p)
    {
        r += tree[p];
        p -= lowbit(p);
    }
    return r;
}
ll query(int l, int r)
{
    return query(r) - query(l - 1);
}

void solve()
{
    cin >> n;
    for(int i = 1; i < n; i++)
    {
        cin >> u[i] >> v[i];
        G[u[i]].push_back(v[i]);
        G[v[i]].push_back(u[i]);
    }
    
    dfs(1, 0);
    
    for(int i = 1; i <= n; i++)
        update(i, 1); // 每个点的初始权值为 1
    
    int q;
    cin >> q;
    while(q--)
    {
        int op;
        cin >> op;
        if(op == 1)
        {
            int x, w;
            cin >> x >> w;
            update(st[x], w); // 更新 x 点的 dfs 序所在位置
        }
        else
        {
            int y;
            cin >> y;
            if(dep[u[y]] < dep[v[y]]) // 先把 y 改成 y 这条边两个点当中深度更深的那个点
                y = v[y];
            else
                y = u[y];
            ll p = query(st[y], ed[y]); // 借助 dfs 序求 y 点的子树和,即 y 所在连通块的权值
            ll q = query(n) - p; // 总和 - 另一个连通块的权值
            cout << labs(p - q) << "\n";
        }
    }
}

代码二 线段树

typedef long long ll;

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

struct node
{
    int l, r;
    ll sum; // 区间和
};
node tr[300005 << 2];

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

void build(int l, int r, int p = 1)
{
    tr[p].l = l;
    tr[p].r = r;
    tr[p].sum = 0;
    if(l == r)
    {
        tr[p].sum = 1; // 初始权值为 1
        return;
    }
    int mid = l + r >> 1;
    build(l, mid, ls);
    build(mid + 1, r, rs);
    push_up(p);
}

// 单点修改 pos 位置加上 val
void update(int pos, int val, int p = 1)
{
    if(tr[p].l == tr[p].r)
    {
        tr[p].sum += val;
        return;
    }
    if(pos <= tr[ls].r)
        update(pos, val, ls);
    else
        update(pos, val, rs);
    push_up(p);
}

// 区间 [l, r] 求和
ll query(int l, int r, int p = 1)
{
    if(l <= tr[p].l && tr[p].r <= r)
        return tr[p].sum;
    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;
}

int n;
int u[300005], v[300005];
vector<int> G[300005];

int dfs_clock = 0, st[300005], ed[300005];
// st[i] 表示 i 点的 dfs 序,st[i] ~ ed[i] 表示 i 子树的 dfs 序区间
int dep[300005];
// dep[i] 表示 i 点的深度

// 深搜求出每个结点的深度以及 dfs 序区间
void dfs(int u, int fa)
{
    dep[u] = dep[fa] + 1;
    st[u] = ++dfs_clock;
    for(int &v : G[u])
    {
        if(v == fa)
            continue;
        dfs(v, u);
    }
    ed[u] = dfs_clock;
}

void solve()
{
    cin >> n;
    for(int i = 1; i < n; i++)
    {
        cin >> u[i] >> v[i];
        G[u[i]].push_back(v[i]);
        G[v[i]].push_back(u[i]);
    }
    
    dfs(1, 0);
    build(1, n);
    
    int q;
    cin >> q;
    while(q--)
    {
        int op;
        cin >> op;
        if(op == 1)
        {
            int x, w;
            cin >> x >> w;
            update(st[x], w); // 更新 x 点的 dfs 序所在位置
        }
        else
        {
            int y;
            cin >> y;
            if(dep[u[y]] < dep[v[y]]) // 先把 y 改成 y 这条边两个点当中深度更深的那个点
                y = v[y];
            else
                y = u[y];
            ll p = query(st[y], ed[y]); // 借助 dfs 序求 y 点的子树和,即 y 所在连通块的权值
            ll q = tr[1].sum - p; // 总和 - 另一个连通块的权值
            cout << labs(p - q) << "\n";
        }
    }
}
posted @ 2025-05-17 23:20  StelaYuri  阅读(298)  评论(0)    收藏  举报