图论小学习(一)

友情链接

图的存储

邻接矩阵

使用二维数组存储,\(i\)\(j\) 有连边则标记为 \(1\),否则置 \(0\)

参考代码
vector<vector<int> >adj;
void init(int n){//n个点
    adj.resize(n);
    for(int i=0;i<n;i++) adj[i].resize(n);
}
void add(int u,int v){
    adj[u][v]++;
    //adj[v][u]++;无向图时添加
}

链式前向星

类似于链表的方式存储每条边的终点,长度和下标

参考代码
int h[N<<1],cnt;
struct node{
    int to,nxt;
    ll w;
}adj[N<<1];
void add(int u,int v,ll w){
    cnt++;
    adj[cnt].to=v;
    adj[cnt].w=w;
    adj[cnt].nxt=h[u];
    h[u]=cnt;
}//单向加边

邻接表

利用vector容器的性质存储

参考代码
vector<vector< pair<int,ll> > >adj;
//vector< pair<int,ll> >adj[N];有时使用这种简便写法
void add(int u,int v,ll w){
    adj[u].push_back({v,w});
    //adj[v].push_back({u,w});无向图建双边
}

图的遍历

DFS深度优先搜索

一直遍历到图中"最深"的节点,当某个节点的连接节点均已遍历过时回溯

void dfs(int u){
    for(auto &v:G[u]){
        if(vis[v]) continue;
        vis[v]=1;
        dfs(v);
    }
}

BFS广度优先搜索

优先遍历每个点的所有连点,然后再考虑下一层

void bfs(int s){
    queue<int>Q;
    memset(tim,0x3f,sizeof tim);
    tim[s]=0;//到起点的最早时间是0
    Q.push(s);//起点入队
    while(!Q.empty()){
        int u=Q.front();
        Q.pop();
        for(auto &v:G[u]){
            if(tim[u]+1<tim[v]){//只有到v的最早时间小于tim[v]才考虑加入队列
                Q.push(v);
                tim[v]=tim[u]+1;
            }
        }
    }
}

拓扑排序

解决有向无环图顺序的一种算法

步骤

构造拓扑序列步骤
1.从图中选择一个入度为零的点。
2.输出该顶点,从图中删除此顶点及其所有的出边。
3.重复上面两步,直到所有顶点都输出,拓扑排序完成,或者图中不存在入度为零的点,此时说明图是有环图,拓扑排序无法完成,陷入死锁。

#include<bits/stdc++.h>
using namespace std;
const int N=1e5+10;
int num,n,in[N];
queue<int>Q;
vector<int>G[N];
void add(int x,int y){
    G[x].push_back(y);
}
void dfs(int u){
    in[u]--;
    for(int i=0;i<G[u].size();i++){
        int v=G[u][i];
        in[v]--;
    }
}
int main(){
    ios::sync_with_stdio(false);
    cin.tie(0),cout.tie(0);
    cin>>n;
    for(int i=1;i<=n;i++){
        while(cin>>num&&num!=0){
            in[num]++;
            add(i,num);
        }
    }
    for(int i=1;i<=n;i++){
        if(in[i]==0){
            Q.push(i);
            break;
        }
    }
    while(!Q.empty()){
        int s=Q.front();
        cout<<s<<" ";
        dfs(s);
        for(int i=1;i<=n;i++){
            if(in[i]==0){
                Q.push(i);
                break;
            }
        }
        Q.pop();
    }
    cout<<endl;
    return 0;
}

应用

dp计算依赖状态时考虑拓扑序的转移,内向基环树类似简单图找环,任务调度

欧拉图

欧拉路径

欧拉路径(Eulerian path)是经过图中每条边恰好一次的路径。如果一个图中不存在欧拉回路但是存在欧拉路径,则这个图被称为半欧拉图(semi-Eulerian graph)。

欧拉回路

欧拉回路(Eulerian circuit)是经过图中每条边恰好一次的回路。 如果一个图中存在欧拉回路,则这个图被称为欧拉图(Eulerian graph)。

性质

对于连通图 G,以下三个性质是互相等价的:
G 是欧拉图;
G 中所有顶点的度数都是偶数(对于有向图,每个顶点的入度等于出度);
G 可被分解为若干条不共边回路的并。

参考代码

