ACM算法总结 图论(二)




单源最短路

给定一个有向图或者无向图G,给定一个原点s,求解从s出发到各个顶点的最短距离,就是单源最短路问题。一般单源最短路有两种简洁高效的求解方法:dijkstraSPFA

dijkstra(迪杰斯特拉)算法的主要思想就是:每次从未确定的结点中选择距离最短的那个,然后把它确定下来(这里确定是指认为当前距离就是从s到这个结点的最短距离),然后用这个新确定的结点去松弛其它未确定的结点(松弛这个概念非常重要,在求最短路过程中的意义就是用d[u]+dis(u,v)去更新d[v],其中u是新确定的结点,v是与u相连的结点)。然后不停重复以上过程,直至所有结点都被确定。

其实仔细想想,这个过程也有一种贪心的意味,感觉好多算法都暗含贪心的思想。

在代码实现中,我们用优先队列去维护可能的每个结点的最短距离,也就是凡是被松弛的都加入优先队列中,然后选取最短的即可。这样复杂度为O(mlogm)。

dijkstra算法代码如下:

// vis记录结点最短距离是否已经确定; 里面松弛的时候顺便更新了d,是为了有一定的优化,使得后面进队列的少一些

const int maxn=1e5+5,inf=1e9+1;
struct edge {int v,w;};
vector<edge> G[maxn];
int n,m,d[maxn],vis[maxn];

void dijkstra(int s)
{
    fill(d,d+n+5,inf);
    //mem(vis,0);
    d[s]=0;
    typedef pair<int,int> P;
    priority_queue<P,vector<P>,greater<P> > Q;
    Q.push(P(0,s));
    while(!Q.empty())
    {
        P p=Q.top(); Q.pop();
        int dis=p.first,u=p.second;
        if(vis[u]) continue;
        d[u]=dis; vis[u]=1;
        REP(i,0,G[u].size()-1)
        {
            int v=G[u][i].v,w=G[u][i].w;
            if(vis[v] || dis+w>d[v]) continue;
            Q.push(P(d[v]=dis+w,v));
        }
    }
}

SPFA算法是一种带有队列优化的Bellman-Ford算法,算法步骤如下:

用队列维护一个待松弛(这里指的是去松弛别的结点)的结点序列,每次取出队首去松弛其它结点,把成功松弛且不在队列中的结点加入队尾;重复直到队列为空。

SPFA一般的复杂度是O(km),其中k是结点平均入队次数,如果不刻意卡数据,k的值一般非常非常小(可以忽略);对于特殊数据最坏的复杂度可以退化到O(nm)。

扫描二维码关注公众号,回复: 8699577 查看本文章

SPFA算法的代码如下:

// vis记录是否在队列中,num记录进入队列次数
// 返回0表示存在负环

const int maxn=1e5+5,inf=1e9+1;
struct edge {int v,w;};
vector<edge> G[maxn];
int n,m,d[maxn],vis[maxn],num[maxn];

bool SPFA(int s)
{
    fill(d,d+n+5,inf);
    //mem(vis,0); mem(num,0);
    d[s]=0; vis[s]=1; num[s]=1;
    queue<int> Q;
    Q.push(s);
    while(!Q.empty())
    {
        int u=Q.front(); Q.pop();
        vis[u]=0;
        REP(i,0,G[u].size()-1)
        {
            int v=G[u][i].v,w=G[u][i].w;
            if(d[u]+w>=d[v]) continue;
            d[v]=d[u]+w;
            if((num[v]=num[u]+1)>n) return 0;
            if(!vis[v]) Q.push(v),vis[v]=1;
        }
    }
    return 1;
}

这两种单源最短路算法其实都挺好的,但是各有春秋:dijkstra很快,而且不会被卡,但是它不能处理负边权;SPFA可以处理任何边权,而且可以判断负环(某个结点入队次数超过n说明有负环)。

  • 在用SPFA判断负环的时候,由于图不一定保证连通性,所以要循环对没有vis过的点都SPFA一次;
  • 或者可以建一个超级源点s,s往每个点连一条边权为0的边,这样保证连通性,直接对s进行一次SPFA即可。




差分约束系统

差分约束系统形如: { x i x j c k     k = 1 , 2 , . . . } \{ x_i-x_j\leq c_k \ | \ k=1,2,... \} ;一般是要求解某个 x a x b x_a-x_b 的最大(或最小)值。

我们考虑求解 x a x b x_a-x_b 的最大值,将 x i x j c k x_i-x_j\leq c_k 变形为 x i x j + c k x_i\leq x_j+ c_k ,这类似于最短路中的松弛过程,事实上变形之后的式子就是我们松弛要达到的最终目标(也就是不再有任何结点可以松弛其它结点)。而最终可以松弛出一个常数 c 0 c_0 ,使得 x a x b c 0 x_a-x_b\leq c_0 ,这个上界 c 0 c_0 就是要求的最大值。

利用最短路的算法流程如下:将所有变量 x x 看成结点,对于每个约束 x i x j c k x_i-x_j\leq c_k ,建立一条从 x j x_j x i x_i 的边权为 c k c_k 的有向边,最终 x a x b x_a-x_b 的最大值就是 x b x_b x a x_a 的最短路。如果最后算出最短路不可达,也即最短距离为inf,那么说明 x a x b x_a-x_b 可以达到无穷大;如果算出存在负环,那么说明该约束系统无解。

一些变形:

  • 如果约束中符号相反,即 x i x j c k x_i-x_j\geq c_k ,可变形为 x j x i c k x_j-x_i\leq -c_k
  • 如果存在约束 x i = x j x_i=x_j ,可变形为 x i x j 0 x_i-x_j\geq 0 x i x j 0 x_i-x_j\leq 0
  • 如果求的是 x a x b x_a-x_b 的最小值,可转变为求 x b x a x_b-x_a 的最大值(或者是同样建图求最长路);
  • 如果求的是解的存在性,可以建一个超级源点s,s往每个点连一条边权为0的边,然后判断是否存在负环;




