注意:两个城市之间可以有多条道路相通,也就是说
3 3
1 2
1 2
2 1
这种输入也是合法的
当N为0时,输入结束,该用例不被处理。
代表元
。
一个集合内的所有元素组织成以代表元为根的树形结构。
对于每一个元素 parent[x]指向x在树形结构上的父亲节点。如果x是根节点,则令parent[x] = x。
对于查找操作,假设需要确定x所在的的集合,也就是确定集合的代表元。可以沿着parent[x]不断在树形结构中向上移动,直到到达根节点。
for(int i=1; i<=maxn; i++) pre[i]=i;//每个点的前导点设为自己
int finded(int a) { int r=a; while(pre[r]!=r) r=pre[r];//找到a的根节点r int i=a,j; while(i!=r)//路径压缩,将a以上的所有节点全部连接到根节点上! { j=pre[i]; pre[i]=r; i=j; } return r;//返回根节点 }
void join(int x,int y) { int fx=finded(x);//找到根结点 int fy=finded(y); if(fx!=fy) { pre[fx]=fy;//合并 } }
版本二:(为了解决大数据下的退化问题,提高查找效率,使用rank数组记录每个节点为根下的深度,深度小的连接在深度大的上面,防止退化!)
void join(int x,int y) { int fx=finded(x); int fy=finded(y); if(fx!=fy) { if(ranked[fx]<ranked[fy])//深度小的连接在深度大的根节点上 pre[fx]=fy; else { pre[fy]=fx; if(ranked[fx]==ranked[fy]) ranked[fx]++; } } }
#include <iostream> #include<bits/stdc++.h> using namespace std; int pre[1001]; int fined(int a) { int r=a; while(pre[a]!=a) a=pre[a]; int i=r,j; while(i!=a) { j=pre[i]; pre[i]=a; i=j; } return a; } void join(int x,int y) { int fx=fined(x),fy=fined(y); if(fx!=fy) pre[fx]=fy; } int main() { int n,m; while(cin>>n&&n) { cin>>m; int sum=0; memset(vis,0,sizeof(vis)); for(int i=1;i<=n;i++) pre[i]=i; for(int j=0;j<m;j++) { int a,b; cin>>a>>b; join(a,b); } for(int i=1;i<=n;i++) { if(pre[i]=i) sum++; } cout<<sum-1<<endl; } return 0; }
注意:输入完数据以后,所有节点的pre并不能全部更新为最终根节点,所以如果想求是否在一个集合里,还要跑一边find
在一个社区里,每个人都有自己的小圈子,还可能同时属于很多不同的朋友圈。我们认为朋友的朋友都算在一个部落里,于是要请你统计一下,在一个给定社区中,到底有多少个互不相交的部落?并且检查任意两个人是否属于同一个部落。
输入在第一行给出一个正整数N(≤104),是已知小圈子的个数。随后N行,每行按下列格式给出一个小圈子里的人:
KP[1]P[2]⋯P[K]
其中K是小圈子里的人数,P[i](i=1,⋯,K)是小圈子里每个人的编号。这里所有人的编号从1开始连续编号,最大编号不会超过104。
之后一行给出一个非负整数Q(≤104),是查询次数。随后Q行,每行给出一对被查询的人的编号。
#include <iostream> #include<bits/stdc++.h> using namespace std; int pre[10000]; int ranked[10000]; int finded(int a) { int r=a; while(pre[r]!=r) r=pre[r]; int i=a,j; while(i!=r) { j=pre[i]; pre[i]=r; i=j; } return r; } void join(int x,int y) { int fx=finded(x); int fy=finded(y); if(fx!=fy) { if(ranked[fx]<ranked[fy]) pre[fx]=fy; else { pre[fy]=fx; if(ranked[fx]==ranked[fy]) ranked[fx]++; } } } int main() { int n; cin>>n; memset(pre,0,sizeof(pre)); memset(ranked,0,sizeof(ranked)); int sum=0; for(int i=1; i<=10000; i++) pre[i]=i; while(n--) { int a,b,c; cin>>a>>b; if(b>sum) sum=b; a--; while(a--) { cin>>c; if(c>sum) sum=c; join(b,c); } } int ans=0; for(int i=1; i<=sum; i++) { finded(i); if(pre[i]==i) ans++; } cout<<sum<<" "<<ans<<endl; int k; cin>>k; while(k--) { int t1,t2; cin>>t1>>t2; if(pre[t1]==pre[t2]) cout<<"Y"<<endl; else cout<<"N"<<endl; } return 0; }
#include <iostream> #include<bits/stdc++.h> using namespace std; int pre[100000]; bool num[100000]; bool judge[100000]; int finded(int a) { int r=a; while(r!=pre[r]) r=pre[r]; int i=a,j; while(i!=r) { j=pre[i]; pre[i]=r; i=j; } return r; } void join(int x,int y) { int fx=finded(x); int fy=finded(y); if(fx!=fy) pre[fx]=fy; } int main() { int m; while(cin>>m) { int pp=m; memset(num,0,sizeof(num)); memset(judge,0,sizeof(judge)); for(int i=1;i<=100000;i++) pre[i]=i; int sum=0; while(m--) { int a,b; cin>>a>>b; join(a,b); if(a>sum) sum=a; if(b>sum) sum=b; judge[a]=1; judge[b]=1; } int c=0,k=0; for(int i=1;i<=sum;i++) { if(judge[i]==1) k++; if(judge[i]&&pre[i]==i) c++; } if(c==1&&k==pp+1)//这里容易错,原来写的k==m+1,忘记了m--,好菜啊。//如果c等于1,则说明途中路全通,当路不全通时,c会大于1. //当然c=1;只是其中一个条件,因为当图中点与点之间都联通时, //假设其中有两个点之间有2条路可通,此时的c也等于1,但不满 //足"任意两个点有且仅有一条路径可以相通"这一条件,所以还需 //加上 k == m + 1 这一条件,(字母含义详见代码) cout<<"Yes"<<endl; else cout<<"No"<<endl; } return 0; }
传送门:点击打开链接
题意:有n台计算机,给出他们的坐标,只有距离小于等于d的才可以直接通信,若a与b可通信,b与c可通信,则a与c可通信,以此类推。O x表示维修x号计算机可以通信,S x y表示询问xy之间是否可以通信,是输出SUCCESS,否则输出FAIL。
思路:O时循环所有计算机寻找建立通信(同一集合的+可以直接通信的),不循环的话会漏掉后面直接通信的!
代码:
#include <iostream> #include<cstring> #include<cmath> #include<cstdio> using namespace std; bool judge[1005]; int pre[1005]; int ranked[1005]; int N,d; struct Node { int x;int y; }; Node node[1005]; int fined(int x) { int v=x; while(pre[v]!=v)v=pre[v]; int i=x,j; while(i!=v) { j=pre[i]; pre[i]=v; i=j; } return v; // return pre[x]==x?x:pre[x]=fined(pre[x]);//递归运行 } void join(int a,int b) { int fx=fined(a); int fy=fined(b); if(fx!=fy) { if(ranked[fx]<ranked[fy]) { pre[fx]=fy; } else { pre[fy]=fx; if(ranked[fx]==ranked[fy]) ranked[fx]++; } } } int main() { scanf("%d%d",&N,&d); memset(judge,0,sizeof(judge)); memset(ranked,0,sizeof(ranked)); for(int i=1;i<=N;i++) pre[i]=i; for(int i=1;i<=N;i++) { scanf("%d%d",&node[i].x,&node[i].y);//直接输入 // node[i].x=dx; // node[i].y=dy; } char instruct[10]; getchar(); while(scanf("%s",instruct)!=EOF) { if(instruct[0]=='O') { int computer; scanf("%d",&computer); judge[computer]=1; for(int i=1;i<=N;i++)//必须全都循环一边,防止后面能直接交流的没join上!只join上了前面同父亲的! { if(i!=computer&&judge[i]&&((node[i].x-node[computer].x)*(node[i].x-node[computer].x)+(node[i].y-node[computer].y)*(node[i].y-node[computer].y))<=d*d) { join(i,computer); //break; } } } else if(instruct[0]=='S') { int s,e; scanf("%d%d",&s,&e); int fs=fined(s); int fe=fined(e); if(fs!=fe) printf("FAIL\n"); else printf("SUCCESS\n"); } } return 0; }
二:最小生成树
1.最小生成树概念:在连通网的所有生成树中,所有边的代价和最小的生成树,称为最小生成树(使所有点联通+建立所有边的代价和最小)
2.最小生成树应用:要在n个城市之间铺设光缆,主要目标是要使这 n 个城市的任意两个之间都可以通信,但铺设光缆的费用很高,且各个城市之间铺设光缆的费用不同,因此另一个目标是要使铺设光缆的总费用最低。这就需要找到带权的最小生成树。
3.Prim(普里姆)算法(加点法)
(1)算法思想:以任意一点为树根出发,集合V是已经确定最短路的点集合,集合U是没有确立最短路的集合。初始时只有树根点在V中。
每一次循环就代表要修建一条最短路,到达没到达的点(U),我们只能从已经建成的局部最短路点集V中选取V中所有已确定点能到达的所有其他点里面最小的来建设,有点贪心思想,每次选取代价最小的路,逐渐完善点,知道恰好覆盖所有的点。
(2)核心代码:
int G[1000][1000];//邻接矩阵存图 int dis[1000];//存储 集合V 里面所有点的可到到达其他点总的最小距离 bool judge[1000];//判断该点是否已经加入最小点集合 int pre[1000];//记录每个点的前导,用于输出路径 int n,m; int prim(int a) { int sum=0;//记录路径总和 int pos;//记录下一个加入V中的点位置 int minn; judge[a]=1; pos=a; for(int i=1; i<=n-1; i++) { minn=INF; for(int j=1; j<=n; j++) { if(!judge[j]&&dis[j]<minn)//寻找集合V中能到达其他所有点的最短路径 { pos=j; minn=dis[j]; } } judge[pos]=1;//找到下一个加入V中的点 sum+=minn;//加上最小路径 cout<<"V"<<pre[pos]<<" -- "<<"V"<<pos<<" is "<<minn<<endl; for(int j=1; j<=n; j++)//从新加入的点更新V的最小距离dis,便于下次寻找最小点 { if(dis[j]>G[pos][j]&&!judge[j]) { dis[j]=G[pos][j]; pre[j]=pos;//记录前导 } } } return sum; }
(3)完整代码
#include <iostream> #include<bits/stdc++.h> using namespace std; #define INF 0x3f3f3f int G[1000][1000];//邻接矩阵存图 int dis[1000];//存储最小距离(总的集合U里的) bool judge[1000];//判断该点是否已经加入最小点集合 int pre[1000];//记录每个点的前导,用于输出路径 int n,m; int prim(int a) { int sum=0;//记录总和 int pos;//记录位置 int minn; judge[a]=1; pos=a; for(int i=1; i<=n-1; i++) { minn=INF; for(int j=1; j<=n; j++) { if(!judge[j]&&dis[j]<minn) { pos=j; minn=dis[j]; } } judge[pos]=1; sum+=minn; cout<<"V"<<pre[pos]<<" -- "<<"V"<<pos<<" is "<<minn<<endl; for(int j=1; j<=n; j++) { if(dis[j]>G[pos][j]&&!judge[j]) { dis[j]=G[pos][j]; pre[j]=pos; } } } return sum; } int main() { int T; cin>>T; while(T--) { cin>>n>>m; for(int i=1; i<=n; i++) { for(int j=1; j<=n; j++) { if(i==j) G[i][j]=0; else G[i][j]=INF; } } memset(judge,0,sizeof(judge)); for(int i=0; i<m; i++) { int a,b,c; cin>>a>>b>>c; G[a][b]=G[b][a]=c; } int s; cin>>s; for(int i=1; i<=n; i++) { pre[i]=s; dis[i]=G[s][i]; } int k=prim(s); cout<<k<<endl; } return 0; }
(4)邻接表优化
#include <iostream> #include<bits/stdc++.h> using namespace std; #define INF 0x3f3f3f int dis[1000];//存储最小距离(总的集合U里的) bool judge[1000];//判断该点是否已经加入最小点集合 int pre[1000];//记录每个点的前导,用于输出 struct Node//记录终点和路径长度 { int e,v; Node(int a,int b):e(a),v(b) {} }; int n,m; vector<Node> num[1000]; int prim(int a) { int sum=0;//记录总和 int pos;//记录位置 int minn; judge[a]=1; pos=a; for(int i=1; i<=n-1; i++) { minn=INF; for(int j=1; j<=n; j++) { if(!judge[j]&&dis[j]<minn) { pos=j; minn=dis[j]; } } judge[pos]=1; sum+=minn; cout<<"V"<<pre[pos]<<" -- "<<"V"<<pos<<" is "<<minn<<endl; for(int i=0;i<num[pos].size();i++) { Node d=num[pos][i]; if(dis[d.e]>d.v&&!judge[d.e]) { dis[d.e]=d.v; pre[d.e]=pos; } } } return sum; } int main() { int T; cin>>T; while(T--) { int s; cin>>n>>m>>s; for(int i=1;i<=n;i++) { dis[i]=INF; num[i].clear(); pre[i]=s; } memset(judge,0,sizeof(judge)); for(int i=0; i<m; i++) { int a,b,c; cin>>a>>b>>c; num[a].push_back(Node(b,c)); num[b].push_back(Node(a,c)); } for(int i=0;i<num[s].size();i++) { dis[num[s][i].e]=num[s][i].v; } int k=prim(s); cout<<k<<endl; } return 0; }四.
克鲁斯克尔(Kruskal)算法(加边法)
(1)算法思想:最小生成树最后一定是只有n-1条边!所以我们只要选取最小的n-1条边来吧n个点联通起来即可,但是注意不能产生回路,于是我们就用到了并查集!
- 记Graph中有v个顶点,e条边;
- 新建图Graphnew,Graphnew中拥有原图中的v个顶点,但没有边;
- 将原图Graph中所有e条边按权值从小到大排序;
- 循环:从权值最小的边开始,判断并添加每条边,直至添加了n-1条边:
注意:加边的条件是不产生回路!即要连接的两定点不在一个集合里面!(并查集判断是否可以加边)
(2)核心代码:
struct Node//建立边(起点+终点+权值) { int s,e,v; bool operator <(const Node &n)const{//排序规则,由小到大权值 return v<n.v; } }; int Kruskal() { sort(node,node+m);//排序权值 int sizen=0;//建立路径条数 int sum=0;//最小值 for(int i=0;i<m&&sizen!=n-1;i++) { if(finded(node[i].s)!=finded(node[i].e))//如果一条边的起点终点不在同一个集合,即可连接成为一条最短路,并且把这两个集合join为一个 { join(node[i].s,node[i].e);//join sum+=node[i].v; sizen++; } } if(sizen<n-1)return -1;//不足n-1 return sum; }
(3)完整代码:
#include <iostream> #include<bits/stdc++.h> using namespace std; struct Node { int s,e,v; bool operator <(const Node &n)const{ return v<n.v; } }; Node node[1000]; int pre[1000]; int ranked[1000]; int n,m; int finded(int v)//查找 {
int i=v;
while(i!=pre[i])//return pre[v]=v?v:pre[v]=find(pre[v]);//递归 i=pre[i]; int j; while(v!=i) { j=pre[v]; pre[v]=i; v=j; } return i; }
void join(int a,int b)//合并 { int fx=finded(a); int fy=finded(b); if(fx!=fy) { if(ranked[fx]<ranked[fy]) { pre[fx]=fy; } else { pre[fy]=fx; if(ranked[fx]==ranked[fy]) ranked[fx]++; } } } int Kruskal() { sort(node,node+m); int sizen=0; int sum=0; for(int i=0;i<m&&sizen!=n-1;i++) { if(finded(node[i].s)!=finded(node[i].e)) { join(node[i].s,node[i].e); sum+=node[i].v; sizen++; } } if(sizen<n-1)return -1; return sum; } int main() { cin>>n>>m; memset(ranked,0,sizeof(ranked)); for(int i=1;i<=n;i++) { pre[i]=i; } for(int i=0;i<m;i++) { cin>>node[i].s>>node[i].e>>node[i].v; } int k=Kruskal(); cout<<k<<endl; return 0; }