#include<bits/stdc++.h>
using namespace std;
#define endl '\n'
typedef long long ll;
const int N=2e5+5;
int d[N];
vector<pair<int,int>>G[N];
bool vis[N];
vector<int>path;
void dfs(int u){
    for(auto &[v,i]:G[u]){
        if(vis[i]) continue;
        vis[i]=1;
        dfs(v);
    }
    path.push_back(u);
}
int main(){
    cin.tie(0)->ios::sync_with_stdio(false);
    int n,m,s=1;
    cin>>n>>m;
    for(int i=1;i<=m;i++){
        int x,y;cin>>x>>y;
        ++d[x],++d[y];
        G[x].push_back({y,i});
        G[y].push_back({x,i});
    }
    for(int i=1;i<=n;i++){
        if(d[i]&1) {s=i;break;}
    }
    dfs(s);
    for(auto &x:path) cout<<x<<' ';
    cout<<endl;
    return 0;
}

最小生成树

我们定义无向连通图的 最小生成树(Minimum Spanning Tree,MST)为边权和最小的生成树

Prim

Prim 算法是另一种常见并且好写的最小生成树算法。该算法的基本思想是从一个结点开始,不断加点

参考代码
#include<bits/stdc++.h>
using namespace std;
#define endl '\n'
typedef long long ll;
const int inf=0x3f3f3f3f;
const int N=5e5+10;
int dis[N];
int n,m,cntn;
bool vis[N];
ll ans;
struct edge{
    int u,w;
};
vector<edge>G[N];
void add(int x,int y,int w){
    G[x].push_back({y,w});
    G[y].push_back({x,w});
}
struct node{
    int u,w;
    friend bool operator <(node a,node b){
        return a.w>b.w;
    }
}a[N];
priority_queue<node>Q;
void Prim(){
    cntn=n;
    dis[1]=0;
    Q.push({1,0});
    while(!Q.empty()){
        auto [u,w]=Q.top();
        Q.pop();
        if(vis[u]) continue;
        cntn--;
        ans+=w;
        vis[u]=1;
        for(int i=0;i<G[u].size();i++){
            auto [v,w]=G[u][i];
            if(vis[v]) continue;
            if(w<dis[v]){
                dis[v]=w;
                Q.push({v,dis[v]});
            }
        }
    }
}
int main(){
    ios::sync_with_stdio(false);
    cin.tie(0),cout.tie(0);
    cin>>n>>m;
    memset(dis,0x3f,sizeof(dis));
    for(int i=1;i<=m;i++){
        int x,y,w;cin>>x>>y>>w;
        add(x,y,w);
        add(y,x,w);
    }
    Prim();
    if(cntn==0) cout<<ans<<endl;
    else cout<<"orz"<<endl;//无解
    return 0;
}

Kruskal

Kruskal 算法是一种常见并且好写的最小生成树算法,由 Kruskal 发明。该算法的基本思想是从小到大加入边,是个贪心算法。

参考代码
#include<bits/stdc++.h>
using namespace std;
#define endl '\n'
typedef long long ll;
ll ans;int n,m,cntn;
const int N=2e5+10;
int f[N];
int find(int x){
    if(x==f[x]) return x;
    return f[x]=find(f[x]);
}
struct node{
    int x,y,w;
    friend bool operator <(node a,node b){
        return a.w<b.w;
    }
}a[N];
void kruskal(){
    cntn=n;
    for(int i=1;i<=m;i++){
        int fx=find(a[i].x);
        int fy=find(a[i].y);
        if(fx!=fy){
            f[fx]=fy;
            cntn--;
            ans+=a[i].w;
            if(cntn==1) break;
        }
    }
}
int main(){
    ios::sync_with_stdio(false);
    cin.tie(0),cout.tie(0);
    cin>>n>>m;
    for(int i=1;i<=n;i++) f[i]=i;
    for(int i=1;i<=m;i++){
        int x,y,w;
        cin>>a[i].x>>a[i].y>>a[i].w;
    }
    sort(a+1,a+1+m);
    kruskal();
    if(cntn==1){
        cout<<ans<<endl;
    }
    else cout<<"orz"<<endl;//无解
    return 0;
}

最短路

求解图上两点最短路径的算法

Bellman–Ford

