图/最小生成树/最短路径/拓扑排序/Tarjan/关键路径

代码通俗易懂 还有练习题练手

博客后台 - 博客园 (cnblogs.com)

基础

一般用V表示点 E表示边
(无向图)连通表示:任意的两个点都可达 连通分量:极大连通子图
(有向图)强连通:任意2个点都可达 强连通分量:极大强连通子图

在这里插入图片描述

无向图连通最少要n-1条边,无向图不连通最多(n-1)(n-2)/2条边,n是点数

有向图强连通最少要n条边,有向图不强连通最多(n-1)(n-1)条边,n是点数

image-20220821104347341

简单路径:不包括重复节点,例如1->2->3 如果是1->2->3->2->4 不是简单路径

Prim

O(V^2) 适合点少边多的

核心:先随便选一个点,加入S集合【生成树集合】,然后在V集合【未加入生成树集合】选离S集合最近的点,把这个点和那条边加入集合S, 加入之后要通过新加入那个点来更新dist数组

dist数组是【离S集合最近距离数组】,dist[3]表示3号点离S集合最近的距离(假设3号点还没加入S集合)

//邻接矩阵
#include<bits/stdc++.h>
#include<iostream>
#include<algorithm>
using namespace std;

const int MAXN = 1000,INF = 0x3f3f3f3f;//定义一个INF表示无穷大。
int g[MAXN][MAXN],dist[MAXN],n,m,res;
//S集合就是生成树的点集合,一开始为空
//我们用g[][]数组存储这个图,dist[]储存到集合S的距离,res保存结果。
bool book[MAXN];//用book数组记录某个点是否加入到集合S中。

void prim() {
    dist[1] = 0;//把点1加入集合S,点1在集合S中,将它到集合的距离初始化为0
    book[1] = true;//表示点1已经加入到了S集合中
    for(int i = 2 ; i <= n ;i++) dist[i] = min(dist[i],g[i][1]);//用点1去更新dist[]
    //还需要n-1趟加入点操作才能生成这个树 与dijkstra算法类似  每一轮确定一个点加入生成树集合
    for(int i = 2 ; i <= n ; i++){
        int temp = INF;//我们要找与S最近的点
        int t = -1;//用t记录与S最近的点的下标
        for(int j = 2 ; j <= n; j++){
            //如果这个点没有加入集合S,而且这个点到集合的距离小于temp就将下标赋给t
            if(!book[j]&&dist[j]<temp){
                temp = dist[j];
                t = j;
            }
        }
        //如果t==-1,意味着在集合V【未构成树的集合】找不到边连向集合S【已经构成树的集合】
        //生成树构建失败,将res赋值正无穷表示构建失败,结束函数
        if(t==-1) {res = INF ; return ;}
        book[t] = true;//如果找到了这个点,就把它加入集合S
        res+=dist[t];//加上这个点到集合S的距离
        for(int j = 2 ; j <= n ; j++) dist[j] = min(dist[j],g[t][j]);//用新加入的点更新dist[]
    }
}

int main(){
    cin>>n>>m;//读入这个图的点数n和边数m
    for(int i = 1 ; i<= n ;i++){
        for(int j = 1; j <= n ;j++){
            g[i][j] = INF;//初始化任意两个点之间的距离为正无穷(表示这两个点之间没有边)
        }
        dist[i] = INF;//初始化所有点到集合S的距离都是正无穷
    }
    for(int i = 1; i <= m ; i++){
        int a,b,w;
        cin>>a>>b>>w;//读入a,b两个点之间的边
        g[a][b] = g[b][a] = w;//由于是无向边,我们对g[a][b]和g[b][a]都要赋值
    }
    prim();//调用prim函数
    if(res==INF)//如果res的值是正无穷,表示不能该图不能转化成一棵树,输出orz
        cout<<"orz";
    else
        cout<<res;//否则就输出结果res
    return 0;
}
/*
4 4
1 2 1
1 3 3
2 3 2
1 4 3
*/

样例

在这里插入图片描述

Kruskal

选短的边 且选的边能连接2个连通分量 换句话说,选中的边要减少1个连通分量,否者不选

标准的kruskal算法流程应该是基于并查集,而算法导论中有证明添加路径压缩以及按秩合并的并查集的复杂度是阿克曼函数的反函数,这是一个增长非常非常快的函数的反函数,所以你可以认为是一个不超过4的数

