洛谷网校:图论:最小生成树和最短路
2020.2.4
图论ex(一)(批注加粗)
定义(定义不需要解释)
图:
图是一个二元组(V,E),其中V称为顶点集,E称为边集
边集E的元素是二元组数对,用(x,y)表示,其中x,y∈V,代表有一条从x到y的边
有向图:
边有方向,(x,y)和(y,x)不表示一条边
无向图:
边无方向,(x,y)和(y,x)表示一条边,实际编程中常常认为每一条边 都是两条有向边
重边:
两点之间有多条边,有的题目会保证无重边
自环:
边(x,x),如右图点1处有一个自环,有的题目会保证无 自环
边权:
每条边有一个权值c,常常代表距离或者费用连通图:任意两点间存在路径
树:
以下定义等价:
任意两点间只存在一条路径的图
无环的连通图
n个点n-1条边的连通图
n个点n-1条边,无环的图
子图:
图G'称作图G的子图如果V(G')⊆V(G)以及E(G')⊆E(G)。
也就是从原图中选出一些点以及和一些边组成的新的图
生成子图:
指满足条件V(G')=V(G)的G的子图G'。
也就是选出所有的点和一些边组成的新的图
生成树:
是指生成子图G'是一颗树
最小/大生成树
最小生成树:对于带权图,权值和最小的生成树
最小瓶颈生成树:对于带权图,最大权值最小的生成树
最小生成树一定是最小瓶颈生成树
反证:
如果最小生成树不是最小瓶颈生成树,那么该树最大边一定有更小的可以将其替代(因为不是最小瓶颈生成树),加入这个更小的边以后,替换掉原来的最大边,总权值就变小了,和最小生成树的条件矛盾。
prim算法
从图上一点开始建树
每次选择((可以连接树里树外的)(最小的)边)加入树,直到所有节点连通
用优先队列来维护被更新的点,类似于Dijkstra,用队列里边权最小的一条边来更新以它终点为起点的所有边的终点的最短路
老师的代码(简化+批注)
bool vis[MAXN];
int minc[MAXN];
priority_queue<pair<int,int>/*队列元素类型:pair的两个量分别是边权和边的编号*/,vector<pair<int,int> >/*这是整个队列的容器,一般是以队列元素类型为单位的vector*/,greater<pair<int,int> >/*比较方式也就是规则,按整个元素pair升序排列*/ > q/*q优先队列存储边,顺便学习一下优先队列用法*/;
int prim() {
int ans = 0;//最小生成树的边权和
q.push(make_pair(0, 1));//无向边0,1入队
while (!q.empty()) {//只要队列不空,就继续访问
int c = q.top().first,now=ed[q.top().second].to;//c存队首边权(因为是升序排列,所以队首就是first(边权)最小的),now存队首边的编号
if (!vis[now]) {//当前边没有被访问过
vis[now] = true;//访问
ans+=c;//c就是当前边权
insert(ed[i].from, ed[i].to, ed[i].cost);//连边(ed是个存储树的结构体数组,空间为O(2n))
insert(ed[i].to, ed[i].from, ed[i].cost);//反向连边
for (int i = he[now]/*(以(now边的终点)为起点)的第一条边*/;i!=0; i=ne[i]) {//((以(now边的终点)为起点的)i边的)下一条边
Edge &e = ed[i];//e存储当前操作的边
if (minc[e.to] > e.cost && !vis[e.to]) {//没访问过并且花费更少(类似于求最短路,但是这里是从树内倒树外的最短路)
minc[e.to] = e.cost;//从树内任一点到e.to节点的最小路程
q.push(make_pair(e.cost, i));//被更新的边入队并排序
}
}
}
}
}
Kruskal算法
选最小边依次连,并且避免出现环
提前升序排列边,用并查集来判断两点的连通性,只要当前边的两点不连通,就连接当前边,因为是从小到大连边,所以保证了正确性
struct UnionSet {//结构体存储每个点的祖先,进行并查集操作
int f[MAXN];
UnionSet(int n) {
for (int i = 1; i <= n; i++) {
f[i] = i;//一开始,建立首项为1,末项为n,公差为1的等差数列,表示没有边连接时,每个点都是自己的唯一祖先
}
}
UnionSet(){}
void uni(int x, int y) {//连接x,y,统一祖先,使其连通
f[find(x)] = find(y);
}
bool query(int x, int y) {//判断祖先是否是一个,借此判断是否连通
return find(x) == find(y);
}
int find(int x) {//这是寻找最大祖先
return f[x] == x?x/*最大祖先就是x本身*/:(f[x] = find(f[x]))/*祖先是x的某个祖先的最大祖先*/;
}
} us;
void kruskal() {
sort(edges, edges+m);//升序排列边
us = UnionSet(n);//初始化并查集
for (int i = 0; i < m && etop <= n * 2 - 2; i++) {//从0到m枚举所有m条边,(因为是双向图,所以存树边的e存的应该是2n-2条边,这时也就成了一棵树)。
Edge/*存边的结构体*/ &e = edges[i];//暂存当前操作的边
if (!us.query(e.from, e.to)) {//只要终点和起点不连通
insert(e.from, e.to, e.dist);//将该边加入树中
insert(e.to, e.from, e.dist);
us.uni(e.from, e.to);//并查集操作,使起点和终点连通
}
}
}
最短路
Floyd
多源最短路,可以求出任意两点之间的最短路,记得一开始要预处理数组,两点间有边的为边权值,无边的就是inf,不然就不会得到正确结果(另外:Floyd可以处理负边权)
思路就是利用中间点,不断更新两点间的最短路,当所有点都枚举过后,所有路径也都被枚举了一遍,这样再筛出最短的,就实现了最短路程的计算
因为三层循环都是n级的,所以复杂度为O(n3)
int d[MAXN][MAXN];
void floyd() {
for (int k = 1; k <= n; k++) {//枚举中间点
for (int i = 1; i <= n; i++) {//枚举起点
for (int j = 1; j <= n; j++) {//枚举终点
if (i != j && i != k && j != k)//k,i,j两两不同
d[i][j]=min(d[i][j],d[i][k]+d[k][j]);//用i,k和j,k距离更新i,j距离(这是有向图写法,无向图别忘了对称处理(d[j][i]=d[i][j]))
}
}
}
}
Dijkstra
单源最短路,可求一个点到其他任意点的最短路,没有负边权就用它(因为不支持负边权,会WA)。在一般的最短路问题中,几乎都是单源,所以Dijkstra是最广泛的最短路算法。
和上面的prim相似,用优先队列升序存储被更新后的点,用这些点的最短路更新与其相邻点的最短路,直到最后队空为止
因为每个点的初始值都是inf,所以一次Dijkstra下来,所有点的最短路都被更新(没有连通路径的除外),所以每个点都会更新一次相邻节点,会遍历所有相连的边一次,如果是完全图,每个点都连着(n-1)条边,每个点都更新,复杂度为O(n2);如果图稀疏,就是O(mlogn)的复杂度。也就是说会讨论从起点开始中间经过任意点的路径,不重不漏,保证了结果的正确性,
int d[MAXN];//存储起点到任一点的最短路
bool vis[MAXN];
priority_queue<pair<int, int>, vector<pair<int, int> >,greater<pair<int,int> > > q;//利用优先队列存储被更新的边
int dijkstra(int s, int t){
memset(d, 0x3f, sizeof(d));//赋初值为inf,保证被更新
d[s] = 0;//当然,自己到自己的最短路为0
q.push(make_pair(0, s));//先将自己入队,试图更新其他点
while(!q.empty()){//只要队列不空
int now = q.top().second;//那就利用队首边来更新接下来的边,用now来存储当前边的终点,更新以now为起点的边的终点的最短路
q.pop();//出队
if(!vis[now]){//判断now是否访问过(每个节点只能将别的点更新一次,避免了负环的死循环,但是仍然会WA负环)
vis[now] = true;//访问now
for(int i = he[now]; i; i = ne[i]){//遍历now的所有相连节点
Edge& e = ed[i];//暂存now到当前相邻节点的边
if(d[e.to] > d[now] + e.dist) {//只要更优就更新
d[e.to] = d[now] + e.dist;//将起点到e.to的最短路更新为到now的最短路加上e的权值
q.push(make_pair(d[e.to], e.to));//接下来用刚被更新过的e.to来更新其他点
}
}
}
}
return d[t] == INF ? -1 : d[t];//如果终点最短路没被更新,那就没有路径连通,就输出-1;否则输出最短路
}
SPFA
和Dijkstra的最大区别是:SPFA可处理负边权,并且可以判断负环,但是时间复杂度最坏为O(nm),比Dijkstra的O(n2)都要慢,所以没有负边还是用Dijkstra
bool inq[MAXN];
queue<int> q;
inline int spfa(int s, int t) {
q.push(s);//将起点入队
inq[s] = true;//这是一个存点i是否在队里的bool数组
memset(d, 0x3f, sizeof(d));//还是将所有最短路初始化为inf
d[s] = 0;//自己到自己的最短路当然是0
while (!q.empty()) {//只要队不空
int now = q.front(); q.pop();//now暂存队首,弹出队首
inq[now] = false;//弹出队首后,队首不在队列
for (int i = he[now]; i; i = ne[i]) {//遍历所有相邻节点
Edge &e = ed[i];//用e暂存当前以now为起点的边
if (d[now] + e.dist < d[e.to]) {//只要e的终点的最短路可以被now更新
d[e.to] = d[now] + e.dist;//先更新最短路
if (!inq[e.to]) {//只要e的终点不在队里,就入队,去更新相邻点的最短路
q.push(e.to);
inq[e.to] = true;
}
}
}
}
return d[t] == INF ? -1 : d[t];//和Dijkstra一样的判断输出
}