Bellman–Ford 算法是一种基于松弛(relax)操作的最短路算法,可以求出有负权的图的最短路,并可以对最短路不存在的情况进行判断。基于公式\(dis(v) = \min(dis(v), dis(u) + w(u, v))\)。在最短路存在的情况下,由于一次松弛操作会使最短路的边数至少 +1,而最短路的边数最多为 \(n-1\),因此整个算法最多执行 \(n-1\) 轮松弛操作。故总时间复杂度为 \(O(nm)\)

参考代码
struct edge{
    int u,v,w;
};
vector<edge>E;
int dis[N],n;
bool bellmanford(int s){
    memset(dis,0x3f,sizeof(int)*(n+1));
    dis[s]=0;
    bool f=false;//当前是否发生松弛操作
    for(int i=1;i<=n;i++){
        f=false;
        for(int j=0;j<E.size();j++){
            int u=E[j].u;
            int v=E[j].v;
            int w=E[j].w;
            if(dis[u]==0x3f3f3f3f) continue;
            if(dis[v]>dis[u]+w){
                dis[v]=dis[u]+w;
                f=true;
            }
        }
        if(!f) break;
    }
    return f;
}

Spfa

很多时候我们并不需要那么多无用的松弛操作。很显然,只有上一次被松弛的结点,所连接的边,才有可能引起下一次的松弛操作。
那么我们用队列来维护「哪些结点可能会引起松弛操作」,就能只访问必要的边了。
ps:Spfa还有栈,堆,双端队列等优化形式,但最劣均是O(n·m)的复杂度

参考代码
struct edge{
    int v,w;
};
vector<edge>E[N];
int dis[N],cnt[N],n;
bool vis[N];//记录每个点是否在队列中
queue<int>Q;
bool spfa(int s){
    memset(dis,0x3f, (n+1)*sizeof(int));
    dis[s]=0;vis[s]=1;
    Q.push(s);
    while(!Q.empty()){
        int u=Q.front();
        Q.pop();
        vis[u]=0;
        for(auto &e:E[u]){
            int v=e.v;
            int w=e.w;
            if(dis[v]>dis[u]+w){
                dis[v]=dis[u]+w;
                cnt[v]=cnt[u]+1;
                if(cnt[v]>=n) return false;//存在负环
                if(!vis[v]) Q.push(v),vis[v]=1;
            }
        }
    }
    return true;
}
卡spfa

知乎链接

负环判定

SPFA 也可以用于判断 s 点是否能抵达一个负环,只需记录最短路经过了多少条边,当经过了至少 n 条边时,说明 s 点可以抵达一个负环。

Dijkstra(非负权边单源最短路径)

将结点分成两个集合:已确定最短路长度的点集(记为 S 集合)的和未确定最短路长度的点集(记为 T 集合)。一开始所有的点都属于 T 集合。
初始化 \(dis(s)=0\),其他点的 \(dis\) 均为 \(+\infty\)
然后重复这些操作:
1.从 T 集合中,选取一个最短路长度最小的结点,移到 S 集合中。
2.对那些刚刚被加入 S 集合的结点的所有出边执行松弛操作。
3.直到 T 集合为空,算法结束。

时间复杂度

朴素的实现方法为每次 \(2\) 操作执行完毕后,直接在 \(T\) 集合中暴力寻找最短路长度最小的结点。2 操作总时间复杂度为 \(O(m)\),1 操作总时间复杂度为 \(O(n^2)\),全过程的时间复杂度为 \(O(n^2 + m) = O(n^2)\)

可以用堆来优化这一过程:每成功松弛一条边 \((u,v)\),就将 \(v\) 插入堆中(如果 \(v\) 已经在堆中,直接执行 Decrease-key),1 操作直接取堆顶结点即可。共计 \(O(m)\) 次 Decrease-key,\(O(n)\) 次 pop,选择不同堆可以取到不同的复杂度。堆优化能做到的最优复杂度为 \(O(n\log n+m)\),能做到这一复杂度的有斐波那契堆等。

特别地,可以使用优先队列维护,此时无法执行 Decrease-key 操作,但可以通过每次松弛时重新插入该结点,且弹出时检查该结点是否已被松弛过,若是则跳过,复杂度 \(O(m\log n)\),优点是实现较简单。

这里的堆也可以用线段树来实现,复杂度为 \(O(m\log n)\),在一些特殊的非递归线段树实现下,该做法常数比堆更小。并且线段树支持的操作更多,在一些特殊图问题上只能用线段树来维护。

