《算法竞赛·快冲300题》每日一题:“小球配对”

算法竞赛·快冲300题》将于2024年出版,是《算法竞赛》的辅助练习册。
所有题目放在自建的OJ New Online Judge
用C/C++、Java、Python三种语言给出代码,以中低档题为主,适合入门、进阶。


小球配对” ,链接: http://oj.ecustacm.cn/problem.php?id=1850

题目描述

【题目描述】 给定n个小球,编号为1-n,给定m个篮子,编号为1-m。
  每个球只允许放入特定的两个篮子其中的1个。
  每个球必须放入某个篮子。
  如果篮子中球的数量为奇数,则该篮子是特殊的。
  计算特殊的篮子最少有多少个。
【输入格式】 第一行为两个正整数n和m,1≤n,m≤200000。
  接下来n行,每行两个数字Ai,Bi,表示第i个球可以放入Ai或者Bi编号的篮子。
  1≤Ai,Bi≤m,Ai≠Bi。
【输出格式】 输出一个数字表示答案。
【输入样例】

4 3
1 2
2 3
1 3
1 2

【输出样例】

0

题解

  n个球放到m个篮子里,最暴力的方法是把所有可能的放法列出来,然后看特殊篮子最少的那种方法。可以用DFS编码找所有可能的排列,但显然超时。
  读者也可能想过用DP,从第一个球开始逐个往篮子里放小球,同时计算最少的特殊篮子,直到放完所有的小球。但DP转移方程似乎写不出来。
  其实本题是一个简单的图问题。把篮子看成图上的点;一个球连接两个篮子,可以把球看成连接点的线。这样,篮子和球就分别抽象为点和线,所有的篮子和球构成了一个图,其中有些点是连通的,有些不连通。一个连通子图上的点,最少有多少个是特殊篮子?很容易证明,如果这个子图的线条是偶数个,则特殊篮子最少为0个;如果子图的线条有奇数个,则特殊篮子最少是1个。请读者自己证明。
  本题这样编程:用并查集合并篮子和球,构成多个连通子图;每个连通子图是一个并查集;在合并2个集时,用并查集的根记录这个连通子图的总线条数量。代码只对每个球(线条)进行了并查集合并操作,一次合并为O(1),n次合并的总复杂度为O(n)。
【重点】 并查集的应用。

C++代码

  一定要用带路径压缩的并查集,一次合并的复杂度才是O(1)的。

#include<bits/stdc++.h>
using namespace std;
const int N = 200005;
int n,m,f[N],s[N],vis[N];      //s是并查集,f[i]是点i上的线条数量
int find(int x){
    
                   //并查集的查询,带路径压缩
    if(x!=s[x]) s[x] = find(s[x]);
    return s[x];
}
void merge(int x, int y){
    
          //合并
    int p = find(x), q = find(y);
    if (p!=q){
    
                      //原来不属于一个集合
        s[p] = s[q];            //并查集合并
        f[q] += f[p]+1;         //用并查集的根记录这个连通子图的线条总数
    }
    else f[p]++;                //用并查集的根记录这个连通子图的线条总数
}
int main(){
    
    
    scanf("%d%d",&n,&m);
    for (int i=1;i<=m;i++) s[i]=i;  //初始化并查集
    for (int i=1;i<=n;i++)    {
    
    
        int x,y;   scanf("%d%d",&x,&y);
        merge(x,y);
    }
    int ans = 0;
    for (int i=1;i<=m;i++){
    
    
        int x = find(i);           //查找有多少个集
        if (!vis[x]) {
    
                 //集x还没有统计过
            if (f[x] & 1)  ans++;  //集x的线条总数是奇数,答案加1
            vis[x] = 1;       
        }
    }
    printf("%d",ans);
    return 0;
}

Java代码

import java.util.Scanner;

public class Main {
    
    
    static int N = 200005;
    static int n,m;
    static int[] f = new int[N];     //f[i]是点i上的线条数量
    static int[] s = new int[N];      //s是并查集
    static int[] vis = new int[N];

    public static void main(String[] args) {
    
    
        Scanner scan = new Scanner(System.in);
        n = scan.nextInt();
        m = scan.nextInt();
        for (int i = 1; i <= m; i++) s[i] = i;    //初始化并查集
        for (int i = 1; i <= n; i++) {
    
    
            int x = scan.nextInt(), y = scan.nextInt();
            merge(x, y);
        }
        int ans = 0;
        for (int i = 1; i <= m; i++) {
    
    
            int x = find(i);                      //查找有多少个集
            if (vis[x] == 0) {
    
                        //集x还没有统计过
                if ((f[x] & 1) == 1) ans++;       //集x的线条总数是奇数,答案加1
                vis[x] = 1;
            }
        }
        System.out.println(ans);
        scan.close();
    }
    static int find(int x) {
    
                           //并查集的查询,带路径压缩
        if (x != s[x]) s[x] = find(s[x]);
        return s[x];
    }
    static void merge(int x, int y) {
    
     //合并
        int p = find(x), q = find(y);
        if (p != q) {
    
                                  //如果原来不属于一个集合
            s[p] = s[q];                           //并查集合并
            f[q] += f[p] + 1;                      //用并查集的根记录这个连通子图的线条总数
        }
        else f[p]++;                 //用并查集的根记录这个连通子图的线条总数
    }
}

Python代码

  注意用setrecursionlimit扩栈,因为find()是递归函数。

import sys
sys.setrecursionlimit(1000000)
N = 200005
n, m = map(int, input().split())
f, s, vis = [0] * N, [0] * N, [0] * N     # s是并查集,f[i]是点i上的线条数量
def find(x):                              # 并查集的查询,带路径压缩
    if x != s[x]:   s[x] = find(s[x])
    return s[x]
def merge(x, y):                          # 合并
    p, q = find(x), find(y)
    if p != q:                            # 如果原来不属于一个集合
        s[p] = s[q]                       # 并查集合并
        f[q] += f[p] + 1                  # 用并查集的根记录这个连通子图的线条总数
    else:  f[p] += 1                      # 用并查集的根记录这个连通子图的线条总数
for i in range(1, m + 1):   s[i] = i      # 初始化并查集
for i in range(1, n + 1):
    x, y = map(int, input().split())
    merge(x, y)
ans = 0
for i in range(1, m + 1):
    x = find(i)                           # 查找有多少个集
    if not vis[x]:                        # 集x还没有统计过
        if f[x] & 1: ans += 1             # 集x的线条总数是奇数,答案加1            
        vis[x] = 1
print(ans)

猜你喜欢

转载自blog.csdn.net/weixin_43914593/article/details/131800622