1. 程式人生 > >網路流 最大流 之 最高標號預流推進演算法(HLPP) [未完]

網路流 最大流 之 最高標號預流推進演算法(HLPP) [未完]

等等我的貌似是一般版的=-=不快啊 好煩還是得補

HLPP 從入門服毒 兩道題目 讓你感受希望及其之後的絕望~~

前提知識大概有 網路流 + 一般預流推進演算法 && 堆 掌握概念即可 (好吧其實之後也有提到一些)

前言:

        以前學的東西 發的部落格的程式碼 大多是看這些高大上的程式碼 然後自己修改成常規做法的 因此本人從中受益匪淺 發的部落格註釋十分仔細 但下面那程式我實在不想改 =-= 鬥宗強者竟恐怖如斯

        時間軸 2018.7.5 學完SAP後 剛好洛谷有加強版題 便交上去 RE了 找不出原因 該拓的拓了 該加的加了

        時間軸 2018.7.20 原來要用預流推進=-= 然而我沒加高標的TLE了五個點 共6個

        時間軸 2018.8.8 HLPP終於弄出來了~~普通模板過得了 又發現那題原來是爆 int 然後我把 long long 和 int 一起用了 非常混亂 最後RE了四個點 還是沒AC這題......這裡乾脆用回普通模板

        時間軸 2018.8.22 第三個點過啦~~ 嘗試中~~

        時間軸 2018.9.4 發現自己的貌似和 真の預流推進 還是有很大差別的 翻了自個學校的 dalao 程式碼 和下面那差不多 =-= 待補

時間軸 2018.7.5 從上午10點在機房到晚上 到處翻HLPP模板的 Frocean 心力交瘁 =-=

首先來一個dalao級別的高標推程序序

/*
Description: 改進的預流推進演算法,有如下優化:
進行了預先逆序BFS分層,
利用結點連結串列,每次都是從高度最大的結點開始處理.
利用指向各頂點的邊表結點的指標,記錄已經訪問到該頂點的第幾條邊了,
下次推進時可直接從該條邊操作,不用從第一條邊開始一路找過來。

Frocean:子程式放在開頭方便瀏覽(好吧其實是懶得刪原文) 

int MaxFlow_relabel_to_front(int src, int des, int n) ;
bool BFS(int src, int des, int n); //廣度優先搜尋構造分層網路
void Check(int u); //對頂點u進行預流推進或重新標號操作,直到其剩餘流量為0
void AddFlow(int u, int v, int w); //增加殘流網路邊<u,v>的容量
*/

