文章目录
最小生成树不唯一(证明可以查看本文的2.2)
一、最小支撑树的概念
最小生成树实际上就是,将一个图连通起来需要花费的最小代价。即让图中的n个点连通起来,互相能抵达的最小代价。并且由于是树,n个结点边数最少的树的特征是有n-1条边,即最少n-1条边可以使图连通。
注意两个特征:
- 无向带权连通图:保证其子图连通的权值最小
- 支撑树结构:自然就是n-1条边的连通无回路的支撑子图(树的性质)
判断最小支撑树是否存在多种的本质
本质是判断是否能替换中间的某些边使得最小支撑树的权重相同。
判断最小支撑树是否存在多种
二、Prim算法(适用于稠密图)
2.1、算法的基本步骤
①将图中的点分为S和V-S,V是图中的所有顶点,初始时S为空
②随便找一个图中的点,加入到S
③在V-S中找到,到S距离最小的点(跨集合最小的边),加入S,并记录这条边,循环③直至V中所有顶点都被加入到S中。
该算法和Dijkstra算法很像,区别在于Dijkstra找的是V-S中离源点最小的点,而Prim算法找的是离S距离最小的点(边)。如何与Kruskal算法区分,那就记住DP吧,DP算法之间很像,D是Dijkstra算法,P是Prim算法。
时间复杂度分析
:使用优先队列优化,且不延迟出队,修改队列中的元素,每个顶点进入S中一次,每个顶点需要遍历一次边,每个顶点只入队出队一次,因此时间复杂度为O(nlogn+e)
,n是顶点个数,e是边数。
2.2、算法的正确性证明
实际上这需要证明的是,为什么加入的那条边一定在最小生成树中。
假设当前的集合分别被分为一个确定的S和V-S,其跨集合边最小边为(u,v),u在S中,v在V-S中。证明(u,v)一定在某个最小生成树中。
反证法证明
:假如(u,v)不在最小生成树T当中,我们将最小生成树T按照我们之前假设的S和V-S划分,其中u在S中,假设S中只有一个元素,那么这个元素将会是u,由于(u,v)是S和V-S的最小跨集合边,因此删除当前u连向V-S的边,换为(u,v)会使得树的权值更小,与T是最小生成树矛盾,所以(u,v)在最小生成树中。加入S中不止一个元素,假如S和V-S是通过u连接的,那么和上面同理,换成(u,v)会更小,与T是最小生成树矛盾;加入不是通过u连接的,而是通过S中的x和V-S中的y连接的(必定只有一种连接,因为如果有两种连接,则树中存在回路,则不是树),而w(x,y)≥w(u,v),如果w(x,y)=w(u,v),则说明最小生成树至少有两种不同的形态,如果w(x,y)>w(u,v)则删去(x,y)换成(u,v)会使得生成树更小,因此与T是最小生成树矛盾,证毕。
2.3、优化算法的实现
- 使用vector<tuple<int,int,int>> TreeEdge存储最小集合树的边,包含起点,终点和权值。
- 使用S[]表示顶点是否在S中,在S中的顶点不需要被更新
- 使用优先队列
bool Prim(vector<vector<pair<int,int> > > & g,int n,int s){
vector<tuple<int,int,int> > TreeEdge;
priority_queue<pair<int,int>,vector<pair<int,int> >,greater<pair<int,int> > > q;//小根堆 优先距离排序dist:v
vector<int> S(n,0);
vector<int> dist(n,INT_MAX);//距S的距离
vector<int> pre(n,-1);//到S的前驱
q.push({0,s});dist[s]=0;
while(!q.empty()){
int u=q.top().second;
int weight=q.top().first;q.pop();
if(S[u]) continue;//在S中说明是残留物,不在S中的第一个必然是距离S最小的
S[u]=1;//标记为在S中
if(pre[u]!=-1) TreeEdge.emplace_back(pre[u],u,weight);//它放入S中,并保存它到S的边
for(auto & i: g[u]){//新加入的点开始 为 V-S中的小伙伴更新值啦。
int v=i.first;
int w=i.second;
if(S[v]==0&&w<dist[v]){//S[v]==0这里为了方便写上吧。相当于明显得说明,只更新不在S中的顶点到S的距离。
q.push({w,v});//和Dijkstra的区别在于w~ 确实ba
pre[v]=u;
dist[v]=w;
}
}
}
if(TreeEdge.size()!=n-1) return false;
return true;
}
三、Kruskal算法(适用于稀疏图)
3.1、算法的基本步骤
Kruskal算法比Prim算法实现简单,Kruskal只需要将边从小到大排序,然后依次取两个集合中的最小边,直至所有顶点在一个集合中,这里借助并查集
就行。
时间复杂度
:边排序O(eloge),取边并查集时间O(αe),因此时间复杂度为O(eloge)
3.2、算法的正确性证明
这个似乎比较显然,因为最小生成树就是最小嘛,这里相当于贪心算法,每次取边我都保证最小,这样我先被取到的边不能被之前的边替代,且满足最小。实际上我们可以这样考虑,假设我们已经按算法得到一个树T,我们任意选择一条不在T中的边(u,v),我们证明,它不能是树T的边,如果将它加入到T中,则T中产生了一个环,我们证明边(u,v)是这个环中值最大的(可能包含多个值最大),如果(u,v)不是值最大的,则在Kruskal算法中会比环中其它边优先考虑,当我们删除环中比(u,v)大的边后,在考虑时(u,v)时,它是能够被算法加入的,因为删除环后,考虑(u,v)时,u和v必然是在两个并查集中,因此可以加入,所以(u,v)不在T中,则(u,v)是环中最大值。
3.3、算法的实现
class UnionFind {
public:
UnionFind(int c): n(c) {
parent.resize(c);
rank.resize(c, 0); // 初始化秩数组
for (int i = 0; i < c; ++i)
parent[i] = i;
}
int find(int x) {
if (parent[x] != x)
parent[x] = find(parent[x]); // 路径压缩
return parent[x];
}
void Union(int x, int y) {
int rootX = find(x);
int rootY = find(y);
if (rootX != rootY) {
// 按秩合并
if (rank[rootX] > rank[rootY]) {
parent[rootY] = rootX;
} else if (rank[rootX] < rank[rootY]) {
parent[rootX] = rootY;
} else {
parent[rootY] = rootX;
rank[rootX] += 1; // 如果秩相同,则任选一个作为根,并增加其秩
}
minus();
}
}
int n;
private:
vector<int> parent;
vector<int> rank; // 秩
void minus() { --n; } // 减少连通分量的数量
};
bool Kruskal(vector<vector<pair<int,int> > > & g,int n,int s){
UnionFind uni(n);
vector<tuple<int,int,int> > TreeEdge;
vector<tuple<int,int,int> > Edge;
for(int i=0;i<n;++i)
for(auto j:g[i])
Edge.emplace_back(j.second,i,j.first);
sort(Edge.begin(),Edge.end());
for(auto i:Edge){
int u=get<1>(i);
int v=get<2>(i);
int w=get<0>(i);
if(uni.find(u)!=uni.find(v)){
uni.Union(u,v);
TreeEdge.emplace_back(u,v,w);//边 ,权值
}
}
if(uni.n!=1) return false;
return true;
}
3.4、最大生成树
Kruskal从大到小排序即可。prim可以边权取负。
3.5、习题
只需寻找环,删除环中最大边。
四、强连通分量
4.1、基本概念
强连通分量(Strongly Connected Components)。在连通分量中,各个点都互相可及。因此求强联通分量的个数只需判断两个顶点是否可及即可,并且强联通分量具有传递性,所以我们只需一个顶点一个顶点查看其他顶点是否与它在同一个强连通分量,根据等价性这些顶点全都在同一个强连通分量中。
4.2、算法的实现
普通算法不做任何改进时间复杂度为:O( n 2 n^2 n2)
int GetNum(int A[N][N],int n,vector<vector<int> >&scc){//A是可及矩阵,可及矩阵用Warshall算法求出,与Floyd类似,求两两之间是否可及<==>求对称两点的最短路(有最短路即可及)需要O(n³)才能求出
vector<int> vis(n,0);
scc.clear();
for(int i=0;i<n;++i){//求顶点i所在强联通分量
if(vis[i]==0){//该点还未加入强联通分量,则它属于一个新的强联通分量,强联通分量是一个等价关系
scc.push_back({i});
vis[i]=1;
for(int j=0;j<n;++j){//只有和i在一个强联通分量才可能满足if条件,因为是顺序遍历所有点,所以每个点只能遍历一次,不需要判断vis[j]==0
if(j!=i&&A[i][j]==1&&A[j][i]==1){
scc.back().push_back(j);
vis[j]=1;
}
}
}
}
return scc.size();
}
4.3、快速算法
- 时间复杂度为
O(n+e)
Tarjan算法
:基本思想可阅读百度百科Tarjan算法Kosaraju算法
:基本思想可阅读百度百科Kosaraju算法
五、最大流问题
网络流问题就是最大流和最大价值流。
待有生之年续···