AtCoder Beginner Contest 451 ABCDF 题目解析

A - illegal

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

题意

判断给定字符串的长度是否为 \(5\) 的倍数。

代码

void solve()
{
    string s;
    cin >> s;
    if(s.size() % 5 == 0)
        cout << "Yes";
    else
        cout << "No";
}

B - Personnel Change

  • 预估难度:入门
  • 标签:计数数组

题意

高桥所在的公司有 \(N\) 名员工,分别编号为 \(1, 2, \dots, N\)
公司有 \(M\) 个部门,分别编号为 \(1, 2, \dots, M\)

员工 \(i\) 本学期属于部门 \(A_i\),下学期属于部门 \(B_i\)

对于每个部门 \(1, 2, \dots, M\),请求出下学期的人数减去本学期的人数。

思路

分别用两个计数数组统计每个部门本学期的人数与下学期的人数,取差输出后即可。

代码

int a[105], b[105];

void solve()
{
    int n, m;
    cin >> n >> m;
    for(int i = 1; i <= n; i++)
    {
        int x, y;
        cin >> x >> y;
        a[x]++;
        b[y]++;
    }
    for(int i = 1; i <= m; i++)
        cout << b[i] - a[i] << "\n";
}

C - Understory

  • 预估难度:普及-
  • 标签:数据结构(堆)

题意

高桥正在管理花园里的树。最初,花园里没有树。

接下来依次给出 \(Q\) 次操作,每次操作都是以下两种类型之一。在处理完每一次操作后,请输出此时花园中的树木数量。

  • 1 h:向花园中添加一棵高度为 \(h\) 的新树。
  • 2 h:移除花园中所有高度不超过 \(h\) 的树木。

思路

可以借助一个小根堆维护所有现有的树的高度。

加一棵树可以视作加一个新数字进堆。

删除所有高度不超过 \(h\) 的树,可以从小根堆的堆顶开始,只要堆顶还 \(\le h\) 就继续删除堆顶,直到堆空或者堆顶 \(\gt h\) 为止。

由于每个数只会进堆/出堆一次,因此时间复杂度为 \(O(Q \log Q)\)

代码

void solve()
{
    int q;
    cin >> q;
    
    priority_queue<int, vector<int>, greater<int>> pq;
    
    while(q--)
    {
        int op, h;
        cin >> op >> h;
        if(op == 1)
            pq.push(h);
        else
        {
            while(!pq.empty() && pq.top() <= h)
                pq.pop();
        }
        cout << pq.size() << "\n";
    }
}

D - Concat Power of 2

  • 预估难度:普及/提高-
  • 标签:数据结构、数学

题意

如果一个正整数 \(N\) 满足以下条件,我们称它为好整数

  • \(N\) 可以通过选择一些 \(2\) 的幂次(\(1, 2, 4, 8, 16, \dots\)),并将它们前后连接成字符串后构造得到。允许重复选择。

请找出第 \(N\) 小的好整数。保证答案不超过为 \(10^9\)

思路

样例已提示 \(10^9\) 内符合条件的整数个数比百万稍多些,可以考虑构造。

考虑构造出所有符合条件的整数,取第 \(N\) 小。

由于过程中可能构造出重复整数(例如 \(2^7 = 128\)\(2^02^12^3=128\)),因此采用 set 辅助对构造过程进行去重。

\(0\) 开始(将 \(0\) 视作第 \(0\) 小),每次在当前已经构造出来但还没看过的整数中选择一个最小的整数(第 \(i\) 次选择的整数就是第 \(i-1\) 小),在其末尾继续拼接 \(2^{0 \sim 29}\),得出的新整数重新放进集合进行排序与去重。然后将选择的这个数从集合中去除。

重复 \(N\) 次操作后,集合内出现的最小整数即为答案。

时间复杂度 \(O(N\log v)\)\(v = 10^9\)

代码

// 取 u 的长度
int len(int u)
{
    int r = 0;
    while(u > 0)
    {
        u /= 10;
        r++;
    }
    return r;
}

// 将 v 拼在 u 后面
// 如果结果超出 1e9 则返回 -1
int cal(int u, int v)
{
    int l = len(v);
    while(l--) // 让 u 乘上 10^l
    {
        if(u > 1e8)
            return -1;
        u = u * 10;
    }
    if(u + v > 1e9)
        return -1;
    return u + v;
}

void solve()
{
    int n;
    cin >> n;
    
    set<int> st;
    st.insert(0);
    
    for(int i = 1; i <= n; i++)
    {
        int u = *st.begin();  // 取出当前最小值
        st.erase(st.begin()); // 删除当前最小值
        for(int j = 0; j < 30; j++)
        {
            int v = cal(u, 1 << j); // 在 u 后面拼上 2^j
            if(v != -1)
                st.insert(v);
        }
    }
    
    cout << (*st.begin()); // n 次操作已删除前 n-1 小,此时的最小即为答案
}

F - Make Bipartite 3

  • 预估难度:普及+/提高
  • 标签:带权并查集

