【Nan's 王道机试指南学习笔记】第十一章 图论
前言与提示
图论有关问题——并查集、最小生成树、最短路径、拓扑排序等。
抽象数据的图结构,主要介绍邻接矩阵和邻接表这两种实现方式。
11.1 概述
重点提醒
图是由顶点(Vertex)集合V和边(Edge)集合E组成。常用V和E的模分析算法的复杂度。
图根据边集合E中的元素(u,v)是否有次序,图被分为无向图,有向图。
1) 邻接矩阵——二维数组
最直接的图结构实现方式,用一个二维矩阵来表示。
二维矩阵的每个单元表述一对顶点之间的邻接关系。
邻接矩阵的缺陷:
①若想遍历与某顶点相邻的所有顶点,需要依次遍历二维数组中某行的所有元素,逐个判断相邻,时间利用率低;
②若为稀疏图会产生稀疏矩阵,浪费空间。
适合稠密图,且需频繁地判断某特定顶点对是否相邻时,才比较适宜
2) 邻接表——向量实现 效率更高!多用!
邻接表为每个顶点建立一个单链表,保存与该顶点相邻的所有顶点的相关信息。
在遍历与某个特定顶点相邻的顶点时,邻接表效率很高。
与邻接矩阵相比:当需要判断两个定点是否存在关系时,需要遍历所有邻接顶点才能确定。
适合存在大量遍历邻接顶点、较少判断关系的情况
11.2 并查集
重点提醒
处理 不交集的 合并和查询问题:
①判断任意两个元素是否属于同一个集合
②按照要求合并不同的集合
(1)集合用树结构表示
(2)查找——根据根节点判断集合
(3)合并
为避免合并出不好的树形,退化成链表,在合并两树时,在查找到某个特定结点的根节点的同时,将其与根节点之间的所有结点都直接指向根节点,这个过程被称为 路径压缩。
在合并时,总是将高度较低的树作为子树进行合并,提高合并后的查找效率。(影响查找效率的时树高)
题目练习
例题11.1 畅通工程(浙大复试)
题目OJ网址(牛客网):
https://www.nowcoder.com/practice/4878e6c6a24e443aac5211d194cf3913
并查集最常用来 判断图是否为连通图,或用来求图的连通分量。
无向图G的一个极大连通子图称为G的一个连通分量
//将每个元素看为一个集合,然后通过并查集合并集合,然后再将余下的集合连接起来即可
#include<iostream>
#include<vector>
using namespace std;
int main(){
int n,m;
while(cin>>n>>m){
int t = n;
//vector(int nSize,const t& t):创建一个vector,元素个数nSize,且值均为t
vector<int> v(n+1,-1);
while(m--){
int x,y;
cin>>x>>y;
int a=x,b=y;
while(v[a]!=-1) a=v[a];//找代表元素
while(v[b]!=-1) b=v[b];
if(a==b) continue;
else {
v[b]=x;
t-=1;//合并
}
}
v.clear();
cout<<t-1<<endl;
}
}
11.3 最小生成树
重点提醒
题目练习
例题10.3 二叉排序树(华科复试)
题目OJ网址(牛客网):
11.4 最短路径
重点提醒
求最短路径问题的经典算法——Dijkstra算法。
采用==优先队列==进行优化可以大幅降低算法时间复杂度
数组dis:来保存源点到各个顶点的最短距离
一个保存已经找到了最短路径的顶点的集合:T(myQueue)
初始时,原点 s 的路径权重被赋为 0 (dis[s] = 0)。若对于顶点 s 存在能直接到达的边(s,m),则把dis[m]设为w(s, m),同时把所有其他(s不能直接到达的)顶点的路径长度设为无穷大。
初始时,集合T只有顶点s。然后,从dis数组选择最小值,则该值就是源点s到该值对应的顶点的最短路径,并且把该点加入到T中,OK,此时完成一个顶点,然后,我们需要看看新加入的顶点是否可以到达其他顶点并且看看通过该顶点到达其他点的路径长度是否 < 源点直接到达,如果是,那么就替换这些顶点在dis中的值。
题目练习
例题11.6 畅通工程续(浙大复试)
#include<iostream>
#include<vector>
#include<queue>
#include<cstdio>
#include<cstring>
#include<climits>
using namespace std;
const int MAXN = 200;
const int INF = INT_MAX;
struct Edge{
int to;
int length;
Edge(int t,int l):to(t),length(l){}
};
struct Point{
int number;//点编号
int distance;//源点到该点的距离
Point(int n,int d):number(n),distance(d){}
//这句看不懂
bool operator<(const Point& p)const{
return distance > p.distance;//距离小的优先级高
}
};
vector<Edge> graph[MAXN];//邻接表实现的图
int dis[MAXN];//源点到各点的距离
void Dijkstra(int s){
priority_queue<Point> myQueue;
dis[s]=0;
myQueue.push(Point(s,dis[s]));
while(!myQueue.empty()){
int u = myQueue.top().number;//离源点最近的点的序号
myQueue.pop();
for(int i= 0;i<graph[u].size();i++){//遍历新的点为起点可到的所有路
//新顶点
int v = graph[u][i].to;
int d = graph[u][i].length;
if(dis[v]>dis[u]+d){
dis[v]=dis[u]+d;//如果比直接到v路径短,则替换
myQueue.push(Point(v,dis[v]));//找到一个路径点
}
}
}
return ;
}
int main(){
int n,m;
while(scanf("%d%d",&n,&m)!=EOF){
memset(graph,0,sizeof(graph));//初始化图
//fill()函数参数:fill(first,last,val); first 为容器的首迭代器,last为容器的末迭代器,val为将要替换的值
fill(dis,dis+n,INF);//距离初始化为正无穷
while(m--){
int from,to,length;
scanf("%d%d%d",&from,&to,&length);
//双向道路
graph[from].push_back(Edge(to,length));
graph[to].push_back(Edge(from,length));
}
int s,t;//起点与终点
scanf("%d%d",&s,&t);
Dijkstra(s);
if(dis[t] == INF) dis[t]=-1;
printf("%d\n",dis[t]);
}
return 0;
}
例题11.7 最短路径问题(浙大复试)
题目OJ网址(牛客网):
https://www.nowcoder.com/practice/e372b623d0874ce2915c663d881a3ff2
跟上题很像,只不过加了个price,dijkstra的判断条件也要考虑到同距离更短花费~
#include<iostream>
#include<vector>
#include<queue>
#include<cstdio>
#include<cstring>
#include<climits>
using namespace std;
const int MAXN = 1001;
const int INF = INT_MAX;
struct Edge{
int to;
int length;
int price;
Edge(int t,int l,int p):to(t),length(l),price(p){}
};
struct Point{
int number;//点编号
int distance;//源点到该点的距离
Point(int n,int d):number(n),distance(d){}
bool operator<(const Point& p)const{
return distance > p.distance;//距离小的优先级高
}
};
vector<Edge> graph[MAXN];//邻接表实现的图
int dis[MAXN];//源点到各点的距离
int cost[MAXN];//记录花费
void Dijkstra(int s){
priority_queue<Point> myQueue;
dis[s]=0;
cost[s]=0;
myQueue.push(Point(s,dis[s]));
while(!myQueue.empty()){
int u = myQueue.top().number;//离源点最近的点的序号
myQueue.pop();
for(int i= 0;i<graph[u].size();i++){//遍历新的点为起点可到的所有路
//新顶点
int v = graph[u][i].to;
int l = graph[u][i].length;
int p = graph[u][i].price;
//最短距离及其花费,如果最短距离有多条路线,则输出花费最少的
if(dis[v]>dis[u]+l||(dis[v]==dis[u]+l && cost[v]>cost[u]+p)){
dis[v] = dis[u]+l;//如果比直接到v路径短,则替换
cost[v] = cost[u]+p;
myQueue.push(Point(v,dis[v]));//找到一个路径点
}
}
}
return ;
}
int main(){
int n,m;
while(scanf("%d%d",&n,&m)!=EOF){
if(n==0&&m==0) break;
memset(graph,0,sizeof(graph));//初始化图
//fill()函数参数:fill(first,last,val); first 为容器的首迭代器,last为容器的末迭代器,val为将要替换的值
fill(dis,dis+n+1,INF);//距离初始化为正无穷 1开始编号点
fill(cost,cost+n+1,INF);//距离初始化为正无穷
while(m--){
int from,to,length,price;
scanf("%d%d%d%d",&from,&to,&length,&price);
//双向道路
graph[from].push_back(Edge(to,length,price));
graph[to].push_back(Edge(from,length,price));
}
int s,t;//起点与终点
scanf("%d%d",&s,&t);
Dijkstra(s);
if(dis[t] == INF) dis[t]=-1;
printf("%d %d\n",dis[t],cost[t]);
}
return 0;
}
习题11.6 最短路径(上交复试)
题目OJ网址(牛客网):
https://www.nowcoder.com/questionTerminal/a29d0b5eb46b4b90bfa22aa98cf5ff17
这题路径长度2的k次方。
所以有两种方法:
第一种就是使用大整数+Dijkstra(我自己改了半天没改对 我用longlong也不可 要用string)
第二种就是使用并查集的方法,如果两个点不属于一个集合,则合并这两个点
#include<cstdio>
#include<iostream>
#include<queue>
#include<algorithm>
#include<cstring>
const int MAXN = 200;
using namespace std;
struct Edge{
int to; /*边的一个顶点*/
string length; /*边长*/
Edge(int t, string l): to(t), length(l) {}
};
struct Point{
int number; /*点编号*/
string distance; /*距离源点距离*/
Point(int n, string d): number(n), distance(d) {}
bool operator<(const Point &p) const{
return distance>p.distance;
/*因为下面用到优先级队列,优先级队列默认是大根堆,而我们要用的是小根堆,所以注意这里return是大于号*/
}
};
vector<Edge> graph[MAXN]; /*使用邻接表存储图*/
string dis[MAXN]; /*距离源点的距离*/
string adds(string s1, string s2){ /*字符串加法*/
int carry=0, current;
int i=s1.size()-1;
int j=s2.size()-1;
string ans;
while(i>-1 && j>-1){
current=s1[i]-'0'+s2[j]-'0'+carry;
carry=current/10;
ans+=current%10+'0';
i--;
j--;
}
while(i>-1){
current=s1[i]-'0'+carry;
carry=current/10;
ans+=current%10 + '0';
i--;
}
while(j>-1){
current=s2[j]-'0'+carry;
carry=current/10;
ans+=current%10+'0';
j--;
}
if(carry) /*加法还有进位要再加1*/
ans+="1";
reverse(ans.begin(),ans.end()); /*因为ans正好与正确结果相反,所以使用reverse函数对字符串进行求逆,头文件是algorithm*/
return ans;
}
string multiple(string s,int k){ /*字符串乘法*/
int carry=0;
for(int i=s.size()-1;i>-1;i--){
int current=(s[i]-'0')*k+carry;
s[i]=current%10+'0';
carry=current/10;
}
if(carry)
s=to_string(carry)+s;
return s;
}
int Divide(string s){ /*字符串除法用于求模*/
int remainder=0;
for(int i=0;i<s.size();i++){
int current=10*remainder+s[i]-'0';
remainder=current%100000;
s[i]=current/100000;
}
return remainder;
}
bool cmpString(string s1,string s2){ /*两个字符串数比较大小*/
if(s1.size()>s2.size()) return true; /*位数多则一定大*/
else if(s1.size()<s2.size()) return false;
else{
for(int i=0;i<s1.size();i++){
if(s1[i]-'0'>s2[i]-'0') return true;
else if(s1[i]-'0'<s2[i]-'0') return false;
}
}
return false; /*两个数相等*/
}
void Dijkstra(){
dis[0]="0";
priority_queue<Point> Q; /*使用优先级队列,方便找出距离源点最近的点*/
Q.push(Point(0,dis[0]));
while(!Q.empty()){
int u=Q.top().number;
Q.pop();
for(int i=0;i<graph[u].size();i++){
int v=graph[u][i].to;
string d=graph[u][i].length;
string l=adds(dis[u],d);
if(cmpString(dis[v],l)){
dis[v]=l;
Q.push(Point(v,dis[v]));
}
}
}
return ;
}
int main(){
int n, m, a, b;
string INF="1";
for(int i=0;i<1000;i++){ /*尽可能求一个大点的数用于路径长度的初始化*/
INF=multiple(INF,2);
}
while(cin>>n>>m){
string l="1";
memset(graph,0,sizeof(graph)); /*初始化*/
fill(dis,dis+n,INF);
for(int i=0;i<m;i++){
cin>>a>>b;
graph[a].push_back(Edge(b,l));
graph[b].push_back(Edge(a,l));
l=multiple(l,2);
}
Dijkstra();
for(int i=1;i<n;i++){
if(dis[i]==INF)
cout<<-1<<endl;
else
cout<<Divide(dis[i])<<endl;
}
}
return 0;
}
法二:并查集?看不懂…
披着最短路径外衣的最小生成树。
因为路径长度是从2^0 开始,其实举例到第3条路径就可以发现:22=4>20+2^1,也就是说,当一条边的两个端点不在同一集合时,这条边的长度就是这两点间的最短距离,可以直接取mod,这时只用更新一下两个集合里各点间的距离就行,而如果这条边的两个端点在同一集合,那么这条边就可以直接舍去了,因为这条边的长度将会比之前出现的所有边长度的总和还要长。
习题11.7 I Wanna Go Home
题目OJ网址(牛客网):
https://www.nowcoder.com/questionTerminal/0160bab3ce5d4ae0bb99dc605601e971
题目中有个最多只能转变1次阵营的限制。
由题意可知,M出发地是1号,目的地总是2号,从1-2必须转变一次阵营,所以就要求在这之前不能出现2-1的
情况,出现了就不符合题目的要求,所以就在Dijkstra中判断松弛条件的时候加一个判断语句就能求解了
#include<iostream>
#include<vector>
#include<queue>
#include<cstdio>
#include<cstring>
#include<climits>
using namespace std;
const int MAXN = 605;
const int INF = INT_MAX;
struct Edge{
int to;
int length;
Edge(int t,int l):to(t),length(l){}
};
struct Point{
int number;//点编号
int distance;//源点到该点的距离
Point(int n,int d):number(n),distance(d){}
bool operator<(const Point& p)const{
return distance > p.distance;//距离小的优先级高
}
};
vector<Edge> graph[MAXN];//邻接表实现的图
int dis[MAXN];//源点到各点的距离
int flag[MAXN];
void Dijkstra(int s){
priority_queue<Point> myQueue;
dis[s]=0;
myQueue.push(Point(s,dis[s]));
while(!myQueue.empty()){
int u = myQueue.top().number;//离源点最近的点的序号
myQueue.pop();
for(int i= 0;i<graph[u].size();i++){//遍历新的点为起点可到的所有路
//新顶点
int v = graph[u][i].to;
int d = graph[u][i].length;
if(!(flag[u-1] == 2 && flag[v-1] == 1)&&dis[v]>dis[u]+d){
dis[v]=dis[u]+d;//如果比直接到v路径短,则替换
myQueue.push(Point(v,dis[v]));//找到一个路径点
}
}
}
return ;
}
int main(){
int n,m;
while(scanf("%d%d",&n,&m)!=EOF){
memset(graph,0,sizeof(graph));//初始化图
//fill()函数参数:fill(first,last,val); first 为容器的首迭代器,last为容器的末迭代器,val为将要替换的值
fill(dis,dis+n,INF);//距离初始化为正无穷
//memset(dis, 0x3f, sizeof(dis));
while(m--){
int from,to,length;
scanf("%d%d%d",&from,&to,&length);
//双向道路
graph[from].push_back(Edge(to,length));
graph[to].push_back(Edge(from,length));
}
for(int i = 0; i < n; ++i){
cin >> flag[i];
}
Dijkstra(1);//出发地1,目的地2
if(dis[2] == INF) dis[2]=-1;
printf("%d\n",dis[2]);
}
return 0;
}