《冬训周报四》
并查集
概念与介绍
并查集是一种树型的数据结构,用于处理一些不相交集合的合并及查询问题。
并查集的思想是用一个数组表示了整片森林(parent),树的根节点唯一标识了一个集合,我们只要找到了某个元素的的树根,就能确定它在哪个集合里。
作用
1.将两个集合合并
2.询问两个元素是否在一个集合当中
基本原理
对于每个集合,我们使用一棵树来表示。树根的编号是整个集合的编号。
集合中每个结点存储它的父节点,fa[x]表示x的父节点。
三个问题
1.如何判断集合编号(树根):fa[x] == x;
2.如何求x的集合编号:while(fa[x] != x) x= fa[x];
3.如何合并两个集合:假设a是第一个集合的编号,b是第二个集合的编号,则fa[a] = b;
路径压缩
如果直接使用 while(fa[x] != x) x= fa[x]; 寻找根节点,那么它的时间复杂度与树的长度成正比,非常容易超时。
所以在实现的过程中,我们可以使用一个非常巧妙地方法—路径压缩。
压缩前
int find(int x){ if(fa[x]!=x) x = find(fa[x]); return x; }//递归实现,容易TLE
压缩后
int find(int k) { if(id[k] != k) id[k] = find(id[k]); return id[k]; //每次找到根节点之后,在其回溯过程中 //将路径上的节点的父节点更新为根节点,大大降低后续查询次数 }
例题
BFS应用
介绍
广度优先搜索:在面临一个路口时,把所有的岔路口都记下来,然后选择其中一个进入,然后将它的分路情况记录下来,然后再返回来进入另外一个岔路,并重复这样的操作
一个最直观经典的例子就是走迷宫,我们从起点开始,找出到终点的最短路程,很多最短路径算法就是基于广度优先的思想成立的。
概念代码实现
将初始状态推入队列
当队列不空的时候,取队列中的元素,扩展
初始化队列Q; Q = {起点}; 标记s; while(Q非空) { 取Q队首元素u; u出队; if(u==目标状态) { …… } else { 所有与u相邻且未被访问的点进入队列; 标记u为已访问; } }
过程图
最小生成树
介绍
现在假设有一个很实际的问题:我们要在n个城市中建立一个通信网络,则连通这n个城市需要布置n-1一条通信线路,这个时候我们需要考虑如何在成本最低的情况下建立这个通信网?
于是我们就可以引入连通图来解决我们遇到的问题,n个城市就是图上的n个顶点,然后,边表示两个城市的通信线路,每条边上的权重就是我们搭建这条线路所需要的成本,所以现在我们有n个顶点的连通网可以建立不同的生成树,每一颗生成树都可以作为一个通信网,当我们构造这个连通网所花的成本最小时,搭建该连通网的生成树,就称为最小生成树。(引)
即可以认为
一个图中可能存在多条相连的边,我们一定可以从一个图中挑出一些边生成一棵树。这仅仅是生成一棵树,还未满足最小,当图中每条边都存在权重时,这时候我们从图中生成一棵树(n - 1 条边)时,生成这棵树的总代价就是每条边的权重相加之和。
一个有N个点的图,边一定是大于等于N-1条的。图的最小生成树,就是在这些边中选择N-1条出来,连接所有的N个点。这N-1条边的边权之和是所有方案中最小的。
实现最小生成树的两种算法
prim(普利姆算法)和Kruskal(克鲁斯卡尔算法);
由于我自己对这两种算法还不能说有着深刻的理解,在这里放上两位大佬的算法分析
最小生成树详解(模板 + 例题)_潘小蓝的博客-CSDN博客_最小生成树
数据结构--最小生成树详解_Ouyang_Lianjun的博客-CSDN博客_数据结构最小生成树
例题(摘自大佬)
题目
两种方法代码实现
#include <iostream> #include <algorithm> using namespace std; const int N=1e5+10,M=2*N,INF=0x3f3f3f3f; int p[N]; //并查集操作,通过该数组来存储父节点,当下标和其数组对应值相等时,该点便为根节点 int n,m; struct Edge { int a; int b; int c; bool operator< (const Edge &W) const //建立一个存有边和两端点的结构体数组,并通过边的权重来比较大小 { return c<W.c; } }edges[M]; int find(int x) { if(x!=p[x]) p[x]=find(p[x]); return p[x]; } int kruskal() { sort(edges,edges+m); //将结构体数组通过边的权重从小到大来排序,这样优先遍历的是最小的边 //某两点的最小边必定会优先被操作,并且通过并查集可以来判断两点是否已经进行连接操作,排除较大重边的影响 for(int i=1;i<=n;i++) p[i]=i; //并查集初始化操作 int sum=1,res=0; for(int i=0;i<m;i++) { int a=edges[i].a,b=edges[i].b,c=edges[i].c; a=find(a),b=find(b); if(a!=b) //如果两点的父节点相同,便表示这两点已经相连,由于边是从小到大遍历,故本次更新操作可以忽略 { sum++; //记录点的个数,初始值为1,每连接一个点边加1 res+=c; //每连接一个点边加上两点之间的权重 p[a]=b; } } if(sum==n) return res; //若相连的点最终有n个,则表示存在最小生成树,返回最小边权和 else return INF; //否则不存在最小生成树 } int main() { cin>>n>>m; for(int i=0;i<m;i++) { int a,b,c; cin>>a>>b>>c; edges[i]={a,b,c}; } int t=kruskal(); if(t==INF) cout<<"impossible"<<endl; else cout<<t<<endl; return 0; } /* 由n个点和n-1条边生成的无向连通子图被称为生成树,其中边的权值之和最小的生成树即为最小生成树 */
代码实现
#include <iostream> #include <cstring> using namespace std; const int N=510; bool st[N]; //将生成树看成一个集合,在集合中即为true,不在即为false int dist[N],g[N][N]; //dist数组是点到该集合的距离,g数组是两点之间的距离 int n,m,res; int prim() { memset(dist,0x3f,sizeof(dist)); for(int i=0;i<n;i++) //因为最小生成树要加入所有点,故要遍历n次 { int t=-1; for(int j=1;j<=n;j++) { if(!st[j]&&(t==-1||dist[t]>dist[j])) //在未加入生成树的点集合中选择一个点 { //且该点距离集合的距离最小,符合最小生成树的边权之和最小的特性 t=j; } } st[t]=true; //选出点后便加入最小生成树集合 if(i&&dist[t]==0x3f3f3f3f) return dist[t]; //如果某点不是第一个加入最小生成树中的点,且该点的值为初始化的0x3f3f3f3f,即该点不能与其他点相连,故不能形成最小生成树 if(i) res+=dist[t]; //如果该点不是第一个点,则将该点与最小生成树集合之间的边的权重加入结果中 for(int j=1;j<=n;j++) dist[j]=min(dist[j],g[t][j]); //每次最小生成树集合中加入新的点后,都更新其他点与集合的距离,本质上是更新未加入集合的点,但由于特判增加复杂度,故直接遍历所有点 } return res; //若未发现不与集合连通的点,则返回点之间的边权重之和 } int main() { memset(g,0x3f,sizeof(g)); cin>>n>>m; while(m--) { int a,b,c; cin>>a>>b>>c; g[a][b]=g[b][a]=min(g[a][b],c); } int t=prim(); if(t==0x3f3f3f3f) cout<<"impossible"<<endl; else cout<<t<<endl; return 0; }
补题
清楚姐姐打怪升级
题目
代码实现
#include<bits/stdc++.h> #define N 100100 #define ll long long using namespace std; ll hx[N],n,h[N],v[N],t,a; int main() { ios::sync_with_stdio(false); cin.tie(0),cout.tie(0); cin >> n >> t >> a; for(ll i=0;i<n;i++) { cin >> h[i] >> v[i]; hx[i] = h[i]; } ll ans=0; bool f=0; for(ll i=0;i<n;i++) { if(v[i]*t >= a && hx[i] > a) { f=1; break; } else { if(hx[i]<=a) ans++; else { hx[i] -= a; ans++; ans += hx[i]/(a-v[i]*t); if(hx[i]%(a-v[i]*t)!=0) ans++; } } if(f) cout << "-1" << endl; else cout << (ans-1)*t+1 << endl; return 0; }
清楚姐姐的01背包
题目
代码实现
#include <bits/stdc++.h> using namespace std; int main(){ int T = 1; while(T--){ int n,m; cin>>n>>m; int v[110]; int w[110]; long long dp[110]; for(int i=1;i<=n;i++){ cin>>w[i]>>v[i]; } for(int i=1;i<=n;i++){ for(int j=1;j<=n;j++){ if(i==j) continue; for(int k=m;k>=w[j];k--){ dp[k] = max(dp[k] , dp[k-w[j]] + v[j]); } } long long ans = dp[m]-dp[m-w[i]] - v[i] + 1; if(ans<0) cout<<0<<endl; else cout<<ans<<endl; } } return 0; }