《算法竞赛·快冲300题》将于2024年出版,是《算法竞赛》的辅助练习册。
所有题目放在自建的OJ New Online Judge。
用C/C++、Java、Python三种语言给出代码,以中低档题为主,适合入门、进阶。
【题目描述】 给定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)。
【重点】 并查集的应用。
一定要用带路径压缩的并查集,一次合并的复杂度才是O(1)的。
#include
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;
}
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]++; //用并查集的根记录这个连通子图的线条总数
}
}
注意用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)