浅谈OI中的图论算法(更新到9.23)

  咕了两个月的我(如果不算Luogu的题解)终于回来写博了qwq,因为我的数据结构知识一直很薄弱,每次考试老是失分,所以我决定写一写关于图论的博客,最近一段时间也正好在复习这部分,这篇博客的内容会涉及到:树与图的遍历,树的深度,图的联通块,拓扑排序,树的重心,最短路,最小生成树,并查集,Tarjan与图的连通性,树的直径,LCA,树链剖分,负环。文章内容与lyd的《算法竞赛进阶指南》重合度比较高(因为我就是按照他的书来复习的),同时文章以讲解为辅,代码为主,适合各位同学复习而并非初学者接触。

  所有模板都是我手打的,也许有错误,欢迎批评指正弱弱的我qwq。

 声明

  博客中如果没有特殊说明,则默认是n个点和无向图,文中会使用vector,邻接表,邻接矩阵这三种方式来存储。

 树与图的遍历

  树与图的存储方式是相同的,遍历可以采用dfs和bfs这两种方式。

  深度优先遍历与深度

  众所周知写这个只是为了知识的完整性,大家随便看看就行。

  dep是深度数组。

  

void dfs(int u)
{
    vis[u]=true;
    for(int i=h[u];i!=-1;i=e[i].nxt)
    {
    int v=e[i].v;
    if(vis[v])    continue;
    dep[v]=dep[u]+1;
    dfs(v);
    }    
} 

  广度优先遍历

  bfs并没有dfs常用,用队列q来存储,每次遇到一个节点u,就入队,然后依次入队它的子节点,如果子节点也全部入队,就把u出队,又入队它第一个子节点的所有子节点,以此类推...

  eg:

  

入队标蓝,出队标红,这张图的bfs模拟过程如下:

1 234

234 5

345 

45 6

56 7

67 8

78 9

89

代码的实现很简单。

void bfs()
{
    memset(d,0,sizeof(d));
    q.push(1);
    d[1]=1;
    while(!q.empty())
    {
    int u=q.front(),q.pop();
    for(int i=h[u];i!=-1;i=e[i].nxt)
    {
    int v=e[i].v;
    if(d[v]) continue;
    d[v]=d[u]+1;
    q.push(v);            
    }
    }    
} 

我们可以看到图中有一个d数组,d[u]的作用是存储从1遍历到节点u所需经过的最少点数,qwq但是请大家不要妄想能用这个来求最短路,stO青君大佬

bfs的遍历有两个性质:

1.在访问完节点i的所有节点后,才会开始访问节点i+1。

2.队列中的元素至多有两个层次的节点,第一部分属于i层,第二部分属于i+1层,所有i的节点排在i+1的节点之前,也就是说,bfs遍历满足两段性和单调性,这也是它的基本性质。

树的dfs序

一般地,我们在dfs时,在刚进入递归前和即将回溯之前各记录一次该点的编号,最后产生的2n长节点的序列就是dfs序,如果用a[N>>1]存储dfs序,l[ ]和r[ ]分别存递归前和回溯前的cnt,a[ l [ u ] ]和a[ r [ u ] ]这一段记录的就是u节点的子树,由此可以很好地把树上操作转化为区间问题~!

代码简短也好理解。

void dfs(int u)
{
    vis[u]=true;
    l[u]=++cnt;
//    a[cnt]=u;
    for(int h[u];i!=-1;i=e[i].nxt)    
    {
        int v=e[i].v;
        if(vis[v]) continue;
        dfs(v);
    }
    r[u]=++cnt;
//    a[cnt]=u;
} 

树的重心和子树大小

如果一棵树有v1~vk个节点,并且以v1~vk为根的子树大小是siz[v1]~siz[vk],那么以u为根的子树大小是siz[u]=siz[v1]+...+siz[vk],也就是说siz数组用来存储节点的子树大小(包括节点本身)。

树的重心指的是,如果我们把一个节点u从树中删除,那么原来的一棵树可能会分成若干个不同的部分,如果在删除一个节点后,剩下的所有子树中最大的一棵是最小的,那么这个被删去的节点称为树的重心,对于无权图,大小一般指的是节点个数。

