《算法竞赛·快冲300题》将于2024年出版,是《算法竞赛》的辅助练习册。
所有题目放在自建的OJ New Online Judge。
用C/C++、Java、Python三种语言给出代码,以中低档题为主,适合入门、进阶。
文章目录
- 题目描述
- 题解
- 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的线条总数是奇数,答案加1vis[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的线条总数是奇数,答案加1vis[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)