蒟蒻的图论学习

蒟蒻的图论学习

  • 图的表示

邻接矩阵存图(适用于稠密图,常数复杂度内查询两点间是否有边,但空间复杂度较高)

#include<bits/stdc++.h>
using namespace std;
const int maxn=1e5+7;
int G[maxn][maxn];
int main(){//存无属性的无向图
    int V,E;
    cin>>V>>E;//V个顶点,E条边
    memset(G,0,sizeof(G));//初始化为两点之间无边
    for(int i=1;i<=E;i++){
        int a,b;//a,b之间有一条边
        cin>>a>>b;
        G[a][b]=G[b][a]=1;//1表示有边
    }
    return 0;
}
#include<bits/stdc++.h>
using namespace std;
const int maxn=1e5+7;
const int inf=0x3f3f3f3f;
int G[maxn][maxn];
int main(){//存有属性的无向图
    int V,E;
    cin>>V>>E;//V个顶点,E条边
    memset(G,inf,sizeof(G));//初始化为两点之间边权为无穷大(可以判断是否有边)
    for(int i=1;i<=E;i++){
        int a,b,val;//a,b之间有一条权值为val的边
        cin>>a>>b>>val;
        G[a][b]=G[b][a]=val;//边a,b权值为val
    }
    return 0;
}

邻接表存图(适用于稀疏图,不会浪费空间,但查询边时往往需要遍历整个图,较为麻烦)

#include<bits/stdc++.h>
using namespace std;
const int maxn=1e5+7;
vector<int>G[maxn];//边上没有属性
int main(){//邻接表存图//将与某个点相连的点存入这个点的vector中
    int V,E;
    cin>>V>>E;
    for(int i=0;i<E;i++){
        //从s向t连边(有向图)
        int s,t;
        cin>>s>>t;
        G[s].push_back(t);
        //G[t].push_back(s)//无向图要再从t向s连边
    }
    return 0;
}
#include<bits/stdc++.h>
using namespace std;
const int maxn=1e5+7;
struct edge{//边上有属性的情况
    int to,cost;
};
vector<edge>G[maxn];
int main(){//邻接表存图
    int V,E;
    cin>>V>>E;
    for(int i=0;i<E;i++){
        int s,t,cost; //从s向t连边(有向图)
        edge e;
        cin>>s>>t>>cost;//从s到t有一条权值为cost的边
        e.to=t;//s相邻的点为t
        e.cost=cost;//s到t的权值为cost
        G[s].push_back(e);//将t点信息存入s的vector
        /*e.to=s;
         * e.cost=cost;
         * G[t].push_back(e)//无向图两条边相同*/
    }
    return 0;
}

  • 相关问题


二分图判定

给定一个具有n个点的图,要给图上每个顶点染色,并且相邻点颜色不同,是否最多用两种颜色进行染色?保证没有重边和自环。(1<=n<=1000)

key:如果只用2种颜色,确定一个点后,它相邻的点颜色也确定了,任选一个顶点出发,依次确定相邻的点的颜色,就可以判断是否可以被2种颜色染色了。可以用dfs简单实现。

代码:

#include<bits/stdc++.h>
using namespace std;
const int maxn=1e5+7;
int V,E;//V个点,E条边
vector<int>G[maxn];//图
int color[maxn];//顶点i的颜色
bool dfs(int v,int c){
    color[v]=c;//把顶点v染成c
    for(int i=0;i<G[v].size();i++){
        if(color[G[v][i]]==c) return false;//如果相邻顶点颜色相同返回0
        if(color[G[v][i]]==0&&!dfs(G[v][i],-c)) return false;//如果相邻点未染色,则染色为-c
    }
    return true;//所有顶点都被染色,返回true
}
void solve(){
    for(int i=0;i<V;i++){
        if(color[i]==0){//任选一个未被染色的点
            if(!dfs(i,1)){
                cout<<"NO"<<endl;
                break;
            }
        }
    }
    cout<<"YES"<<endl;
}
int main(){
    cin>>V>>E;
    for(int i=0;i<E;i++){//邻接表存图
        int s,t;
        cin>>s>>t;
        G[s].push_back(t);
        G[t].push_back(s);
    }
    solve();
    return 0;
}

最短路问题

单源最短路问题1(Bellman-Ford)

复杂度O(VE)

单源最短路问题是固定一个起点,求它到其它所有点的最短路问题。
记从起点s到顶点i的最短距离为的d[i],则下述等式成立:d[i]=min{d[j]+(从j到i的边权)|e=(j,i)∈E}

Bellman-ford算法适用于单源最短路径,图中边的权重可为负数即负权边,但不可以出现负权环。

负权边:权重为负数的边。
负权环:源点到源点的一个环,环上权重和为负数。

求s点到所有点的最短路