#include <iostream>
#include <fstream>
using namespace std;
typedef struct EdgeNode //邊表結點
{
	int adjvex; //鄰接點域,儲存該頂點對應的下標
	int weight; //權值,對於非網圖可以不需要
	struct EdgeNode *next; //鏈域,指向下一個鄰接點
}EdgeNode;
typedef struct VertexNode //頂點表結點
{
	int data; //頂點域,儲存頂點資訊
	EdgeNode *firstEdge; //邊表頭指標
}VertexNode;
typedef struct VertexNodeLink //結點連結串列
{
	int num; //儲存該頂點對應的下標
	struct VertexNodeLink *next; //鏈域,指向下一個結點
}VertexNodeLink;
const int MAXV = 2000; //最大頂點數量
const int MAXE = 2000; //最大邊數量
const int INFINITY = 0x7fffffff; //無窮大
VertexNode GL[MAXV]; //儲存頂點表結點資訊
EdgeNode * EdgeNodePoint[MAXV]; //儲存指向各頂點的邊表結點的指標
int flow[MAXV];  //標記當前節點的剩餘流量
int dis[MAXV]; //標記節點所在的層次
VertexNodeLink *head; //結點連結串列頭結點
bool BFS(int src, int des, int n)
{ //逆向廣度優先搜尋構造分層網路,若不存在增廣路,則返回false
	EdgeNode *e;
	int Queue[MAXV]; //求最短增廣路演算法需要用到的佇列
	int v, front = 0, rear = 0; //清空佇列
	for(int i = 0; i < n; ++ i) //初始化列表
	{
		dis[i] = 0;
		EdgeNodePoint[i] = GL[i].firstEdge; //初始化指向各頂點的邊表結點的指標
	}
	Queue[rear++] = des; //匯點加入佇列
	while (front < rear) //佇列非空
	{
		v = Queue[front++];
		for(e = GL[v].firstEdge; e != NULL; e = e->next) //尋找逆向邊,並設定層數
			if (e->weight == 0 && dis[e->adjvex] == 0)
			{ //是一條逆向邊
				dis[e->adjvex] = dis[v] + 1;
				Queue[rear++] = e->adjvex;
			}
	}
	if (dis[src] == 0) return false;//未找到源點,說明不存在流
	VertexNodeLink *s;
	head = new VertexNodeLink;
	head->next = NULL;
	for (front = 1; front < rear; front ++) //使用頭插法將結點按高度順序插入連結串列
		if (Queue[front] != src)
		{
			s = new VertexNodeLink;
			s->num = Queue[front];
			s->next = head->next;
			head->next = s;
		}
	return true;
}
void AddFlow(int u, int v, int w) //增加殘流網路邊<u,v>的容量
{
	EdgeNode *e;
	for (e = GL[u].firstEdge; e != NULL; e = e->next) //將源點的流量推進到鄰接點
		if (e->adjvex == v)
		{
			e->weight += w;
			break;
		}
}
void Check(int u) //對頂點u進行預流推進或重新標號操作,直到其剩餘流量為0
{
	int minFlow, minLevel, v;
	EdgeNode *e;
	while (flow[u] > 0)
	{
		e = EdgeNodePoint[u];
		if (e == NULL) //沒有可以push的頂點,執行relabel
		{
			minLevel = INFINITY;
			for (e = GL[u].firstEdge; e != NULL; e = e->next)
			if (e->weight > 0 && minLevel > dis[e->adjvex]) //尋找下一層節點
				minLevel = dis[e->adjvex];
			dis[u] = minLevel + 1;
			EdgeNodePoint[u] = GL[u].firstEdge;
		}
		else
		if (dis[u] == dis[e->adjvex]+1 && e->weight > 0)
		{ //尋找可行弧,並預流推進
			minFlow = (flow[u] < e->weight) ? flow[u] : e->weight;
			flow[u] -= minFlow;
			flow[e->adjvex] += minFlow;
			e->weight -= minFlow;
			AddFlow(e->adjvex, u, minFlow);
		}
		else EdgeNodePoint[u] = e->next; //處理下一條邊
	}
}
int MaxFlow_relabel_to_front(int src, int des, int n)
{
	int u, v, oldLevel, minFlow;
	VertexNodeLink *p = head;
	EdgeNode *e;
	if (BFS(src, des, n)) //先逆向構造分層網路
	{
		dis[src] = n;  //直接設定源點的高度為n
		for (e = GL[src].firstEdge; e != NULL; e = e->next)
		 //將源點的流量推進到鄰接點
			if (e->weight > 0) //更新結點剩餘流量和殘流網路
			{
				flow[src] -= e->weight;
				flow[e->adjvex] = e->weight;
				AddFlow(e->adjvex, src, e->weight);
				e->weight = 0;
			}
	VertexNodeLink *pre = head;
		while (pre->next)
		{
			u = pre->next->num;
			oldLevel = dis[u];
			Check(u);
			if (oldLevel < dis[u]) //層高增加了,插入到連結串列首部
			{
				p = pre->next;
				pre->next = p->next; //刪除結點p
				p->next = head->next;//將結點p插入到頭結點後面
				head->next = p;
				pre = head;
			}
			pre = pre->next; //檢視下一個結點
		}
	}
	p = head->next;
	while (p)
	{
		delete head;
		head = p;
		p = head->next;
	}
	return flow[des];
}
int main()
{
	int  m, n, u, v, w;
	for (int i = 0; i < MAXV; i ++) //初始化圖
	{
		GL[i].data = i;
		GL[i].firstEdge = NULL;
	}
	cin >> n >> m;
	EdgeNode *e;
	for(int i=0; i<m; ++i)
	{
		cin >> u >> v >> w;
		e = new EdgeNode;; //採用頭插法插入邊表結點
		if (!e) exit(1);
		e->adjvex = v;
		e->weight = w;
		e->next = GL[u].firstEdge;
		GL[u].firstEdge = e;
		//為了構造殘流網路,還要生成一條逆向邊,根據weight的值確定該邊是否存在
		e = new EdgeNode;; //採用頭插法插入邊表結點
		if (!e) exit(1);
		e->adjvex = u;
		e->weight = 0;
		e->next = GL[v].firstEdge;
		GL[v].firstEdge = e;
	}
	cout << MaxFlow_relabel_to_front(0, n-1, n) << endl;
	system("pause");
	return 0;
}

