1,并查集
有一篇csdn的文章对并查集的原理讲解的十分通透,可以学一学
(并查集(详细解释+完整C语言代码)_~在下小吴的博客-CSDN博客)
模板 【模板】并查集 - 洛谷
#include<bits/stdc++.h>
using namespace std;
const int N = 2e5 + 5;
int a[N];
//以自己为代表
void makeset(int size) {
for (int i = 1; i <= size; i++) a[i] = i;
return;
}
//查找
int find(int i) {
if (i == a[i]) return i;
return a[i] = find(a[i]);//压缩路径
}
//合并
void merge(int x, int y) {
a[find(x)] = find(y);
return;
}
int main()
{
int n, m;
cin >> n >> m;
makeset(n);
for (int i = 0; i < m; i++) {
int flag, x, y;
cin >> flag >> x >> y;
if (flag == 1) merge(x, y);
else {
if (find(x) == find(y)) cout << "Y" << endl;
else cout << "N" << endl;
}
}
return 0;
}
题目练习
1,朋友 - 洛谷
思想:因为女员工全是负数,处理起来很麻烦,所以我们用两个数组(男和女),且把女全改为正数,方便后序操作。我在统计与小明关系好的人数上犯了错误,误以为全部要以小明为祖先,实际上是要与小明是同一个祖先作为判断条件。
AC代码
#include<bits/stdc++.h>
using namespace std;
const int N = 1e5 + 5;
int fa1[N], fa2[N];
// 初始化第一个并查集
void makeset_1(int size) {
for (int i = 1; i <= size; i++) fa1[i] = i;
}
// 在第一个并查集中查找根节点
int find_1(int i) {
if (i == fa1[i]) return i;
else return fa1[i] = find_1(fa1[i]);
}
// 在第一个并查集中合并两个集合
void merge_1(int x, int y) {
fa1[find_1(x)] = find_1(y);
}
// 初始化第二个并查集
void makeset_2(int size) {
for (int i = 1; i <= size; i++) fa2[i] = i;
}
// 在第二个并查集中查找根节点
int find_2(int i) {
if (i == fa2[i]) return i;
else return fa2[i] = find_2(fa2[i]);
}
// 在第二个并查集中合并两个集合
void merge_2(int x, int y) {
fa2[find_2(x)] = find_2(y);
}
int main() {
int n1, n2, m1, m2;
cin >> n1 >> n2 >> m1 >> m2;
makeset_1(n1);
makeset_2(n2);
// 处理 m1 对朋友关系
for (int i = 0; i < m1; i++) {
int x, y;
cin >> x >> y;
merge_1(x, y);
}
// 处理 m2 对朋友关系(注意:这里将员工编号取负值,表示不同性别)
for (int i = 0; i < m2; i++) {
int x, y;
cin >> x >> y;
x = -x, y = -y;
merge_2(x, y);
}
int count1 = 0, count2 = 0;
// 统计第一个公司中与小明关系好的人数
for (int i = 1; i <= n1; i++) {
if (find_1(i) == find_1(1)) count1++;
}
// 统计第二个公司中与小红关系好的人数
for (int i = 1; i <= n2; i++) {
if (find_2(i) == find_2(1)) count2++;
}
// 输出最终答案,即能够组成情侣的最大人数
cout << min(count1, count2) << endl;
return 0;
}
2,修复公路 - 洛谷
思想:这道题的难点在于输出最早什么时候任意两个村庄能够通车。这个其实不用开两个for循环挨个村庄去判断,如果这样做的话,第二个点会TLE。每次合并前,判断两个村庄是否连通,否的话,就n--, 当n==1的时候,所有的村庄就都连通了。
AC代码
#include<bits/stdc++.h>
using namespace std;
const int N = 1e5 + 5;
int fa[N];
set<int> s;
struct node {
int a, b, time;
} w[N];
// 比较函数,用于排序
bool cmp(node x, node y) {
return x.time < y.time;
}
// 初始化并查集
void makeset(int size) {
for (int i = 1; i <= size; i++) fa[i] = i;
}
// 在并查集中查找根节点
int find(int i) {
if (i == fa[i]) return i;
else return fa[i] = find(fa[i]);
}
// 合并两个集合
void merge(int x, int y) {
fa[find(x)] = find(y);
}
int main() {
int n, m;
cin >> n >> m;
// 读取公路修复信息并按时间排序
for (int i = 1; i <= m; i++)
cin >> w[i].a >> w[i].b >> w[i].time;
sort(w + 1, w + 1 + m, cmp);
makeset(n);
// 遍历每条公路修复信息
for (int i = 1; i <= m; i++) {
if (find(w[i].a) != find(w[i].b)) n--; // 如果两个村庄不连通,n--
merge(w[i].a, w[i].b);
if (n == 1) {
cout << w[i].time << endl; // 当只剩一个村庄时输出最早时间
return 0;
}
}
cout << "-1" << endl; // 如果无法使所有村庄连通则输出-1
return 0;
}
3,一中校运会之百米跑 - 洛谷
思想:使用map来保存名字,其他的套模板即可。
AC代码
#include<bits/stdc++.h>
using namespace std;
const int N = 1e6 + 5;
int a[N]; // 存储并查集的父节点
map<string, int> mp; // 存储姓名和对应的编号
// 初始化并查集
void makeset(int size) {
for (int i = 1; i <= size; i++) a[i] = i;
}
// 在并查集中查找根节点
int find(int i) {
if (i == a[i]) return i;
return a[i] = find(a[i]);
}
// 合并两个集合
void merge(int x, int y) {
a[find(x)] = find(y);
}
int main() {
int n, m;
cin >> n >> m;
makeset(n);
// 读取每个学生的姓名,并建立姓名和编号的映射关系
for (int i = 1; i <= n; i++) {
string x;
cin >> x;
mp[x] = i;
}
// 处理已知的学生同组关系
for (int i = 0; i < m; i++) {
string name1, name2;
cin >> name1 >> name2;
merge(mp[name1], mp[name2]);
}
cin >> m;
// 处理体育老师的询问
for (int i = 0; i < m; i++) {
string name1, name2;
cin >> name1 >> name2;
// 判断两个学生是否在同一组里,并输出结果
if (find(mp[name1]) == find(mp[name2]))
cout << "Yes." << endl;
else
cout << "No." << endl;
}
return 0;
}
4,并查集 - Virtual Judge
思想:这题唯一的难点就是输入,其他的套模板就行
AC代码
#include <iostream>
#include <set>
#include <cstdio>
using namespace std;
const int N = 1e5 + 5;
// 初始化并查集,每个学生最初都是独立的一个集合
void makeset(int* a, int size) {
for (int i = 0; i < size; i++) a[i] = i;
}
// 并查集的查找操作
int find(int* a, int i) {
if (i == a[i]) return i;
else return a[i] = find(a, a[i]);
}
// 并查集的合并操作
void merge(int* a, int x, int y) {
a[find(a, x)] = find(a, y);
}
int main() {
int n, m;
while (cin >> n >> m) {
if (n == 0 && m == 0) break;
int a[N];
makeset(a, n);
// 处理每个组
for (int i = 0; i < m; i++) {
int x, b[N];
cin >> x;
for (int j = 0; j < x; j++) cin >> b[j];
//让学生两两一组开始合并
for (int j = 0; j < x - 1; j++) {
merge(a, b[j], b[j + 1]);
}
}
int zero = find(a, a[0]), count = 0;
// 统计与“学生0”在同一组的学生数量
for (int i = 0; i < n; i++) {
int x = find(a, a[i]);
if (x == zero) count++;
}
// 输出疑似患者的数量
cout << count << endl;
}
return 0;
}
5,家谱 - 洛谷
思想:本题的难点就是输入,如果没有想好对策会面临很多问题。
#include<bits/stdc++.h>
using namespace std;
const int N = 1e5 + 5;
map<string, string> mp; // 用map记录每个人的父亲关系
// 递归查找最早祖先
string find(string s) {
if (s == mp[s])
return s;
else
return mp[s] = find(mp[s]);
}
int main() {
char c;
string s, name;
cin >> c;
while (c != '$') {
cin >> name;
if (c == '#') { // 父亲行
s = name;
if (mp[name] == "")
mp[name] = s; // 初始化每个人的祖先为自己
} else if (c == '+') { // 儿子行
mp[name] = s; // 记录儿子的父亲
} else { // 查询祖先行
cout << name << " " << find(name) << endl; // 输出人和最早祖先
}
cin >> c;
}
return 0;
}
6,[BOI2003] 团伙 - 洛谷
思想:实在解决不了“敌人的敌人是朋友”这句话,看了一下大佬的题解。
如果a和b是敌人,合并n+b和a,n+a和b
如果c和a是敌人,合并n+c和a,n+a和c
那么b和c就并在一起了
这样就符合了题目敌人的敌人是朋友的规则
AC代码
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 5;
int a[N];
// 初始化并查集,将每个元素的父节点设为自身
void makeset(int size) {
for (int i = 1; i <= 2 * size; i++) a[i] = i;
return;
}
// 并查集的查找操作,找到元素的根节点
int find(int i) {
if (i == a[i]) return i;
else return a[i] = find(a[i]); // 路径压缩,将节点直接指向根节点
}
// 并查集的合并操作,将两个元素所在的集合合并
void merge(int x, int y) {
a[find(x)] = find(y); // 将x的根节点的父节点指向y的根节点,即合并两个集合
return;
}
int main() {
int n, m;
cin >> n >> m;
// 初始化并查集
makeset(n);
// 处理每个关系
for (int i = 0; i < m; i++) {
char c;
int x, y;
cin >> c >> x >> y;
if (c == 'F') // 如果是朋友关系,合并x和y所在的集合
merge(x, y);
else {
// 如果是敌人关系,将x的敌人与y合并,将y的敌人与x合并
merge(x + n, y);
merge(y + n, x);
}
}
int ans = 0;
for (int i = 1; i <= n; i++) {
if (i == a[i]) ans++; // 统计根节点的数量,即团体的数量
}
cout << ans << endl;
return 0;
}
2,线段树
这个部分学习起来比较困难,模板较多,我专门发一篇文章进行讲解。
3,树状数组
这里也是一样,树这个数据结构太重要了,所以专门发一篇来进行讲解
4,递推
简单来说就是根据题意找规律,得出答案。
对于这类问题,我们一开始可以设置一个小一点的数据慢慢寻找规律。
(1)数楼梯 - 洛谷
思路:这道题就是典型的斐波拉契数列题,不过还有一个问题就是斐波拉契数列增长极快,用longlong都会爆掉,所以这里使用高精度,具体可以看看这篇博客
AC代码
#include<bits/stdc++.h>
using namespace std;
int f[5005][5005],len=1;
void hp(int k)
{
for(int i=1;i<=len;i++) f[k][i]=f[k-1][i]+f[k-2][i];//套用公式
for(int i=1;i<=len;i++){ //进位
if(f[k][i]>=10)
{
f[k][i+1]+=f[k][i]/10;
f[k][i]=f[k][i]%10;
if(f[k][len+1]) len++;
}
}
}
int main()
{
int n;
cin>>n;
f[1][1]=1,f[2][1]=2;
for(int i=3;i<=n;i++) hp(i);
for(int i=len;i>=1;i--) //逆序输出
printf("%d",f[n][i]);
return 0;
}
(2)蜜蜂路线 - 洛谷
思考:这题经过简单的推理后也是一道斐波拉契题,需要注意的是,要使用高精度。溢出警告!!!
AC代码
#include<bits/stdc++.h>
using namespace std;
int f[1005][1005],len=1;
void hp(int k)
{
for(int i=1;i<=len;i++) f[k][i]=f[k-1][i]+f[k-2][i];//套用公式
for(int i=1;i<=len;i++){ //进位
if(f[k][i]>=10)
{
f[k][i+1]+=f[k][i]/10;
f[k][i]=f[k][i]%10;
if(f[k][len+1]) len++;
}
}
}
int main()
{
int start,end;
cin>>start>>end;
f[1][1]=1,f[2][1]=2;
for(int i=3;i<=end-start;i++) hp(i);
for(int i=len;i>0;i--) cout<<f[end-start][i];
return 0;
}
(3)数塔问题 万里ACM
思路:从倒数第二层开始,对每个节点计算从它出发的最大路径和。对于第
i
层第j
个节点,其最大路径和等于该节点的值加上从下一层相邻节点中选择较大路径和的值。这样,逐层向上计算,直到达到顶部的根节点。
AC代码
#include <stdio.h>
#define max(x,y) (x>y)?x:y // 定义一个宏,用于返回两个数中的较大值
int dp[100][100]; // 存储每个节点的最大路径和的数组
int main() {
int n; // 数塔的层数
scanf("%d", &n); // 读取数塔的层数
int i, j;
// 读取数塔中每个节点的值
for (i = 0; i < n; i++) {
for (j = 0; j <= i; j++) {
scanf("%d", &dp[i][j]);
}
}
// 从倒数第二层开始逐层计算最大路径和
for (i = n - 2; i >= 0; i--) {
for (j = 0; j <= i; j++) {
// 当前节点的值加上从下一层相邻节点的最大路径和中选择较大的值
dp[i][j] += max(dp[i + 1][j], dp[i + 1][j + 1]);
}
}
// 最终最大路径和就是从顶部到根节点的路径和,即 dp[0][0]
printf("%d\n", dp[0][0]);
return 0;
}
(4)平面分割 万里ACM
思路:这题看起来很难想,实际上一画图思路就出来啦
AC代码
#include<bits/stdc++.h>
using namespace std;
int main()
{
int n, m; // 总直线数和已知相交的直线数
cin >> n >> m; // 读取输入的直线数
long long sum = 0; // 用于累计新增区域的数量
for (int i = m + 1; i <= n; i++) {
sum += i; // 计算从第 m+1 条直线开始到第 n 条直线结束的区域数之和
}
// 最终结果为 sum 加上已知相交的直线数乘以 2,因为每条相交的直线会增加 2 个新区域
cout << sum + m * 2 << endl;
return 0;
}
(5)骨牌铺法 万里ACM
思路:这题也是同样的道理,找出规律即可
AC代码
#include<bits/stdc++.h>
using namespace std;
long long a[1000] = { 0,1,2,4 };
int main()
{
int n;
cin >> n;
//当前这项等于先三项的和
for (int i = 4; i <= n; i++) a[i] = a[i - 1] + a[i - 2] + a[i - 3];
cout << a[n] << endl;
return 0;
}
(6)位数问题 万里ACM
思路:关于这题的思路嘛,我直接推荐一篇博客,还是别人的语言组织能力比较高,自己怎么也说不明白呀!(http://t.csdn.cn/bJx5S)
AC代码
#include<bits/stdc++.h>
using namespace std;
const int mod = 12345;
const int K = 12345;
long long odd[1005], even[1005]; // 用于存储奇数个3和偶数个3的数量
int main()
{
int n; // 总位数
cin >> n;
// 处理特殊情况:1位数
if (n == 1) {
odd[1] = 1;
even[1] = 8;
cout << even[1] << endl;
}
else {
odd[1] = 1;
even[1] = 9;
// 计算除了n位数的情况
for (int i = 2; i <= n - 1; i++) {
// 计算偶数个3的情况
even[i] = ((odd[i - 1] % mod) + ((9 * (even[i - 1] % mod)) % mod)) % mod;
// 计算奇数个3的情况
odd[i] = ((even[i - 1] % mod) + ((9 * (odd[i - 1] % mod)) % mod)) % mod;
}
// 单独计算n位数,因为第一项不能为0,所以乘8而不是9
even[n] = ((odd[n - 1] % mod) + ((8 * (even[n - 1] % mod)) % mod)) % mod;
cout << even[n] << endl;
}
return 0;
}
(7)昆虫繁殖 万里ACM
思路:这题虽然也是找规律,但他难就难在规律不怎么好找,比较乱。我也是看了一篇博客才彻底搞懂这道题的规律(http://t.csdn.cn/pVRa1)
AC代码
#include<bits/stdc++.h>
using namespace std;
long long c[110], r[110];
int main()
{
int x, y, z; // 输入的 x, y 和 z 值
cin >> x >> y >> z;
// 初始化数组 c 和 r
for (int i = 0; i <= x-1; i++) {
c[i] = 1;
r[i] = 0;
}
c[x] = 1; // 经过 x 个月后
r[x] = y;
// 使用动态规划计算过 z 个月以后的成虫数量
for (int i = x + 1; i <= z; i++) {
r[i] = c[i - x] * y; // 经过 x 个月后产生的卵
c[i] = c[i - 1] + r[i - 2]; // 总成虫数量,包括新成虫和从卵长大的成虫
}
cout << c[z] << endl; // 输出过 z 个月以后的成虫对数
return 0;
}
(8)邮票问题 万里ACM
思想:这题我觉得很有意思,但我不会做,看来题解用的是动态规划。目前感觉还是优点疑问,等学了动态规划后可能会好一点吧。(http://t.csdn.cn/0mKK2)
AC代码
#include<bits/stdc++.h>
using namespace std;
int a[1000];
int main()
{
int n, m;
cin >> n >> m;
for (int i = 1; i <= n; i++) cin >> a[i];
sort(a + 1, a + 1 + n);
int count[10000] = { 0 };//需要凑出x的最小邮票个数
int i = 0;
while (count[i] <= m) {
i++;
count[i] = 99999;
for (int j = 1; j <= n && a[j] <= i; j++) {
count[i] = min(count[i], count[i - a[j]] + 1);
}
}
cout << i - 1 << endl;
return 0;
}
(9)母牛的故事 动态规划基础题目练习 - Virtual Judge
思想:其实是一种广义上的斐波拉契数列,做了这题多斐波拉契会有更深的理解
AC代码
#include<iostream>
using namespace std;
#define N 55
int main()
{
int n, i;
int num[N] = {0,1,2,3,4}; //num[0]=0,num[1]=1,num[2]=2,num[3]=3,num[4]=4
for (i = 5; i < 55; i++)
num[i] = num[i - 1] + num[i - 3];
while (cin >> n && n != 0) //输入 n 的值,且 n 不等于0,则进入,否则退出
{
cout << num[n] << endl;
}
return 0;
}
(10)折线分割平面 动态规划基础题目练习 - Virtual Judge
思想:这题的话,我是通过画图得出结论的,其实本质上并不难。
AC代码
#define _CRT_SECURE_NO_WARNINGS
#include<bits/stdc++.h>
using namespace std;
const int N = 10005;
long long a[N] = { 0,2 };
int main()
{
for (int i = 2; i <= N; i++) a[i] = a[i - 1] + (i - 1) * 4 + 1;
int T;
cin >> T;
while (T--) {
int n;
cin >> n;
cout << a[n] << endl;
}
return 0;
}