從最小割角度解決最大權閉合圖問題及其在二分圖形式下的優化
引入問題:
Kejin Game
給出一張技能圖,有的技能需要先置條件才能學習,而通過氪金可以取消某一些先置條件或者直接學習某一項技能,假設學習某項技能或者氪金都需要你付出一些代價,問代價最少為多少?
這個問題我寫過了https://blog.csdn.net/baiyifeifei/article/details/83276927
解題思路
首先我們來審視某一個技能C,假設他存在先置技能A,B,那麼我們使用一些原則來構建一張圖:
假設一個技能直接習得的代價是D則在從該技能的入點向出點連線一條邊;
假如一個技能是另一個的前置技能,那麼就從前置技能的出點向待學習技能的入點連線一條邊;
源點向所有技能的入點連線一條學習該技能所需的代價。
按照上面的法則,我們就可以構建出下面這樣一張圖
假使在ss,想要學習的技能的出點之間形成一個割,那麼意味著這個技能就算被習得了。
以C為例,假設C想要脫離A這個前置的控制,那麼就需要在Va,Dva,EDac之間任意選取一條邊劃去,同理取消B的限制也是一樣的,再割裂Vc就算完全從正常學習的流程中學得了C技能了,當然也可以通過直接割裂Dvc這條邊來達到形成割的目的
這題的思路並不難,但是把一個點和源點之間的關係割裂作為選擇上了這個點的根據的做法提供了一種新的思路。
接著,我們來換一個問題,假設技能之間存在著如上面這個題目一樣的依賴關係。現在每個技能存在一個權重,要求你學會一些技能使他們的權重儘可能大。
而這就是我們的“最大權閉合子圖”
關於最大權閉合圖:《最小割模型在資訊學競賽中的應用》
什麼是最大權閉合子圖?
給出一張圖,圖中的點與點之間通過有向邊連線,選出一些點,保證這些選出的點的後繼點也屬於這些被選出來的點,且權值最大則是最大權閉合圖。
如何尋找最大權閉合子圖?
設立一個虛源點和一個虛匯點,所有的正權點都向虛源點連線容量為其權值的邊,所有的負權點都向匯點連線容量為權值的邊,原本有邊的點之間連線容量無限大的邊即可,最後權值之和減去這幅圖的源點與匯點之間的最大流即可。
正確性:
我們知道最大流即是最小割,而這個模型中,我們只可能從與源點還有匯點連線的點去割,而其餘的邊由於容量無限,所以並不可能割,而這種割邊的一端是源點或者匯點的割稱作為簡單割。
而可以證明的是對於一個網路N的簡單割[S,T]與圖G的閉合圖V1方案存在一個對應關係,
證明:
閉合圖對應簡單割:由於V1閉合,所以V1與V2之間不存在邊,s與t之間不存在邊,所以,之間也不存在邊,所以,其就對應著割,而本圖的所有割定為簡單割,所以閉合圖對應簡單割
簡單割對應閉合圖:由於S,T為簡單割,故S,T之間不存在邊,而s,t之間也不存在邊,故,之間也不存在邊,因而圖的所有後繼點(除了s,t外)必然仍然在內,同理,所以簡單割對應閉合圖
這裡我們再引入設一個點集V中的權值為正的點的集合為
最小割的流量
那麼最小割[S,T]之間的割去的最小流量就是:
ps:[S,T]表示S,T之間的連邊
證明:
因為S,T為簡單割,所以[V2,V1]為空集
因為只有正權與V1之間有連邊,負權與V2之間有連邊
所以:
所以
所以
得證
如何保證得到的一定是最大權的閉合圖?
最大權閉合圖的總權重為:
令w(V1)+c[S,T]則有
經移項,得:
所以使正權和減去最小割既是答案的最大值
帶二分圖性質的最大權閉合圖
當選擇一條邊兩端的點時視作該邊被選擇,選擇一條邊,可以獲得一個價值,選擇點則需要付出一定的代價,問如何獲得最大的價值。
例題 最大獲利 HYSBZ - 1497
設E為邊,V為點,則問題的本質就變為:
考慮到選了某條邊,那麼這條邊的點就必選,其本質就變成了一個最大權閉合圖問題,我們可以將邊也抽象為點,向其兩個端點連邊,然後在這幅圖上跑最大權閉合圖即可,複雜度為O(MaxFlow(n+m,n+m))。
考慮上面這個問題,假如原圖是一張稠密圖(就如例題),那麼這個演算法的複雜度就會變得非常糟糕了,為此我們需要作出優化。
再來看這個式子
我們可以通過乘上-1來使這個問題轉化為求最小值
考慮到邊的總價值等於所有的點相連的邊的總價值減去那些已經選擇的點和不選擇的點之間的邊除去2就是選擇的邊的總價值。也就是選擇的點所連線的邊的總價值減去與不選擇的點之間的最小割/2,具體上可以看胡伯濤論文中的這幅圖:
其中藍紫色與棕色的數字是我自己標上的,其中藍紫色的為邊的價值,棕色的為與這個端點連線的邊的價值之和,黑色加粗的邊為選擇的邊,黑色虛線圓內的點(1,2,3)即是選擇的邊,我們不難發現圖中選擇的邊的總價值就是
[(21+27+12)-(1+2+8+9+7+3)]/2=15
其中(1+2+8+9+7+3)也就是紅色邊的價值之和就是已選擇的1,2,3與未選擇的4,5,6,7.8,9之間的最小割
我們用來表示與一點相連的所有的邊的價值總和,已經選擇的點集為V未選擇的點集為V',那麼原式就可以表示為
進一步轉化,原式就變成了
為了使得原式更加整齊,所以對原式乘2,那麼原式就變成了
因此原圖中的每個點的權值就是,考慮最小割只能接受非負的邊權,所以要為所有的點權加上一個極大值,令為U,然後答案就是
可行性證明
所以
據此我們得到上面例題的解:
首先是直接暴力建圖的超時答案:
/**************************************************************
Problem: 1497
User: FlyWhite
Language: C++
Result: Time_Limit_Exceed
****************************************************************/
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<queue>
#include<cstring>
#include<string>
#include<map>
using namespace std;
const int maxn=60005;
const int maxm=3e5+5;
const int inf=0x3f3f3f3f;
struct Edge{
int to,nxt,cap,flow;
}edge[maxm];
int tol;
int head[maxn];
void init(){
tol=2;
memset(head,-1,sizeof(head));
}
void addedge(int u,int v,int w,int rw=0){
edge[tol].to=v;edge[tol].cap=w;edge[tol].flow=0;
edge[tol].nxt=head[u];head[u]=tol++;
edge[tol].to=u;edge[tol].cap=rw;edge[tol].flow=0;
edge[tol].nxt=head[v];head[v]=tol++;
}
int Q[maxn];
int dep[maxn],cur[maxn],sta[maxn];
bool bfs(int s,int t,int n){
int front=0,tail=0;
memset(dep,-1,sizeof(dep[0])*(n+1));
dep[s]=0;
Q[tail++]=s;
while(front<tail){
int u=Q[front++];
for(int i=head[u];i!=-1;i=edge[i].nxt){
int v=edge[i].to;
if(edge[i].cap>edge[i].flow&&dep[v]==-1){
dep[v]=dep[u]+1;
if(v==t) return true;
Q[tail++]=v;
}
}
}
return false;
}
int dinic(int s,int t,int n){
int maxflow=0;
while(bfs(s,t,n)){
for(int i=0;i<n;i++) cur[i]=head[i];
int u=s,tail=0;
while(cur[s]!=-1){
if(u==t){
int tp=inf;
for(int i=tail-1;i>=0;i--)
{
tp=min(tp,edge[sta[i]].cap-edge[sta[i]].flow);
}
maxflow+=tp;
for(int i=tail-1;i>=0;i--){
edge[sta[i]].flow+=tp;
edge[sta[i]^1].flow-=tp;
if(edge[sta[i]].cap-edge[sta[i]].flow==0) tail=i;
}
u=edge[sta[tail]^1].to;
}
else if(cur[u]!=-1&&edge[cur[u]].cap>edge[cur[u]].flow&&dep[u]+1==dep[edge[cur[u]].to]){
sta[tail++]=cur[u];
u=edge[cur[u]].to;
}
else{
while(u!=s&&cur[u]==-1) u=edge[sta[--tail]^1].to;
cur[u] = edge [cur[u]].nxt;
}
}
}
return maxflow;
}
int main()
{
int n,m;
scanf("%d%d",&n,&m);
int ss=0,tt=n+m+1;
init();
for(int i=1;i<=n;i++)
{
int V;
scanf("%d",&V);
addedge(i,tt,V);
}
int W=0;
for(int i=1;i<=m;i++)
{
int u,v,w;
scanf("%d%d%d",&u,&v,&w);
W+=w;
addedge(ss,i+n,w);
addedge(i+n,u,inf);
addedge(i+n,v,inf);
}
printf("%d\n",W-dinic(ss,tt,tt+1));
}
下面是優化建圖之後的AC程式碼:
/**************************************************************
Problem: 1497
User: FlyWhite
Language: C++
Result: Accepted
Time:464 ms
Memory:7388 kb
****************************************************************/
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<queue>
#include<cstring>
#include<string>
#include<map>
using namespace std;
const int maxn=60005;
const int maxm=3e5+5;
const int inf=0x3f3f3f3f;
struct Edge{
int to,nxt,cap,flow;
}edge[maxm];
int tol;
int head[maxn];
void init(){
tol=2;
memset(head,-1,sizeof(head));
}
void addedge(int u,int v,int w,int rw=0){
edge[tol].to=v;edge[tol].cap=w;edge[tol].flow=0;
edge[tol].nxt=head[u];head[u]=tol++;
edge[tol].to=u;edge[tol].cap=rw;edge[tol].flow=0;
edge[tol].nxt=head[v];head[v]=tol++;
}
int Q[maxn];
int dep[maxn],cur[maxn],sta[maxn];
bool bfs(int s,int t,int n){
int front=0,tail=0;
memset(dep,-1,sizeof(dep[0])*(n+1));
dep[s]=0;
Q[tail++]=s;
while(front<tail){
int u=Q[front++];
for(int i=head[u];i!=-1;i=edge[i].nxt){
int v=edge[i].to;
if(edge[i].cap>edge[i].flow&&dep[v]==-1){
dep[v]=dep[u]+1;
if(v==t) return true;
Q[tail++]=v;
}
}
}
return false;
}
int dinic(int s,int t,int n){
int maxflow=0;
while(bfs(s,t,n)){
for(int i=0;i<n;i++) cur[i]=head[i];
int u=s,tail=0;
while(cur[s]!=-1){
if(u==t){
int tp=inf;
for(int i=tail-1;i>=0;i--)
{
tp=min(tp,edge[sta[i]].cap-edge[sta[i]].flow);
}
maxflow+=tp;
for(int i=tail-1;i>=0;i--){
edge[sta[i]].flow+=tp;
edge[sta[i]^1].flow-=tp;
if(edge[sta[i]].cap-edge[sta[i]].flow==0) tail=i;
}
u=edge[sta[tail]^1].to;
}
else if(cur[u]!=-1&&edge[cur[u]].cap>edge[cur[u]].flow&&dep[u]+1==dep[edge[cur[u]].to]){
sta[tail++]=cur[u];
u=edge[cur[u]].to;
}
else{
while(u!=s&&cur[u]==-1) u=edge[sta[--tail]^1].to;
cur[u] = edge [cur[u]].nxt;
}
}
}
return maxflow;
}
int val[maxn];
int main()
{
int n,m;
scanf("%d%d",&n,&m);
memset(val,0,sizeof(val));
int U=5e6+10;
int ss=0,tt=n+1;
init();
for(int i=1;i<=n;i++)
{
int V;
scanf("%d",&V);
val[i]+=2*V;
addedge(ss,i,U);
}
int W=0;
for(int i=1;i<=m;i++)
{
int u,v,w;
scanf("%d%d%d",&u,&v,&w);
W+=w;
val[u]-=w;
val[v]-=w;
addedge(u,v,w);
addedge(v,u,w);
}
for(int i=1;i<=n;i++)
{
addedge(i,tt,val[i]+U);
}
printf("%d\n",(n*U-dinic(ss,tt,tt+1))/2);
}
最大密度子圖
定義一個無向圖的密度為邊數|E|,除其點數|V|,即 ,給出一個無向圖G=(V,E),其具有最大密度的子圖G=(V',E')稱為最大密度子圖。
我們設最大密度為Dmax那麼就可以得出下式
我們令,不難發現,當D>Dmax時,f(D)將小於0,D<Dmax時,f(D)將大於0,當D=Dmax時,f(D)=0
因此我們可以通過二分來確定D的值。接下來我們需要做的就是,在一個確定的D下maximize (f(D))
為此,我們將邊權設為1,點權設為D再去跑上述的那個二分性質的最大權閉合子圖即可。
例題: The Problem Needs 3D Arrays UVALive - 7037
AC程式碼(資料水,暴力建圖)
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<queue>
#include<cstring>
#include<string>
#include<vector>
#include<map>
using namespace std;
const int maxn=8005;
const double eps=1e-8;
const int maxm=1000005;
const double inf=0x3f3f3f3f;
struct Edge{
int to,nxt;
double cap,flow;
}edge[maxm];
int tol;
int head[maxn];
void init(){
tol=2;
memset(head,-1,sizeof(head));
}
void addedge(int u,int v,double w,double rw=0.0){
edge[tol].to=v;edge[tol].cap=w;edge[tol].flow=0;
edge[tol].nxt=head[u];head[u]=tol++;
edge[tol].to=u;edge[tol].cap=rw;edge[tol].flow=0;
edge[tol].nxt=head[v];head[v]=tol++;
}
int Q[maxn];
int dep[maxn];
int cur[maxn];int sta[maxn];
bool bfs(int s,int t,int n){
int front=0,tail=0;
memset(dep,-1,sizeof(dep[0])*(n+1));
dep[s]=0;
Q[tail++]=s;
while(front<tail){
int u=Q[front++];
for(int i=head[u];i!=-1;i=edge[i].nxt){
int v=edge[i].to;
if(edge[i].cap>edge[i].flow&&dep[v]==-1){
dep[v]=dep[u]+1;
if(v==t) return true;
Q[tail++]=v;
}
}
}
return false;
}
double dinic(int s,int t,int n){
double maxflow=0.0;
while(bfs(s,t,n)){
for(int i=0;i<n;i++) cur[i]=head[i];
int u=s,tail=0;
while(cur[s]!=-1){
if(u==t){
double tp=inf;
for(int i=tail-1;i>=0;i--)
{
tp=min(tp,edge[sta[i]].cap-edge[sta[i]].flow);
}
maxflow+=tp;
for(int i=tail-1;i>=0;i--){
edge[sta[i]].flow+=tp;
edge[sta[i]^1].flow-=tp;
if(edge[sta[i]].cap-edge[sta[i]].flow==0) tail=i;
}
u=edge[sta[tail]^1].to;
}
else if(cur[u]!=-1&&edge[cur[u]].cap>edge[cur[u]].flow&&dep[u]+1==dep[edge[cur[u]].to]){
sta[tail++]=cur[u];
u=edge[cur[u]].to;
}
else{
while(u!=s&&cur[u]==-1) u=edge[sta[--tail]^1].to;
cur[u] = edge [cur[u]].nxt;
}
}
}
return maxflow;
}
pair<int,int> Ex[10005];
int cnt;
int n;
int ss,tt;
void build(double val)
{
init();
ss=0,tt=cnt+n+1;
for(int i=1;i<=cnt;i++)
{
addedge(ss,i,1.0);
}
for(int i=1;i<=n;i++) addedge(i+cnt,tt,val);
for(int i=1;i<=cnt;i++)
{
addedge(i,cnt+Ex[i].first,inf);
addedge(i,cnt+Ex[i].second,inf);
}
}
bool check(double mid)
{
return 1.0*cnt-dinic(ss,tt,tt+1)>=eps;
}
int arr[105];
int main()
{
int t;
scanf("%d",&t);
int kace = 1;
while(t--)
{
printf("Case #%d: ",kace++);
scanf("%d",&n);
for(int i=1;i<=n;i++)
{
scanf("%d",&arr[i]);
}
cnt=0;
for(int i=1;i<=n;i++)
{
for(int j=1+i;j<=n;j++)
{
if(arr[j]<arr[i])
{
Ex[++cnt]=pair<int,int>(i,j);
}
}
}
double L=0,R=cnt;
while(R-L>eps)
{
double mid=(R+L)/2;
build(mid);
if(check(mid)) L=mid;
else R=mid;
}
printf("%.12f\n",L);
}
}