Skip to content

Latest commit

 

History

History
99 lines (93 loc) · 5.54 KB

6E.md

File metadata and controls

99 lines (93 loc) · 5.54 KB

Dinic算法

Dinic算法是最大流问题的经典算法,源自于一种称为Ford-Fulkerson方法的思想。

剥削之术

  Ford-Fulkerson方法的核心思想和Floyd-Warshall算法相似,使用一种浪淘沙式地筛选策略。这里不求一步到位,但求不断地从残图中找到可行路径以榨取剩余流量。

func (ctx *contextL) dinic() uint {
    flow := uint(0)
    for ctx.separate() {                   //抽取子图(层次图)
        flow += ctx.search()               //从子图中榨取剩余流量
        ctx.flushBack()                    //残图整合(将榨干的子图合并回母图)
    }
    return flow
}

  为了方便剥削,Dinic算法引入了等级制度,先把点分成三六九等,然后按阶层逐级征收。虽然同一阶层的点暂无往来,但由于每轮征收过后总有一些关系破裂(边断掉),等级会因此重新洗牌,所以任何关系最终都不会漏网。

                                                                     A
                             A                                       |
                            / \                                      D
    A - B - E              B   D              A   B - E             / \
    | / | / |     =>      / \ /               | /   / |     =>     B   C
    D - C - Z            E   C                D - C   Z             \ /
                          \ /                                        E
                           Z                                         |
                                                                     Z

阶层分化

阶层分化过程采用宽度优先搜索,再排除掉暂时没有利用价值的点:

func (ctx *contextL) separate() bool {
    if !ctx.markLevel() {               //标记节点层次
        return false
    }
    for curr := 0; curr < len(ctx.level); curr++ {
        if ctx.level[curr] == fakeLevel {
            continue
        }
        paths := ctx.origin[curr]
        for i := 0; i < len(paths); i++ {
            next := paths[i].Next
            if ctx.level[next] == ctx.level[curr]+1 {
                ctx.shadow[curr] = append(ctx.shadow[curr], paths[i])
                paths[i].Weight = 0        //将主图内容抽取到残图
    }   }   }
    return true
}

搜刮

我们采用深度优先搜索,试探是否有可以搜刮的路径:

func (ctx *contextL) search() uint {
    for flow, stream := uint(0), uint(0); ; flow += stream {        //每一轮都至少会删除图的一条边
        stream = math.MaxUint
        ctx.stack.Clear()
        for curr := ctx.start; curr != ctx.end; {
            sz := len(ctx.shadow[curr])
            if sz != 0 {                                            //可通
                ctx.stack.Push(memo{idx: curr, stream: stream})     //下探
                path := ctx.shadow[curr][sz-1]
                curr, stream = path.Next, utils.Min(stream, path.Weight)
            } else {                                                //碰壁,退一步
                if ctx.stack.IsEmpty() { return flow }              //退无可退
                tmp := ctx.stack.Pop()
                curr, stream = tmp.idx, tmp.stream
                last := len(ctx.shadow[curr]) - 1
                fillBack(ctx.origin[curr], ctx.shadow[curr][last])
                ctx.shadow[curr] = ctx.shadow[curr][:last]
            }
        }
        for !ctx.stack.IsEmpty() {                                  //处理找到的增广路径
            tmp := ctx.stack.Pop()
            curr := tmp.idx
            last := len(ctx.shadow[curr]) - 1
            path := &ctx.shadow[curr][last]
            path.Weight -= stream                                   //抽出顺流
            ctx.reflux[path.Next] = append(ctx.reflux[path.Next],
                graph.Path{Next: curr, Weight: stream})             //添加逆流容限,防止贪心断路
            if path.Weight == 0 {
                ctx.shadow[curr] = ctx.shadow[curr][:last]          //剔除无效残边
}   }   }   }

  这里有一点要注意,有些时候切断边会意外破坏图的连通性。物理上,不存在管道消失一说,今后显然还可以引入逆流来抵消本次的流选择。而在算法中,我们也要给予相应的支持。

    A → C → D          A   C → D                 A   C → D
    ↓   ↓   ↓    =>    ↓       ↓     =(fix)=>    ↓   ↑   ↓
    B → E → F          B → E   F                 B → E   F

性能分析

  我们看到Dinic算法的核心是抽取子图、榨取剩余流量以及残图整合这三个操作的循环。
  其中,榨取剩余流量的过程包括试探和整理两部分。一次整理需要遍历记录栈,操作次数为O(V)级,同时至少删除子图中的一条边。整理过程对边的删除是试探成功的结果,另一方面,试探失败也会逐渐删除子图里无效路径中的边,故试探过程总是会删除子图中的边。由此可以推知,驱动试探过程的三重循环累计执行次数为O(E),而其中最外层循环的执行次数必然小于O(E)。
  基于二分查找的残边回收操作的复杂度为O(logV),基于归并的图整合操作的复杂度为O(E)。另外,抽取子图的复杂度是O(E)。最后,我们不难算出Dinic算法的复杂度为O(E/k) × (O(E) + O(kV) × O(logV) + O(kV + E)) = O(V2E)。


目录 上一节 下一节