看 高標推進是不是很厲害??

好了就是嚇唬人用的 =w= 當然肯定有人能弄懂的說~

下面進入

正題

經過 Frocean 的努力 2018.8.8 通過拼湊和各種理解優化程式碼 高標推進successfully被弄出來了~~

然而普通模板那題的高標推進比ISAP要慢了5倍 剛好5倍......貌似是常數大的緣故? 不過隨機資料ISAP的確快得多 =-=

來隨便說說高標推進的概念 (專業的就自己去搜其他的吧......)

我們首先給源點一大堆流 大概有 INF 那麼多 通常 INF 定義為 0x7fffffff 等於 2147483647 不過那毒瘤模板裡這個還小了......

當然根據理論 要把每個點的流推完 這裡可以設他能流到其他點的所有流量 當然會慢點~

最高標號是把點高度用優先佇列存起來的 每次取出佇列裡最高的點 然後這裡用手打大根堆 來實現 (死不用STL的Frocean)

定義陣列

e[MAXM] 和 st[MAXN] 鄰接表存邊

h[MAXN] 某節點高度

heap[MAXN] 同字面意思 (大根) 堆

o[MAXN] 判斷某節點是否在堆裡面 0 即沒有 1 即有

gap[MAXN] 網上的dalao們說HLPP可以用gap優化~不過gap這裡不是找斷層而是找斷的高度~

ans[MAXN] 流到某節點時的最大流

步驟

1. 首先把源點丟進去啦~ 源點高度要最高 俗話說水往低處流 源點高度低怎麼流嘛

    因為有n個節點 因此源點高度設為 n 或 n - 1 如圖 (借了下 杭州學軍中學 的課件的說 然而是從我們學校課件裡找到的2333)

2. 然後都丟了點了 肯定要處理對吧

    直接找出來處理~ 利用大根堆 找到heap[1] 就是堆裡最大的數 (Tip:堆存的雖然是節點編號 但是要根據節點的深度來排序)

    堆的尋常操作——pop~ (自帶音效) 彈出heap[1] 當然別忘了彈出之前把該節點編號存下來 在這次更新要用到

2*.但在更新之前 我們要從ans數組裡面看看該點有沒有流量呀=w= 沒有還流什麼呢 當然要初始化源點的流量

    如果沒有 就直接 continue 當然這步不要也可以 不過會慢那麼一點的說

3. 開始更新 由鄰接表把當前節點能到的點都遍歷一遍 但要根據高度和兩點間剩餘流量來判斷(開始流量是滿的)

    之前說了水往低處流嘛......於是要判斷高度 源點要特判一下 如果能把流推走 就把當前點流量減去相應數值 (不一定是邊流量 可能只剩下其中的一部分 因此要 min 判斷餘流) 當然反向邊要相應地加上

4. 之後別忘了要把推到的點加入堆裡 當然堆裡如果有了就不用加了 至此 一次迴圈完畢