#include<bits/stdc++.h>
using namespace std;
const int maxn=1e5+7;
const int inf=0x3f3f3f3f;
struct edge {
    int from,to,cost;
}es[maxn];//图
int d[maxn];
int V,E;//V个点,E条边
void Bellman_Ford(int s){//求解从s出发到所有点的最短距离
    for(int i=0;i<=V;i++){
        d[i]=inf;
    }
    d[s]=0;
    while(1){//最多循环n-1次,超过n-1次则存在负环(故该算法能够判断是否存在负环)
        bool update=false;
        for(int i=0;i<E;i++){
            edge e=es[i];
            if(d[e.from]!=inf){//松弛
                d[e.to]=min(d[e.to],d[e.from]+e.cost);
                update=true;
            }
        }
        if(!update) break;
    }
}

判断负环

#include<bits/stdc++.h>
using namespace std;
const int maxn=1e5+7;
const int inf=0x3f3f3f3f;
struct edge {
    int from,to,cost;
}es[maxn];//图
int d[maxn];
int V,E;//V个点,E条边
bool Bellman_Ford(){//判断负环
    memset(d,0,sizeof(d));
    for(int i=0;i<V;i++){//与求最短路中while作用相同
        for(int j=0;j<E;j++){
            edge e=es[j];
            if(d[e.to]>d[e.from]+e.cost){
                d[e.to]=d[e.from]+e.cost;
                //如果第V次仍然更新,则存在负环
                if(i==V-1) return true;
            }
        }
    }
    return false;
}

单源最短路问题2(Dijkstra)

让我们考虑一下没有负边的情况,在Bellman-Ford算法中,如果d[i]还不是最短距离的话,即使进行d[j]=d[i]+(从i到j边的权值)的更新,d[j]也不会变成最短距离。并且即使d[i]没有变化,每一次循环也要检查一遍从i出发的所有边。这显然是很浪费时间的。因此可以对算法做如下修改:

1.找到最短距离已经确定的顶点,从它出发更新相邻顶点的最短距离。
2.此后不需要再关心1中“最短距离已经确定的顶点”。

由此我们就可以得到效率更高的单源最短路算法:Dijkstra算法
具体详解请见Dijkstra图文详解

邻接矩阵实现Dijkstra

#include<bits/stdc++.h>
using namespace std;
const int maxn=1e5+7;
const int inf=0x3f3f3f3f;
int cost[maxn][maxn];//邻接矩阵存图,cost[u][v]表示e=(u,v)的权值(不存在时这条边设为inf)
int d[maxn];//顶点s出发的最短距离
bool used[maxn];//已经使用过的点
int V;//顶点数
//求s出发到各个顶点的最短距离
void Dijkstra(int s){
    fill(d,d+V,inf);
    fill(used,used+V,false);
    d[s]=0;
    while(true){
        int v=-1;
        //从尚未使用过的顶点中选择一个距离最小的点
        for(int u=0;u<V;u++){
            if(!used[u]&&(v==-1||d[u]<d[v])){
                v=u;
            }
        }
        if(v==-1) break;
        used[v]=true;
        for(int u=0;u<V;u++){//松弛
            d[u]=min(d[u],d[v]+cost[v][u]);
        }
    }
}

使用邻接矩阵实现Dijkstra算法的复杂度是O(V ^2)。使用邻接表的话,更新最短路只需要访问每条边一次即可,此部分复杂度为O(E),但每次要枚举所有顶点来查找下一个使用的顶点,因此最终的复杂度还是O(V ^2)。在E比较小时,大部分时间花在了查找下一个使用的顶点上,因此需要使用合适的数据结构进行优化。
需要优化的是数值的插入(更新)和取出最小值两个操作,使用堆(priority_queue)维护每个顶点当前的最短距离,这样堆中的元素一共有O(V)个,更新和取值操作有O(E)次,因此整个算法的复杂度是O(E logV)

以下是堆优化的Djikstra代码实现

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef pair<int,int> P;//fisrt是最短距离,second是定点编号
const int maxn=1e5+7;
const int inf=0x3f3f3f3f;
struct edge{
    int to,cost;
};
int V;
vector<edge> G[maxn];
int d[maxn];
void Dijkstra(int s){
    //通过指定greater<P>参数,堆按照first从小到大取值
    priority_queue<P,vector<P>,greater<P> > que;
    fill(d,d+V,inf);//初始化
    d[s]=0;
    que.push(P(0,s));
    while(!que.empty()){
        P p=que.top();
        que.pop();
        int v=p.second;
        if(d[v]<p.first)continue;
        for(int i=0;i<G[V].size();i++){//松弛
            edge e=G[v][i];
            if(d[e.to]>d[v]+e.cost){
                d[e.to]=d[v]+e.cost;
                que.push(P(d[e.to],e.to));
            }
        }
    }
}

相对于Bellman-Ford算法的O(VE)复杂度,Dijkstra算法的复杂度是O(E logV),可以更加高效的计算最短路的长度。但是在图中存在负边的情况下,Djikstra算法就无法正确求解,还是需要使用Bellman-Ford算法。

任意两点间的最短路问题(Floyd-Warshall)