在稀疏图中,\(m = O(n)\),堆优化的 Dijkstra 算法具有较大的效率优势;而在稠密图中,\(m = O(n^2)\),这时候使用朴素实现更优。

参考代码
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=5e5+10;
const int inf=0x7fffffff;
struct edge{
    int nxt,to,w;
}a[N];
int cnt,h[N],dis[N];
bool vis[N];
int n,m,s;
void add(int x,int y,int w){
    cnt++;
    a[cnt].to=y;
    a[cnt].w=w;
    a[cnt].nxt=h[x];
    h[x]=cnt;
}
struct node{
    int u,w;
    friend bool operator<(node a,node b){
        return a.w>b.w;
    }
};
priority_queue<node>Q;
void Dijkstra(){
    for(int i=1;i<=n;i++) dis[i]=inf;
    dis[s]=0;
    Q.push({s,0});
    while(!Q.empty()){
        auto [u,w]=Q.top();
        Q.pop();
        if(vis[u]) continue;
        vis[u]=1;
        for(int i=h[u];i;i=a[i].nxt){
            int v=a[i].to;
            if(dis[v]>dis[u]+a[i].w){
                dis[v]=dis[u]+a[i].w;
                Q.push({v,dis[v]});
            }
        }
    }
}
int main(){
    ios::sync_with_stdio(false);
    cin.tie(0),cout.tie(0);
    cin>>n>>m>>s;
    for(int i=1;i<=m;i++){
        int x,y,w;
        cin>>x>>y>>w;
        add(x,y,w);
    }
    Dijkstra();
    for(int i=1;i<=n;i++) cout<<dis[i]<<' ';
    cout<<endl;
    return 0;
}

Floyed(多源最短路径)

类似于动态规划的思想,第 \(k\)\(x\)\(y\) 的最短路径是由上一层 \(dis_{x,y} = 经过方程min(dis_{x,y}, dis_{x,k} + dis_{k,y})\)转移得到,每层转移复杂度是 \(O(n^2)\) 的,一共转移 \(n\) 次综上时间复杂度是 \(O(N^3)\),空间复杂度是 \(O(N^2)\)

参考代码
for (k = 1; k <= n; k++) {
  for (x = 1; x <= n; x++) {
    for (y = 1; y <= n; y++) {
      f[x][y] = min(f[x][y], f[x][k] + f[k][y]);
    }
  }
}
求有向图的传递闭包

我们只需要按照 Floyd 的过程,逐个加入点判断一下。

只是此时的边的边权变为 \(1/0\),而取 \(\min\) 变成了 运算。

再进一步用 \(bitset\) 优化,复杂度可以到

\(O(\frac{n^3}{w})\)

// std::bitset<SIZE> f[SIZE];
for (k = 1; k <= n; k++)
  for (i = 1; i <= n; i++)
    if (f[i][k]) f[i] = f[i] | f[k];

二分图

二分图,又称二部图,英文名叫 Bipartite graph。
二分图是什么?节点由两个集合组成,且两个集合内部没有边的图。
换言之,存在一种方案,将节点划分成满足以上性质的两个集合。

二分图的判定

一个图是二分图,当且仅当不存在不存在长度为奇数的环

二分图的最大匹配

给定一个二分图 G,即分左右两部分,各部分之间的点没有边连接,要求选出一些边,使得这些边没有公共顶点,且边的数量最大。

参考代码
#include<bits/stdc++.h>
using namespace std;
#define endl '\n'
typedef long long ll;
const int N=505;
vector<int>G[N];
int match[N];
bool vis[N];
bool dfs(int u){
    for(auto &v:G[u]){
        if(vis[v]) continue;
        vis[v]=1;
        if(!match[v]||dfs(match[v])){//女方没有配偶或者女方的配偶可以拥有其他配偶,那就和她组成配偶关系
            match[v]=u;
            return 1;
        }
    }
    return 0;
}
int main(){
    cin.tie(0)->ios::sync_with_stdio(false);
    int n,m,e;cin>>n>>m>>e;
    for(int i=1;i<=e;i++){
        int u,v;
        cin>>u>>v;
        G[u].push_back(v);
    }
    ll ans=0;
    for(int i=1;i<=n;i++) {
        ans+=dfs(i);
        fill(vis+1,vis+1+n,false);
    }
    cout<<ans<<endl;
    return 0;
}
posted @ 2025-06-07 19:51  usedchang  阅读(10)  评论(0)    收藏  举报