图论部分模板(基于链式前向星的存储方式)

图论篇部分模板(基于链式前向星的存储方式)

你还在为暴力枚举复杂度太高而苦恼吗?你还在为DP而痛苦吗?你还在为搜索剪枝而绞尽脑汁吗?选择链式前向星吧,链式前向星——专注存图20年

1. 链式前向星

int e[maxn], ne[maxn], h[N],weight[maxn],idx;
int in_degree[N];
void add(int u, int v, int w) {
	e[idx] = v, ne[idx] = h[u], h[u] = idx++;//前插链表
}

2. dfs

void dfs(int u, int fa) {
	for (int i = h[u]; i != -1; i = ne[i]) {
		int t = e[i];
		if (t != fa) {
			dfs(t, i);
		}
	}
}

3. bfs

void bfs(int root) {
	queue<int> q;
	q.push(root);
	while (!q.empty()) {
		int t = q.front(); q.pop();
		for (int i = h[t], i != -1, i = ne[i]) {
			q.push(e[i]);
		}
	}
}

4. 拓扑排序(判断有无环)

bool vis[N];
void top_sort(int n) {//n个节点
	for (int i = 0; i < n; i++) {
		if (in_degree[i] == 0) 
			q.push(i);
	}
	if (!q.size()&&n!=0) puts("有环");
	while (!q.empty()) {
		int t = q.front(); q.pop();
		vis[t] = true;
		for (int i = h[t], i != -1; i++){
			int k = e[i];
			if (--in_degree[k] == 0) {
				q.push(k);
			}

		}
	}
	for (int i = 0; i < n; i++) {
		if (!vis[i]) puts("有环");
	}
}

5. 欧拉路

无向图所有度节点均为偶数才存在欧拉回路。uva10054

6.无向图的连通性

无向图的连通性
割点:
定义dfn[u]:记录dfs对每个点的访问顺序,dfn随着地推深度的增加而变大
定义low[i]:记录vv的后代能连回的祖先的num
只要low[v]>=dfn[u],就说明u是割点,对于v无其他回退边

只要将条件改为low[v]>dfn[u],即是判断割边的条件,表示v只能回退到u
常用数组 dfn,_time-时间戳,dfn顺序赋值,low数组,回退赋值

int dfn[maxn], low[maxn];
int _time;
int cnt;
void tarjan_V(int u,int fa) {  //割点
	dfn[u] = low[u] = ++_time;
	cnt = 0;
	for (int i = h[u]; i != -1; i=ne[i]) {
		int v = e[i];
			if (!dfn[v]) {
				cnt++;
				tarjan_V(v, u);
				low[u] = min(low[u], low[v]);
				if (dfn[u] <= low[v]&&u!=1) { //不是根
					printf("%d是割点\n",u);
				}
			}
			else if (dfn[v] < dfn[u] && v != fa) {
				low[u] = min(low[u], dfn[v]);
			}
	}
	if (u == 1 && cnt > 1) {
		printf("%d是割点\n", u);
	}
}

void tarjan_E(int u, int fa) { //割边
	dfn[u] = low[u] = ++_time;
	for (int i = h[u]; i != -1; i = ne[i]) {
		int v = e[i];
		if (!dfn[v]) {
			tarjan_E(v, u);
			low[u] = min(low[u], low[v]);
			if (low[v] > dfn[u]) {
				printf("%d---%d是割边", u, v);
			}
		}
		else if (dfn[v] && v != fa) { //v被遍历过
			 low[u] = min(low[u], dfn[v]);
		}
	}
}

6. 最短路

图的规模较小:Floyd,如果边的权值有负数,需要判断负圈。(n<200)

n*m<107 : B-F;

图的规模较大,且边的权值非负数,用Dijstra ,更大

图的规模大,且边的权值有负数,用SPFA,需要判断负圈。更大

Floyd(O(n3))

能一次性求所有节点的最短路,只能适用于小规模的图。区间DP。

memset(graph,INF,sizeof graph);
void floyd(){
    int s=1; //定义起点
    for(int k=1;k<=n;k++)          //先枚举k,保证后续枚举有意义。
        for(int i=1;i<=n;i++){
            if(graph[i][k]!=INF){
                for(int j=1;j<=n;j++){
                      if(graph[k][j]!=INF)
                    if(graph[i][j]>graph[i][k]+graph[k][j])
                        graph[i][j]=graph[i][k]+graph[k][j];
                }
            }
        }
    printf("%d",graph[s][n]);
}
判断负圈