代码简短好理解,如下。

void dfs(int u)
{
    max_point=0;
    siz[u]=1;
    vis[u]=true;
    for(int i=h[u];i!=-1;i=e[i].nxt)
    {
        int v=e[i].v;
        if(vis[v]) continue;
        dfs(v);//要先遍历子节点才能找到子节点的siz值 qwq为什么总是忘记dfs 我有罪 
        siz[u]+=siz[v];
        max_point=max(max_point,siz[v]);
    }    
    max_point=max(max_point,n-siz[u]);
    if(max_point<ans)
    {
        ans=max_point;
        pos=u;
    }
} 

图的连通块划分

经过多次dfs,可以找出一张图的每一个连通块,不会连通块的同学可以去看看知识点再做两道题,这个很简单,不赘述了。

void dfs(int u)
{
    scc[u]=cnt;
    for(int i=h[u];i!=-1;i=e[i].nxt)
    {
    int v=e[i].v;
    if(scc[v]) continue;
    scc[v]=cnt;
    dfs(v);        
    }    
} 
int main()
{
    for(int i=1;i<=n;i++)
    if(!scc[i]) dfs(i);
}

 拓扑排序

  给定一张有向无环图,若一个由图中所有点构成的序列A满足:对于图中边(x,y),x在A中都出现在y之前,则称A是这张图的一个拓扑序,求解这个序列的过程我们称其为拓扑排序。

  拓扑排序的实现过程很简单:

  1.建立空的序列A

  2.预处理所有的入度d[i],入队初始入度为0的点。

  3.取出队头节点x,把x加入A的末尾。

  4.对于从A出发的每条边(x,y),如果被减为0,就把y入队。

  5.重复3,4步直到队列为空,这个时候我们就求出了拓扑序。

  