求解所有两点间的最短路的问题叫做任意两点间的最短路问题。让我们试着用DP来解决任意两点间的最短路问题。只使用顶点0 ~ k和i,j的情况下,记i到j的最短路长度为d[k+1][i][j]。k=-1时,认为使用i和j,所以d[0][i][j]=cost[i][j]。接下来让我们把只使用0 ~ k的问题归约到只使用0 ~ k-1的问题上。

只使用0 ~ k时,不经过点k的情况下d[k][i][j]=d[k-1][i][j]。
经过点k的情况下d[k][i][j]=d[k-1][i][k]+d[k-1][k][j]。
于是我们可以得到状态转移方程:
d[k][i][j]=min(d[k-1][i][j],d[k-1][i][k]+d[k-1][k][j])。
我们还可以进行滚动数组优化,使用一个二维数组:
d[i][j]=min(d[i][j],d[i][k]+d[k][j])来实现。

这个算法叫做Floyd-Warshall算法,可以在O(V ^3)时间里求出所有两点间的最短路长度。Floyd-Warshall算法与Bellman-Ford算法一样,可以处理负权边的情况。而判断图中是否有负圈,只需要检查是否存在d[i][j]是负数的顶点i就好了。

Floyd-Warshall算法代码实现:

int d[maxn][maxn];//d[u][v]表示边e(u,v)的权值(不存在时设为inf,不过d[i][i]=0)
int V;
void warshall_floyd(){
	for(int k=0;k<V;k++){
		for(int i=0;i<V;i++){
			for(int j=0;j<V;j++){
				d[i][j]=min(d[i][j],d[i][k]+d[k][j]);
			}
		}
	}
}
路径还原

最小生成树

给定一个无向图,如果它的某个子图中任意两个顶点都互相联通并且是一棵树,那么这棵树就叫做生成树(SpanningTree)。如果边上有权值,那么使得边权和最小的生成树叫做最小生成树(MST,Minimum Spanning Tree)

最小生成树问题1(Prim\加点法)

Prim算法和Dijkstra算法十分相似,都是从某个顶点出发,不断添加边的算法。
首先,我们假设有一颗只包含一个顶点v的树T,然后贪心地选取T和其他顶点之间相连的最小权值的边,并把它加入T中。不断进行这个操作,就可以得到一棵生成树了。

Prim代码实现:

#include<bits/stdc++.h>
using namespace std;
const int maxn=1e5+7;
const int inf=0x3f3f3f3f;
int cost[maxn][maxn];//图
int mincost[maxn];//当前点到每一个点的最小距离
bool used[maxn];
int V;
int Prim(){
    for(int i=0;i<V;i++){//初始化
        mincost[i]=inf;
        used[i]=false;
    }
    mincost[0]=0;
    int res=0;
    while(true){
        int v=-1;
        //从不属于X的顶点中选取X到其权值最小的顶点
        for(int u=0;u<V;u++){
            if(!used[u]&&(v==-1||mincost[u]<mincost[v])) v=u;
        }
        if(v==-1) break;
        used[v]=true;//把顶点v加入X
        res+=mincost[v];//把长度加入结果
        for(int u=0;u<V;u++){//选取最小权值的边
            mincost[u]=min(mincost[u],cost[v][u]);
        }
    }
    return res;
}
最小生成树问题2(Kruskal\加边法)

Kruskal算法按照边的权值顺序从小到大查看一遍,如果不产生圈(重边也算在内),就把当前这条边加入到生成树中。
接下来我们介绍如何判断是否产生圈。假设现在要把连接顶点u和顶点v的边e加入生成树中。如果加入之前u和v不在同一个连通分量中,那么加入e不会产生圈。反之如果u和v在同一个连通分量中,那么一定会产生圈。我们可以使用并查集高效判断是否属于同一个连通分量
Kruskal算法在边的排序上最费时,算法的复杂度是O(E logV)。

Kruskal代码实现:

#include<bits/stdc++.h>
using namespace std;
const int maxn=1e5+7;
const int inf=0x3f3f3f3f;
struct edge{
    int u,v,cost;
}es[maxn];
bool cmp(edge e1,edge e2){
    return e1.cost<e2.cost;
}
int V,E;
int father[maxn];//并查集
void init_union_find(int x){
    father[x]=x;
}
int find_set(int x){
    if(x!=father[x]){
        father[x]=find_set(father[x]);
    }
    return father[x];
}
void unite(int x,int y){
    x=find_set(x);
    y=find_set(y);
    if(x!=y){
        father[x]=y;
    }
}
int Kruskal(){
    sort(es,es+E,cmp);
    for(int i=0;i<E;i++){
        init_union_find(es[i].u);
        init_union_find(es[i].v);
    }
    int res=0;
    for(int i=0;i<E;i++){
        edge e=es[i];
        if(find_set(e.u)!=find_set(e.v)){
            unite(e.u,e.v);
            res+=e.cost;
        }
    }
    return res;
}

例题

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值