graph[i][i]最终是绕一圈之后的最小路径,如果graph[i][i]<0 就表明有负圈,此时可置graph[i][i]=0这样可以加快判断过程。

Luogu1870最长路:(不建议floyd,数据范围较大)

#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int N=1501;
int graph[N][N];
int n,m,x,y,z;
#define 0x3f INF
int main(){
    cin>>n>>m;
    for(int i=1;i<=n;i++)
        for(int j=1;j<=n;j++)
            if(i==j) graph[i][j]=0;
             else   graph[i][j]=INF;
     for(int i=1;i<=m;i++){
         cin>>x>>y>>z;
         graph[x][y]=min(graph,-z);
     }   
    for(int k=1;k<=n;k++)
        for(int i=1;i<=n;i++){
             if(graph[i][k]!=INF)
            for(int j=1;j<=n;j++)
                 if(graph[k][j]!=INF)
                if(graph[i][k]+graph[k][j]<graph[i][j])
                    graph[i][j]=graph[i][k]+graph[k][j];
  }
}
if(graph[1][n]==INF) cout<<-1;
else cout<<-graph[1][n];
 return 0;
}
B-F(O(n*m))

解决单源最短路径问题。

给定一个起点s,求它到图中所有n个节点的最短路径.。

每一轮给所有n个人每人一次机会,问他的邻居到s的最短距离是多少,如果他的邻居到s的距离不是INF,他就可以借道他的邻居到s,以更新自己到s的距离,一共需要n次松弛

非优化存储版:采用的邻接矩阵,不得不遍历n*n条边

void bellman(){
    int s=1;//定义起点
    int d[maxn];
    for(int i=1;i<=n;i++)
        d[maxn]=INF;
    d[s]=0;
    for(int k=1;k<=n;k++)
        for(int i=1;i<=n;i++)
            for(int j=1;j<=n;j++)
                if(d[j]>d[i]+graph[i][j])
                    d[j]=d[i]+graph[i][j];
    printf("%d\n",d[n]);
}

换用结构体存储

struct edg{int u,v,w;}e[10005];
void bellman(){
    int s=1;//定义起点
    int d[maxn];
    for(int i=1;i<=n;i++)
        d[maxn]=INF;
    d[s]=0;
    for(int k=1;k<=n;k++)
        for(int i=0;i<cnt;i++){ //cnt是存储的边的数量
            int x=e[i].u,y=e[i].v;
            if(d[y]>d[x]+e[i].w){
                d[y]=d[x]+e[i].w;
                pre[y]=x;   //如果有需要记录路径
            }
        }
    printf("%d\n",d[n]);
}
判断负圈

当没有负圈时,只需要n轮就结束,如果超过n轮,最短路径还有变化,那么肯定有负圈。(加上一个负数,一定在一直减少)。

