記錄一下自己被吊打的過程
一些全部程式碼的除錯資訊沒刪
主要是一些模型。
部分內容摘自最大流-OI-WiKi
網路流
網路
網路是指一個有向圖 \(G=(V,E)\),其中每條邊\((u,v)\in E\),都有一個權值 \(c(u,v)\) ,稱為邊的容量,若\((u,v)\notin E\),則\(c(u,v)=0\) 。在整個網路中,有兩個特殊的節點,分別為源點 \(S\in V\) 和匯點 \(T\in V\) 並且\(S\ne T\)。
流
設 \(f(u,v)\) 是定義在二元組 \((u\in V,v\in V)\) 上的實數函式,且滿足:
- 容量限制,對於每條邊,流經該邊的的流量不大於該邊的容量。即 \(f(u,v)\le c(u,v)\) 。
- 斜對稱性,每條邊的流量和其相反邊的流量之和為0。即 \(f(u,v)=-f(v,u)\)
- 流守恆性,從源點流出的流量等於流入匯點的流量。即 \(\forall x\in V-\{S,T\},\sum_{(u,x)\in E}f(u,x)=\sum_{(u,v)\in E}f(x,v)\)。
也稱為可行流。
流\(f\) 的值定義為 \(|f|=\sum_{v\in V}f(s,v)\)。
最大流
何為最大流?
最大的可行流
我們有一張圖,要求從源點流向匯點的最大流量(可以有很多條路徑到達匯點),就是所謂的最大流問題。
貌似ISAP一不小心就會被卡這跟我這個dinic選手有什麼關係
其實是不會
在學習Dinic之前,先了解幾個概念:
剩餘容量
表示這條邊的容量與流量之差,即 \(c_{f}(u,v)=c(u,v)-f(u,v)\) 。
殘量網路
殘量網路 \(G_{f}\) 是網路 \(G\) 中所有剩餘容量大於0的邊所構成的子圖。
增廣路
在原圖G中,若一條源點到匯點的路徑上的所有邊的剩餘容量都大於0,則這條路經被稱為增廣路,或者說,在殘量網路中一條從源點到匯點的路徑。
Dinic
模板
EK
// Ek O(V×E^2)
#include<queue>
#include<cstdio>
#include<cstring>
#include<climits>
#define MAX 210
#define re register
#define INF INT_MAX
using std::queue;
namespace OMA
{
int n,m,s,t;
bool vis[MAX];
long long ans;
int rec[MAX],pre[MAX];
struct graph
{
int next;
int to;
int w;
}edge[MAX*50];
int cnt=1,head[MAX];
inline void add(int u,int v,int w)
{
edge[++cnt] = (graph){head[u],v,w},head[u] = cnt;
edge[++cnt] = (graph){head[v],u,0},head[v] = cnt;
}
inline int read()
{
int s=0,w=1; char ch=getchar();
while(ch<'0'||ch>'9'){ if(ch=='-')w=-1; ch=getchar(); }
while(ch>='0'&&ch<='9'){ s=s*10+ch-'0'; ch=getchar(); }
return s*w;
}
inline int min(int a,int b)
{ return a<b?a:b; }
inline bool bfs()
{
queue<int>q; q.push(s);
memset(vis,0,sizeof(vis));
rec[s] = INF,vis[s] = true;
while(!q.empty())
{
int u = q.front(); q.pop();
for(re int i=head[u]; i; i=edge[i].next)
{
if(edge[i].w)
{
int v = edge[i].to;
if(!vis[v])
{
rec[v] = min(rec[u],edge[i].w);
pre[v] = i,vis[v] = true,q.push(v);
if(v==t)
{ return 1; }
}
}
}
}
return 0;
}
inline void update()
{
int u = t;
while(u!=s)
{
int i = pre[u];
edge[i].w -= rec[t];
edge[i^1].w += rec[t];
u = edge[i^1].to;
}
ans += rec[t];
}
signed main()
{
n = read(),m = read(),s = read(),t = read();
for(re int i=1,u,v,w; i<=m; i++)
{ u = read(),v = read(),w = read(); add(u,v,w); }
while(bfs())
{ update(); }
printf("%lld\n",ans);
return 0;
}
}
signed main()
{ return OMA::main(); }
Dinic
// Dinic O(V^2×E)
#include<queue>
#include<cstdio>
#include<climits>
#include<cstring>
#define MAX 210
#define re register
#define int long long
#define INF LONG_LONG_MAX
using std::queue;
namespace OMA
{
int n,m,s,t;
struct graph
{
int next;
int to;
int w;
}edge[MAX*50];
int dis[MAX];
long long ans;
int cnt=1,head[MAX][2];
inline void add(int u,int v,int w)
{
edge[++cnt] = (graph){head[u][0],v,w},head[u][0] = cnt;
edge[++cnt] = (graph){head[v][0],u,0},head[v][0] = cnt;
}
inline int read()
{
int s=0,w=1; char ch=getchar();
while(ch<'0'||ch>'9'){ if(ch=='-')w=-1; ch=getchar(); }
while(ch>='0'&&ch<='9'){ s=s*10+ch-'0'; ch=getchar(); }
return s*w;
}
inline bool bfs()
{
queue<int>q; q.push(s);
memset(dis,0,sizeof(dis));
dis[s] = 1; head[s][1] = head[s][0];
while(!q.empty())
{
int u = q.front(); q.pop();
for(re int i=head[u][0],v; i; i=edge[i].next)
{
v = edge[i].to;
if(!dis[v]&&edge[i].w)
{
q.push(v);
dis[v] = dis[u]+1;
head[v][1] = head[v][0];
if(v==t)
{ return 1; }
}
}
}
return 0;
}
inline int min(int a,int b)
{ return a<b?a:b; }
inline int dinic(int u,int flow)
{
if(u==t)
{ return flow; }
int rest = flow;
for(re int i=head[u][1],v,w; i&&rest; i=edge[i].next)
{
v = edge[i].to,w = edge[i].w;
if(w&&dis[v]==dis[u]+1)
{
int k = dinic(v,min(rest,w));
if(!k)
{ dis[v] = 0; }
edge[i].w -= k;
edge[i^1].w += k;
rest -= k;
}
head[u][1] = i;
}
return flow-rest;
}
signed main()
{
n = read(),m = read(),s = read(),t = read();
for(re int i=1,u,v,w; i<=m; i++)
{ u = read(),v = read(),w = read(); add(u,v,w); }
while(bfs())
{ ans += dinic(s,INF); }
printf("%lld\n",ans);
return 0;
}
}
signed main()
{ return OMA::main(); }
然後你發現這過不了加強版,那個要用預流推進或者叫什麼HLPP?,不過我不會,所以...
然後你就可以開始做題了,但你發現,你基本上都不會做。
其實是我
因為網路流最重要的是建模,有了模型,直接跑板子即可。
在正式接觸一些模型之前,可以先做一道水題來愉悅一下身心。
拆點
先引入幾個大家都知道的東西:
超級(虛擬)源點/匯點:
- 處理有多個源點的最大流問題,通常選擇建立虛擬源點的方法,即設一個虛
擬源點S,再從S到每個真實源點之間建一條流量無窮大的邊。 - 原網路的流一定與新網路的流一一對應: S到真實源點的流量等於該真實源
點流出的流量之和,且真實源點在新網路中滿足流守恆。 - 多個匯點的處理方式類似。
摘自課件其實大家應該都知道吧QAQ
例題:蜥蜴
這道題應用到了網路流建模時的一個技巧,拆點。
之前應該有接觸過拆點,所以此處不再贅述
何為拆點? 以下內容摘自拆點-oi-wiki
拆點是一種圖論建模思想,常用於 網路流,用來處理 點權或者點的流量限制 的問題,也常用於 分層圖 。
簡單一點來說,就是當流量限制存在於節點上時,通常選擇將一個節點拆成兩個節點,同時中間連一條容量為點權的邊。
節點的流量限制對應到本題上,便是石柱的高度,也就是一個石柱能被蜥蜴跳上去的次數,就是一個節點的流量限制。
所以這題為什麼要拆點?
發現有石柱高度或者說能跳的次數限制這一條件,如果我們不拆點的話,是不好去表示的,但如果我們拆點之後,我們就只需要去限制經過石柱多少次即可,而不用去管它向外連了多少條邊。
所以如何解決本題?
我們將一個石柱看成兩個部分,入口和出口。
如何建圖?
首先,我們將超級源點與每個蜥蜴所在的石柱的入口,建一條容量為1的邊,表示有一條蜥蜴可以通過。
然後,我們列舉當前石柱所能到達的石柱,將該石柱的出口於其能到達的石柱的入口之間建一條容量為 \(INF\) 的邊。
同時注意每個石柱本身存在流量限制,所以需要再將每個石柱的入口和出口之間建一條容量為石柱的高度的邊。
最後,將邊界距離不超過 \(d\) 的石柱的出口與超級源點建一條容量為 \(INF\) 的邊。
然後跑一邊最大流。
根據我們的建圖,考慮跑完最大流後,得到的是什麼,顯然,應該是能逃走的蜥蜴的數量。
最後答案拿蜥蜴總數量做差就好。
Code
#include<queue>
#include<cmath>
#include<cstdio>
#include<cstring>
#include<climits>
#define MAX 810
#define re register
#define INF INT_MAX
using std::min;
using std::queue;
namespace OMA
{
int r,c,d,s,t,ans;
int dis[MAX];
struct graph
{
int next;
int to;
int w;
}edge[MAX<<5];
int tot,p[22][22][2];
int cnt=1,head[MAX][2];
char s1[22][22],s2[22][22];
inline void add(int u,int v,int w)
{
edge[++cnt] = (graph){head[u][0],v,w},head[u][0] = cnt;
edge[++cnt] = (graph){head[v][0],u,0},head[v][0] = cnt;
}
inline bool bfs()
{
queue<int>q; q.push(s);
memset(dis,0,sizeof(dis));
dis[s] = 1,head[s][1] = head[s][0];
while(!q.empty())
{
int u = q.front(); q.pop();
for(re int i=head[u][0],v; i; i=edge[i].next)
{
v = edge[i].to;
if(!dis[v]&&edge[i].w)
{
q.push(v);
dis[v] = dis[u]+1;
head[v][1] = head[v][0];
if(v==t)
{ return 1; }
}
}
}
return 0;
}
inline int dinic(int u,int flow)
{
if(u==t)
{ return flow; }
int rest = flow;
for(re int i=head[u][1],v; i&&rest; i=edge[i].next)
{
v = edge[i].to;
if(edge[i].w&&dis[v]==dis[u]+1)
{
int k = dinic(v,min(rest,edge[i].w));
if(!k)
{ dis[v] = 0; }
rest -= k;
edge[i].w -= k;
edge[i^1].w += k;
}
head[u][1] = i;
}
return flow-rest;
}
inline double len(int x1,int y1,int x2,int y2)
{ return (double)sqrt((x1-x2)*(x1-x2)+(y1-y2)*(y1-y2)); }
signed main()
{
scanf("%d%d%d",&r,&c,&d);
s = 0,t = r*c*2+1;
for(re int i=1; i<=r; i++)
{ scanf("%s",s1[i]+1); }
for(re int i=1; i<=r; i++)
{ scanf("%s",s2[i]+1); }
for(re int i=1; i<=r; i++)
{
for(re int j=1; j<=c; j++)
{
if(s1[i][j]!='0')
{
p[i][j][0] = !p[i][j][0]?++tot:p[i][j][0];
p[i][j][1] = !p[i][j][1]?++tot:p[i][j][1];
add(p[i][j][0],p[i][j][1],s1[i][j]-'0');
if(i<=d||j<=d||i+d>r||j+d>c)
{ add(p[i][j][1],t,INF); }
for(re int k=1; k<=r; k++)
{
for(re int l=1; l<=c; l++)
{
if(s1[k][l]!='0'&&(k!=i||l!=j)&&d*1.0>=len(i,j,k,l))
{
p[k][l][0] = !p[k][l][0]?++tot:p[k][l][0];
add(p[i][j][1],p[k][l][0],INF);
}
}
}
}
if(s2[i][j]=='L')
{ p[i][j][0] = !p[i][j][0]?++tot:p[i][j][0]; add(s,p[i][j][0],1); ans++; }
}
}
int flow,times = 0;
while(bfs())
{ ans -= dinic(s,INF); }
printf("%d\n",ans);
return 0;
}
}
signed main()
{ return OMA::main(); }
判定性問題
課件裡這麼叫
例題:星際戰爭
趕進度,blog就先放放了。
被ac自動機+dp虐爆,所以又爬回來了。
所謂判定行問題,顧名思義,就是判斷可行不可行,對應到本題上就是判斷當前的時間能否摧毀Y軍團。
讀題後可知,本題時間是一個未知量,時間未知,傷害也就不知道,就沒辦法建圖。
既然直接求解不好搞,考慮如何間接求解。
不難發現,隨著時間的增加,武器造成的傷害是單調遞增的,既然有了單調性,考慮用二分來求解,這樣問題也就轉換成了一個判定性問題,用最大流來檢驗是否可行。
具體來說,先二分出來一個時間,那就有了鐳射武器所能造成的總傷害,那麼現在只需要考慮如何將傷害分到每個機器人上,使得它們都能被摧毀。
具體建圖:
- 從源點向所有武器之間連一條容量為武器當前總傷害的邊。
- 所有武器向機器人連一條容量為 \(INF\) 的邊。
- 所有機器人向匯點連一條容量為 裝甲值的邊。
這樣跑出來的最大流就是當前時間下,鐳射武器能對機器人所造成的總傷害,只需判斷其與裝甲值之和的關係即可。
程式碼裡的圖是反過來建的,都一樣的。
Code
#include<queue>
#include<cmath>
#include<cstdio>
#include<cstring>
#include<climits>
#define MAX 5010
#define re register
using std::min;
using std::fabs;
using std::queue;
namespace OMA
{
int n,m,s,t;
int jud[55][55];
int sum,a[55],b[55];
const double eps = 1e-8;
const double INF = 1e8;
struct graph
{
int next;
int to;
double w;
}edge[MAX<<1];
int cnt=1,head[110][2];
int dis[110],p[55][2],tot;
inline void add(int u,int v,double w)
{
edge[++cnt] = (graph){head[u][0],v,w},head[u][0] = cnt;
edge[++cnt] = (graph){head[v][0],u,0},head[v][0] = cnt;
}
inline int read()
{
int s=0,w=1; char ch=getchar();
while(ch<'0'||ch>'9'){ if(ch=='-')w=-1; ch=getchar(); }
while(ch>='0'&&ch<='9'){ s=s*10+ch-'0'; ch=getchar(); }
return s*w;
}
inline bool bfs()
{
queue<int>q; q.push(s);
memset(dis,0,sizeof(dis));
dis[s] = 1,head[s][1] = head[s][0];
while(!q.empty())
{
int u = q.front(); q.pop();
for(re int i=head[u][0],v; i; i=edge[i].next)
{
v = edge[i].to;
if(!dis[v]&&fabs(edge[i].w)>eps)
{
q.push(v);
dis[v] = dis[u]+1;
head[v][1] = head[v][0];
if(v==t)
{ return 1; }
}
}
}
return 0;
}
inline double dinic(int u,double flow)
{
if(u==t)
{ return flow; }
double rest = flow;
for(re int i=head[u][1],v; i&&fabs(rest)>eps; i=edge[i].next)
{
v = edge[i].to;
if(fabs(edge[i].w)>eps&&dis[v]==dis[u]+1)
{
double k = dinic(v,min(rest,edge[i].w));
if(fabs(k)<=eps)
{ dis[v] = 0; continue ; }
rest -= k;
edge[i].w -= k;
edge[i^1].w += k;
}
head[u][1] = i;
}
return flow-rest;
}
inline void build(double T)
{
cnt = 1; memset(head,0,sizeof(head));
for(re int i=1; i<=n; i++)
//{ add(s,p[i][0],1.0*a[i]); }
{ add(p[i][0],t,1.0*a[i]); }
for(re int i=1; i<=m; i++)
{ add(s,p[i][1],b[i]*1.0*T); }
//{ add(p[i][1],t,b[i]*1.0*T); }
for(re int i=1; i<=m; i++)
{
for(re int j=1; j<=n; j++)
{ if(jud[i][j]){ add(p[i][1],p[j][0],INF); } }
}
}
inline bool check(double T)
{
double ans = 0; build(T);
while(bfs())
{ ans += dinic(s,INF); }
if(ans>=1.0*sum)
{ return true; }
else
{ return false; }
}
signed main()
{
n = read(),m = read(),t = n+m+1;
for(re int i=1; i<=n; i++)
{ a[i] = read(); p[i][0] = ++tot; sum += a[i]; }
for(re int i=1; i<=m; i++)
{ b[i] = read(); p[i][1] = ++tot; }
for(re int i=1; i<=m; i++)
{
for(re int j=1; j<=n; j++)
{ jud[i][j] = read(); }
}
double l = 0,r = INF,mid;
while(r-l>eps)
{ mid = (l+r)/2; if(check(mid)){ r = mid; } else{ l = mid; } }
printf("%0.6lf\n",mid);
return 0;
}
}
signed main()
{ return OMA::main(); }
黑白染色
黑白染色適用於一類各元素之間有聯絡的棋盤問題上,染色後,同類顏色之間不會互相影響。
例題:奇怪的遊戲
既然是二維棋盤上的問題,考慮黑白染色。
設黑格個數為 \(sum_{1}\) ,權值和為 \(val_{1}\) ,白格個數為 \(sum_{2}\) ,權值和為 \(val_{2}\) 。
每次操作都是對相鄰的黑白格子操作,所以黑白各格子增加的總權值是相同的。
設最後棋盤上格子的權值都變成了 \(x\),則有,
\(sum_{1}\times x-val_{1}=sum_{2}\times x-val_{2}\) ,則有 \(x=\frac{val_{1}-val_{2}}{sum_{1}-sum_{2}}\) 。
那麼就有兩種情況,
-
當 \(sum_{1} \neq sum_{2}\) 時,可以直接求解,用網路流去檢驗即可。
-
當 \(sum_{1}=sum_{2}\) 時,可以發現,對於一個合法的 \(x\),那麼 \(x+1\) 也是合法的。 所以可以二分 \(x\) ,用網路流來檢驗。
然後就是喜聞樂見的建圖環節,
設棋盤中原來的數為 \(val\) ,則
- 源點向所有白點連一條容量為 \(x-val\) 的邊。
- 白點和黑點之間連一條容量為 \(INF\) 的邊。
- 黑點向匯點之間連一條容量為 \(x-val\) 的邊。
只需檢驗最大流是否等於 \(\sum x-val\)。
需要注意此題二分的下界原棋盤中最大的數。
Code
#include<queue>
#include<cmath>
#include<cstdio>
#include<cstring>
#include<climits>
#define MAX 16010
#define re register
#define int long long
using std::min;
using std::max;
using std::queue;
namespace OMA
{
int T,n,m,s,t;
struct graph
{
int next;
int to;
int w;
}edge[MAX*10];
int num[2],sum[3];
int dis[MAX],a[42][42];
int cnt=1,head[MAX][2];
int col[42][42],p[42][42],tot;
const int INF = 1e14;
inline void add(int u,int v,int w)
{
edge[++cnt] = (graph){head[u][0],v,w},head[u][0] = cnt;
edge[++cnt] = (graph){head[v][0],u,0},head[v][0] = cnt;
}
inline int read()
{
int s=0,w=1; char ch=getchar();
while(ch<'0'||ch>'9'){ if(ch=='-')w=-1; ch=getchar(); }
while(ch>='0'&&ch<='9'){ s=s*10+ch-'0'; ch=getchar(); }
return s*w;
}
inline bool bfs()
{
queue<int>q; q.push(s);
memset(dis,0,sizeof(dis));
dis[s] = 1,head[s][1] = head[s][0];
while(!q.empty())
{
int u = q.front(); q.pop();
for(re int i=head[u][0],v; i; i=edge[i].next)
{
v = edge[i].to;
if(edge[i].w&&!dis[v])
{
q.push(v);
dis[v] = dis[u]+1;
head[v][1] = head[v][0];
if(v==t)
{ return 1; }
}
}
}
return 0;
}
inline int dinic(int u,int flow)
{
if(u==t)
{ return flow; }
int rest = flow;
for(re int i=head[u][1],v; i&&rest; i=edge[i].next)
{
v = edge[i].to;
if(edge[i].w&&dis[v]==dis[u]+1)
{
int k = dinic(v,min(rest,edge[i].w));
if(!k)
{ dis[v] = 0; }
rest -= k;
edge[i].w -= k;
edge[i^1].w += k;
}
head[u][1] = i;
}
return flow-rest;
}
inline void build(int x)
{
sum[2] = 0;
cnt = 1,memset(head,0,sizeof(head));
for(re int i=1; i<=n; i++)
{
for(re int j=1; j<=m; j++)
{
if(!col[i][j])
{
sum[2] += x-a[i][j];
//printf("delta=%lld\n",x-a[i][j]);
add(s,p[i][j],x-a[i][j]);
if(col[i][j+1])
{ add(p[i][j],p[i][j+1],INF); }//printf("x2=%lld y2=%lld\n",i,j+1); }
if(col[i][j-1])
{ add(p[i][j],p[i][j-1],INF); }//printf("x3=%lld y3=%lld\n",i,j-1); }
if(col[i+1][j])
{ add(p[i][j],p[i+1][j],INF); }//printf("x4=%lld y4=%lld\n",i+1,j); }
if(col[i-1][j])
{ add(p[i][j],p[i-1][j],INF); }//printf("x5=%lld y5=%lld\n",i-1,j); }
}
else
{ add(p[i][j],t,x-a[i][j]); }
}
}
}
inline bool check(int x)
{
build(x);
int delta = 0;
while(bfs())
{ delta += dinic(s,INF); }
//printf("%lld\n",delta);
if(delta==sum[2])
{ return true; }
else
{ return false; }
}
signed main()
{
//freopen("my.out","w",stdout);
T = read();
for(re int i=1; i<=40; i++)
{
for(re int j=1; j<=40; j++)
{ p[i][j] = ++tot; }
}
t = 1601;
while(T--)
{
int tmp = 0;
num[0] = num[1] = sum[0] = sum[1] = 0,tot = 0;
n = read(),m = read();
memset(col,0,sizeof(col));
for(re int i=1; i<=n; i++)
{
for(re int j=1; j<=m; j++)
{ col[i][j] = (i+j)%2,sum[col[i][j]] += a[i][j] = read(),num[col[i][j]]++; tmp = max(tmp,a[i][j]); }
}
if(num[0]!=num[1])
{ int x = (sum[0]-sum[1])/(num[0]-num[1]); if(check(x)){ printf("%lld\n",x*num[0]-sum[0]); } else{ printf("-1\n"); } }
else
{
int l = tmp,r = INF,mid;
while(l<=r)
{
mid = (l+r)>>1;
//printf("l=%lld r=%lld mid=%lld\n",l,r,mid);
if(check(mid))
{ r = mid-1; }
else
{ l = mid+1; }
}
if(!check(l))
{ printf("-1\n"); }
else
{ printf("%lld\n",l*num[0]-sum[0]); }
}
}
return 0;
}
}
signed main()
{ return OMA::main(); }
逆向思維
發現有些題目正著做直接求滿足條件的不好搞,可以考慮反著做求出不符合條件的,再拿總數做差得到答案。
例題:士兵佔領
發現直接做不好搞其實是不會,考慮反著做,求出能省多少個士兵,那麼最後拿士兵總數一減就是答案。
具體建圖:
- 源點向各行之間連一條容量為 \(l_{i}\) 的邊。
- 沒有限制的行列之間連一條容量為1的邊。
- 各列向匯點之間連一條容量為 \(r_{i}\) 的邊。
這樣跑出來的最大流就是可以省下多少個士兵,拿總數一減就是答案。
Code
#include<queue>
#include<cmath>
#include<cstdio>
#include<cstring>
#include<climits>
#define re register
#define INF INT_MAX
using std::min;
using std::queue;
namespace OMA
{
int m,n,k,sum,s,t;
int l[110],r[110];
struct graph
{
int next;
int to;
int w;
}edge[110*110<<1];
int cnt=1,head[210][2];
bool jud[110][110];
int tot,p[110][2],dis[210];
inline void add(int u,int v,int w)
{
edge[++cnt] = (graph){head[u][0],v,w},head[u][0] = cnt;
edge[++cnt] = (graph){head[v][0],u,0},head[v][0] = cnt;
}
inline int read()
{
int s=0,w=1; char ch=getchar();
while(ch<'0'||ch>'9'){ if(ch=='-')w=-1; ch=getchar(); }
while(ch>='0'&&ch<='9'){ s=s*10+ch-'0'; ch=getchar(); }
return s*w;
}
inline bool bfs()
{
queue<int>q; q.push(s);
memset(dis,0,sizeof(dis));
dis[s] = 1,head[s][1] = head[s][0];
while(!q.empty())
{
int u = q.front(); q.pop();
for(re int i=head[u][0],v; i; i=edge[i].next)
{
v = edge[i].to;
if(!dis[v]&&edge[i].w)
{
q.push(v);
dis[v] = dis[u]+1;
head[v][1] = head[v][0];
if(v==t)
{ return 1; }
}
}
}
return 0;
}
inline int dinic(int u,int flow)
{
if(u==t)
{ return flow; }
int rest = flow;
for(re int i=head[u][1],v; i&&rest; i=edge[i].next)
{
v = edge[i].to;
if(edge[i].w&&dis[v]==dis[u]+1)
{
int k = dinic(v,min(rest,edge[i].w));
if(!k)
{ dis[v] = 0; }
rest -= k;
edge[i].w -= k;
edge[i^1].w += k;
}
head[u][1] = i;
}
//printf("%d %d\n",flow,rest);
return flow-rest;
}
signed main()
{
//freopen("node.in","r",stdin);
m = read(),n = read(),k = read(),t = n+m+1;
for(re int i=1; i<=m; i++)
{ sum += l[i] = read(); p[i][0] = ++tot; add(s,p[i][0],l[i]); }
for(re int i=1; i<=n; i++)
{ sum += r[i] = read(); p[i][1] = ++tot; add(p[i][1],t,r[i]); }
for(re int i=1; i<=k; i++)
{ jud[read()][read()] = true; }
for(re int i=1; i<=m; i++)
{
for(re int j=1; j<=n; j++)
{
if(!jud[i][j])
{ add(p[i][0],p[j][1],1); }
}
}
int ans = 0;
while(bfs())
{ ans += dinic(s,INF); }
printf("%d\n",sum-ans);
return 0;
}
}
signed main()
{ return OMA::main(); }
最小割
一些概念
割
對於一個網路流圖 \(G=(V,E)\) ,其割的定義為一種點的劃分方式:將所有點劃分為 \(S\) 和 \(T=V-S\) 兩個集合,其中 \(s\in S\) ,\(t\in T\) 。
割的容量
割 \((S,T)\) 的容量 \(c(S,T)\)表示所有從 \(S\) 到 \(T\) 的邊容量之和,即 \(c(S,T)=\sum_{u\in S,v\in T}c(u,v)\) 也可以用 \(c(s,t)\)來表示。
最小割
即求得一個割 \((S,T)\) 使得 \(c(S,T)\) 最小。
最大流最小割定理
\(f(s,t)_{max}=c(s,t)_{min}\)
證明:
對於任意一個可行流 \(f(s,t)\) 的割 \((S,T)\),有:
若我們已經求出了最大流 \(f\) ,則殘餘網路中一定不存在 \(s\) 到 \(t\) 的增廣路徑,也就是說,\(S\) 的出邊一定是滿流, \(S\) 的入邊一定是零流,於是有:
結合前面的不等式可知,此時 \(f\) 已經最大。
摘自oi-wiki
所以求最小割跑遍最大流即可。
板子:狼抓兔子
直接按題意建圖,跑遍最小割即可。
Code
#include<queue>
#include<cmath>
#include<cstdio>
#include<climits>
#include<cstring>
#define MAX 1010
#define re register
#define INF INT_MAX
using std::min;
using std::queue;
namespace OMA
{
int n,m,s,t,ans;
struct graph
{
int next;
int to;
int w;
}edge[MAX*MAX*6];
int cnt=1,head[MAX*MAX][2];
int dis[MAX*MAX],p[MAX][MAX],tot;
inline void add(int u,int v,int w)
{
edge[++cnt] = (graph){head[u][0],v,w},head[u][0] = cnt;
edge[++cnt] = (graph){head[v][0],u,w},head[v][0] = cnt;
}
inline int read()
{
int s=0,w=1; char ch=getchar();
while(ch<'0'||ch>'9'){ if(ch=='-')w=-1; ch=getchar(); }
while(ch>='0'&&ch<='9'){ s=s*10+ch-'0'; ch=getchar(); }
return s*w;
}
inline bool bfs()
{
queue<int>q; q.push(s);
memset(dis,0,sizeof(dis));
dis[s] = 1,head[s][1] = head[s][0];
while(!q.empty())
{
int u = q.front(); q.pop();
for(re int i=head[u][0],v; i; i=edge[i].next)
{
v = edge[i].to;
if(edge[i].w&&!dis[v])
{
q.push(v);
dis[v] = dis[u]+1;
head[v][1] = head[v][0];
if(v==t)
{ return 1; }
}
}
}
return 0;
}
inline int dinic(int u,int flow)
{
if(u==t)
{ return flow; }
int rest = flow;
for(re int i=head[u][1],v; i&&rest; i=edge[i].next)
{
v = edge[i].to;
if(edge[i].w&&dis[v]==dis[u]+1)
{
int k = dinic(v,min(rest,edge[i].w));
if(!k)
{ dis[v] = 0; }
rest -= k;
edge[i].w -= k;
edge[i^1].w += k;
}
head[u][1] = i;
}
return flow-rest;
}
signed main()
{
n = read(),m = read(),s = 1,t = n*m;
for(re int i=1; i<=n; i++)
{
for(re int j=1; j<=m; j++)
{ p[i][j] = ++tot; }
}
for(re int i=1; i<=n; i++)
{
for(re int j=1; j<=m-1; j++)
{ add(p[i][j],p[i][j+1],read()); }
}
for(re int i=1; i<=n-1; i++)
{
for(re int j=1; j<=m; j++)
{ add(p[i][j],p[i+1][j],read()); }
}
for(re int i=1; i<=n-1; i++)
{
for(re int j=1; j<=m-1; j++)
{ add(p[i][j],p[i+1][j+1],read()); }
}
while(bfs())
{ ans += dinic(s,INF); }
printf("%d\n",ans);
return 0;
}
}
signed main()
{ return OMA::main(); }