上讲习题
AcWing 2279
这个题看着很懵,这根本不是最小割啊!然而我们可以思考一下,这种 a b \cfrac{a}{b} ba的模型,且求的最小或最大值,那么不就是分数规划吗?于是,我们可以设平均值为 g g g,则: h ( g ) = ∑ e ∈ C w e − ∣ C ∣ × g h(g)=\displaystyle\sum_{e\in C}w_e-|C|\times g h(g)=e∈C∑we−∣C∣×g = ∑ e ∈ C w e − ∑ e ∈ C g =\displaystyle\sum_{e\in C}w_e-\displaystyle\sum_{e\in C}g =e∈C∑we−e∈C∑g = ∑ e ∈ C ( w e − g ) =\displaystyle\sum_{e\in C}(w_e-g) =e∈C∑(we−g)我们要求最小,则这样的式子要取一个 min \min min,如果 h ( g ) < 0 h(g)<0 h(g)<0,则说明平均值还可以更小, r = m i d r=mid r=mid。如果 h ( g ) > 0 h(g)>0 h(g)>0则说明平均值太小了, l = m i d l=mid l=mid。如果 h ( g ) = 0 h(g)=0 h(g)=0,则说明平均值刚好,得到答案。于是我们就来思考如何计算这个式子的值。我们可以发现,要想得到尽量小的答案,且要让 s → t s\to t s→t不连通,则我们可以先把所有 w e − g < 0 w_e-g<0 we−g<0的边用上。剩下的就全都是正权边了。我们又可以根据正权来推算,发现假设得到了一个割集 c u t ( S , T ) cut(S,T) cut(S,T),则割掉了所有割边后因为 s ∈ S s\in S s∈S, t ∈ T t\in T t∈T,然而 S S S到 T T T的所有边都被割掉了,则 s , t s,t s,t一定不连通。那么我们要求最小,所以已经不连通了就不会再在集合内部选割掉的边了。于是对于正权边,求一个最小割即可。两个相加即是我们这个式子的值。
#include<bits/stdc++.h>
using namespace std;
const int NN=10004,MM=200004;
int n,m,s,t,h[NN],ne[MM],e[MM],idx=-1,head[NN],d[NN],l[NN];
double c[MM];
void add(int u,int v,int w)
{
e[++idx]=v;
l[idx]=w;
ne[idx]=h[u];
h[u]=idx;
e[++idx]=u;
l[idx]=w;
ne[idx]=h[v];
h[v]=idx;
}
bool bfs()
{
queue<int>q;
memset(d,-1,sizeof(d));
q.push(s);
d[s]=0;
head[s]=h[s];
while(q.size())
{
int f=q.front();
q.pop();
for(int i=h[f];~i;i=ne[i])
{
int v=e[i];
if(d[v]==-1&&c[i])
{
d[v]=d[f]+1;
head[v]=h[v];
if(v==t)
return true;
q.push(v);
}
}
}
return false;
}
double find(int u,double sum)
{
if(u==t)
return sum;
double res=0;
for(int i=head[u];~i&&res<sum;i=ne[i])
{
head[u]=i;
int v=e[i];
if(d[v]==d[u]+1&&c[i])
{
double temp=find(v,min(c[i],sum-res));
if(!temp)
d[v]=-1;
c[i]-=temp;
c[i^1]+=temp;
res+=temp;
}
}
return res;
}
double dinic()
{
double res=0,sum;
while(bfs())
while(sum=find(s,1e9))
res+=sum;
return res;
}
double check(double mid)
{
double res=0;
for(int i=0;i<=idx;i+=2)
if(l[i]<=mid)
res+=l[i]-mid;
else
c[i]=c[i^1]=l[i]-mid;
return res+dinic();
}
int main()
{
scanf("%d%d%d%d",&n,&m,&s,&t);
memset(h,-1,sizeof(h));
for(int i=1;i<=m;i++)
{
int u,v,w;
scanf("%d%d%d",&u,&v,&w);
add(u,v,w);
}
double l=0,r=1e9;
while(r-l>1e-8)
{
double mid=l+(r-l)/2;
if(check(mid)<0)
r=mid;
else
l=mid;
}
printf("%.2lf",r);
return 0;
}
AcWing 2280
既然这个题牵扯到了位运算异或,数据范围也不是很大,那么很容易想到一种思路,枚举每一位分别计算。可以设计一个割集 c u t ( S , T ) cut(S,T) cut(S,T), S S S内的点表示这个点用 0 0 0, T T T内的点就表示用 1 1 1。如果两个点之间有边,那么这两个点用同样的数字(放在一个集合里),是不会让答案变大的,如果用不同的数字(放在不同集合里),则会让答案变大。这也正好满足最小割的性质。如果一个点已经赋值了,那么就向 s s s或 t t t连一条容量为正无穷的边,让最小割不选这条边。如果两个点之间有边,则连一条容量为 1 1 1的边,表示如果你把我们分开了这一位异或值的和就会增加 1 1 1。注意因为我们枚举的是每一位,则最后答案要乘一个 2 k 2^k 2k。
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int NN=504,MM=(3004+NN)*2,INF=1e9;
pair<int,int>edge[3004];
int h[NN],e[MM],ne[MM],c[MM],head[NN],d[NN],idx,p[NN],n,m,s,t;
void add(int u,int v,int w1,int w2)
{
e[++idx]=v;
ne[idx]=h[u];
c[idx]=w1;
h[u]=idx;
e[++idx]=u;
ne[idx]=h[v];
c[idx]=w2;
h[v]=idx;
}
void build(int k)
{
memset(h,-1,sizeof(h));
idx=-1;
for(int i=1;i<=n;i++)
if(p[i]>0)
{
if(p[i]>>k&1)
add(i,t,INF,0);
else
add(s,i,INF,0);
}
for(int i=1;i<=m;i++)
add(edge[i].first,edge[i].second,1,1);
}
bool bfs()
{
queue<int>q;
q.push(s);
memset(d,-1,sizeof(d));
d[s]=0;
head[s]=h[s];
while(q.size())
{
int f=q.front();
q.pop();
for(int i=h[f];~i;i=ne[i])
{
int v=e[i];
if(d[v]==-1&&c[i])
{
d[v]=d[f]+1;
head[v]=h[v];
if(v==t)
return true;
q.push(v);
}
}
}
return false;
}
int find(int u,int sum)
{
if(u==t)
return sum;
int res=0;
for(int i=head[u];~i&&res<sum;i=ne[i])
{
head[u]=i;
int v=e[i];
if(d[v]==d[u]+1&&c[i])
{
int temp=find(v,min(c[i],sum-res));
if(!temp)
d[v]=-1;
c[i]-=temp;
c[i^1]+=temp;
res+=temp;
}
}
return res;
}
ll dinic()
{
int res=0,sum;
while(bfs())
while(sum=find(s,INF))
res+=sum;
return res;
}
int main()
{
scanf("%d%d",&n,&m);
t=n+1;
for(int i=1;i<=m;i++)
scanf("%d%d",&edge[i].first,&edge[i].second);
int k;
scanf("%d",&k);
memset(p,-1,sizeof(p));
for(int i=1;i<=k;i++)
{
int a,b;
scanf("%d%d",&a,&b);
p[a]=b;
}
ll res=0;
for(int i=0;i<=30;i++)
{
build(i);
res+=dinic()<<i;
}
printf("%lld",res);
return 0;
}
AcWing 381
这个题难度不是很大,但是输入太毒瘤。这也是我很少见用cin输入的题目之一。题目不让我们用边割开,那么我们就可以拆点。两个边之间为了让他们不被割开,容量设为正无穷。拆点后把一个点割开代价是 1 1 1,入点向出点连一条容量为 1 1 1的边。注意读入时要用浪费掉的char来占掉括号和逗号的位置。
#include<bits/stdc++.h>
using namespace std;
const int NN=104,MM=(NN+NN*NN)*2,INF=1e9;
int h[NN],e[MM],ne[MM],c[MM],head[NN],d[NN],idx,s,t;
void add(int u,int v,int w)
{
e[++idx]=v;
ne[idx]=h[u];
c[idx]=w;
h[u]=idx;
e[++idx]=u;
ne[idx]=h[v];
c[idx]=0;
h[v]=idx;
}
bool bfs()
{
queue<int>q;
q.push(s);
memset(d,-1,sizeof(d));
d[s]=0;
head[s]=h[s];
while(q.size())
{
int f=q.front();
q.pop();
for(int i=h[f];~i;i=ne[i])
{
int v=e[i];
if(d[v]==-1&&c[i])
{
d[v]=d[f]+1;
head[v]=h[v];
if(v==t)
return true;
q.push(v);
}
}
}
return false;
}
int find(int u,int sum)
{
if(u==t)
return sum;
int res=0;
for(int i=head[u];~i&&res<sum;i=ne[i])
{
head[u]=i;
int v=e[i];
if(d[v]==d[u]+1&&c[i])
{
int temp=find(v,min(c[i],sum-res));
if(!temp)
d[v]=-1;
c[i]-=temp;
c[i^1]+=temp;
res+=temp;
}
}
return res;
}
int dinic()
{
int res=0,sum;
while(bfs())
while(sum=find(s,INF))
res+=sum;
return res;
}
int main()
{
int n,m;
while(cin>>n>>m)
{
memset(h,-1,sizeof(h));
idx=-1;
for(int i=0;i<n;i++)
add(i,n+i,1);
for(int i=1;i<=m;i++)
{
int u,v;
char c1,c2,c3;
cin>>c1>>u>>c2>>v>>c3;
add(u+n,v,INF);
add(v+n,u,INF);
}
int ans=n;
for(int i=0;i<n;i++)
for(int j=0;j<i;j++)
{
s=i+n,t=j;
for(int k=0;k<=idx;k+=2)
{
c[k]+=c[k^1];
c[k^1]=0;
}
ans=min(ans,dinic());
}
cout<<ans<<endl;
}
return 0;
}
AcWing 2176
很明显,最大权闭合图板子题。同样,输入太毒瘤!先整一个 s t r i n g s t r e a m stringstream stringstream,读入一行,把那一行的数一个一个提取出来放在 i n t int int里面。如果不懂 s t r i n g s t r e a m stringstream stringstream的可以上博客找找。然后直接套我上一篇文章的最大权闭合图的建图方法就可以过。
#include<bits/stdc++.h>
using namespace std;
const int NN=104,MM=(NN+2500)*2,INF=1e9;
int h[NN],e[MM],ne[MM],c[MM],head[NN],d[NN],idx=-1,s,t;
bool st[NN];
void add(int u,int v,int w)
{
e[++idx]=v;
ne[idx]=h[u];
c[idx]=w;
h[u]=idx;
e[++idx]=u;
ne[idx]=h[v];
c[idx]=0;
h[v]=idx;
}
bool bfs()
{
queue<int>q;
q.push(s);
memset(d,-1,sizeof(d));
d[s]=0;
head[s]=h[s];
while(q.size())
{
int f=q.front();
q.pop();
for(int i=h[f];~i;i=ne[i])
{
int v=e[i];
if(d[v]==-1&&c[i])
{
d[v]=d[f]+1;
head[v]=h[v];
if(v==t)
return true;
q.push(v);
}
}
}
return false;
}
int find(int u,int sum)
{
if(u==t)
return sum;
int res=0;
for(int i=head[u];~i&&res<sum;i=ne[i])
{
head[u]=i;
int v=e[i];
if(d[v]==d[u]+1&&c[i])
{
int temp=find(v,min(c[i],sum-res));
if(!temp)
d[v]=-1;
c[i]-=temp;
c[i^1]+=temp;
res+=temp;
}
}
return res;
}
int dinic()
{
int res=0,sum;
while(bfs())
while(sum=find(s,INF))
res+=sum;
return res;
}
void dfs(int u)
{
st[u]=true;
for(int i=h[u];~i;i=ne[i])
if(c[i]&&!st[e[i]])
dfs(e[i]);
}
int main()
{
int n,m;
scanf("%d%d",&n,&m);
getchar();
memset(h,-1,sizeof(h));
t=n+m+1;
int sum=0;
for(int i=1;i<=n;i++)
{
string str;
getline(cin,str);
stringstream line(str);
int get,id;
line>>get;
sum+=get;
add(s,i,get);
while(line>>id)
add(i,id+n,INF);
}
for(int i=1;i<=m;i++)
{
int use;
scanf("%d",&use);
add(i+n,t,use);
}
int res=dinic();
dfs(s);
for(int i=1;i<=n;i++)
if(st[i])
printf("%d ",i);
puts("");
for(int i=1;i<=m;i++)
if(st[i+n])
printf("%d ",i);
printf("\n%d",sum-res);
return 0;
}
AcWing 2199
这个题还是很简单。两个骑士互相攻击,那么就是个最大点权独立集,每个位置的点权是 1 1 1,如果不能放就不用连边,也没有点权。这种格子图建二分图的板子就不用我多说了吧。
#include<bits/stdc++.h>
using namespace std;
const int NN=40004,MM=NN*16,INF=1e9,dx[8]={
-1,-1,-2,-2,1,1,2,2},dy[8]={
-2,2,1,-1,-2,2,-1,1};
int n,h[NN],e[MM],ne[MM],c[MM],head[NN],d[NN],idx=-1,s,t;
bool g[204][204];
int get(int x,int y)
{
return (x-1)*n+y;
}
void add(int u,int v,int w)
{
e[++idx]=v;
ne[idx]=h[u];
c[idx]=w;
h[u]=idx;
e[++idx]=u;
ne[idx]=h[v];
h[v]=idx;
}
bool bfs()
{
queue<int>q;
q.push(s);
memset(d,-1,sizeof(d));
d[s]=0;
head[s]=h[s];
while(q.size())
{
int f=q.front();
q.pop();
for(int i=h[f];~i;i=ne[i])
{
int v=e[i];
if(d[v]==-1&&c[i])
{
d[v]=d[f]+1;
head[v]=h[v];
if(v==t)
return true;
q.push(v);
}
}
}
return false;
}
int find(int u,int sum)
{
if(u==t)
return sum;
int res=0;
for(int i=head[u];~i&&res<sum;i=ne[i])
{
head[u]=i;
int v=e[i];
if(d[v]==d[u]+1&&c[i])
{
int temp=find(v,min(c[i],sum-res));
if(!temp)
d[v]=-1;
c[i]-=temp;
c[i^1]+=temp;
res+=temp;
}
}
return res;
}
int dinic()
{
int res=0,sum;
while(bfs())
while(sum=find(s,INF))
res+=sum;
return res;
}
int main()
{
int m;
scanf("%d%d",&n,&m);
memset(h,-1,sizeof(h));
t=n*n+1;
for(int i=1;i<=m;i++)
{
int x,y;
scanf("%d%d",&x,&y);
g[x][y]=true;
}
int res=0;
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
{
if(g[i][j])
continue;
res++;
if(i+j&1)
{
add(s,get(i,j),1);
for(int k=0;k<8;k++)
{
int nx=i+dx[k],ny=j+dy[k];
if(nx<=n&&ny<=n&&nx>=1&&ny>=1&&!g[nx][ny])
add(get(i,j),get(nx,ny),INF);
}
}
else
add(get(i,j),t,1);
}
printf("%d",res-dinic());
return 0;
}
概念
费用流,全称最小费用最大流。是网络流中的一类题型。和最大流的区别就是每条边加了一个单位费用,每流 1 1 1的流量就要花费 m o n e y e money_e moneye。要求的东西也变成了在最大流中费用最小的一个。
残留网络:在费用流中,残留网络还需要建立反向边的费用为 − m o n e y e -money_e −moneye,因为退货肯定要退钱。
算法
EK算法
方法
把求最大流 E K EK EK算法中的 b f s bfs bfs换成最短路即可。因为残留网络中有负权边,所以 d i j dij dij会出问题,最好用 s p f a spfa spfa。加上一个极大值可以让 d i j dij dij都变成正权,这个技巧有点像最大密度子图,详见我的博客最小割。但是一般 s p f a spfa spfa足够了,所以这里只介绍 s p f a spfa spfa的方法。不知道 E K EK EK怎么做的可以看我的博客最大流。然后我们继续来看,我们可以求出一条增广路的费用和以及流量。因为这一条路径每一条边流量一定相等(少了流不过去,多了又不流量守恒),则设增广路中边集为 E E E,增广路路径之和为 M o n e y Money Money,所以路径的费用 = ∑ e ∈ E ( m o n e y e × f l o w ) =\displaystyle\sum_{e\in E}(money_e\times flow) =e∈E∑(moneye×flow) = ∑ e ∈ E ( m o n e y e ) × f l o w =\displaystyle\sum_{e\in E}(money_e)\times flow =e∈E∑(moneye)×flow = M o n e y × f l o w =Money\times flow =Money×flow那么,每次增广路的费用加上一个 M o n e y × f l o w Money\times flow Money×flow即可。
时间复杂度 O ( n 2 m 2 ) O(n^2m^2) O(n2m2)。
例题
AcWing 2174
这个题套板子即可。
#include<bits/stdc++.h>
using namespace std;
const int NN=5004,MM=100004;
int head[NN],ne[MM],e[MM],w[MM],c[MM],f[NN],d[NN],pre[NN],idx=-1,flow,cost,s,t;
bool st[NN];
void add(int u,int v,int l,int money)
{
e[++idx]=v;
ne[idx]=head[u];
c[idx]=l;
w[idx]=money;
head[u]=idx;
e[++idx]=u;
ne[idx]=head[v];
w[idx]=-money;
head[v]=idx;
}
bool spfa()
{
memset(f,0,sizeof(f));
memset(d,0x3f,sizeof(d));
queue<int>q;
q.push(s);
f[s]=1e9;
d[s]=0;
st[s]=true;
while(q.size())
{
int u=q.front();
q.pop();
st[u]=false;
for(int i=head[u];~i;i=ne[i])
{
int v=e[i];
if(c[i]&&d[v]>d[u]+w[i])
{
f[v]=min(f[u],c[i]);
d[v]=d[u]+w[i];
pre[v]=i;
if(st[v])
continue;
q.push(v);
st[v]=true;
}
}
}
return f[t]>0;
}
void EK()
{
while(spfa())
{
flow+=f[t];
cost+=d[t]*f[t];
for(int i=t;i!=s;i=e[pre[i]^1])
{
c[pre[i]]-=f[t];
c[pre[i]^1]+=f[t];
}
}
}
int main()
{
memset(head,-1,sizeof(head));
int n,m;
scanf("%d%d%d%d",&n,&m,&s,&t);
for(int i=1;i<=m;i++)
{
int u,v,l,money;
scanf("%d%d%d%d",&u,&v,&l,&money);
add(u,v,l,money);
}
EK();
printf("%d %d",flow,cost);
return 0;
}
费用流解二分图最大权匹配
概念
二分图最大权匹配,就是二分图匹配加上了边权,要求一个边权最大的匹配。
方法
还是最大流解二分图的建图方式,两点之间的边加上费用即可。因为是求最大权,所以边权变成负的求最小费用最大流,最后再把 E K EK EK的结果变回正的即可。不知道怎么最大流解二分图的,见我的博客最大流。
例题
AcWing 2193
这个题目就是一个二分图最大权匹配问题。但是又要求一个最小权匹配,直接就正权求最小费用最大流即可。
#include<bits/stdc++.h>
using namespace std;
const int NN=104,MM=5204;
int head[NN],ne[MM],e[MM],w[MM],c[MM],f[NN],d[NN],pre[NN],idx=-1,s,t;
bool st[NN];
void add(int u,int v,int l,int money)
{
e[++idx]=v;
ne[idx]=head[u];
c[idx]=l;
w[idx]=money;
head[u]=idx;
e[++idx]=u;
ne[idx]=head[v];
w[idx]=-money;
head[v]=idx;
}
bool spfa()
{
memset(f,0,sizeof(f));
memset(d,0x3f,sizeof(d));
queue<int>q;
q.push(s);
f[s]=1e9;
d[s]=0;
st[s]=true;
while(q.size())
{
int u=q.front();
q.pop();
st[u]=false;
for(int i=head[u];~i;i=ne[i])
{
int v=e[i];
if(c[i]&&d[v]>d[u]+w[i])
{
f[v]=min(f[u],c[i]);
d[v]=d[u]+w[i];
pre[v]=i;
if(st[v])
continue;
q.push(v);
st[v]=true;
}
}
}
return f[t]>0;
}
int EK()
{
int res=0;
while(spfa())
{
res+=d[t]*f[t];
for(int i=t;i!=s;i=e[pre[i]^1])
{
c[pre[i]]-=f[t];
c[pre[i]^1]+=f[t];
}
}
return res;
}
int main()
{
memset(head,-1,sizeof(head));
int n;
scanf("%d",&n);
t=2*n+1;
for(int i=1;i<=n;i++)
{
add(s,i,1,0);
add(i+n,t,1,0);
}
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
{
int x;
scanf("%d",&x);
add(i,j+n,1,x);
}
printf("%d\n",EK());
for(int i=0;i<=idx;i+=2)
{
c[i]+=c[i^1];
c[i^1]=0;
w[i]=-w[i];
w[i^1]=-w[i^1];
}
printf("%d",-EK());
return 0;
}
技巧:拆点
概念
不知道怎么拆点的,见我的博客最大流。这里和最大流的拆点一样,只不过边加上了费用而已。在费用流中还有一种情况要拆点,就是有点权,拆点之间边的费用就是点权。
方法
和最大流的一样。
例题
AcWing 2191
这个题有点权,要拆点。第一问就是点只能用一次的情况,拆点限制为 1 1 1即可。第二问就是每个点用多次,把入点到出点的限制放开即可。第三问就是边的限制放开。更详细的见我在AcWing上写的题解
#include<bits/stdc++.h>
using namespace std;
const int NN=1004,MM=3124;
int g[20][40],id[20][40],head[NN],ne[MM],w[MM],c[MM],e[MM],d[NN],f[NN],pre[NN],idx,s,t;
bool st[NN];
void add(int u,int v,int l,int money)
{
e[++idx]=v;
ne[idx]=head[u];
c[idx]=l;
w[idx]=money;
head[u]=idx;
e[++idx]=u;
ne[idx]=head[v];
c[idx]=0;
w[idx]=-money;
head[v]=idx;
}
bool spfa()
{
memset(f,0,sizeof(f));
memset(d,0x3f,sizeof(d));
queue<int>q;
q.push(s);
f[s]=1e9;
d[s]=0;
st[s]=true;
while(q.size())
{
int u=q.front();
q.pop();
st[u]=false;
for(int i=head[u];~i;i=ne[i])
{
int v=e[i];
if(c[i]&&d[v]>d[u]+w[i])
{
f[v]=min(f[u],c[i]);
d[v]=d[u]+w[i];
pre[v]=i;
if(st[v])
continue;
q.push(v);
st[v]=true;
}
}
}
return f[t]>0;
}
int EK()
{
int res=0;
while(spfa())
{
res+=d[t]*f[t];
for(int i=t;i!=s;i=e[pre[i]^1])
{
c[pre[i]]-=f[t];
c[pre[i]^1]+=f[t];
}
}
return res;
}
int main()
{
int n,m,cnt=0;
scanf("%d%d",&m,&n);
for(int i=1;i<=n;i++)
for(int j=1;j<=i+m-1;j++)
{
scanf("%d",&g[i][j]);
id[i][j]=++cnt;
}
t=2*cnt+2;
memset(head,-1,sizeof(head));
idx=-1;
for(int i=1;i<=n;i++)
for(int j=1;j<=i+m-1;j++)
{
add(id[i][j]*2,id[i][j]*2+1,1,-g[i][j]);
if(i==1)
add(s,id[i][j]*2,1,0);
if(i==n)
add(id[i][j]*2+1,t,1,0);
if(i<n)
{
add(id[i][j]*2+1,id[i+1][j]*2,1,0);
add(id[i][j]*2+1,id[i+1][j+1]*2,1,0);
}
}
printf("%d\n",-EK());
memset(head,-1,sizeof(head));
idx=-1;
for(int i=1;i<=n;i++)
for(int j=1;j<=i+m-1;j++)
{
add(id[i][j]*2,id[i][j]*2+1,1e9,-g[i][j]);
if(i==1)
add(s,id[i][j]*2,1,0);
if(i==n)
add(id[i][j]*2+1,t,1e9,0);
if(i<n)
{
add(id[i][j]*2+1,id[i+1][j]*2,1,0);
add(id[i][j]*2+1,id[i+1][j+1]*2,1,0);
}
}
printf("%d\n",-EK());
memset(head,-1,sizeof(head));
idx=-1;
for(int i=1;i<=n;i++)
for(int j=1;j<=i+m-1;j++)
{
add(id[i][j]*2,id[i][j]*2+1,1e9,-g[i][j]);
if(i==1)
add(s,id[i][j]*2,1,0);
if(i==n)
add(id[i][j]*2+1,t,1e9,0);
if(i<n)
{
add(id[i][j]*2+1,id[i+1][j]*2,1e9,0);
add(id[i][j]*2+1,id[i+1][j+1]*2,1e9,0);
}
}
printf("%d\n",-EK());
return 0;
}
AcWing 382
这个题有点权,需要拆点解决。把一个点拆成入点和出点,费用是价值的容量为 1 1 1,再来一种以后走的边,费用为 0 0 0容量为正无穷,求最大费用最大流。
#include<bits/stdc++.h>
using namespace std;
const int NN=10004,MM=30004;
int n,head[NN],ne[MM],w[MM],c[MM],e[MM],d[NN],f[NN],pre[NN],idx=-1,s,t=1;
bool st[NN];
int get(int x,int y)
{
return ((x-1)*n+y)*2;
}
void add(int u,int v,int l,int money)
{
e[++idx]=v;
ne[idx]=head[u];
c[idx]=l;
w[idx]=money;
head[u]=idx;
e[++idx]=u;
ne[idx]=head[v];
w[idx]=-money;
head[v]=idx;
}
bool spfa()
{
memset(f,0,sizeof(f));
memset(d,0x3f,sizeof(d));
queue<int>q;
q.push(s);
f[s]=1e9;
d[s]=0;
st[s]=true;
while(q.size())
{
int u=q.front();
q.pop();
st[u]=false;
for(int i=head[u];~i;i=ne[i])
{
int v=e[i];
if(c[i]&&d[v]>d[u]+w[i])
{
f[v]=min(f[u],c[i]);
d[v]=d[u]+w[i];
pre[v]=i;
if(st[v])
continue;
q.push(v);
st[v]=true;
}
}
}
return f[t]>0;
}
int EK()
{
int res=0;
while(spfa())
{
res+=d[t]*f[t];
for(int i=t;i!=s;i=e[pre[i]^1])
{
c[pre[i]]-=f[t];
c[pre[i]^1]+=f[t];
}
}
return res;
}
int main()
{
int k;
scanf("%d%d",&n,&k);
memset(head,-1,sizeof(head));
add(s,get(1,1),k,0);
add(get(n,n)+1,t,k,0);
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
{
int x;
scanf("%d",&x);
add(get(i,j),get(i,j)+1,1,-x);
add(get(i,j),get(i,j)+1,1e9,0);
if(i<n)
add(get(i,j)+1,get(i+1,j),1e9,0);
if(j<n)
add(get(i,j)+1,get(i,j+1),1e9,0);
}
printf("%d\n",-EK());
return 0;
}
AcWing 2184
这个题就是典型的多种状态。把每天的毛巾拆成两个点,一个是洗过,一个是没有洗。每天没洗过的毛巾可以流到下一天,也可以经过清洗流到干净的毛巾,快洗流到新毛巾的 i + t 1 i+t1 i+t1天,费用 m 1 m1 m1,慢洗同理。也可以购买新毛巾,费用是 m 0 m0 m0,新毛巾用了可以流到汇点。每天都会用完刚好 x x x条毛巾,所以会有 x x x条脏毛巾,但是他们流向汇点计算答案了,所以这里就改成从源点向脏毛巾的点流 x x x。
#include<bits/stdc++.h>
using namespace std;
const int NN=2004,MM=20004;
int head[NN],e[MM],ne[MM],c[MM],w[MM],idx=-1,f[NN],d[NN],pre[NN],s,t;
bool st[NN];
void add(int u,int v,int l,int money)
{
e[++idx]=v;
ne[idx]=head[u];
c[idx]=l;
w[idx]=money;
head[u]=idx;
e[++idx]=u;
ne[idx]=head[v];
w[idx]=-money;
head[v]=idx;
}
bool spfa()
{
memset(f,0,sizeof(f));
memset(d,0x3f,sizeof(d));
queue<int>q;
q.push(s);
f[s]=1e9;
d[s]=0;
st[s]=true;
while(q.size())
{
int u=q.front();
q.pop();
st[u]=false;
for(int i=head[u];~i;i=ne[i])
{
int v=e[i];
if(c[i]&&d[v]>d[u]+w[i])
{
d[v]=d[u]+w[i];
f[v]=min(f[u],c[i]);
pre[v]=i;
if(!st[v])
{
st[v]=true;
q.push(v);
}
}
}
}
return f[t]>0;
}
int EK()
{
int res=0;
while(spfa())
{
res+=d[t]*f[t];
for(int i=t;i!=s;i=e[pre[i]^1])
{
c[pre[i]]-=f[t];
c[pre[i]^1]+=f[t];
}
}
return res;
}
int main()
{
int n,m0,t1,m1,t2,m2;
scanf("%d%d%d%d%d%d",&n,&m0,&t1,&m1,&t2,&m2);
t=2*n+1;
memset(head,-1,sizeof(head));
for(int i=1;i<=n;i++)
{
int x;
scanf("%d",&x);
add(s,i,x,0);
add(i+n,t,x,0);
add(s,i+n,1e9,m0);
if(i<n)
add(i,i+1,1e9,0);
if(i+t1<=n)
add(i,n+i+t1,1e9,m1);
if(i+t2<=n)
add(i,n+i+t2,1e9,m2);
}
printf("%d",EK());
return 0;
}
上下界可行流
概念
不知道上下界可行流见我的博客最大流,这里就是多了个费用。但是在费用流中要求从源点和流向汇点的边是没有费用的,因为它们只是为了流量守恒。
方法
和最大流的上下界可行流一样。
例题
AcWing 969
这个题就是一个上下界可行流。但是这个是没有上界的,所以上界可以设为正无穷。于是,上界减下界还是正无穷。前一天的志愿者可以到后一天,到了最后一天就流出去。然后我们发现,原图 G G G从志愿者结束工作的时间是流到 t t t了一些人,开始从 s s s流进来了一些开始工作的志愿者,但是新图 G G G的源点和汇点是满足流量守恒的,不能提供志愿者。所以为了流量守恒,结束向开始连一条边即可。如果要公式化的解释我看到了网上一篇解释地特别漂亮的博客。围观点我
#include<bits/stdc++.h>
using namespace std;
const int NN=1004,MM=24004;
int head[NN],ne[MM],w[MM],c[MM],e[MM],d[NN],f[NN],pre[NN],idx=-1,s,t;
bool st[NN];
void add(int u,int v,int l,int money)
{
e[++idx]=v;
ne[idx]=head[u];
c[idx]=l;
w[idx]=money;
head[u]=idx;
e[++idx]=u;
ne[idx]=head[v];
w[idx]=-money;
head[v]=idx;
}
bool spfa()
{
memset(f,0,sizeof(f));
memset(d,0x3f,sizeof(d));
queue<int>q;
q.push(s);
f[s]=1e9;
d[s]=0;
st[s]=true;
while(q.size())
{
int u=q.front();
q.pop();
st[u]=false;
for(int i=head[u];~i;i=ne[i])
{
int v=e[i];
if(c[i]&&d[v]>d[u]+w[i])
{
f[v]=min(f[u],c[i]);
d[v]=d[u]+w[i];
pre[v]=i;
if(st[v])
continue;
q.push(v);
st[v]=true;
}
}
}
return f[t]>0;
}
int EK()
{
int res=0;
while(spfa())
{
res+=d[t]*f[t];
for(int i=t;i!=s;i=e[pre[i]^1])
{
c[pre[i]]-=f[t];
c[pre[i]^1]+=f[t];
}
}
return res;
}
int main()
{
int n,m;
scanf("%d%d",&n,&m);
memset(head,-1,sizeof(head));
t=n+2;
int last=0;
for(int i=1;i<=n;i++)
{
int x;
scanf("%d",&x);
if(last>x)
add(s,i,last-x,0);
else
add(i,t,x-last,0);
add(i,i+1,1e9,0);
last=x;
}
add(s,n+1,last,0);
for(int i=1;i<=m;i++)
{
int b,e,money;
scanf("%d%d%d",&b,&e,&money);
add(e+1,b,1e9,money);
}
printf("%d\n",EK());
return 0;
}
习题
AcWing 2192
AcWing 2194
AcWing 2195
解析和代码在下一篇博客——2-sat给出(下一次有可能会做算法提高课同步内容)