题意

有一张包含 \(N\) 个点的无向图 \(G\),点编号分别为 \(1\)\(N\),初始无边。

给定 \(Q\) 次操作,第 \(i\) 次操作会向图 \(G\) 添加一条连接点 \(u_i\)\(v_i\) 的边。

在处理完每一次操作后,请立即确定是否有可能将 \(G\) 的每个点染成白色或黑色,从而满足以下条件。如果有可能,请找出染成黑色的点的最小数量。

  • 每条边所连接的两个点有着不同的颜色。

思路

无向图的黑白染色,即判断图是否是一张二分图。

如果不是二分图,说明已经出现奇环,此后所有操作均为 \(-1\)

而如果是一张二分图,此时图中有可能存在多个连通块。对于每个连通块,都可以假设去尝试黑白染色一次,取两种颜色各自涂的点数的较小值计入最终答案。

由于每次加边可能会对连通块产生影响,考虑借助并查集维护,并将每个集合假设涂色的信息记录在集合的根结点上。

  • 例如,用 \(p\) 表示与根结点同色的点数,\(q\) 表示与根结点异色的点数。那么每个集合对答案的贡献就是 \(\min(p, q)\)

然后考虑加边合并。


当合并两点 \(u, v\) 时,如果两点此时位于不同的集合内,那么本次合并就需要考虑这条边对两个集合答案的影响。

首先集合肯定需要合并,然后分类讨论 \(u, v\) 到各自根结点的距离之和,记作 \(S\),那么 \(S+1\) 便是将 \(u, v\) 这两个点之间的边连通后,两个根结点之间的距离

  • 如果 \(S+1\) 是一个奇数,此时两个根结点不能涂成相同的颜色。在集合合并时,被合并的集合的 \(p, q\) 需要交叉添加到目标集合的 \(q, p\) 上。
  • 如果 \(S+1\) 是一个偶数,此时两个根结点必须涂成相同的颜色。在集合合并时,两个集合的 \(p, q\) 对应相加即可。

合并两点 \(u, v\) 时,如果两点此时位于相同的集合内,就得判断这条边所导致出现的环长奇偶性。

由于 \(u, v\) 之间现在也存在一条边,为了保证答案存在,环长必须是偶数。如果记 \(D_u, D_v\) 分别表示 \(u, v\) 两点到当前集合根结点的距离,也就是要保证 \(D_u + D_v + 1\) 是一个偶数。或者说,\(D_u\)\(D_v\) 的奇偶性不同。

倘若上述条件不满足,则奇环出现,之后所有操作的答案均为 \(-1\)


由于这里还需要知道每个点到根结点的距离(其实只需要知道距离奇偶性即可),因此考虑使用带权并查集维护,将每条边的权值设置为 \(1\) 即可。

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

代码

int fa[200005];
int val[200005]; // i -> fa[i] 的边权

int p[200005], q[200005];
// p[i], q[i] 表示以 i 作为集合根结点时,集合内与 i 距离为偶数/奇数的点数

int ans = 0; // 当前答案

int find(int p)
{
    if(p == fa[p])
        return p;
    int root = find(fa[p]);
    val[p] += val[fa[p]]; // 将边权修改为到根结点的总权值
    return fa[p] = root;
}

// a 到 b 连一条边权为 1 的边
bool merge(int a, int b)
{
    int aa = find(a), bb = find(b);
    if(aa == bb) // 已经在一个集合内
    {
        // 此时两点到根结点的距离奇偶性必须不同
        // 如果奇偶性相同,加上现在新加的边,将会出现奇环
        return (val[a] - val[b]) % 2 != 0;
    }
    
    // 统计答案
    ans -= min(p[aa], q[aa]);
    ans -= min(p[bb], q[bb]); // 集合合并前,将旧答案去掉
    
    if((val[a] + val[b] + 1) % 2 == 0) // 两个根结点同色
    {
        p[bb] += p[aa];
        q[bb] += q[aa];
    }
    else // 两个根结点异色
    {
        p[bb] += q[aa];
        q[bb] += p[aa]; 
    }
    
    ans += min(p[bb], q[bb]); // 加入新答案
    
    // 让 bb 成为 aa 的父结点
    val[aa] = -val[a] + 1 + val[b];
    fa[aa] = bb;
    
    return true;
}

void solve()
{
    int N, Q;
    cin >> N >> Q;
    
    // 并查集初始化
    for(int i = 1; i <= N; i++)
    {
        fa[i] = i;
        p[i] = 1; // i 到 i 距离为 0,偶数
    }
    
    bool flag = false; // 判断是否出现了奇环
    
    while(Q--)
    {
        int u, v;
        cin >> u >> v;
        
        if(flag || !merge(u, v))
        {
            flag = true;
            cout << -1 << "\n";
        }
        else
            cout << ans << "\n";
    }
}
posted @ 2026-03-28 22:19  StelaYuri  阅读(124)  评论(0)    收藏  举报