void bellman(){
    int s=1;//定义起点
    int d[maxn];
    for(int i=1;i<=n;i++)
        d[maxn]=INF;
    d[s]=0;
    bool update=true;int k=0;
    while(update){
        k++;
        update=false;
        if(k>n){ //超过了n轮操作
            printf("有负圈\n");
            return;
        }
        for(int i=0;i<cnt;i++){ //cnt是存储的边的数量
            int x=e[i].u,y=e[i].v;
            if(d[y]>d[x]+e[i].w){
                d[y]=d[x]+e[i].w;
                pre[y]=x;   //如果有需要记录路径
            }
        }
    printf("%d\n",d[n]);
}
SPFA (不稳定)

用队列处理Bellman-Floyd,单源最短路径

  1. 存图采用邻接表。
struct edg{
    int from,to,w;
    edg(int a,int b,int c){
        form=a,to=b,w=c;
    }
}
vector<edg>e[maxn];  //输入以from做为下标存储
int n,m;
int pre[maxn];
int spfa(int s){
    int dis[maxn];
    int neg[maxn];
    bool vis[maxn];//vis表示当前节点是否在队列中
    memset(vis,0,sizeof vis);
    vis[s]=1;
    memset(dis,INF,sizeof dis);
    dis[s]=0;
    queue<int>q;
    q.push(s);
    while(!q.empty()){
        int u=q.front();
        q.pop();
        vis[u]=false;
        for(int i=0;i<e[u].size();i++){
          int v=e[u][i].to,w=e[u][i].w;
            if(dis[u]+w<dis[v]){
                dis[v]=dis[u]+w;
                pre[v]=w;   //记录路径
                if(!vis[v]){   //该状态更新了,需要放入队列
                    vis[v]=1;
                    q.push(v);
                    neg[v]++;
                    if(neg[v]>n) return 1; //出现负圈,有点被松弛的次数大于n
                } 
            }
        }
    }
    printf("%d",dis[n]);
    return 0;
}
  1. 如果图依旧较大,采用链式前向星存储
int h[maxn],ne[2*maxn],idx,to[2*maxn],w[2*maxn];
void add(int u,int v,int wight){
    to[idx]=v,ne[idx]=h[u],w[idx]=weight,h[u]=idx++;
}
int spfa(int s){
    int dis[maxn],neg[maxn],vis[maxn];
    for(int i=1;i<=n;i++) dis[i]=INF,vis[i]=false,neg[i]=0;
    dis[s]=0;
    queue<int>q;
    q.push(s);
    vis[s]=1;
    while(!q.empty()){
        int u=q.front();q.pop();
        for(int i=h[u];i!=-1;i=ne[i]){ //遍历邻居
            int t=to[i];
            if(dis[t]>dis[u]+w[i]){
                dis[t]=dis[u]+w[i];
                pre[t]=u;
                if(!vis[t]){
                    q.push(t);
                    neg[t]++;
                    if(neg[t]>n){
                        printf("出现负圈\n");
                        return 0;
                    }
                }
            }
        }
        
    }
    printf("%d\n",dis[n]);
    return 0;
}
Dijkstra(不能有负权边)(O(m*log2n))

解决单源最短路径问题。

维护两个集合A(已经确定的最短路径的集合),B(扩展的邻居的集合),每次从B中选择出距离起点s最近的v,将v加入集合A,B中去掉v并将v的邻居加入集合B,这样重复,直至B为空。

bool done[maxn]数组记录当前节点是否被处理或者在集合A中,也可用于舍弃B中不优的选择

eg:B={2_3,2_4}舍弃2_42_3:表示起点到2的距离为3,两种信息,当前节点以及距离

int h[maxn],ne[2*maxn],idx,to[2*maxn],w[2*maxn];
bool done[maxn];
void add(int u,int v,int wight){
    to[idx]=v,ne[idx]=h[u],w[idx]=weight,h[u]=idx++;
}
struct node{ //存节点信息
    int id,n_dis;//id:节点,n_dis:这个节点到起点的距离
    node(int b,int c){id=b,n_dis=c;}
    bool operator<(const node&a)const{
        return n_dis>a.n_dis;// 从小到大
    }
};
void dijkstra(){
    int s=1; //定义起点
    int dis[maxn];   //dis相当于集合A
    bool done[maxn];
    for(int i=1;i<=n;i++) dis[i]=INF,done[i]=false;
    dis[s]=0; 
    priority_queue<node> q; //集合B
    q.push(node(s,dis[s]));
    while(!q.empty()){
        node u=q.top();q.pop();
        if(done[u.id]) continue;//已经被舍弃或者加入A的id
        done[u.id]=true;
        for(int i=h[u.id],i!=-1;i=ne[i]){ //遍历邻居
            int y=to[i];  //y就是邻居
            if(done[y]) continue; //被舍弃的情况
            if(dis[y]>w[i]+u.n_dis){  //未被舍弃并且可以更新。
                dis[y]=w[i]+u.n_dis;
                q.push(node(y,dis[y]));
                pre[y]=u.id;
            }
        }
    }
    printf("%d",dis[n]);
}

7.最小生成树

针对无向图——连通所有节点的情况下保持路径和最短。

hdu1233.

Problem Description:某省调查乡村交通状况,得到的统计表中列出了任意两村庄间的距离。省政府“畅通工程”的目标是使全省任何两个村庄间都可以实现公路交通(但不一定有直接的公路相连,只要能间接通过公路可达即可),并要求铺设的公路总长度为最小。请计算最小的公路总长度。

Prim算法(O(E*log2V))

维护一个集合U表示已知的点,每次将距离集合U中所有点的最近的点加入U,优先队列优化O(E*log2V)。

int dis[maxn];// 存距离
bool vis[maxn];// 是否在队列中
struct node{
   int id,w;
    node(int a,int b){
        id=a,w=b; //id表示边的编号
    }
    bool operator<(const node&a)const{
   return w>a.w;// 从小到大
    }     
}edg[maxn*2];
int prim(int s){
    int ans=0;
  priority_queue<Edg>q;
    //加入起点的边
    for(int i=h[s];i!=-1;i=ne[i]){
        if(!vis[to[i]]){
            q.push(new node{i,w[i]});
        }
    }
    while(!q.empty()){
        node t=q.front();q.pop();
        ans+=t.w;
        int y=to[t];
        vis[y]=true;
        int min_x=cc=0,min_num=INF;
        for(int i=h[y];i!=-1;i=ne[i]){
          int x=to[i],we=w[i];
          if(!vis[x]){ //不成环
             if(min_num>we) we=min_num,min_cc=x;
             q.push(new node{i,we}); //如果未被遍历过就加入队列
          }
        }
   }
}
Kruskal算法(O(E*log2E))
  1. 对边进行排序(边是集合,不能分开算——用结构体算(存下标和权值也可)),依次将最短的边加入T中
  2. 判断圈,可以选用并查集
int parent[maxn],depth[maxn];//并查集
struct Edg{int u,v,w}edg[maxn*maxn];
bool cmp(const Edg&a,const Edg&b){return a.w<b.w;}
int find(int x){
    return x==parent[x]?parent[x]:x=find(parent[x]);
}
void Union(int x,int y){
    int rootx=find(x),rooty=find(y);
    if(rootx==rooty) return;
    if(depth[rootx]>dept[rooty]) parent[rooty]=rootx;
    else{
        parent[rootx]=rooty;
        if(depth[rootx]==depth[rooty]) depth[rooty]++;
    }
}
int kruskal(){
    int ans=0;
    for(int i=1;i<=n;i++)
        parent[i]=i;  //初始化,开始每个村庄都是单独的集,并查集
    sort(edg+1,edg+1+m,cmp);// m条边
    for(int i=1;i<=m;i++){
        int b=find(edg[i].u),c=find(edg[i].v);
        if(b==c) continue;
        else ans+=edg[i].w;
    }
    return ans;
}

8最大流

网络流的基本问题,原型是水流问题。

Ford-Fulkerson方法:(复杂度很高,只能用于小图)

求图G中最多有多少条不相交的s->t路径:

S-T割:

  1. 找到一个大小为l的S-T割

k<=l ,当l为0时表示不存在路径

  1. 找到m条不相交路径

    m<=k

求解:如果我们能同时找到m条不相交路径和大小为m的S-T割,则k=m

hdu 1532

Edmonds-Karp算法:

Problem Description:一个有向图,求1到N的最大流。

#include<iostream>
using namespace std;
#define INF 0x3f
const int maxn=300;
int n,m,graph[maxn][maxn],pre[maxn]; //graph 不仅记录图,还是残留网格 , n是边数,m是节点数
int bfs(int s,int t){
    int flow[maxn];
    memset(pre,-1,sizeof pre); 
    flow[s]=INF,pre[s]=0;//初始化起点
    queue<int>	q;q.push(s);
    while(!q.empty()){
        int u=q.front();q.pop();
        if(u==t) break;           //搜索到一个路径
        for(int i=1;i<=m;i++){  //BFS所有的点
            if(i!=s&&graph[u][i]>0&&pre[i]==-1){ //graph[u][i]>0表明这条路可用
                pre[i]=u;    //记录路径
                q.push(i);
                flow[i]=min(flow[u],graph[u][i]); //更新节点流量
            }
        }
    }
    if(pre[t]==-1) return -1; //没有找到新的路径
    return flow[t];  //返回这个增广路径的流量
}
int maxflow(int s,int t){
    int ans=0;
    while(1){
        int flow=bfs(s,t); //执行一次bfs,找到一条路径
        if(flow==-1) break;
        int cur=t;
        while(cur!=s){
            int father=pre[cur];  //pre[]记录路径上前一个点
            graph[father][cur]-=flow; //更新残留网络
            graph[cur][father]+=flow; //反向
            cur=father;
        }
       ans+=flow;   
    }
    return ans;
}
int mian(){
    while(~scanf("%d %d",&n,&m)){
        memset(graph,0,sizeof graph);
        for(int i=0;i<n;i++){
            int u,v,w;
            scanf("%d%d%d",&u,&v,&w);
            graph[u][v]+=w;
        }
        printf("%d\n",maxflow(1,m));
    }
    return 0;
}


  • 记录一次错误,关闭流同步后不能混用coutprintf,会出问题。
 std::ios::sync_with_stdio(false);
 cin.tie(0);
 cout.tie(0);
 printf("%d",1);
 cout<<"222";
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值