5. 最後還有個判斷 如果當前點流量還有剩餘 就要把他的高度提高 讓他的流能到一些原本到不了的高度的點

    舉個例子 如圖 此處的 y 點的流 把8單位的流推給z後 還有剩餘

    因此要把 y 擡高一點 通過 y 到 x 的弧把盈餘推給 x (這裡是通過反向弧推 你看箭頭) 如圖

    引用原文 : 一次必須把贏餘全部推光.所以y被重標號,當前弧指標從頭開始查詢,找到(y,x)這條可行弧之後進行推進.實際上是把多推的贏餘還給了x.因為h(u)<=h(v)+1的保證,它沒有把贏餘錯推給s

    然而他還是有盈餘 =-=

    因此我們再把他擡高 (因為中間斷層 這裡有個gap優化 直接跳到 s 上 某高度沒點就直接跳上去 可加可不加 不加會慢些) 如圖

    推到 s 或者 t 還有盈餘的話 就可以不推了 t 就不說了 s的話 概念裡 s 流量都是 INF 多的流量都剩在這裡 因此...... 

    就這麼愉快地推完了 然後處理到堆裡面沒數了 (就是沒有能增廣的流了) 就退出 輸出 ans[t] 即可

就這麼多?

就這麼多!

下放不正常的程式碼 + 例題 (洛谷的普通模板) ~ Tip:前文有標註這裡就不詳細標註啦 然後之前那洛谷的毒瘤模板這裡也放個連結

#include <algorithm>
#include <cstdio>
using namespace std;
const int MAXN = 10010;
const int MAXM = 200010;
struct Edge {
	int next,to,w;
} e[MAXM];
int st[MAXN],h[MAXN],heap[MAXN],gap[MAXN],ans[MAXN],n,s,t,tot = 1;
short o[MAXN];
void add(int x,int y,int z)
{ //鄰接表存邊 懶得標了 想了解請移步
	e[++tot].next = st[x],st[x] = tot,e[tot].to = y,e[tot].w = z;
	e[++tot].next = st[y],st[y] = tot,e[tot].to = x;
}
void push(int p)
{ //壓入堆裡
	heap[++tot] = p;
	o[p] = 1;
	int now = tot;
	while (now > 1 && h[heap[now]] > h[heap[now / 2]])
	{ //常規大根堆操作
		swap(heap[now],heap[now / 2]);
		now /= 2;
	}
}
void pop() //彈出堆首
{
	o[heap[1]] = 0; //堆首元素不在堆裡了 設為0
	heap[1] = heap[tot--];
	int now = 1,nxt;
	while ((now * 2 <= tot && h[heap[now]] < h[heap[now * 2]])
	|| (now * 2 + 1 <= tot && h[heap[now]] < h[heap[now * 2 + 1]]))
	{ //常規大根堆操作
		nxt = now * 2;
		if (nxt + 1 <= tot && h[heap[nxt]] < h[heap[nxt + 1]]) ++ nxt;
		swap(heap[now],heap[nxt]);
		now = nxt;
	}
}
int preflow()
{
	h[s] = n;
	for (int a = st[s] ; a ; a = e[a].next) ans[s] += e[a].w; //初始化源點流量 可改為ans[s] = INF
	heap[++tot] = s;//源點壓入堆裡
	//o[s] = 1; //打標記 事實上這裡不用
	while (tot > 0)
	 {
		int p = heap[1];
		pop();
		if (!ans[p]) continue;
			for (int a = st[p],b = e[a].to ; a ; a = e[a].next,b = e[a].to) //搜能推到的點
			if ((h[p] == h[b] + 1 || p == s) && e[a].w) //高度 流量及特判
			{
				int flow = min(ans[p],e[a].w); //取餘流 然後下面更新
				ans[b] += flow;
				ans[p] -= flow;
				e[a].w -= flow;
				e[a ^ 1].w += flow;
				if (!o[b] && b != s && b != t) push(b); //若推到的點不在堆裡 壓進去
			}
		if (ans[p] > 0 && p != s && p != t) //遍歷完了 能推的點都推了還有盈餘 加特判
		{ //gap優化融合高度修改 搞得我都改不回去了=-=
			if (!--gap[h[p]]) for (int a = 1 ; a <= n ; ++ a)
			if (a != s && a != t && h[p] < h[a] && h[a] <= n)
			h[a] = n + 1;
			++gap[++h[p]];
			push(p);
		}
	}
	return ans[t]; //返回答案
}
int main()
{
	int m,x,y,z;
	scanf("%d%d%d%d",&n,&m,&s,&t); while (m--)
	scanf("%d%d%d",&x,&y,&z),add(x,y,z);
	tot = 0; //重複利用 上面用來存邊的編號 下面用來記錄堆裡的元素數量
	printf("%d\n",preflow());
	return 0;
}