初始化 所有点各自为一个森林 这一步是O(n)的

并且把边集进行从小到大排序,这一步如果使用快速排序或者堆排序是O(mlogm)

然后在这一片森林中添加边,我们知道n个点构成的树是有n-1条边,因此需要执行n-1次以下操作

从已经排序的边序列中,挑选长度最短的,且两端不在同一棵树中的一条边,判断是否是同一棵树是利用并查集进行查询,挑出这一条边之后,把两个端点代表的树合并为一棵,即并查集的合并,这也是O(1)的

注意到在选取边的过程中,只要挑选其中的n-1条,因此挑选边的n-1次挑选边的复杂度之和是O(m)的(可以理解为看最后一条连接的边在序列的第几条,最坏情况就是最后一条才能使n个点连通,因此最坏复杂度是O(m)

因此总复杂度为O(n+mlogm) 最坏是O(m+mlogm)

在这里插入图片描述

#include<bits/stdc++.h>
#define MAXN 10000 + 10
#define MAXM 200000 + 10
using namespace std;

int father[MAXN];//father[i]: i号点的祖先,一开始是i本身
struct Node{
    int a, b, price;//a-->b
};

Node a[MAXM];

int cmp(const void *a, const void *b){
    return ((Node*)a)->price - ((Node*)b)->price; //小到大排序
}

void Init(int n){
    for(int i = 1; i <= n; i++){
        father[i] = i;
    }
}

int find(int x){
    int root = x;
    while(root != father[root]) root = father[root];
    //压缩路径
    while(x != root){
        int t = father[x];
        father[x] = root;
        x = t;
    }
    return root;
}

void unite(int x, int y){
    x = find(x);
    y = find(y);
    if(x == y){
        return;
    }
    else{
        father[x] = y;
    }
}

int Kruskal(int n, int m){
    int nEdge = 0, res = 0;
    //将边按照权值从小到大排序
    qsort(a, m, sizeof(a[0]), cmp);
    //n个点 只需合并n-1次
    for(int i = 0; i < m && nEdge != n - 1; i++){
        //判断当前这条边的两个端点是否属于同一集合
        if(find(a[i].a) != find(a[i].b)){
            unite(a[i].a, a[i].b);
            res += a[i].price;
            nEdge++;
        }
    }
    //如果加入边的数量小于n - 1,则表明该图不连通,等价于不存在最小生成树
    if(nEdge < n-1) res = -1;
    return res;
}

int main(){
    int n, m, ans;
    scanf("%d%d", &n, &m); // n点数  m边数

    Init(n);
    for(int i = 0; i < m; i++){
        //边用结构体存起来
        scanf("%d%d%d", &a[i].a, &a[i].b, &a[i].price);
    }

    ans = Kruskal(n, m);
    if(ans == -1) printf("orz\n");
    else printf("%d\n", ans);

    return 0;
}

Dijkstra

每次找到一个距离源点最近的点,然后用这个新找到的点更新路径长度,不能解决带负边的问题

因为每一轮都要扫描一边dist数组找最小的,一共循环V-1轮 邻接矩阵O(V^2) 邻接表O(V^2)

【邻接表+堆优化】在每一轮都要找dist的最小值, 普通做法是扫一遍,也可以用最小堆优化,优化后O(E+VlogV)

外层for循环V-1次,内部找到最小的dist并维护堆O(logV), 邻接表缩短路径一共需要O(E),所以O(E+VlogV),只能用邻接表

在这里插入图片描述

不能解决带负权问题

在这里插入图片描述

Dijkstra邻接矩阵

#include<bits/stdc++.h>
// Dijkstra邻接矩阵
using namespace std;

const int INF=0x3f3f3f3f;// 正无穷
const int Maxsize=1e3+5;// 顶点数
int e[Maxsize][Maxsize];// 邻接矩阵
int book[Maxsize];// 标记
int dis[Maxsize];// 距离表
int n,m;// n:节点;m:边
int v1,v2,w;

int main(){
    scanf("%d%d",&n,&m);
    // 初始化邻接矩阵
    for(int i=1;i<=n;i++){
        for(int j=1;j<=n;j++){
            if(i==j) e[i][j]=0;
            else e[i][j]=INF;
        }
    }
    // 单向图
    for(int i=1;i<=m;i++){
        scanf("%d%d%d",&v1,&v2,&w);
        e[v1][v2]=w;
    }
    // init dis
    for(int i=1;i<=n;i++){
        dis[i]=e[1][i];
    }
    // init book
    for(int i=1;i<=n;i++) book[i]=0;
    book[1]=1;// 程序以源点为1来举例  为1表示dis已经计算结束

    //dijkstra
    // n-1次循环,而非n次循环(因为 1节点自身已确定)
    for(int i=1;i<=n-1;i++) {
        // 找到距离1号顶点最近的顶点(min_index)
        int min_num=INF;
        int min_index=0;
        for(int k=1;k<=n;k++){
            if(book[k]==0 && min_num>dis[k]){
                min_num=dis[k];
                min_index=k;
            }
        }
        if(min_index==0) {
            cout<<"此图不连通";
            return 0;
        }
        book[min_index]=1; // 标记  该点的最短路径已经确认  后面通过该点缩短其他点的路径
        for(int j=1;j<=n;j++){
            if(book[j]==0){
                // 使得距离变得更短
                dis[j]=min(dis[j],dis[min_index]+e[min_index][j]);
            }
        }
    }
    // print
    for(int i=1;i<=n;i++){
        printf("%d ",dis[i]);
    }
    return 0;
}
/* 连通
 5 5
1 2 4
1 3 1
3 2 2
3 4 5
3 5 4   
 */
/* 不连通
6 5
1 2 4
1 3 1
3 2 2
3 4 5
3 5 4
 */

dijkstra邻接表,此邻接表用链表连接起来,但是不方便使用 并加入path数组记录路径

//dijkstra邻接表
#include<bits/stdc++.h>
using namespace std;
int n,m;// n:顶点数   m:边数
const int inf = 0x3f3f3f3f;
const int MaxN = 1000; // 最多节点个数
int dist[MaxN],path[MaxN],book[MaxN];//dist表示距离  path表示前驱节点  book作为是否已经得到结果的标记
struct Node{
    Node* next;
    int num; // 点的编号
    int cost;//边权
};
Node *head[MaxN]={NULL};
//dijkstra 邻接表法
void dijkstra(int k){
    dist[k] = 0;
    book[k] = 1;
    for(Node *h=head[k];h!=NULL;h=h->next){
        dist[h->num] = h->cost;
        path[h->num] = k;
    }
    //n-1轮
    for(int i=0;i<=n-1;i++){
        int min_dist = inf, t = -1;
        for(int j=1;j<=n;j++){
            if(!book[j] && dist[j]<min_dist){
                min_dist = dist[j];
                t = j;
            }
        }
        if(t==-1) return; // 提前结束 说明小于inf的dist没有找到 即有不可到的点
        book[t] = 1;
        //通过t节点 更新dist数组
        for(Node *h = head[t]; h != NULL ; h = h->next){
            int to = h->num;
            if(!book[to]){
                if(dist[t] + h->cost < dist[to]){
                    dist[to] = dist[t] + h->cost;
                    path[to] = t;
                }
            }
        }
    }
}
void dfs(int k){
    if(path[k]==k){
        cout<<k<<" ";
        return;
    }
    dfs(path[k]);
    cout<<k<<" ";
}
int main(){
    //n 点  m边
    scanf("%d%d",&n,&m);

    //【core code】
    for(int i=1;i<=m;i++){
        int a,b,c;
        cin>>a>>b>>c; //a-->b 边权c
        //构造邻接矩阵
        if(head[a]==NULL){
            head[a] = (Node*) malloc(sizeof (Node));
            head[a]->next = NULL;
            head[a]->cost = c;
            head[a]->num = b;
        } else {
            Node *h = head[a];
            while (h->next!=NULL){
                h = h->next;
            }
            h->next = (Node*) malloc(sizeof (Node));
            h = h->next;
            h->next = NULL;
            h->cost = c;
            h->num = b;
        }
    }
    for(int i=1;i<=n;i++){
        dist[i] = inf;
        path[i] = i;
    }
    dijkstra(1);//以k为源点
    for(int i=1;i<=n;i++){
        cout<<dist[i]<<" ";
    }
    cout<<endl;
    //输出到1-->2最短路径的经过的节点
    dfs(2);
    return 0;
}
/*
 5 5
1 2 4
1 3 1
3 2 2
3 4 5
3 5 4
 */
/*
6 5
1 2 4
1 3 1
3 2 2
3 4 5
3 5 4
 */

在这里插入图片描述

dijkstra邻接表:使用数组连接起来,使用方便

image-20220819161227930

struct edge {
    int to,next,dis;
};
edge e[MaxM];//存放边
int head[MaxN],cnt;
void addedge(int a,int b) {
    cnt++;
    e[cnt].to = b; //在cnt位置存放a-->b的边
    e[cnt].next = head[a];
    head[a] = cnt;
}

也可建立逆邻接表

image-20220819161332378

邻接表最方便就是使用vector

//没有权重
vector<int>G[maxn];
int main(){
    int N,M;scanf("%d%d",&N,&M);//点数  边数
    //输出边
    for(int i=1;i<=M;i++){
        int x,y;
        scanf("%d%d",&x,&y);
        G[x].push_back(y);
        G[y].push_back(x);
    }
    //遍历x向外连接的边
    for(int i=0;i<G[x].size();i++){
        int t=G[x][i];//x-->t
    }
}

//有权重
#include<bits/stdc++.h>
using namespace std;
const int maxn=1000000+1,maxm=2000000+1,INF=0x7f7f7f7f,MOD=100003;
struct node{
    int to,dis;
};
vector<node>G[maxn];
int main(){
    int n,m;
    cin>>n>>m;
    for(int i=1;i<=m;i++){
        int a,b,c;
        cin>>a>>b>>c;
        G[a].push_back(node{b,c});
        G[b].push_back(node{a,c});
    }
    cout<<"";
    return 0;
}

Floyd

核心:使用中转点让邻接矩阵的值变小

image-20220819174250227

P1119 灾后重建 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

#include<bits/stdc++.h>
using namespace std;
const int maxn=200+1,INF=50000;
int e[maxn][maxn];
int n,m;
int t[maxn],q;
int cnt=-1;//0~cnt的村装已经修建好 可以通路
void Floyd(int k){
    //对标准Floyd算法进行了修改  w表示可中转的站点
    for(int w=cnt+1;w<n;w++){
        if(t[w]<=k){ //w号村可以通路
            cnt=w;
            for(int i=0;i<n;i++){
                for(int j=0;j<n;j++){
                    e[i][j] = min(e[i][j],e[i][w]+e[w][j]);
                    e[j][i] = e[i][j];
                }
            }
        }
    }
}
int main(){
    cin>>n>>m;
    for(int i=0;i<n;i++){
        cin>>t[i];//重建时间
    }
    for(int i=0;i<n;i++)
        for(int j=0;j<n;j++){
            e[i][j] = e[j][i] = INF;
        }
    //m条边
    for(int i=1;i<=m;i++){
        int a,b,c;
        cin>>a>>b>>c;
        e[a][b] = e[b][a] = c;
    }
    //q条询问 z逐渐变大
    cin>>q;
    for(int i=1;i<=q;i++){
        int x,y,z;
        cin>>x>>y>>z;//x-->y的最短距离【第z天】
        Floyd(z);
        if(t[x]>z || t[y]>z || e[x][y]==INF) cout<<"-1"<<endl;
        else cout<<e[x][y]<<endl;
    }
    return 0;
}

DFS

题目:输出顶点x到顶点y的所有简单路径

#include<bits/stdc++.h>
using namespace std;
const int MaxN = 100000,MaxM = 100000;
bool book[MaxN];
int n,m,path[MaxN];//path记录x-->y的路径
int x,y;
struct edge {
    int to,next,dis;
};
edge e[MaxM];
int head[MaxN],cnt;
void addedge(int a,int b) {
    cnt++;
    e[cnt].to = b;
    e[cnt].next = head[a];
    head[a] = cnt;
}
void DFS(int a,int d){
    if(a==y){
        for(int i=0;i<d;i++){
            cout<<path[i]<<"->";
        }
        cout<<a<<endl;
        return ;
    }
    path[d] = a;
    book[d] = true;
    for(int w = head[a];w>0;w=e[w].next){
        int to = e[w].to;
        if(!book[to]){
            DFS(to,d+1);
        }
    }
    book[d] = false;
}
int main(){
    cin>>n>>m;//n点数  m边数
    for(int i=1;i<=m;i++){
        int a,b;
        cin>>a>>b;
        addedge(a,b);
    }
    cin>>x>>y;//x-->y
    DFS(x,0);
    return 0;
}
/*
7 11
1 2
2 5
5 6
1 3
1 4
2 4
2 6
3 4
4 6
4 7
7 6
1 6
 */

样例的图

image-20220819161107104

BFS

P1144 最短路计数 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

因为所有的边权都为1,所以一个点的最短路就相当于是它在BFS搜索树中的深度

#include<bits/stdc++.h>
using namespace std;

const int maxn=1000000+1,maxm=2000000+1,INF=0x7f7f7f7f,MOD=100003;
vector<int>G[maxn];
int dep[maxn];bool vis[maxn];int cnt[maxn];

int main(){
    int N,M;scanf("%d%d",&N,&M);//点数  边数
    for(int i=1;i<=M;i++){
        int x,y;
        scanf("%d%d",&x,&y);
        G[x].push_back(y);
        G[y].push_back(x);
    }
    queue<int>Q;
    dep[1]=0;vis[1]=1;Q.push(1);cnt[1]=1;
    //vis[i]==1表示i号节点到1号节点的最短路dep[i]已经确定  不过路径的条数cnt[i]可能还没最终确定
    while(!Q.empty()){
        int x=Q.front();Q.pop();
        for(int i=0;i<G[x].size();i++){
            int t=G[x][i];
            if(!vis[t]){vis[t]=1;dep[t]=dep[x]+1;Q.push(t);}
            if(dep[t]==dep[x]+1){cnt[t]=(cnt[t]+cnt[x])%MOD;}
        }
    }
    for(int i=1;i<=N;i++){
        printf("%d\n",cnt[i]);
    }
    return 0;
}

拓扑排序

可拓扑排序:有向图是没有环的。可以证明:这类图的强连通分量是n,n是节点个数

n个点的图想要构成强连通分量,那么这n个点的图至少要有n条边构成回路。因为可拓扑图没有环,所以任意2个点都不会构成强连通分量。

DFS的拓扑排序在每一层遍历结束的时候打印节点,是拓扑排序的逆序

BFS可以将队列换成优先队列,输出的拓扑排序是字典序的

菜肴制作 - 洛谷 | 通过BFS 需要字典序判断

//手写大根堆
#include<bits/stdc++.h>
using namespace std;
#define to e[i].v
#define mem(i) memset(i,0,sizeof(i))
#define swap(a,b) {a = a + b; b = a - b; a = a - b;}
using namespace std;
const int N=100000+11;
// n点数 m边数 t数据组数  rd入度数组  que模拟大根堆 qnum队列元素数目
int n,m,head[N],rd[N],cnt,ans[N],tot,que[N],qnum=0,t;
struct node
{
    int v,nxt;
}e[N];
void add(int a,int b){
    e[++tot].v=b;e[tot].nxt=head[a];head[a]=tot;
}
//堆上浮
void up(){
    int i = qnum;
    while (i>1){
        if(que[i]>que[i/2]) {swap(que[i],que[i/2]);}
        else break;
        i = i/2;
    }
}
//向堆中加入元素
void push(int x){
    que[++qnum] = x;
    up();
}
int top(){
    return que[1];
}
bool empty(){
    return qnum==0;
}
void pop(){
    que[1] = que[qnum--];
    int i = 1;
    while(i*2<=qnum){
        int tmp = i*2;
        if(i*2+1<=qnum && que[i*2+1]>que[i*2]){
            tmp++;
        }
        if(que[i]<que[tmp]) {swap(que[i],que[tmp]);}
        else break;
        i = tmp;
    }
}
void work(){
    for(int i=1;i<=n;i++) if(!rd[i]) push(i);
    while(!empty()){
        int t=top();ans[++cnt]=t,pop();
        for(int i=head[t];i;i=e[i].nxt){
            rd[to]--;
            if(!rd[to]) push(to);
        }
    }
}
int main(){
    cin>>t;
    while(t--){
        cin>>n>>m;
        for(int i=1;i<=m;i++){
            int a,b;
            cin>>a>>b;
            add(b,a); // 反向建边 b-->a
            rd[a]++;
        }
        work();//拓扑
        if(cnt==n){
            for(int i=n;i>=1;i--)
                printf("%d ",ans[i]);
            printf("\n");
        }
        else{
            printf("Impossible!\n");
        }
        mem(e);mem(head);mem(rd);cnt=0,tot=0;qnum=0;
    }
}

关键路径

节点的最早解锁时间和最晚解锁时间, 规定只有节点之前的活动全部完成,该节点才解锁

活动的最早开始时间和最晚开始时间,最早开始和最晚开始时间一样的活动就是关键活动

image-20220823002503300

image-20220823002330252

image-20220823002357772

image-20220822225926057

#include<bits/stdc++.h>
using namespace std;
#define to e[i].v
using namespace std;
const int N=100000+11;
int n,m,head[N],rd[N],cnt,tot,head_back[N];
int ve[N],vl[N];
int early[N],late[N];
int order[N];//正拓扑的顺序
stack<int> q;
struct node
{
    int v,nxt,dis;
}e[N],e_back[N];
void add(int a,int b,int c){
    e[++tot].v=b;e[tot].nxt=head[a];e[tot].dis=c;head[a]=tot;
}
void init(){
    tot=0;
}
void add_back(int a,int b,int c){
    e_back[++tot].v=b;e_back[tot].nxt=head_back[a];e_back[tot].dis=c;head_back[a]=tot;
}
void work(){
    for(int i=0;i<n;i++) {
        if(!rd[i]) q.push(i);
        ve[i] = 0;
    }
    while(!q.empty()){
        int t=q.top();order[cnt++]=t;q.pop();
        for(int i=head[t];i;i=e[i].nxt){
            rd[to]--;
            ve[to] = max(ve[to],ve[t]+e[i].dis);
            if(!rd[to]) q.push(to);
        }
    }
}

void work_back(){
    for(int i=0;i<n;i++) {
        vl[i] = ve[order[n-1]]; //初始的时候让每个节点的最晚开始时间是,拓扑排序最后一个节点的最早开始时间
    }
    for(int i=n-1;i>=0;i--){
        int t = order[i];
        for(int i=head_back[t];i;i=e_back[i].nxt){
            int v = e_back[i].v;
            vl[v] = min(vl[v],vl[t]-e_back[i].dis);
        }
    }
}

int main(){
    cin>>n>>m;
    for(int i=1;i<=m;i++){
        int a,b,c;
        cin>>a>>b>>c;
        add(a,b,c);
        add_back(b,a,c);
        rd[b]++;
    }
    work();
    init();
    work_back();
    for(int i=0;i<n;i++){
        cout<<ve[i]<<" "<<vl[i]<<endl;
    }
    return 0;
}
/*
9 11
0 1 6
0 2 4
0 3 5
1 4 1
2 4 1
3 5 2
4 6 9
4 7 7
5 7 4
6 8 2
7 8 4
 */

割点算法 Tarjan

普通算法:对于每一个点,先扇区这个点已经相连的边,然后DFS,如果连通分量的个数大于1则是割点,O(V*(V+E))

Tarjan算法的流程如下:

1-任选无向图中的一个点,作为树的根,然后通过dfs遍历全图。

2-设 dfn[i] 代表节点 i 的dfs序。

3-设 low[i] 代表节点 i 在不通过父亲节点的情况下,通过“绕路”,能够访问到的节点里,最小的dfn。

注:dfs序指的是在dfs过程中,该节点是第几个被访问到的节点。显而易见的,dfn数组的值随dfs过程单调上升。

判断割点

4-对于根节点,若其有两棵及以上的子树,则根节点为割点。因为如果去掉根节点,两棵子树将无法互相连通。

5-对于非根节点u,u的子节点(通过DFS,u会访问的节点,已经访问的节点不算)v,如果low[v]>=dfn[u], u是割点,如果去掉u节点,v节点也没办法访问比dfn[u]小的节点

image-20220821173612789

tarjan伪代码:理解tarjan可以帮助理解图的DFS

//当前节点u  u的前驱节点fa  DFS的根节点root
dfs(u,fa,root){
    for (与u相连的节点v){
        if(v未访问){
            访问v;
            low[u] = min(low[u],low[v]);
            if(low[v]>=dfn[u] && u不是树根节点) u是割点;
        }
        else if(v访问了) low[u] = min(dfn[v],low[u]);
    }
    if(u是树根节点 && u的子树个数大于等于2) u是割点;
}

Tarjan的代码

#include <bits/stdc++.h>
#define ll long long
using namespace std;
const int maxn=20000+10;//最大节点数
int n,m;//节点数与边数
vector<int>e[maxn];//vector存图
int dfn[maxn],low[maxn];//含义如上阐述
int cut[maxn];//表示某点是否是割点
int dfs_clock;//全局变量记录dfs序
//当访问到一个节点,这个节点的dnf确定,但是low还没确定,当这个节点的子节点全都DFS完成后才能确定low
void tarjan(int now,int root,int fa)//记录当前节点、树的根节点、父节点
{
    dfn[now]=low[now]=++dfs_clock;
    int child=0;//记录该节点子树的个数 注意:这里的孩子个数不是图所展现的孩子个数
    for(int i=0;i<e[now].size();i++)
    {
        int to=e[now][i];
        if(!dfn[to])//若to尚未访问过 说明访问的不是father节点
        {
            child++;//to是now的一个子树
            tarjan(to,root,now);//向下遍历 toot在tarjan过程中不变
            low[now]=min(low[now],low[to]);//注意:这里用low[to]更新low[now]
            //判断是否是割点:只要有一个与之相连的to节点,to节点能访问的最早节点也大于
            if(low[to]>=dfn[now]&&now!=root) cut[now]=1;
        }
        //即将访问的节点已经访问过了,而且不是father节点,更新当前节点的low
        //通过某条边,能到dfn较小的节点  注意:用dfn[to]更新low[now]
        else if(to!=fa) low[now]=min(low[now],dfn[to]);
        //若to已访问过,且to不是父节点,则to是祖先节点
    }
    //DFS根节点的特判
    if(child>=2&&now==root) cut[now]=1;
}
int main()
{
    scanf("%d%d",&n,&m);
    for(int i=1;i<=m;i++)
    {
        int u,v;
        scanf("%d%d",&u,&v);
        e[u].push_back(v);
        e[v].push_back(u);
    }//读入无向图
    dfs_clock=0;//初始化
    for(int i=1;i<=n;i++)
        if(!dfn[i]) tarjan(i,i,-1);
    for(int i=1;i<=n;i++) {
        cout<<dfn[i]<<" "<<low[i]<<" "<<cut[i]<<endl;
    }
    printf("\n");
    return 0;
}
/*
 6 6
 1 2
 2 3
 3 4
 4 5
 2 5
 5 6
 */

样例

image-20220821173551993

题目

模板 最小生成树

单源最短路径(弱化版)

没用堆优化

#include<bits/stdc++.h>
using namespace std;
int n,m,s;// n:顶点数   m:边数  s源点
const int inf = (1<<31) - 1 ;
const int MaxN = 100000 + 10; // 最多节点个数
int dist[MaxN],book[MaxN];//dist表示距离  path表示前驱节点  book作为是否已经得到结果的标记
struct Node{
    Node* next;
    int num; // 点的编号
    int cost;//边权
};
Node *head[MaxN]={NULL};
//dijkstra 邻接表法
void dijkstra(int k){
    dist[k] = 0;
    book[k] = 1;
    for(Node *h=head[k];h!=NULL;h=h->next){
        dist[h->num] = min(h->cost,dist[h->num]); // 因为有重复的边
    }

    //n-1轮
    for(int i=0;i<=n-1;i++){
        int min_dist = inf, t = -1;
        for(int j=1;j<=n;j++){
            if(!book[j] && dist[j]<min_dist){
                min_dist = dist[j];
                t = j;
            }
        }
        if(t==-1) return; // 提前结束 说明小于inf的dist没有找到 即有不可到的点
        book[t] = 1;
        //通过t节点 更新dist数组
        for(Node *h = head[t]; h != NULL ; h = h->next){
            int to = h->num;
            if(!book[to]){
                //inf不能直接运算  会爆炸
                if(dist[to]==inf || dist[t] + h->cost < dist[to]){
                    dist[to] = dist[t] + h->cost;
                }
            }
        }
    }
}
int main(){
    //n 点  m边
    cin>>n>>m>>s;
    for(int i=1;i<=m;i++){
        int a,b,c;
        cin>>a>>b>>c; //a-->b 边权c
        //构造邻接矩阵
        if(head[a]==NULL){
            head[a] = (Node*) malloc(sizeof (Node));
            head[a]->next = NULL;
            head[a]->cost = c;
            head[a]->num = b;
        } else {
            Node *h = head[a];
            while (h->next!=NULL){
                h = h->next;
            }
            h->next = (Node*) malloc(sizeof (Node));
            h = h->next;
            h->next = NULL;
            h->cost = c;
            h->num = b;
        }
    }
    for(int i=1;i<=n;i++){
        dist[i] = inf;
    }
    dijkstra(s);
    for(int i=1;i<=n;i++){
        cout<<dist[i]<<" ";
    }
    return 0;
}

使用堆优化

//方法2:使用堆优化
#include<bits/stdc++.h>

const int MaxN = 100010, MaxM = 500010,inf  = 0x7fffffff;

struct edge
{
    int to, dis, next;
};

edge e[MaxM];
//head[x] x向外连的边 在edge数组的序号  head[x]=20 说明edge[20]存了一条x-->to的边
int head[MaxN], dis[MaxN], cnt;
bool vis[MaxN];
int n, m, s;

inline void add_edge( int u, int v, int d )
{
    cnt++;
    e[cnt].dis = d;
    e[cnt].to = v;
    e[cnt].next = head[u];
    head[u] = cnt;
}

struct node
{
    int dis;
    int pos;
    bool operator <( const node &x )const
    {
        return x.dis < dis;
    }
};

std::priority_queue<node> q;


inline void dijkstra(){
    dis[s] = 0;
    q.push( ( node ){0, s} );
    while( !q.empty() ){
        node tmp = q.top();
        q.pop();
        int x = tmp.pos;
        if(vis[x]==1) continue; //之前重复添加的
        vis[x] = 1;//dis[x]确定  并且从堆中出来
        for( int i = head[x]; i!=0 ; i = e[i].next ){
            int y = e[i].to;//x--->y
            if(!vis[y] && dis[y] > dis[x] + e[i].dis ){
                dis[y] = dis[x] + e[i].dis;
                q.push( ( node ){dis[y], y} ); //可能重复添加关于y的dis
            }
        }
    }
}


int main(){
    scanf( "%d%d%d", &n, &m, &s );
    for(int i = 1; i <= n; ++i) dis[i] = inf;
    for( register int i = 0; i < m; ++i )
    {
        register int u, v, d;
        scanf( "%d%d%d", &u, &v, &d );
        add_edge( u, v, d );
    }
    dijkstra();
    for( int i = 1; i <= n; i++ ){
        if(dis[i]==inf)
            printf( "2147483647 ");
        else
            printf( "%d ", dis[i] );
    }
    return 0;
}

单源最短路径(要用到堆)

#include<bits/stdc++.h>

const int MaxN = 100010, MaxM = 500010,inf  = 0x7fffffff;

struct edge
{
    int to, dis, next;
};

edge e[MaxM];
//head[x] x向外连的边 在edge数组的序号  head[x]=20 说明edge[20]存了一条x-->to的边
int head[MaxN], dis[MaxN], cnt;
bool vis[MaxN];
int n, m, s;

inline void add_edge( int u, int v, int d )
{
    cnt++;
    e[cnt].dis = d;
    e[cnt].to = v;
    e[cnt].next = head[u];
    head[u] = cnt;
}

struct node
{
    int dis;
    int pos;
    bool operator <( const node &x )const
    {
        return x.dis < dis;
    }
};

std::priority_queue<node> q;


inline void dijkstra(){
    dis[s] = 0;
    q.push( ( node ){0, s} );
    while( !q.empty() ){
        node tmp = q.top();
        q.pop();
        int x = tmp.pos;
        if(vis[x]==1) continue; //之前重复添加的
        vis[x] = 1;//dis[x]确定  并且从堆中出来
        for( int i = head[x]; i!=0 ; i = e[i].next ){
            int y = e[i].to;//x--->y
            if(!vis[y] && dis[y] > dis[x] + e[i].dis ){
                dis[y] = dis[x] + e[i].dis;
                q.push( ( node ){dis[y], y} ); //可能重复添加关于y的dis
            }
        }
    }
}


int main(){
    scanf( "%d%d%d", &n, &m, &s );
    for(int i = 1; i <= n; ++i) dis[i] = inf;
    for( register int i = 0; i < m; ++i )
    {
        register int u, v, d;
        scanf( "%d%d%d", &u, &v, &d );
        add_edge( u, v, d );
    }
    dijkstra();
    for( int i = 1; i <= n; i++ ){
        if(dis[i]==inf)
            printf( "2147483647 ");
        else
            printf( "%d ", dis[i] );
    }
    return 0;
}

P3388 【模板】割点(割顶) - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

本文作者:cyfly

本文链接:https://www.cnblogs.com/cyfuture/p/16600363.html

版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。

posted @   cyfly  阅读(131)  评论(0编辑  收藏  举报
点击右上角即可分享
微信分享提示
评论
收藏
关注
推荐
深色
回顶
收起