蒟蒻的图论学习
邻接矩阵存图(适用于稠密图,常数复杂度内查询两点间是否有边,但空间复杂度较高)
#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;
}