2017.8.13做的两道网络流题,难度5.5左右。
1. 1363餐巾计划问题(YZOJ)
2. 1357魔术球问题(YZOJ)
首先这道题用费用流做,是因为问题需要解决“第 i 天需要 ri 的餐巾”,且 “要使总费用最小”。
接下来考虑建模。
这个问题的难点在于,第 i 天所用的脏餐巾可供 i+m 和 i+n 天以及之后再度使用。于是设流量在通过第 i 天所代表的点之后直接与其他点连边代表再度使用。
但是第 i 天只有用脏的餐巾在可以继续使用,所以此时流量需要一个限制。那么我们可以通过拆点使 i 点本身具有容量。经过 i 点限制的流量流向 i+m 和 i+n 天之后。
图示大概这样:
其中S向A连边代表直接购买,Ai向Bi连边代表第 i 天的容量,Bi 向 B 之后连边代表提供洗好的餐巾,边的容量和费用根据意义不说了。目前为止看起来都很正常。
建模完用笔模拟一下发现不对。购买的流量会受到最后一天容量的限制。那不是可以把每一天的B向汇点连边吗?如果这样的话就无法表示继续使用了,所谓鱼和熊掌不可兼。
换个角度看,如果需要流量来同时表示对购买的餐巾计数和提供给之后的,不如设计两份流量? 第 i 天需要的流量是确定的,那提供的脏餐巾状态本来也是确定的。
那就有了如下建图:
A表示提供的脏餐巾,B表示当天需要的餐巾。
1. S往Ai连(容量为 ri,费用为 0 )的边,流量表示一天固定剩下的脏餐巾。
2. Ai 可转移到 Bi+m 之后和 Bi+n 之后,如果与之后的点都连边(inf,f或s)就是 O(n2) 的空间复杂度。通过Ai 与Ai+1连边( inf,0),同时Ai 与 Bi+m 和 Bi+n连两条边( inf,f或s),使空间复杂度降为 O(n) 。
3. S与 Bi 连边(inf,p),表示直接购买。
4. Bi 与 T 连边(ri,0),流量表示每天用过的餐巾数之和。
#include
#include
#include
#define R register
#define inf 1<<30
struct Edge{int v,c,f,nex;}edge[10000];
int et,full,S,T;
int st[1610],vis[1610],dis[1610],prv[1610],pre[1610];
int q[1000000],l,r;
void read(int &aa)
{
R char ch;while(ch=getchar(),ch>'9'||ch<'0');aa=ch-'0';
while(ch=getchar(),ch>='0'&&ch<='9')aa=aa*10+ch-'0';
}
void add(int f,int c,int a,int b)
{
edge[et].v=b,edge[et].c=c,edge[et].f=f,edge[et].nex=st[a],st[a]=et++;
edge[et].v=a,edge[et].c=-c,edge[et].f=0,edge[et].nex=st[b],st[b]=et++;
}
bool spfa()
{
for(R int i=1;i<=full;i++)dis[i]=inf,vis[i]=0,prv[i]=-1;
dis[S]=0,vis[S]=1,q[0]=S,l=0,r=1;
R int u,v,e,tmp;
while(r>l)
{
u=q[l++],vis[u]=0;
for(e=st[u];e!=-1;e=edge[e].nex)if(edge[e].f>0)
{
v=edge[e].v,tmp=dis[u]+edge[e].c;
if(tmpif(!vis[v])q[r++]=v,vis[v]=1;
}
}
}
return dis[T]!=inf;
}
void spfa_flow()
{
R int cost=0,tmp,now,e;
while(spfa())
{
tmp=inf;
for(now=T;now!=S&&((e=pre[now])|1);now=prv[now])
if(edge[e].ffor(now=T;now!=S&&((e=pre[now])|1);now=prv[now])
edge[e].f-=tmp,edge[e^1].f+=tmp;
cost+=tmp*dis[T];
}
printf("%d",cost);
}
int main()
{
R int N1,P1,M1,F1,S1,i,j,r;
read(full),read(P1),read(M1),read(F1),read(N1),read(S1);
S=full*2+2,T=full*2+1,et=0;
memset(st,-1,sizeof(st));
for(i=1;i<=full;i++)
{
read(r);
add(r,0,S,i);
add(inf,P1,S,i+full);
add(r,0,i+full,T);
if(i0,i,i+1);
if(i+M1<=full)add(inf,F1,i,i+full+M1);
if(i+N1<=full)add(inf,S1,i,i+full+N1);
}
full=full*2+2;
spfa_flow();
return 0;
}
这道题用暴力的话就是枚举方案数,时间复杂度是指数级的。
考虑如果确定了一个位置放的数,那么它上面可以放什么数?
可以想见,数的数量不确定,有太多方案。这种情况下,通过确定一个上界,使问题转化为判定性问题(判定性算法:“生成问题的一个解通常比验证一个给定的解时间花费要多得多。”—-百度百科)
对于一个数A,向它上面可能放的数B连一条有向边。由于B>A,这张图构成DAG。
我们发现,对于一个图求最小路径覆盖,若覆盖数<=柱子数,则该方案可行。
最小路径覆盖可以通过拆点成为二分图,通过跑最大匹配可以求出(最小路径覆盖=|V|-最大匹配)。
理论上,验证一个一个问题 期望时间 最少的是二分。但这道题跑二分的话每次都要重新建图;而从1开始枚举却满足可持久化,每次只要加入一个点,跑起来实际更快。
后面输出答案常数有点大,不够优秀。
#include
#include
#include
#include
#define dmin(_a,_b)(_a)<(_b)?(_a):(_b)
#define R register
#define inf 1<<30
using namespace std;
struct Edge{int to,nex,f,fo;}edge[200000];
int M,N,S,T,et,st[5000],aa[5000],t1,vis[5000],to1[5000];
int q[10000],l,r,level[10000];
void add(int a,int b,int c)
{
edge[et]=(Edge){b,st[a],0,c},st[a]=et++;
edge[et]=(Edge){a,st[b],0,0},st[b]=et++;
}
bool bfs()
{
memset(level,0,sizeof(level));
l=0,r=1,q[0]=S,level[S]=1;
R int u,v,e;
while(lq[l++];
for(e=st[u];e!=-1;e=edge[e].nex)
if(!level[edge[e].to]&&edge[e].f>0)
{
v=edge[e].to;
level[v]=level[u]+1;
q[r++]=v;
if(v==T)return true;
}
}
return false;
}
int dfs(int u,const int flow)
{
if(u==T)return flow;
R int v,e,f,ret=0,tmp;
for(e=st[u];e!=-1;e=edge[e].nex)
if(level[edge[e].to]==level[u]+1&&edge[e].f>0)
{
v=edge[e].to;
tmp=dmin(flow-ret,edge[e].f);
f=dfs(v,tmp);
edge[e].f-=f,edge[e^1].f+=f,ret+=f;
if(ret==flow)return ret;
}
return ret;
}
bool dinic(R int tmp)
{
for(R int i=0;iwhile(bfs())tmp-=dfs(S,inf);
return tmp<=N;
}
void rec(R int u,R int pre)
{
if(u==T){aa[t1++]=-1;return;}
aa[t1++]=u>>1;
R int v,e;
for(e=st[u];e!=-1;e=edge[e].nex)
if(edge[e].fo==1&&edge[e].f==0&&edge[e].to!=pre)
{
v=edge[e].to;
rec(v,u);
}
}
int main()
{
R int i,j,k,a,b;
scanf("%d",&N);
memset(st,-1,sizeof(st));
S=0,T=1,et=0;
for(i=1;1;++i)
{
add(S,i<<1,1);add(i<<1|1,T,1);
b=(int)sqrt(i+i-1),a=(int)sqrt(i)+1;
for(j=a;j<=b;j++)
if(j*j-i*j-i)<<1,i<<1|1,1);
if(!dinic(i))break;
t1=1,rec(S,S);
}
M=i-1;printf("%d\n",M);
j=aa[2];
for(i=3;iif(aa[i]==-1){j=aa[i+1];continue;}
to1[j]=aa[i],j=aa[i];
}
for(i=1;i<=M;++i)if(vis[i]==0)
{
j=i;
while(j!=0)printf("%d ",j),vis[j]=1,j=to1[j];
printf("\n");
}
return 0;
}