void add(int u,int v)
{
    e[+cnt].v=v;
    e[cnt].nxt=h[u];
    h[u]=cnt;
    d[v]++;//d数组表示v的入度,入度指的是以某一个点为终点的边数    
} 
void topsort()
{
    queue<int>q;
    for(int i=1;i<=n;i++)
    if(!d[i]) q.push(i);//入度为0则入队 
    while(!q.empty())
    {
        int u=q.front(),q.pop();
        a[++cnt]=u;
        for(int i=h[u];i!=-1;i=e[i].nxt)
        {
            int v=e[i].v;
            if(--d[v]) q.push(v);//--d[v]表示的是,u此刻为v的出度,如果减去u之后v是0个节点的入度,那么可以入队 
        }
    }    
} 
int main()
{
    for(int i=1;i<=n;i++)
    printf("%d ",a[i]);
}

 

 最短路径

  如题,这里求的是图论当中的最短路,最短路有多源和单源最短路。

  Floyd算法

  Floyd属于多源最短路算法,非常简单,但时间复杂度为o(n^3),太高了qwq,优点是简单好理解,思想是dp。

  dp[k][i][j]=min(dp[k-1][i][j],dp[k-1][i][k]+dp[k-1][k][j);

  

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
#define N 1005
int d[N][N];
int n,m;
int main()
{
    scanf("%d%d",&n,&m);
    memset(d,0x3f,sizeof(d));
    for(int i=1;i<=n;i++) d[i][i]=0;
    for(int i=1;i<=m;i++)
    {
        int u,v,w;
        scanf("%d%d%d",&u,&v,&w);
        d[u][v]=min(d[u][v],w);
    }
    for(int k=1;k<=n;k++)
        for(int i=1;i<=n;i++)
            for(int j=1;j<=n;j++)
            d[i][j]=min(d[i][j],d[i][k]+d[k][j]);
    for(int i=1;i<=n;i++)
    {
        for(int j=1;j<=n;j++) 
        printf("%d ",d[i][j]);    
        printf("\n");
    }
    return 0;
}

  Dijkstra算法

  qwq这是基于贪心思想的算法,一个伪代码就能体现

  d[v]=min(d[v],d[u]+w)(u为当前到起点路最短的点)

 邻接矩阵实现

  

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
#define N 3005
int d[N],a[N][N],n,m,v[N];
void dijkstra()
{
    memset(d,0x3f,sizeof(d));
    d[1]=0;
    for(int i=1;i<n;i++)
    {
        int x=0;
        for(int j=1;j<=n;j++)
        if(!v[j]&&(x==0||d[j]<d[x])) x=j;
        v[x]=1;
        for(int j=1;j<=n;j++) d[j]=min(d[j],d[x]+a[x][j]);
    }
}
int main()
{
    scanf("%d%d",&n,&m);
    memset(a,0x3f,sizeof(a));
    for(int i=1;i<=n;i++) a[i][i]=0;
    for(int i=1;i<=m;i++)
    {
        int u,v,w;
        scanf("%d%d%d",&u,&v,&w);
        a[u][v]=min(a[u][v],w);
    }
    dijkstra();
    for(int i=1;i<=n;i++)
    printf("%d ",d[i]);
    return 0;
}
  邻接链表+堆优化
#include<cstdio>
#include<cstring>
#include<queue>
#include<algorithm>
using namespace std;
#define N 100005
struct node
{
    int v,nxt,w;
}e[N];
int d[N],h[N],v[N];
int n,m,cnt;
priority_queue< pair <int,int> >q;
void add(int u,int v,int w)
{
    e[++cnt].v=v;
    e[cnt].w=w;
    e[cnt].nxt=h[u];
    h[u]=cnt;
}
void dijkstra()
{
    q.push(make_pair(0,1));
    d[1]=0;
    while(q.size())
    {
        int u=q.top().second;
        q.pop();
        if(v[u]) continue;
        v[u]=1;
        for(int i=h[u];i!=-1;i=e[i].nxt)
        {
            int v=e[i].v;
            int w=e[i].w;
            d[v]=min(d[v],d[u]+w);
            q.push(make_pair(-d[v],v));//运用相反数思想实现一个小根堆 每次找到权值最小的u访问一个G(U,V) 满足贪心思想 
        }
    }
}
int main()
{
    memset(h,-1,sizeof(h));
    memset(d,0x3f,sizeof(d));
    scanf("%d%d",&n,&m);
    for(int i=1;i<=m;i++)
    {
    int u,v,w;
    scanf("%d%d%d",&u,&v,&w);
    add(u,v,w);    
    }    
    dijkstra();
    for(int i=1;i<=n;i++)
    printf("%d ",d[i]);
    return 0;
} 

SPFA算法

qwq俗话说得好:SPFA死了,所以对于它不做过多介绍,只提一句:

边(u,v,w)满足三角形不等式d[v]<=d[u]+w;

#include<cstdio>
#include<queue>
#include<cstring>
#include<algorithm>
using namespace std;
#define N 100005
struct node
{
    int nxt,v,w;
}e[N];
int n,m,d[N],h[N],cnt,v[N];
void add(int u,int v,int w)
{
    e[++cnt].v=v;
    e[cnt].w=w;
    e[cnt].nxt=h[u];
    h[u]=cnt;
}
queue<int>q;
void spfa()
{
    memset(d,0x3f,sizeof(d));
    d[1]=0,v[1]=1;
    q.push(1);
    while(q.size())
    {
    int u=q.front();q.pop();
    v[u]=0;
    for(int i=h[u];i!=-1;i=e[i].nxt)
    {
    int y=e[i].v;
    int w=e[i].w;
    d[y]=min(d[y],d[u]+w);
    if(!v[y]) q.push(y),v[y]=1; 
    }    
    }    
}
int main()
{
    scanf("%d%d",&n,&m);
    memset(h,-1,sizeof(h));
    for(int i=1;i<=m;i++)
    {
        int u,v,w;
        scanf("%d%d%d",&u,&v,&w);
        add(u,v,w); 
    }
    spfa();
    for(int i=1;i<=n;i++)
    printf("%d ",d[i]);
    return 0;
}

 

  

转载于:https://www.cnblogs.com/valentino/p/11536701.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值