然後下放優化程式碼~ (開O2得240ms/4.03MB)

#include <algorithm>
#include <cstring>
#include <cstdio>
#define ten(a) ((a<<(1<<1|1))+(a<<1))
#define asc ((1<<1|1)<<(1<<(1<<1)))
#define re register
using namespace std;
const int MAXN=10010;
struct Edge {
	int next,to,w;
} e[200010];
int st[MAXN],dep[MAXN],heap[MAXN],gap[MAXN],ans[MAXN],n,s,t,tot=1;
short o[MAXN];
inline void r(re int &x)
{
	re char q=getchar(); x=0;
	while(q<'0'||q>'9')q=getchar();
	while(q>='0'&&q<='9')x=ten(x)+q-asc,q=getchar();
}
void add(re int x,re int y,re int z) {
	e[++tot].next=st[x],st[x]=tot,e[tot].to=y,e[tot].w=z;
	e[++tot].next=st[y],st[y]=tot,e[tot].to=x;
}
void push(re int p) {
	heap[++tot]=p;
	o[p]=1;
	re int now=tot;
	while(now>1&&dep[heap[now]]>dep[heap[now>>1]]) {
		swap(heap[now],heap[now>>1]);
		now>>=1;
	}
}
void pop()
{
	o[heap[1]]=0;
	heap[1]=heap[tot--];
	re int now=1,nxt;
	while((now<<1<=tot&&dep[heap[now]]<dep[heap[now<<1]])
	||((now<<1|1)<=tot&&dep[heap[now]]<dep[heap[now<<1|1]])) {
		nxt=now<<1;
		if(nxt+1<=tot&&dep[heap[nxt]]<dep[heap[nxt+1]])++nxt;
		swap(heap[now],heap[nxt]);
		now=nxt;
	}
}
int preflow() {
	dep[s]=n;
	for(re int a=st[s];a;a=e[a].next)ans[s]+=e[a].w;
	heap[++tot]=s;
	o[s]=1;
	while(tot>0) {
		re int p=heap[1];
		pop();
		if (!ans[p]) continue;
			for(re int a=st[p],b=e[a].to;a;a=e[a].next,b=e[a].to)
			if((dep[p]==dep[b]+1||p==s)&&e[a].w) {
				int flow=min(ans[p],e[a].w);
				ans[b]+=flow;
				ans[p]-=flow;
				e[a].w-=flow;
				e[a^1].w+=flow;
				if(!o[b]&&b!=s&&b!=t)push(b);
			}
		if(ans[p]>0&&p!=s&&p!=t) {
			if(!--gap[dep[p]])for(int a=1;a<=n;++a)
			if(a!=s&&a!=t&&dep[p]<dep[a]&&dep[a]<=n)
			dep[a]=n+1;
			++gap[++dep[p]];
			push(p);
		}
	}
	return ans[t];
}
int main() {
	int m,x,y,z;
	r(n),r(m),r(s),r(t); while(m--)
	r(x),r(y),r(z),add(x,y,z);
	tot=0;
	printf("%d",preflow());
	return 0;
}