前言
又到了kanon的季节。
还记得我省选前刷了不少网络流的题目,那个时候做完题没有写总结,结果现在再看题,全都懵逼QAQ,故在此记录一下其中三道经典题的建模方法。
洛谷P1251 餐巾计划问题
题目传送门:https://www.luogu.org/problemnew/show/P1251
题目分析:这道题的建模十分巧妙,我看了网上的题解都不是很明白,于是尝试着自己推导了一下。在YY出无数种错误的构图后,我终于把图构对了。
首先题目要求第i天有r[i]块干净的餐巾可以用,于是我们先画出一个最简单的图:
其中逗号左边的数代表流量上限,右边的数代表花费。
接下来题目又说可以快洗和慢洗,于是我们将第i天右边的节点分别向左边第i+m天的节点和第i+n天的节点连对应的边,前提是i+m或i+n小于等于天数:
此时一条流过绿色边的流就代表快洗了一张餐巾,流过红色边就代表慢洗。流向t就代表扔掉这张餐巾。
为什么要右边的点向左边的点连边呢?因为每天快洗+慢洗+扔掉的餐巾要刚好等于r[i],而右边的点本身就刚好受到r[i]的流量限制。
然后我们再观察一下构图,发现还是有问题:某一天用完的餐巾不一定要当天就快洗或慢洗,然后送给第i+m或i+n天用;也不一定用完了就扔掉。它可能留着以后再洗,所以要修改一下构图:
这个图看上去是没问题了,但事实上它有一个很严重的问题。我们的最终目的是要使中间所有r[i],0的边都流满,于是现在来模拟一条流(用下图中的棕色表示):
那么它的含义是:在第一天买进来1条餐巾,然后在第一天结束时送去了慢洗,第二天用完后就一直没再用。
那么这条餐巾实际上用了两天,但它只代表了从s到t的1的流量。也就是说,如果有一些餐巾用了多天,最终的流量就会小于r的总和。而我们知道,最小费用最大流是优先跑最大流的,所以最后解出来的流一定是这样:
因为这样才是最大流量。也就是说,最终答案是p*(r的总和)。这样显然错误。
那有没有什么办法,让一条用了两天的餐巾代表2的流量呢?这就是本题构图的巧妙之处。我们不妨先让每天开始时得到的r[i]条干净的餐巾(左边一列的节点)流向t,然后再在每天结束时从s补回r[i]条脏的餐巾(从s向右边一列的节点连r[i],0的边):
这样,之前的那条棕色的流就被拆成了下面两条流:
然后问题就巧妙地解决啦!(注意流量要开long long)
CODE:
#include<iostream>
#include<string>
#include<cstring>
#include<cmath>
#include<cstdio>
#include<cstdlib>
#include<stdio.h>
#include<algorithm>
using namespace std;
const int maxn=4010;
const int maxm=200000;
const int oo=1000000001;
const long long INF=100000000000000000LL;
typedef long long LL;
struct data
{
LL cap;
int obj,cost;
data *rev,*Next;
} e[maxm];
data *head[maxn];
data *nhead[maxn];
int cur=-1;
int dis[maxn];
bool vis[maxn];
int que[maxn];
int he,ta;
int r[maxn/2];
int N,p,m,f,n,s,t;
void Add(int x,int y,LL flow,int c)
{
cur++;
e[cur].obj=y;
e[cur].cap=flow;
e[cur].cost=c;
e[cur].rev=&e[cur+1];
e[cur].Next=head[x];
head[x]=e+cur;
cur++;
e[cur].obj=x;
e[cur].cap=0;
e[cur].cost=-c;
e[cur].rev=&e[cur-1];
e[cur].Next=head[y];
head[y]=e+cur;
}
bool Release(int x,int y,int c)
{
if (dis[x]+c>=dis[y]) return false;
dis[y]=dis[x]+c;
return true;
}
bool SPFA()
{
for (int i=0; i<=t; i++)
{
nhead[i]=head[i];
dis[i]=oo;
vis[i]=false;
}
dis[0]=0;
vis[0]=true;
que[1]=0;
he=0,ta=1;
while (he!=ta)
{
he=(he+1)%maxn;
int node=que[he];
for (data *p=head[node]; p; p=p->Next)
if ( p->cap && Release(node,p->obj,p->cost) && !vis[p->obj] )
{
ta=(ta+1)%maxn;
que[ta]=p->obj;
vis[p->obj]=true;
}
vis[node]=false;
int nhe=(he+1)%maxn;
if ( dis[ que[ta] ]<dis[ que[nhe] ] ) swap(que[nhe],que[ta]);
}
for (int i=0; i<=t; i++) vis[i]=false;
return dis[t]<oo;
}
LL Dfs(int node,LL mf)
{
if ( node==t || !mf ) return mf;
LL nf=0;
vis[node]=true;
for (data *&p=nhead[node]; p; p=p->Next)
if ( p->cap && dis[p->obj]==dis[node]+p->cost && !vis[p->obj] )
{
LL d=Dfs(p->obj, min(p->cap,mf) );
p->cap-=d;
p->rev->cap+=d;
mf-=d;
nf+=d;
if (!mf) break;
}
if (mf) dis[node]=oo;
vis[node]=false;
return nf;
}
LL Min_cost_flow()
{
LL mcf=0;
while ( SPFA() )
mcf+=(long long)dis[t]*Dfs(0,INF);
return mcf;
}
int main()
{
freopen("c.in","r",stdin);
freopen("c.out","w",stdout);
scanf("%d",&N);
t=(N<<1)+1;
for (int i=0; i<=t; i++) head[i]=NULL;
for (int i=1; i<=N; i++) scanf("%d",&r[i]);
scanf("%d%d%d%d%d",&p,&m,&f,&n,&s);
for (int i=1; i<=N; i++)
{
Add(0,i,INF,p);
Add(0,N+i,r[i],0);
Add(i,t,r[i],0);
if (i<N) Add(N+i,N+i+1,INF,0);
if (i+m<=N) Add(N+i,i+m,INF,f);
if (i+n<=N) Add(N+i,i+n,INF,s);
}
LL ans=Min_cost_flow();
cout<<ans<<endl;
return 0;
}
洛谷P2765 魔术球问题
题目传送门:https://www.luogu.org/problemnew/show/P2765
题目分析:这题的模型主要是DAG的最少路径覆盖。
很明显我们可以二分答案。假设二分出ans,如何判断1~ans中的数要用几根柱子?用 ans2 的时间,判断每两个数是否能组成平方数,然后让小的数向大的数连边,就变成了DAG的最少路径覆盖,可以用二分图匹配解决。
如果原图中x向y有一条边,就让左边的第x个点向右边第y个点连边。匹配完后如果x流向了y,就说明x在最终方案中走向了y。为什么可以这样对应呢?我们注意到在最终方案中,每个点不会超过1条出边,而一个没有出边的点,就意味着一条链的结束,所以我们要使没有出边的点尽可能少,也就是二分图中失配的数量尽可能少,所以就是二分图最大匹配了。
至于打印路径,找到所有失配的点,然后递归打印就好了。
CODE:
#include<iostream>
#include<string>
#include<cstring>
#include<cmath>
#include<cstdio>
#include<cstdlib>
#include<stdio.h>
#include<algorithm>
using namespace std;
const int maxn=100000;
const int maxm=1000000;
const int oo=1000000001;
struct data
{
int obj,cap;
data *rev,*Next;
} e[maxm];
data *head[maxn<<1];
data *nhead[maxn<<1];
int cur;
int level[maxn<<1];
int que[maxn<<1];
int he,ta;
bool vis[maxm];
int N,n,t=maxn,match;
void Add(int x,int y,int c)
{
cur++;
e[cur].obj=y;
e[cur].cap=c;
e[cur].rev=&e[cur+1];
e[cur].Next=head[x];
head[x]=e+cur;
cur++;
e[cur].obj=x;
e[cur].cap=0;
e[cur].rev=&e[cur-1];
e[cur].Next=head[y];
head[y]=e+cur;
}
int Bfs(int x)
{
for (int i=0; i<=x; i++)
{
nhead[i]=head[i];
level[i]=0;
}
for (int i=t; i<=maxn+x; i++)
{
nhead[i]=head[i];
level[i]=0;
}
level[0]=1;
que[1]=0;
he=0,ta=1;
while (he<ta)
{
int node=que[++he];
for (data *p=head[node]; p; p=p->Next)
if ( p->cap && !level[p->obj] )
{
level[p->obj]=level[node]+1;
que[++ta]=p->obj;
}
}
return level[t];
}
int Dfs(int node,int mf)
{
if ( node==t || !mf ) return mf;
int nf=0;
for (data *&p=nhead[node]; p; p=p->Next)
if ( p->cap && level[p->obj]==level[node]+1 )
{
int d=Dfs(p->obj, min(p->cap,mf) );
p->cap-=d;
p->rev->cap+=d;
mf-=d;
nf+=d;
if (!mf) break;
}
if (mf) level[node]=-1;
return nf;
}
int Dinic(int x)
{
int max_flow=0;
while ( Bfs(x) )
max_flow+=Dfs(0,oo);
return max_flow;
}
int Work(int x)
{
cur=-1;
for (int i=0; i<=x; i++) head[i]=NULL;
for (int i=t; i<=t+x; i++) head[i]=NULL;
for (int i=1; i<=x; i++)
{
Add(0,i,1);
Add(i+maxn,t,1);
}
for (int i=2; i<=x; i++)
for (int j=1; j<i; j++)
if (vis[i+j])
Add(i,maxn+j,1);
int temp=Dinic(x);
return x-temp;
}
void Print(int i)
{
vis[i]=true;
for (data *p=head[i]; p; p=p->Next)
if ( p->obj && p->rev->cap )
Print(p->obj-maxn);
printf("%d ",i);
}
int Binary()
{
int L=0,R=10001;
while (L+1<R)
{
int mid=(L+R)>>1;
if ( Work(mid)<=n ) L=mid;
else R=mid;
}
return L;
}
int main()
{
freopen("c.in","r",stdin);
freopen("c.out","w",stdout);
for (int i=1; i*i<maxm; i++) vis[i*i]=true;
scanf("%d",&n);
N=Binary();
printf("%d\n",N);
Work(N);
for (int i=0; i<=N; i++) vis[i]=false;
for (int i=N; i>=1; i--)
if (!vis[i])
{
Print(i);
printf("\n");
}
return 0;
}
POJ3680 Intervals
题目传送门:http://poj.org/problem?id=3680
题目分析:记得之前APIO的时候,某THU神犇给选手讲关于如何用网络流解决区间选择问题,然后就举了一道类似的例题(或许就是本题)。
这里要求每个数不能被包含于k个区间,那么我们可以将每个数都看成一条边,容量为无穷大,费用为0。出现了一个[a,b]的区间,就将a前面的点向b后面的点连一条流量为1,费用为区间权值的边。跑一次从最左边到最右边,流量为k的最大费用流即可(将边权取反,就是最小费用流):
其中每一条1的流量就是选择了一些不相交的区间。
CODE:
#include<iostream>
#include<string>
#include<cstring>
#include<cmath>
#include<cstdio>
#include<cstdlib>
#include<stdio.h>
#include<algorithm>
using namespace std;
const int maxn=410;
const int maxm=10010;
const int oo=1000000001;
struct edge
{
int obj,cap,cost;
edge *rev,*Next;
} e[maxm];
edge *head[maxn];
edge *nhead[maxn];
int cur;
int dis[maxn];
bool vis[maxn];
int que[maxn];
int he,ta;
int a[maxn];
int b[maxn];
int w[maxn];
struct data
{
int P,id,val;
} bot[maxn];
int T,n,k,t;
int ans;
bool Comp(data x,data y)
{
return x.val<y.val;
}
void Add(int x,int y,int f,int c)
{
cur++;
e[cur].obj=y;
e[cur].cap=f;
e[cur].cost=c;
e[cur].rev=&e[cur+1];
e[cur].Next=head[x];
head[x]=e+cur;
cur++;
e[cur].obj=x;
e[cur].cap=0;
e[cur].cost=-c;
e[cur].rev=&e[cur-1];
e[cur].Next=head[y];
head[y]=e+cur;
}
bool Release(int x,int y,int c)
{
if (dis[x]+c>=dis[y]) return false;
dis[y]=dis[x]+c;
return true;
}
bool SPFA()
{
for (int i=0; i<=t; i++)
{
nhead[i]=head[i];
dis[i]=oo;
vis[i]=false;
}
dis[0]=0;
vis[0]=true;
he=0,ta=1;
que[1]=0;
while (he!=ta)
{
he=(he+1)%maxn;
int node=que[he];
for (edge *p=head[node]; p; p=p->Next)
if ( p->cap && Release(node,p->obj,p->cost) && !vis[p->obj] )
{
ta=(ta+1)%maxn;
que[ta]=p->obj;
vis[p->obj]=true;
}
vis[node]=false;
int nhe=(he+1)%maxn;
if ( dis[ que[nhe] ]>dis[ que[ta] ] ) swap(que[nhe],que[ta]);
}
return dis[t]<0;
}
int Dfs(int node,int mf)
{
if ( node==t || !mf ) return mf;
vis[node]=true;
int nf=0;
for (edge *&p=nhead[node]; p; p=p->Next)
if ( p->cap && dis[p->obj]==dis[node]+p->cost && !vis[p->obj] )
{
int d=Dfs(p->obj, min(p->cap,mf) );
p->cap-=d;
p->rev->cap+=d;
mf-=d;
nf+=d;
if (!mf) break;
}
if (mf) dis[node]=oo;
vis[node]=false;
return nf;
}
int Min_cost_flow()
{
int mcf=0;
while ( SPFA() )
mcf+=(dis[t]*Dfs(0,oo));
return mcf;
}
int main()
{
freopen("c.in","r",stdin);
freopen("c.out","w",stdout);
scanf("%d",&T);
while (T--)
{
scanf("%d%d",&n,&k);
for (int i=1; i<=n; i++)
{
int x=(i<<1)-1;
bot[x].P=0;
bot[x].id=i;
scanf("%d",&bot[x].val);
x++;
bot[x].P=1;
bot[x].id=i;
scanf("%d%d",&bot[x].val,&w[i]);
}
sort(bot+1,bot+(n<<1)+1,Comp);
int temp=0;
for (int i=1; i<=(n<<1); i++)
{
if (bot[i-1].val<bot[i].val) temp++;
if (bot[i].P) b[ bot[i].id ]=temp;
else a[ bot[i].id ]=temp;
}
t=temp+1,cur=-1;
for (int i=0; i<=t; i++) head[i]=NULL;
for (int i=1; i<t; i++) Add(i,i+1,oo,0);
Add(0,1,k,0);
for (int i=1; i<=n; i++) Add(a[i],b[i],1,-w[i]);
ans=Min_cost_flow();
ans=-ans;
printf("%d\n",ans);
}
return 0;
}