分层图

把图分层是一种思想,典型问题是这样的:在一个带边权的图中,我们有k次机会可以忽略某条边的边权,然后求单源最短路。

分层图的思想就是把图复制k份,然后看成一层一层的,在相邻的两层中连边,边权为0,这些边是原来图中存在的边,只不过把末结点换成了相邻层的相同位置的结点。然后求解 s 到其它所有层所有点的最短路,可以发现最后相同位置的 k 个 t 结点的最短距离就是我们要求的最短路。

然而我们可以用另外一种思路去考虑。求解最短路的本质思想就是用短的路径去松弛别的路径,将松弛的想法运用到分层图这种问题中,我们可以将原来的距离数组 d[i] 改成 d[i][j] ,表示从 s 到 i 使用了 j 次机会的最短路;一开始 d[s][0]=0 ,其它都为 inf ,然后不停地用当前某个最小距离去松弛其它的 d[i][j] 就可以了(类似dijkstra)。(容易看出对于某个最小距离 d[u][q] ,可以用 d[u][q]+dis(u,v) 松弛 d[v][q] ,以及用 d[u][q] 松弛 d[v][q+1])

贴一个经典例题([JLOI2011]飞行路线)的代码:

const int maxn=1e4+5,inf=1e9+1;
struct edge {int v,w;};
vector<edge> G[maxn];
int n,m,k,s,t,d[maxn][12],vis[maxn][12];

void dijkstra(int s)
{
    REP(i,1,n+1) REP(j,0,k) d[i][j]=inf;
    //mem(vis,0);
    d[s][0]=0;
    typedef pair<int,int> P;
    typedef pair<int,P> PP;
    priority_queue<PP,vector<PP>,greater<PP> > Q;
    Q.push(PP(0,P(s,0)));
    while(!Q.empty())
    {
        PP p=Q.top(); Q.pop();
        int dis=p.first,u=p.second.first,q=p.second.second;
        if(vis[u][q]) continue;
        d[u][q]=dis; vis[u][q]=1;
        REP(i,0,G[u].size()-1)
        {
            int v=G[u][i].v,w=G[u][i].w;
            if(!vis[v][q] && dis+w<d[v][q]) Q.push(PP(d[v][q]=dis+w,P(v,q)));
            if(q<k && !vis[v][q+1] && dis<d[v][q+1]) Q.push(PP(d[v][q+1]=dis,P(v,q+1)));
        }
    }
}

int main()
{
    //freopen("input.txt","r",stdin);
    n=read(),m=read(),k=read();
    s=read()+1,t=read()+1;
    while(m--)
    {
        int u=read()+1,v=read()+1,w=read();
        G[u].push_back((edge){v,w});
        G[v].push_back((edge){u,w});
    }
    dijkstra(s);
    int ans=inf;
    REP(i,0,k) ans=min(ans,d[t][i]);
    cout<<ans;

    return 0;
}




多源最短路

多源最短路就是要求出图G中两两结点之间的最短路,不难看出最后会得到一个二维距离矩阵d。

经典算法是Floyd算法,枚举每一个中间节点去松弛,时间复杂度为 O ( n 3 ) O(n^3)

核心代码也就这样:

REP(k,1,n) REP(i,1,n) REP(j,1,n) if(i!=j) d[i][j]=min(d[i][j],d[i][k]+d[k][j]);

然后 d 矩阵(距离矩阵)的初始值是这样的:如果i=j,d[i][j]=0;如果有从 i 到 j 的边,d[i][j]赋为该边的权值;否则,d[i][j]=inf 。

  • Floyd算法不能处理负环;

如果想用快一点的算法,其实很简单,算 n 次单源最短路就可以啦。


最后对最短路这一类问题说一下输出路径的方法。由于最短路算法本质上都是不断松弛的过程,所以我们只要开一个pre数组记录前驱结点,在每次松弛的时候更新前驱结点,最后反着寻找路径就可以了。




欧拉图

欧拉图定义为:图中所有结点的度数都是偶数。

欧拉路径定义为:图中的一条路径,这条路径经过了每条边恰好一次。

欧拉回路定义为:是欧拉路径的回路。

一个重要的定理:一个无向连通图是欧拉图当且仅当它有欧拉回路

  • 如果一个图是欧拉图的话,那么它一定是若干个环的并。
  • 如果一个图有欧拉路径,那么它最多只能有两个奇结点(及度数为奇数)。

对于有向图来说的话,如果每个结点的入度都等于出度,那么该图存在欧拉回路。

判断一个图是否具有欧拉回路或者欧拉路径很简单,比较难的是如何输出欧拉回路或者欧拉路径。一个简洁的方式是用dfs从一个恰当的结点(欧拉回路无所谓,欧拉路径要奇结点优先)开始,搜索整个图,搜索的过程中删去走过的边,回溯的时候记录当前结点。回溯所记录的路径就是要找的欧拉回路或者欧拉路径。




哈密顿图

哈密顿路径的定义:一条遍历了所有结点一次的路径。

哈密顿回路的定义:是哈密顿路径的回路。

哈密顿图:具有哈密顿回路的图。

哈密顿图的判定并没有充要条件,如果要求一个图的哈密顿路径的话,一般使用dfs或者状态压缩dp什么的去求解。

发布了12 篇原创文章 · 获赞 5 · 访问量 523

猜你喜欢

转载自blog.csdn.net/dragonylee/article/details/103916546