文章出处:http://blog.sina.com.cn/s/blog_60a0e97e0101bfj9.html
因为最近在研究图像分割,看到经典方法graph cut涉及的最大流/最小割问题,查了很多资料,基本上很只讲了一些方法实现的思想,没有具体实现能地让我们直观地明白整个理论。后来啃了很多资料,结合自己编代码,终于把最大流/最小割的问题具体实现了(代码见文章最后)。下面开始详细地讲解最大流/最小割的具体实现步骤(理论部分随意在网上都可以找到很多)。
![[原创]最大流/最小割(maxflow/mincut)的原理讲解和代码实现 [原创]最大流/最小割(maxflow/mincut)的原理讲解和代码实现](https://i-blog.csdnimg.cn/blog_migrate/96cc11e218dd29d8376347ae5edb2919.jpeg)
Ford-Fulkerson算法
直观思想:从任意一个可行流(如零流)出发,找到一条源s到汇t的增广路,并在该增广路上增加流值,于是得到一个新的可行流。循环此过程直到找不出s到t的新的增广路。
该算法关键是找到s到t的增广路,这可通过标号法实现,具体规则为:
- 一个节点先进行标号,再检查。一个节点可处于三个状态之一:已标号并且已检查;已标号但未检查;未标号。每次检查是从已标号的节点开始,所以设置源点s为永久标号点,当其他都未标号时,算法从已标号的源点开始。
- 一个节点i的标号表现形式由两个分量组成(+j, δ(i))或 (-j, δ(i)) ,第一个分量表示了前继点或后继点(即某条通过节点i的可行流的前一个节点j或后一个节点j),第二个分量表示了允许增加的流量 δ(i)=min( δ(j), cij-fij )
- 从每个已标号但未检查的节点i开始,对其进行检查:检查该所相邻的所有节点j(前继点或后继点),如果存在未标号点j使得有向弧(i, j)的现有流量 fij <该弧的容量 cij ,则对该节点j进行前继标记(+i, δ(j)) 其中 δ(j)=min( δ(i), cij-fij ) ;或者存在未标号点j使得有向弧(j,i)的现有流量 fji >0,则对该节点j进行后继标记(-i, δ(j)) 其中 δ(j)=min( δ(i), fji ) 。
算法步骤:
1°. 令x=(xij)是任意整数可行流,给s一个永久标号 (*,∞);
2°. (1)如果所有标号顶点都已检查,转第4步
(2)找到一个已标号但未检查的顶点i,做如下检查:对每一条有向弧(i,j)且j未标号, 如果fij < cij 则给j标号(+i, δ(j)),δ(j)=min(δ(i), cij-fij) ;对每一条有向弧(j,i)且j未标号,如果 fji >0,则给j标号(-i, δ(j)),δ(j)=min(δ(i), fji) 。
(3)如果汇t被标记,转3°,否则转(1)
3°. 根据得到的增广路上各项标号来增加流量,并抹去除了源s外的标号转2°
4°. 此时当前流是最大流,且把所有标好点的集合记为S,(S, /S)是G的最小割。
Push-Relabel算法:
直观思想:先加入充足的流(跟s相连的所有边的容量之和),加入之后呢,再慢慢一个边一个边的向汇点渗透。直到没法再渗透(类似于ford-fulkerson算法中找不到增广路径了),那么这时再把一些剩余的流回收到source就可。
主要分为两个步骤:push和relabel。push表示从任一个节点找出次节点的存水量和另一个相邻节点的剩余流量,比较哪一个小,借此可得此节点在增加该路径的流量之后是否还有存水量。要实现该push的操作必须满足三个条件:该点存水量>0,管线的容量大于流量,该点高度大于另一个点高度。relabel表示某一个节点存水量大于0但水流不出去时,重新设置该节点高度,使得该节点的存水量流入比它低的节点。
算法步骤:
- 初始化前置流:将与源点s相连的管道流量f(0,i)设为该管道的容量,即 f(0,i)=c(0,i);将源点s的高度h(0)=V,(V表示图的顶点个数),其余顶点高度h(i)=0;将源的点余量e(0)设为源容量减去源的流出量,即e(0)=-∑f(0,i)=-∑c(0,i),与源s相连的点余量设为该点的流入量e(i)=c(0,i),其余点都为0。
- 构造一个存储顶点的队列vlist,用以检查点的压栈。从源点s出发,将与之相连的顶点压入栈。
- 每次从栈中取首个元素,即某一个点,检查其点余量e(i),若不为0,表示要对该点进行操作——重标记或者压入流:检查与该点i全部的相邻点j,若该点比它相邻点的高度大h(i)>h(j)且该管道的容量c(i,j)大于流量f(i,j)时,将该点的余量以最大方式压入该管道delta=min(e(i), c(i,j)-f(i,j)), 点余量e、流量f相应的进行减加,另外在队列中加入满足点余量e(j)>0的相邻点j(vlist.push(j); j原不存在该队列中);若没有相邻点满足上述条件,则将该点的高度值h(i)根据相邻点j进行增加,h(i)=min(h(j))+1。以上的重标记或压入流操作循环进行,直至该点的余量e(i)为0。
- 重复第3步,直至队列vlist中没有元素,停止算法,最后输出汇点t的余量e(t),t=V-1, 该值就是最后所求的最大流。最小割就是选择以上
扩展:
- Push-Relabel算法中的顶点i的最大增加量为2V-1,(数学上可以证明),因此到最后可能与源相邻的某个点i会以高度h(i)>V的方式将多余量返回给源(源的高度值始终是V);
- Push-Relabel算法的复杂度为O(V2E)
以下就是代码的实现,你需要做的只是在项目的文件夹中建立一个名为graph_data.txt 的文件,将下面提到的测试值放进去,该测试值与上面的图是对应的,除了第一二个数据分别表示图中的边数、顶点个数之外,下面每三个数据分别表示了节点1、节点2、节点1,2间的最大容量(capacity)。两种方法的结果肯定是一样的,都是以下结果,8是最大流值,后面的0、1、2表示顶点0、1、2属于最小割方案的集合S,其余点归入另一个集合/S,G={S, /S}就是最小割方案。
![[原创]最大流/最小割(maxflow/mincut)的原理讲解和代码实现 [原创]最大流/最小割(maxflow/mincut)的原理讲解和代码实现](https://i-blog.csdnimg.cn/blog_migrate/589371c6ce7a710fc131ee1a86f6058f.jpeg)
//Ford_Fulkerson.cpp : 定义控制台应用程序的入口点。
//测试值:
#include "stdafx.h"
#include
#include
#include
#include
#include
using namespace std;
#define INFI 1000
typedef struct _mark
{
int pre_suc;
int max_incr;
}MARK;
int iteration = 0;
const int N = 100;
list setS;
bool isMark[N], isCheck[N], isDone;
MARK markList[N];
int c[N][N], f[N][N];
int n; //顶点数
void Mark(int index, int pre_suc, int max_incr)
{
isMark[index] = true;
markList[index].pre_suc = pre_suc;
markList[index].max_incr = max_incr;
}
void Check(int i)
{
isCheck[i] = true;
for (int j=0; j
{
if (c[i][j]>0&& !isMark[j]&& c[i][j]>f[i][j])
Mark(j, i, min(markList[i].max_incr, c[i][j]-f[i][j]));
if(c[j][i]>0&& !isMark[j]&& f[j][i]>0)
Mark(j, -i, min(markList[i].max_incr, f[j][i]));
}
}
int Maxflow()
{
int flow =0;
for (int i=0; i
{
flow += f[0][i];
}
return flow;
}
void Mincut()
{
int i = 0;
while (i
{
if(isMark[i])
setS.push_back(i);
i++;
}
}
int IncrFlowAuxi(int index)//辅助函数:计算增广路径中的最大可增量
{
if(index==0) return markList[index].max_incr;
int prev = markList[index].pre_suc;
int maxIncr = markList[index].max_incr;
return min(maxIncr, IncrFlowAuxi(prev));
}
void IncrFlow()//增广路径的增加
{
iteration++;
int incr = IncrFlowAuxi(n-1); //最大可增量
int index = n-1;
int prev;
while(index!=0)
{
prev = markList[index].pre_suc;
f[prev][index] += incr; //增广路径增加后,相应的流量进行更新
index = prev;
}
}
//ford_fulkerson算法
int ford_fulkerson()
{
int i;
while (1)
{
isDone = true;
i=0;
while(i
{
if (isMark[i] && !isCheck[i]) //判断是否所有标记的点都已被检查:若是,结束整个算法
{
isDone = false;
break;
}
i++;
}
if (isDone) //算法结束,则计算最小割和最大流
{
Mincut();
return Maxflow();
}
while (i
{
if(isMark[i]&& !isCheck[i]) {
Check(i);
i = 0;
}
if(isMark[n-1]) //如果汇t被标记,说明找到了一条增广路径,则增加该条路径的最大可增加量
{
IncrFlow();
memset(isMark+1, false, n-1); //增加该增广路径后,除了源s,其余标记抹去
memset(isCheck, false, n);
}
else i++;
}
}
}
int main()
{
int m, i, j;
fstream fptr;
fptr.open("graph_data2.txt",ios::in);
fptr>>m;
fptr>>n;
for (int k = 0; k < n; ++k)
{
memset(c[k], 0, sizeof(c[0][0])*n);
memset(f[k], 0, sizeof(f[0][0])*n); //初始各分支流量为0
memset(isMark, false, n);
memset(isCheck, false, n);
}
isMark[0] = true; //给源做永久标记
markList[0].max_incr = INFI;
markList[0].pre_suc = INFI;
while (!fptr.eof())
{
fptr>>i;
fptr>>j;
fptr>>c[i][j];
}
fptr.close();
printf("max flow = %d\n",ford_fulkerson());
printf("segment set S = {");
for(list::iterator i=setS.begin(); i!=setS.end(); i++)
{
printf("%d ",*i);
}
printf("}\n");
printf("iteration=%d",iteration);
printf("\n\n");
system("PAUSE");
}
//The Push-Relabel Algorithm 最大流的压入与重标记算法
#include "stdafx.h"
#include
#include
#include
#include
#include
using namespace std;
const int N = 100;
bool isMark[N] = {false};
bool isCheck[N] = {false};
bool flag[N]; //顶点是否在队列 vlist 中
int c[N][N], //有向边的容量
e[N], //余流
f[N][N], //流
h[N], //高度
n; //顶点数
list vlist, //待排除顶点
vNearArr[N]; //邻接表
void Push(int x, int y)
{
int df = min(e[x], c[x][y]- f[x][y]);
f[x][y] += df;
f[y][x] = -f[x][y];
e[x] -= df;
e[y] += df;
}
void Relabel(int x)
{
h[x] = n*2-1;
for (list::iterator iter = vNearArr[x].begin(); iter != vNearArr[x].end(); ++iter)
if (h[*iter] < h[x] && c[x][*iter]> f[x][*iter])
h[x] = h[*iter];
++h[x];
}
void Discharge(int x)
{
list::iterator iter = vNearArr[x].begin();
while (e[x] > 0) //有余流
{
if (iter == vNearArr[x].end())
{
Relabel(x);
iter = vNearArr[x].begin();
}
if (h[x] == h[*iter]+1&& c[x][*iter] > f[x][*iter])
{
Push(x, *iter);
if (e[*iter]> 0 && !flag[*iter])
vlist.push_back(*iter);
}
++iter;
}
}
void Check(int index)
{
isCheck[index] = true;
int i=0;
while (i
{
if(c[index][i]>0&& (c[index][i]-f[index][i]>0)) //有余流
isMark[i] = true;
i++;
}
}
void FindMinCut()//最小割存在于源s能够到达的所有点集合(包括s),即s能够通过余量到达该点
{
isMark[0] = true; //s永久可达到
int i=0;
while (i
{
if(isMark[i]&& !isCheck[i]) {
Check(i);
i = 0;
}
else i++;
}
}
int Push_Relabel()
{
vlist.clear();
//初始化前置流
h[0] = n;
e[0] = 0;
memset(flag+1, 0, n-2); //1和n-1(即源和汇)不在 vlist 中
flag[0] = true;
flag[n-1] = true; //防止源、汇进入 vlist
for (int i = 1; i < n; ++i)
{
h[i] = 0; //初始化各顶点高度为 h(i)=0
f[0][i] = c[0][i]; //初始化源与其他与之相连的 边流量f(0,i)=边容量c(0,i)
f[i][0] = -c[0][i]; //反向边容量
e[0] -= c[0][i]; //初始化源的 点余量e(0)=-c(0,i)
e[i] = c[0][i]; //初始化其他 点余量e(i)=c(0,i)
if (c[0][i] > 0 && i != n-1)
{
vlist.push_back(i); //待排除顶点,压入栈
flag[i] = true;
}
}
//构造邻接表,每个点i的列表vNearArr[i]中存储与点i在图上相邻的点
for (int i = 0; i < n-1; ++i)
for (int j = i; ++j< n; )
if (c[j][i]> 0 || c[i][j] > 0)
{
vNearArr[i].push_back(j);
vNearArr[j].push_back(i);
};
//排除队列中的顶点
while (!vlist.empty())
{
int x = vlist.front();
Discharge(x);
vlist.pop_front();
flag[x] = false;
}
FindMinCut();
return e[n-1];
}
int main()
{
int m;
fstream fptr;
fptr.open("graph_data.txt",ios::in);
fptr>>m;
fptr>>n;
for (int i = 0; i < n; ++i)
{
memset(c, 0, sizeof(c[0][0])*n);
memset(f, 0, sizeof(f[0][0])*n);
vNearArr[i].clear();
}
int x, y, w;
while (!fptr.eof())
{
fptr>>x;
fptr>>y;
fptr>>c[x][y];
}
fptr.close();
printf("%d\n", Push_Relabel()); //计算从0(源)到 n-1(汇)的最大流
for (int i=0; i
{
if(isMark[i]) printf("%d ", i);
}
printf("\n");
system("PAUSE");
}