ACM算法总结 图论(一)




概述

的严格定义是一个表达式 G = < V , E , Ψ > G=<V,E,\Psi> ,其中V表示点集,E表示边集, Ψ \Psi 表示边与点的映射关系。

  • 如果 Ψ : E { { v 1 , v 2 }      v 1 V , v 2 V } \Psi: E \rightarrow \{\{v_1,v_2\} \ | \ \ v_1 \in V,v_2 \in V\} ,那么 G 为无向图;
  • 如果 Ψ : E V × V \Psi: E \rightarrow V\times V ,那么 G 为有向图。

平时我们使用的时候就不用那么严谨了。

图的计算机存储方式一般有两种,一种是邻接矩阵的方法,一种是邻接表。邻接矩阵相对来说对于大数据的稀疏图不适用,所以我们一般使用邻接表的存储方法。C++中用 vector<type> G[maxn] 这种方式非常方便,还有一种链式向前星的方法不用使用STL,也挺好的。(vector开了O2优化应该也是很快的)

还有一些常见的名词,比如说连通性强连通性分支完全图强连通分量生成树有向无环等等。




图的遍历

图有两种遍历方式:深度优先(dfs)广度优先(bfs),深度优先一般使用递归实现,广度优先一般使用队列实现。这其实跟搜索差不多。




二分图判断

使用深度优先搜索可以判断一个图是否为二分图,这对于其它的处理很有帮助。

判断方法是交替染色,如果遇到矛盾(相邻结点颜色一样)说明存在奇环,不是二分图。

判断二分图代码如下:

const int maxn=1e5+5;
vector<int> G[maxn];
int n,m,vis[maxn];

bool dfs(int u)
{
    REP(i,0,G[u].size()-1)
    {
        int v=G[u][i];
        if(vis[u]==vis[v]) return 0;
        if(!vis[v])
        {
            vis[v]=3-vis[u];	// 这里用1和2来交替染色
            if(!dfs(v)) return 0;
        }
    }
    return 1;
}

int main()
{
    n=read(),m=read();
    while(m--)
    {
        int u=read(),v=read();
        G[u].push_back(v);
        G[v].push_back(u);
    }
    int flag=1;
    REP(i,1,n) if(!vis[i]) vis[i]=1,flag&=dfs(i);
    puts(flag?"Yes":"No");

    return 0;
}




拓扑排序

拓扑排序是针对有向无环图(DAG)的,所谓有向无环图,就是没有的有向图(这里的环在图论中对应于有向回路)。任何有向无环图都至少有一个拓扑序列。

拓扑排序:指一个DAG的所有顶点的线性序列,使得对于任何有向边<u,v>,序列中u都在v的前面。

要注意的是有时候我们笼统地把反拓扑序列也称为拓扑序列,及所有u都在v后面,总之这个序列满足一定的先后性。

拓扑排序的方法很简单:建图的时候记录每个结点的入度,然后用队列去维护所有入度为0的点,每去掉一个点的时候遍历其所有的边,将边的末端结点的入度减一,遇到入度为0的结点就入队即可。




最小生成树

生成树定义为(n个点)无向图的一个具有n-1条边的连通生成子图。而最小生成树是对应于具有边权的连通无向图来说的,最小生成树就是所有生成树中边权和最小的那一个。

计算最小生成树往往采用Kruskal算法,算法流程是:将所有边按照边权从小到大排序,然后从小到大遍历,用并查集维护结点的连通性,每遇到未连通的两个结点,就将该边加入生成树的边集之中。这实际上是一种贪心算法。

Kruskal算法的代码如下:

const int maxn=2e5+5;
struct edge
{
    int u,v,w;
    bool operator < (const edge &x) const {return w<x.w;}
}e[maxn];
int far[maxn];

int findd(int x) {return x==far[x]?x:far[x]=findd(far[x]);}
bool isSame(int x,int y) {return findd(x)==findd(y);}
void unite(int x,int y) {far[findd(x)]=findd(y);}

int main()
{
    int n=read(),m=read(),tot=0,ans=0;
    REP(i,1,m)
    {
        int u=read(),v=read(),w=read();
        e[i]=(edge){u,v,w};
    }
    sort(e+1,e+m+1);
    REP(i,1,n) far[i]=i;
    REP(i,1,m) if(!isSame(e[i].u,e[i].v))
        unite(e[i].u,e[i].v),tot++,ans+=e[i].w;
    if(tot<n-1) puts("orz");
    else printf("%d",ans);

    return 0;
}

注意,这样算出来的最小生成树同时也是最小瓶颈生成树,即生成树中最大边权值在所有生成树中是最小的。

还有一种次小生成树,即最小的大于等于最小生成树边权和的生成树,这里我们可以先求出最小生成树之后,对于每一个不在最小生成树中的边e=<u,v,w>,我们寻找u到v的路径(这条路径一定是唯一的)上的最大边权的那条边,然后用w去替换它的边权;这样构建的所有树当中边权和最小的就是次小生成树。其中u到v最大边权的求解可以使用树链剖分+线段树。




最小树形图

最小树形图是指:在一个带边权的有向图中,给定一个根root,构建一个以root为根节点的有向树,使得其边权和最小。

计算最小边权和采用朱刘算法,时间复杂度为O(VE)。

算法的流程为不断重复以下过程:

  1. 对除root之外的每个结点找出一个边权最小的入边(如果没有入边说明不存在树形图,直接返回-1),这些入边(总共n-1条)构成一个边集E;
  2. 将E中所有环缩点,如果E中没有环说明已经找到了最小树形图,跳出;(由于每个点只有一个入边,故E要么是一棵树,要么由一些树加上一些环组成)
  3. 缩点过后重新建图,建图时对边权做一些处理(反悔机制,减去上一次已选入边的边权);

这其实本质上还是一个贪心算法。

朱刘算法代码如下:

// pre记录前驱结点,in记录最小入边权,vis用于循环枚举pre找环,id用于记录缩点后各个结点的编号

const int maxn=1e4+5,inf=1e8;
struct edge{int u,v,w;}e[maxn];
int pre[maxn],in[maxn],vis[maxn],id[maxn],n,m;

int zhuliu(int root)
{
    int ans=0;
    while(1)
    {
        REP(i,1,n) in[i]=inf,vis[i]=id[i]=0;
        REP(i,1,m) if(e[i].u!=e[i].v && e[i].w<in[e[i].v]) in[e[i].v]=e[i].w,pre[e[i].v]=e[i].u;
        REP(i,1,n) if(i!=root && in[i]==inf) return -1;
        int cnt=0; in[root]=0;
        REP(i,1,n)
        {
            ans+=in[i];
            int v=i;
            while(vis[v]!=i && !id[v] && v!=root) vis[v]=i,v=pre[v];
            if(!id[v] && v!=root)
            {
                id[v]=++cnt;
                for(int u=pre[v];u!=v;u=pre[u]) id[u]=cnt;
            }
        }
        if(!cnt) break;
        REP(i,1,n) if(!id[i]) id[i]=++cnt;
        REP(i,1,m)
        {
            int u=e[i].u,v=e[i].v;
            e[i].u=id[u],e[i].v=id[v];
            if(id[u]!=id[v]) e[i].w-=in[v];
        }
        root=id[root]; n=cnt;
    }
    return ans;
}
发布了12 篇原创文章 · 获赞 5 · 访问量 524

猜你喜欢

转载自blog.csdn.net/dragonylee/article/details/103906250
今日推荐