费用流
分析
目前只会EK(毕竟难点在构图)
如果原图中不存在负环,则之后的残余网络中不可能出现负环
构图的关键:在最大流的基础上最小费用(最大流必须跑满流起着限制作用,这是关键中的关键)
PS:最好每次都保证建双向边,之前有一道题由于是无向图想当然地建了单向边,导致钩钩了
inline void add(int u,int v,int k,int l) {
to[++cnt]=v,nxt[cnt]=he[u],he[u]=cnt;
w[cnt]=k,ex[cnt]=l;
}
inline void adde(int u,int v,int k,int l) {
add(u,v,k,l),add(v,u,0,-l);
}
inline bool spfa() {
for(int i=1;i<=n;i++) {
d[i]=INF; fl[i]=0;
}
int l=1,r=1; d[q[1]=S]=0;
while(l<=r) {
int u=q[l%n]; l++; fl[u]=0;
for(int e=he[u];e;e=nxt[e]) {
int v=to[e];
if(w[e]&&d[v]>d[u]+ex[e]) {
d[v]=d[u]+ex[e],pre[v]=e;
if(!fl[v]){
fl[v]=1;
r++,q[r%n]=v;
}
}
}
}
return d[T]!=INF;
}
inline void dfs() {
int u=T,flow=INF;
for(;u!=S;u=to[pre[u]^1]) {
flow=min(flow,w[pre[u]]);
}
for(u=T;u!=S;u=to[pre[u]^1]) {
w[pre[u]]-=flow;
w[pre[u]^1]+=flow;
}
sum+=flow; ans+=flow*d[T];
}
例题1(裂边)
如果费用一定,则是费用流板子
费用是分段函数,且单调增,所以可以裂边,或者动态加边(前者方便)
int main(){
scanf("%d%d",&m,&n); S=n+m+1,T=S+1;
cnt=1;
for(int i=1;i<=n;i++) {
int t; scanf("%d",&t);
adde(i+m,T,t,0);
}
for(int i=1;i<=m;i++) {
for(int j=1;j<=n;j++) {
int op; scanf("%d",&op);
if(op) adde(i,j+m,INF,0);
}
}
for(int i=1;i<=m;i++) {
int num; scanf("%d",&num);
for(int j=1;j<=num;j++) {
scanf("%d",&a[j]);
}
a[num+1]=INF;
for(int j=1;j<=num+1;j++) {
int t; scanf("%d",&t);
adde(S,i,a[j]-a[j-1],t);
}
}
while(spfa()) dfs();
printf("%lld\n",ans);
return 0;
}
例题2
为了维护一天需要\(r_i\)块餐巾,需要将一天裂成早上和晚上两个点
想法1:将早上和晚上用流量为\(r_i\),费用为0的边,然后早上和\(S\)连流量正无穷,费用为\(p\)的边,晚上和\(T\)连流量正无穷费用为\(0\) 的边,然后再安排洗毛巾
这感觉是正确的,结果是错误无比,因为答案并不是在最大流时取到
想法2:将\(S\)点向晚上连流量为\(r_i\),费用为0的边,再让早上和\(T\)连流量为\(r_i\),费用为0的边,要使早上有毛巾,从\(S\)点向早上连流量为\(INF\),费用为\(p\)的边,然后安排洗毛巾和毛巾的传递
相当于枚举的是早上的毛巾情况,然后建晚上的点来辅助分析,最大流的情况保证了所有早上的点都满流
int main(){
scanf("%d",&n);
for(int i=1;i<=n;i++) scanf("%d",&c[i]);
scanf("%d%d%d%d%d",&f,&a,&fa,&b,&fb); S=n+n+1,T=S+1;
cnt=1;
for(int i=1;i<=n;i++) {
int t=c[i];
adde(S,i+n,t,0);
adde(i,T,t,0);
adde(S,i,INF,f);
if(i!=n) adde(i,i+1,INF,0);
if(i+a<=n) adde(i+n,i+a,INF,fa);
if(i+b<=n) adde(i+n,i+b,INF,fb);
}
while(spfa()) dfs();
printf("%lld\n",ans);
return 0;
}
例题3
答案不具有二分性,\(n\leq 40\),所以考虑枚举行的最多个数\(Lim\),求在条件1和\(Lim\)约束下的棋子数\(ans\)
则条件2:
显然\(ans\)越大越有可能符合条件2
\(\therefore\)求在条件1和\(Lim\)约束下的最大棋子数,可以把行列分开建节点
正难则反,考虑求全部可放置的节点-最少不要的节点数
接下来是神仙构图:将\(S\)向所有行连边,流量为每行最多放的节点数,权值为0,表示\(All\)
将所有列向\(T\)连边,流量为每列最多放的节点数,权值为0,表示\(All\)
\(i\)行向\(i\)列连边,流量为\(INF\),权值为0,流过\(k\)流量表示保留\(k\)个点
点\((x,y)\)表示\(x\)行向\(y\)列连边,流量为1,权值为1,流过表示删掉该点
必须满流
int main() {
int A, B;
scanf("%d%d%d", &n, &A, &B);
int id = 0;
while (n || A || B) {
printf("Case %d: ", ++id);
S = n + n + 1, T = S + 1;
int Ans = -1, yl = 0, sum = 0;
memset(mx, 0, sizeof(mx));
for (int i = 1; i <= n; i++) {
scanf("%s", s[i] + 1);
for (int j = 1; j <= n; j++) {
if (s[i][j] == '.' || s[i][j] == 'C')
mx[i]++, mx[j + n]++, sum++;
if (s[i][j] == 'C')
yl++;
}
}
int lim = 0;
for (int i = 1; i <= n; i++) {
int ss = 0;
for (int j = 1; j <= n; j++) ss += s[i][j] == 'C';
lim = max(lim, ss);
}
for (int i = 1; i <= n; i++) {
int ss = 0;
for (int j = 1; j <= n; j++) ss += s[j][i] == 'C';
lim = max(lim, ss);
}
for (; lim <= n; lim++) {
cnt = 1;
memset(he, 0, sizeof(he));
for (int i = 1; i <= n; i++) {
adde(S, i, mx[i], 0);
adde(i + n, T, mx[i + n], 0);
adde(i, i + n, lim, 0);
}
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
if (s[i][j] == '.') {
adde(i, j + n, 1, 1);
}
}
}
ans = num = 0;
while (spfa()) dfs();
ans = sum - ans;
if (num == sum && lim * B <= A * ans)
Ans = max(Ans, ans);
}
if (Ans == -1)
puts("impossible");
else
printf("%d\n", Ans - yl);
scanf("%d%d%d", &n, &A, &B);
}
return 0;
}
例题4(裂点)
假设一台机器有\(K\)个订单,所需时间分别为\(c[i]\),则其花费的时间为\(\sum_{i=1}^{K}c[i]\times i\)
要求时间最小,则\(c[i]\)应单调增,所以\(c[i]\times i\)单调增,所以裂点后可以保证订单按次序
所以可以把一台机器裂成\(n\)个点,分别表示1倍权值,2倍权值\(\cdots\)
然后通过建边保证每个权值倍数只能用1次,最小费用最大流
int main(){
int Tim; scanf("%d",&Tim);
while(Tim--) {
scanf("%d%d",&n,&m); S=n*(m+1)+1,T=S+1;
cnt=1; ans=0;
memset(he,0,sizeof(he));
for(int i=1;i<=n;i++) {
for(int j=1;j<=m;j++) {
int t; scanf("%d",&t);
for(int k=1;k<=n;k++) {
adde(id(j,k),i,1,t*k);
}
}
adde(i,T,1,0);
}
for(int i=1;i<=m;i++) {
for(int j=1;j<=n;j++) {
adde(S,id(i,j),1,0);
}
}
while(spfa()) dfs();
printf("%.6lf\n",(double)ans/n);
}
return 0;
}
例题5
这是一张二分图,可以用约数个数的奇偶性划分
连边后跑最大费用最大流
因为每次EK找的是最大费用的流,所以当前的费用和\(<0\)即可结束
inline bool dfs() {
int u=T,flow=INF;
for(;u!=S;u=to[pre[u]^1]) {
flow=min(flow,w[pre[u]]);
}
for(u=T;u!=S;u=to[pre[u]^1]) {
w[pre[u]]-=flow;
w[pre[u]^1]+=flow;
}
if(ans+d[T]*flow<0) {
sum+=ans/(-d[T]);
return 0;
}
ans+=d[T]*flow;
sum+=flow;
return 1;
}
inline int fj(int x) {
int u=x,ret=0;
for(int i=2;i<=sqrt(x);i++) {
while(u%i==0) {
u/=i; ret++;
}
}
if(u>1) ret++;
return ret;
}
int main(){
scanf("%d",&n); S=n+1,T=S+1,cnt=1;
for(int i=1;i<=n;i++) {
scanf("%d",&a[i]); co[i]=fj(a[i]);
}
for(int i=1;i<=n;i++) {
scanf("%d",&b[i]);
}
for(int i=1;i<=n;i++) {
scanf("%d",&c[i]);
}
for(int i=1;i<=n;i++) {
if(co[i]&1) {
for(int j=1;j<=n;j++) {
if(abs(co[i]-co[j])==1&&(a[i]%a[j]==0||a[j]%a[i]==0)) {
adde(i,j,INF,(ll)c[i]*c[j]);
}
}
adde(S,i,b[i],0);
} else {
adde(i,T,b[i],0);
}
}
while(spfa()) {
if(!dfs()) break;
}
printf("%d\n",sum);
return 0;
}
例题6
先按题意建出残余网络,spfa找负环,然后沿负环流一遍
注意:spfa找负环是一定要\(>n+1\),防止的是1个节点
inline int spfa() {
for(int i=1;i<=T;i++) {
d[i]=INF; fl[i]=0;
}
d[T]=0; q.push(T);
while(!q.empty()) {
int u=q.front(); q.pop(); fl[u]=0;
for(int e=he[u];e;e=nxt[e]) {
int v=to[e];
if(w[e]&&d[v]>d[u]+ex[e]) {
d[v]=d[u]+ex[e],pre[v]=e;
if(!fl[v]){
fl[v]=1;q.push(v);
vis[v]++;
if(vis[v]>n+1) {
return v;
}
}
}
}
}
return -1;
}
struct A{int a,b,c; }a[N],b[N];
int main(){
scanf("%d%d",&n,&m); S=n+m+1,T=S+1,cnt=1;
for(int i=1;i<=n;i++) {
scanf("%d%d%d",&a[i].a,&a[i].b,&a[i].c);
}
for(int i=1;i<=m;i++) {
scanf("%d%d%d",&b[i].a,&b[i].b,&b[i].c);
}
for(int i=1;i<=n;i++) {
for(int j=1;j<=m;j++) {
int t; scanf("%d",&t);
add(i,j+n,INF,abs(a[i].a-b[j].a)+abs(a[i].b-b[j].b)+1);
add(j+n,i,t,-abs(a[i].a-b[j].a)-abs(a[i].b-b[j].b)-1);
c[j]+=t;
E[i][j]=t;
}
adde(i,S,a[i].c,0);
}
for(int i=1;i<=m;i++) {
add(i+n,T,b[i].c-c[i],0),add(T,i+n,c[i],0);
}
int ans=spfa();
if(ans==-1) printf("OPTIMAL");
else {
puts("SUBOPTIMAL");
memset(fl,0,sizeof(fl));
for(;!fl[ans];ans=to[pre[ans]^1]) {
fl[ans]=1;
}
memset(fl,0,sizeof(fl));
int flow=INF;
for(;!fl[ans];ans=to[pre[ans]^1]) {
flow=min(flow,w[pre[ans]]);
fl[ans]=1;
}
int v=ans;
if(ex[pre[v]]>0) {
E[to[pre[v]^1]][v-n]+=flow;
} else {
E[v][to[pre[v]^1]-n]-=flow;
}
v=to[pre[v]^1];
for(;v!=ans;v=to[pre[v]^1]) {
if(ex[pre[v]]>0) {
E[to[pre[v]^1]][v-n]+=flow;
} else {
E[v][to[pre[v]^1]-n]-=flow;
}
}
for(int i=1;i<=n;i++) {
for(int j=1;j<m;j++) {
printf("%d ",E[i][j]);
}
printf("%d\n",E[i][m]);
}
}
return 0;
}