网络流学习笔记II

· · 个人记录

这里是网络流学习笔记II。上一篇笔记写够一百题了,故开个新坑。

本文中各种约定同上一篇笔记中一致。

现在开始!

CI.[国家集训队]部落战争

第一题,挑道比较板子的题罢。

首先很明显可以抽象出一张从一个位置走到另一个的有向无环图出来(因为只能从上往下打)

然后就是最小路径覆盖问题的板子。很遗憾的是,我忘记了最小路径覆盖问题的解法,于是使用了XLIII.[SDOI2010]星际竞速中的拆点+最小费用最大流的做法,一样能过,就是被卡掉一个点,不得不吸氧。

代码:

#include<bits/stdc++.h>
using namespace std;
int n,m,a,b;
char s[60][60];
namespace MCMF{
    const int N=5010,M=200000;
    int head[N],cnt,dis[N],fr[N],id[N],S,T,cost;
    struct node{
        int to,next,val,cost;
    }edge[M];
    void ae(int u,int v,int w,int c){
    //  printf("%d %d %d %d\n",u,v,w,c);
        edge[cnt].cost=c,edge[cnt].next=head[u],edge[cnt].to=v,edge[cnt].val=w,head[u]=cnt++;
        edge[cnt].cost=-c,edge[cnt].next=head[v],edge[cnt].to=u,edge[cnt].val=0,head[v]=cnt++;
    }
    queue<int>q;
    bool in[N];
    bool SPFA(){
        memset(dis,0x3f,sizeof(dis)),dis[S]=0,q.push(S),in[S]=true;
        while(!q.empty()){
            int x=q.front();q.pop(),in[x]=false;
    //      printf("%d\n",x);
            for(int i=head[x];i!=-1;i=edge[i].next){
                if(!edge[i].val)continue;
                if(dis[edge[i].to]>dis[x]+edge[i].cost){
                    dis[edge[i].to]=dis[x]+edge[i].cost,fr[edge[i].to]=x,id[edge[i].to]=i;
                    if(!in[edge[i].to])in[edge[i].to]=true,q.push(edge[i].to);
                }
            }
        }
        if(dis[T]==dis[T+1])return false;
        int x=T,mn=0x3f3f3f3f;
        while(x!=S)mn=min(mn,edge[id[x]].val),x=fr[x];
        cost+=dis[T]*mn,x=T;
        while(x!=S)edge[id[x]].val-=mn,edge[id[x]^1].val+=mn,x=fr[x];
        return true;
    }
}
using namespace MCMF;
int main(){
    scanf("%d%d%d%d",&n,&m,&a,&b),S=2*n*m,T=S+1,memset(head,-1,sizeof(head));
    int dx[4]={a,b,b,a},dy[4]={-b,-a,a,b};
    for(int i=0;i<n;i++)scanf("%s",s[i]);
    for(int i=0;i<n;i++)for(int j=0;j<m;j++){
        if(s[i][j]=='x')continue;
        ae(S,i*m+j,1,0);
        ae(i*m+j,T,1,1);
        ae(n*m+i*m+j,T,1,0);
        for(int k=0;k<4;k++){
            int ii=i+dx[k],jj=j+dy[k];
            if(ii<0||ii>=n||jj<0||jj>=m||s[ii][jj]=='x')continue;
    //      printf("(%d,%d):(%d,%d)\n",i,j,ii,jj);
            ae(i*m+j,n*m+ii*m+jj,1,0);
        }
    }
    while(SPFA());
//  for(int i=0;i<cnt;i++)if(edge[i^1].val&&edge[i^1].to<n*m)printf("%d->%d\n",edge[i^1].to,edge[i].to);
    printf("%d\n",cost);
    return 0;
} 

CII.[NOI2015]小园丁与老司机

首先,老司机部分考虑DP:明显 y 值不同的状态间转移是有阶段性的,但是 y 值相同时则不然;于是为了凸显阶段性,我们设 f_x 表示从位置 x 进入某个 y 值的最大收益,g_x 表示从位置 x 离开某个 y 值的最大收益。

记录路径,我们就完成了老司机部分。 然后是小园丁部分。为了找到所有最优路径的并,我们需要建出图来,每个 $f_x,g_x$ 各独立作一个节点(因此,总节点数应为 $2n+2$ 个),在可能出现的位置连边,并且从每个最优的终局节点反向推出所有合法的边。这样,我们便可以建出一张所有需要经过的边所构成的DAG。 此DAG上,每条边被经过的下限是 $1$,上限是 $\infty$(实际应用中取 $3n$ 即可,因为最多只会有不超过 $3n$ 条边)。于是我们建图跑上下界最小流即可。 代码: ```cpp #include<bits/stdc++.h> using namespace std; const int N=50100; namespace MaxFlow{ const int M=2000000; int head[N],cur[N],dep[N],cnt,S,T,s,t,ans,deg[N]; struct node{ int to,next,val; }edge[M]; void ae(int u,int v,int w){ edge[cnt].next=head[u],edge[cnt].to=v,edge[cnt].val=w,head[u]=cnt++; edge[cnt].next=head[v],edge[cnt].to=u,edge[cnt].val=0,head[v]=cnt++; } void AE(int u,int v,int l,int r){ // printf("%d %d [%d,%d]\n",u,v,l,r); deg[v]+=l,deg[u]-=l; edge[cnt].next=head[u],edge[cnt].to=v,edge[cnt].val=r-l,head[u]=cnt++; edge[cnt].next=head[v],edge[cnt].to=u,edge[cnt].val=0,head[v]=cnt++; } queue<int>q; inline bool bfs(){ memset(dep,0,sizeof(dep)),q.push(S),dep[S]=1; while(!q.empty()){ register int x=q.front();q.pop(); for(register int i=cur[x]=head[x];i!=-1;i=edge[i].next)if(edge[i].val&&!dep[edge[i].to])dep[edge[i].to]=dep[x]+1,q.push(edge[i].to); } return dep[T]>0; } bool reach; inline int dfs(int x,int flow){ if(x==T){ ans+=flow; reach=true; return flow; } int used=0; for(register int &i=cur[x];i!=-1;i=edge[i].next){ if(!edge[i].val||dep[edge[i].to]!=dep[x]+1)continue; register int ff=dfs(edge[i].to,min(edge[i].val,flow-used)); if(ff){ edge[i].val-=ff; edge[i^1].val+=ff; used+=ff; if(used==flow)break; } } return used; } inline void Dinic(){ while(bfs()){ reach=true; while(reach)reach=false,dfs(S,0x3f3f3f3f); } } } using namespace MaxFlow; int n,x[N],y[N],nw[N],ne[N],f[N],g[N],F[N],G[N],res;//f:maximal when arriving at i g:maximal when leaving from i vector<int>v[N],u[N],a; void discrete(int *arr){ a.clear(); for(int i=0;i<=n;i++)a.push_back(arr[i]); sort(a.begin(),a.end()),a.resize(unique(a.begin(),a.end())-a.begin()); for(int i=0;i<=n;i++)u[arr[i]=lower_bound(a.begin(),a.end(),arr[i])-a.begin()].push_back(i); } void buildgraph(){ for(int i=0;i<a.size();i++){ sort(u[i].begin(),u[i].end(),[](int X,int Y){return y[X]<y[Y];}); for(int j=1;j<u[i].size();j++)v[u[i][j-1]].push_back(u[i][j]); u[i].clear(); } } bool cmp(int X,int Y){return x[X]<x[Y];} stack<int>st; bool lim[N<<1],vis[N<<1];//if position i is able to reach maximum vector<int>w[N<<1]; bool dfscheck(int x){ if(vis[x])return lim[x];vis[x]=true; for(auto y:w[x])if(dfscheck(y)){ lim[x]=true; if(x>n)AE(x-n-1,y,1,3*n); } return lim[x]; } int main(){ scanf("%d",&n),memset(f,-1,sizeof(f)),memset(g,-1,sizeof(g)),memset(head,-1,sizeof(head)); for(int i=1;i<=n;i++)scanf("%d%d",&x[i],&y[i]),nw[i]=x[i]+y[i],ne[i]=x[i]-y[i]; discrete(nw),buildgraph(); discrete(ne),buildgraph(); discrete(x),buildgraph(); // for(int i=0;i<=n;i++){for(auto j:v[i])printf("%d ",j);puts("");} discrete(y),f[0]=0; for(int i=0;i<a.size();i++){ sort(u[i].begin(),u[i].end(),cmp); for(int j=0;j<u[i].size();j++)if(f[u[i][j]]!=-1)for(int k=0;k<u[i].size();k++){ int now=f[u[i][j]]; if(j<k)now+=k; if(j>k)now+=u[i].size()-k-1; if(g[u[i][k]]<=now)g[u[i][k]]=now,G[u[i][k]]=u[i][j]; } for(auto j:u[i])if(g[j]!=-1)for(auto k:v[j])if(f[k]<=g[j]+1)f[k]=g[j]+1,F[k]=j; } for(int i=0;i<=n;i++)res=max(res,g[i]); printf("%d\n",res); for(int i=0;i<=n;i++){ if(g[i]!=res)continue; while(i){ st.push(i); int P=y[i]; int I=lower_bound(u[P].begin(),u[P].end(),i,cmp)-u[P].begin(),J=lower_bound(u[P].begin(),u[P].end(),G[i],cmp)-u[P].begin(); if(J<I){ for(int k=I-1;k>J;k--)st.push(u[P][k]); for(int k=0;k<=J;k++)st.push(u[P][k]); } if(J>I){ for(int k=I+1;k<J;k++)st.push(u[P][k]); for(int k=u[P].size()-1;k>=J;k--)st.push(u[P][k]); } i=F[G[i]]; } break; } while(!st.empty())printf("%d ",st.top()),st.pop();puts(""); s=n+1,t=n+2,S=n+3,T=n+4; for(int i=0;i<a.size();i++){ for(int j=0;j<u[i].size();j++)if(f[u[i][j]]!=-1)for(int k=0;k<u[i].size();k++){ int now=f[u[i][j]]; if(j<k)now+=k; if(j>k)now+=u[i].size()-k-1; if(g[u[i][k]]==now)w[u[i][j]].push_back(u[i][k]+n+1); } for(auto j:u[i])if(g[j]!=-1)for(auto k:v[j])if(f[k]==g[j]+1)w[j+n+1].push_back(k); } for(int i=0;i<=n;i++)if(g[i]==res)lim[i+n+1]=true; dfscheck(0); for(int i=0;i<=n;i++)ae(s,i,3*n),ae(i,t,3*n); for(int i=0;i<=n;i++)if(deg[i]>0)ae(S,i,deg[i]);else ae(i,T,-deg[i]); Dinic(); ae(t,s,3*n); Dinic(); printf("%d\n",edge[cnt-1].val); return 0; } ``` # CIII.[[SCOI2014]方伯伯运椰子](https://www.luogu.com.cn/problem/P3288) 首先,对于题目中的一条边 $(u,v,a,b,c,d)$,若其容量增大 $1$,则相当于有 $b+d$ 的费用;减小 $1$,则相当于有 $a-d$ 的费用。因为一开始所有点都是收支平衡的(除了源点和汇点;但是源点因为保证无论如何流出的流量都不会变,汇点同理,所以也可以把它们看作收支平衡),故经历修改后它们仍然收支平衡。于是,上述增大或减小的容量,就相当于增大或减小的流量,仍然要使所有点平衡。 于是我们连边 $(u,v,\infty,b+d)$ 代表增大的容量,$(v,u,c,a-d)$ 代表减小的容量,则所有东西叠加起来的效果,应该仍相当于一个无源汇可行流。 现在我们看向我们的答案,会发现它应是一个**0/1分数规划**的形式;故我们二分答案为 $T$,然后check时将所有边权都加上 $T$。假如跑出来最小费用可行流的结果为负,则二分结果合法,否则不合法。 但是,最小费用可行流的求法是比较非主流的——因为有负环。所谓的**消圈算法**非常罕见,更好的方式是对于负边,强制将其满流并让其反向(使用上下界网络流的科技)。 代码: ```cpp #include<bits/stdc++.h> using namespace std; const double eps=1e-3; const double inf=0x3f3f3f3f; namespace MCMF{ const int N=5100,M=2000000; int head[N],cnt,fr[N],id[N],S,T,deg[N],flow; double dis[N],cost; struct node{ int to,next,val; double cost; }edge[M]; void ae(int u,int v,int w){ // printf("ae:%d %d %d\n",u,v,w); edge[cnt].cost=0,edge[cnt].next=head[u],edge[cnt].to=v,edge[cnt].val=w,head[u]=cnt++; edge[cnt].cost=0,edge[cnt].next=head[v],edge[cnt].to=u,edge[cnt].val=0,head[v]=cnt++; } void AE(int u,int v,int w,double c){ // printf("AE:%d %d %d %lf\n",u,v,w,c); if(c<0)cost+=c*w,deg[v]+=w,deg[u]-=w,swap(u,v),c=-c; edge[cnt].cost=c,edge[cnt].next=head[u],edge[cnt].to=v,edge[cnt].val=w,head[u]=cnt++; edge[cnt].cost=-c,edge[cnt].next=head[v],edge[cnt].to=u,edge[cnt].val=0,head[v]=cnt++; } queue<int>q; bool in[N]; bool SPFA(){ for(int i=1;i<=T;i++)dis[i]=inf;dis[S]=0,q.push(S),in[S]=true; while(!q.empty()){ int x=q.front();q.pop(),in[x]=false; // printf("%d\n",x); for(int i=head[x];i!=-1;i=edge[i].next){ // printf("(%d,%d,%d,%lf)\n",x,edge[i].to,edge[i].val,edge[i].cost); if(!edge[i].val)continue; if(dis[edge[i].to]>dis[x]+edge[i].cost){ dis[edge[i].to]=dis[x]+edge[i].cost,fr[edge[i].to]=x,id[edge[i].to]=i; if(!in[edge[i].to])in[edge[i].to]=true,q.push(edge[i].to); } } } if(dis[T]==inf)return false; // printf("DEST:%d\n",dis[T]); int x=T,mn=0x3f3f3f3f; while(x!=S)mn=min(mn,edge[id[x]].val),x=fr[x]; cost+=dis[T]*mn,flow+=mn,x=T; while(x!=S)edge[id[x]].val-=mn,edge[id[x]^1].val+=mn,x=fr[x]; return true; } } using namespace MCMF; int n,m,lim; struct EDGE{ int u,v,a,b,c,d; void read(){scanf("%d%d%d%d%d%d",&u,&v,&a,&b,&c,&d);} }e[3010]; bool che(double now){ memset(head,-1,sizeof(head)),cnt=flow=0,cost=0; // printf("%lf:\n",now); for(int i=1;i<=m;i++){ AE(e[i].u,e[i].v,lim,e[i].b+e[i].d+now); AE(e[i].v,e[i].u,e[i].c,e[i].a-e[i].d+now); } int sum=0; for(int i=1;i<=n;i++){ // printf("%d\n",deg[i]); if(deg[i]>0)ae(S,i,deg[i]),sum+=deg[i]; if(deg[i]<0)ae(i,T,-deg[i]); deg[i]=0; } while(SPFA()); return sum==flow&&cost<0; } int main(){ scanf("%d%d",&n,&m),n++,S=n+1,T=n+2; for(int i=1;i<=m;i++){ e[i].read(); if(e[i].u==n||e[i].v==n){ if(e[i].u==n)lim=e[i].c; i--,m--;continue; } if(e[i].u==n+1)e[i].u=n; if(e[i].v==n+1)e[i].v=n; } double l=0,r=2000; while(r-l>eps){ double mid=(l+r)/2; if(che(mid))l=mid; else r=mid; } printf("%.2lf\n",l); return 0; } ``` # CIV.[[POI2005]KOS-Dicing](https://www.luogu.com.cn/problem/P3425) “最大值最小”条件反射二分,于是就从源点连向每个人(二分的值)点流量,然后对于每场比赛,从两个人那里各连来一点流量,再连到汇点一点流量,然后跑最大流check是否满流即可。 ~~犯了很多低级错误啊~~ 代码: ```cpp #include<bits/stdc++.h> using namespace std; namespace MaxFlow{ const int N=21000,M=2000000; int head[N],cur[N],dep[N],cnt,S,T,res; struct node{ int to,next,val; }edge[M]; void ae(int u,int v,int w){ edge[cnt].next=head[u],edge[cnt].to=v,edge[cnt].val=w,head[u]=cnt++; edge[cnt].next=head[v],edge[cnt].to=u,edge[cnt].val=0,head[v]=cnt++; } queue<int>q; inline bool bfs(){ memset(dep,0,sizeof(dep)),q.push(S),dep[S]=1; while(!q.empty()){ register int x=q.front();q.pop(); for(register int i=cur[x]=head[x];i!=-1;i=edge[i].next)if(edge[i].val&&!dep[edge[i].to])dep[edge[i].to]=dep[x]+1,q.push(edge[i].to); } return dep[T]>0; } bool reach; inline int dfs(int x,int flow){ if(x==T){ res+=flow; reach=true; return flow; } int used=0; for(register int &i=cur[x];i!=-1;i=edge[i].next){ if(!edge[i].val||dep[edge[i].to]!=dep[x]+1)continue; register int ff=dfs(edge[i].to,min(edge[i].val,flow-used)); if(ff){ edge[i].val-=ff; edge[i^1].val+=ff; used+=ff; if(used==flow)break; } } return used; } inline void Dinic(){ while(bfs()){ reach=true; while(reach)reach=false,dfs(S,0x3f3f3f3f); } } } using namespace MaxFlow; int n,m,u[10100],v[10100]; bool che(int ip){ memset(head,-1,sizeof(head)),cnt=res=0; for(int i=1;i<=n;i++)ae(S,i,ip); for(int i=1;i<=m;i++)ae(u[i],i+n,1),ae(v[i],i+n,1),ae(i+n,T,1); Dinic(); return res==m; } int main(){ scanf("%d%d",&n,&m),S=n+m+1,T=n+m+2; for(int i=1;i<=m;i++)scanf("%d%d",&u[i],&v[i]); int l=0,r=m; while(l<r){ int mid=(l+r)>>1; if(che(mid))r=mid; else l=mid+1; } printf("%d\n",r); che(r); for(int i=1;i<=m;i++)for(int j=head[n+i];j!=-1;j=edge[j].next)if(edge[j].to<=n&&edge[j].val)puts(edge[j].to==u[i]?"1":"0"); return 0; } ``` # CV.[[CQOI2017]老C的方块](https://www.luogu.com.cn/problem/P3756) [题解](https://www.luogu.com.cn/blog/Troverld/solution-p3756) # CVI.[[八省联考2018]劈配](https://www.luogu.com.cn/problem/P4382) [题解](https://www.luogu.com.cn/blog/Troverld/solution-p4382) # CVII.[[CTSC2008]祭祀](https://www.luogu.com.cn/problem/P4298) 定义“**偏序集**”为一个集合 $\mathbb{S}$ 以及一种运算 $\leq$ 构成的二元组 $(\mathbb{S},\leq)$,满足如下性质: - $\forall x\in\mathbb{S},x\leq x$ 均成立 - $\forall x,y,z\in\mathbb{S}\text{ s.t. }x\leq y,y\leq z$,则 $x\leq z$ 成立 - $\forall x,y\in\mathbb{S}\text{ s.t. }x\leq y,y\leq x$,则 $x=y$ 成立 典型的例子是DAG上是否可达的关系,例如本题,当对所有的边跑完传递闭包后,得到的转移矩阵 $trans$ 即是一组偏序关系。另一个例子是集合间的**包含**关系($\subseteq$)。 定义一组偏序集上的**链**是一组**有序序列** $\{a\}$ 满足 $\forall i<\Big|\{a\}\Big|$,均有 $a_i\leq a_{i+1}$。 定义一组偏序集上的**反链**是一组**无序序列** $\{a\}$ 满足 $\forall i\neq j\in\{a\}$,均有 $i\nleq j\text{ and } j\nleq i$。换句话说,两两集合不相互包含。 Dilworth定理:对于任何一组偏序集,其**最长链**长度等于其**最小反链划分**大小,其**最长反链长度**等于其**最小链划分**大小,其中一组**划分**指的是将一个集合划分成若干集合,使得两两集合的交集为空,所有集合的并集为全集。 最长链长度一定等于最小反链划分大小,因为我们每次均可挑出所有极小元素组成一条反链;第一次挑选一定挑出了最长链上最小元素(因为其不可能有后继元素,不然最长链还能更长),第二次挑选一定挑出了最长链上次小元素(因为其所有的后继元素一定都是极小元素,不然最长链还能更长),以此类推,最后一次挑选一定挑出最长链上最大元素。 关于最长反链长度的证明类似。 回到本题。明显,本题我们需要找出传递闭包后的偏序集的最长反链长度,并输出方案。最长反链长度等于最小链划分;最小链划分,其与**最小路径覆盖**等价,因为DAG的偏序集仍是DAG,所以可以通过经典的拆点后建二分图,并求出最大独立集的套路来求出最小链划分大小。 但是,求出最小链划分还不够,我们还要构造出一条最长反链。但首先先让我们找出求最大独立集的方法。 最大独立集等于点数减最小点覆盖,因为对于最小点覆盖外的任意一对点对间都不可能有边(不然就违背了点覆盖的定义)。而最长反链,就由拆点后两端均在独立集内的点构成。具体而言,设独立集大小为 $|I|$,反链大小为 $|A|$,最小点覆盖大小为 $|S|$,则应有 $|I|=2n-|S|$。考虑 $|I|-|A|$,应为只有一端在 $I$ 内的点数,必定是 $\leq n$ 的。故必有 $|I|-|A|\leq n$,即 $|A|\geq|I|-n=n-|S|$。而 $n-|S|$,就是最小路径覆盖问题的答案,也即最小链划分大小,即最长反链长度。则 $A$ 即为最长反链。 于是问题转变为求出最大独立集,也即最小点覆盖的补集。故我们只需求出最小点覆盖。 考虑求出最大匹配。考虑从右侧所有非匹配点出发遍历图,当从右到左时只走非匹配边,从左往右时只走匹配边,则最终一定得到众多两端均在右侧的增广路,因为右侧的非匹配点不可能连到左侧的非匹配点(不然可以直接连接两者,与最大匹配前提相悖),则从遍历到的左侧点一定可以沿某条匹配边回到右侧。我们在左侧选取所有被遍历到的点,以及右侧所有未被遍历到的点,它们构成一组最小点覆盖。 显然,最小点覆盖的大小等于最大匹配大小,因为对于一个左侧遍历到的点,与其匹配的右侧点也一定被遍历到了,故此处只有左侧点被选择;对于一个右侧未被遍历到的点,因为其必定是一个被匹配的点(所有未被匹配的点都被作为起点进行了遍历),而其对应的左侧点又没有被选择,故此处只有右端点被选择;二者综合来看,即是所有匹配边选且仅被选一次,故上述结论成立。 找到最小点覆盖后,取补集即得到了左侧未被遍历到的点和右侧遍历到的点,它们构成最大独立集。 将上述结论结合后,便得到了求最长反链的方法。 下面我们考虑什么点可能出现在最长反链内。因为我们上述解法,若二分图匹配采用 $m\sqrt{n}$ 的Dinic法的话,单次是 $O(n^{2.5})$ 的,所以对于每个点都跑一次匹配,复杂度也不过 $O(n^{3.5})$,对于 $n=100$ 是轻松通过的;于是我们考虑每个点,强制令其在反链内(这意味着所有它能到达或能到达它的点全部不可选择),并求出新图中的最长反链判断其是否比原图中长度只减小 $1$ 即可。 总复杂度 $O(n^{3.5})$。 代码: ```cpp #include<bits/stdc++.h> using namespace std; int n,m; namespace MaxFlow{ const int N=210,M=200000; int head[N],cur[N],dep[N],cnt,S,T,res; struct node{int to,next,val;}edge[M]; void ae(int u,int v,int w){ edge[cnt].next=head[u],edge[cnt].to=v,edge[cnt].val=w,head[u]=cnt++; edge[cnt].next=head[v],edge[cnt].to=u,edge[cnt].val=0,head[v]=cnt++; } queue<int>q; inline bool bfs(){ memset(dep,0,sizeof(dep)),q.push(S),dep[S]=1; while(!q.empty()){ register int x=q.front();q.pop(); for(register int i=cur[x]=head[x];i!=-1;i=edge[i].next)if(edge[i].val&&!dep[edge[i].to])dep[edge[i].to]=dep[x]+1,q.push(edge[i].to); } return dep[T]>0; } bool reach; inline int dfs(int x,int flow){ if(x==T){res+=flow,reach=true;return flow;} int used=0; for(register int &i=cur[x];i!=-1;i=edge[i].next){ if(!edge[i].val||dep[edge[i].to]!=dep[x]+1)continue; register int ff=dfs(edge[i].to,min(edge[i].val,flow-used)); if(ff){edge[i].val-=ff,edge[i^1].val+=ff,used+=ff;if(used==flow)break;} } return used; } inline void Dinic(){while(bfs()){reach=true;while(reach)reach=false,dfs(S,0x3f3f3f3f);}} } using namespace MaxFlow; bool trans[110][110],on[210],vis[210],vld[110]; int mat[210]; char s[110],t[110]; void findpath(int x){ if(vis[x]==true)return;vis[x]=true; for(int y=1;y<=n;y++)if(y!=mat[x]&&vld[y]&&trans[y][x-n]&&!vis[y])vis[y]=true,findpath(mat[y]); } int MaximalAntiChain(int ban){ memset(head,-1,sizeof(head)),cnt=res=0; int all=0; for(int i=1;i<=n;i++)vld[i]=!trans[ban][i]&&!trans[i][ban]&&i!=ban,all+=vld[i]; for(int i=1;i<=n;i++)for(int j=1;j<=n;j++)if(vld[i]&&vld[j]&&trans[i][j])ae(i,j+n,1); for(int i=1;i<=n;i++)if(vld[i])ae(S,i,1),ae(i+n,T,1); Dinic(); return all-res; } void GetScheme(){ memset(mat,0,sizeof(mat)); for(int x=1;x<=n;x++)for(int i=head[x];i!=-1;i=edge[i].next)if(edge[i].to>n&&edge[i].to<=2*n&&!edge[i].val)mat[x]=edge[i].to,mat[edge[i].to]=x; // for(int i=1;i<=n;i++)printf("%d ",mat[i]);puts(""); // for(int i=1;i<=n;i++)printf("%d ",mat[i+n]);puts(""); memset(vis,false,sizeof(vis)); for(int i=1;i<=n;i++)if(vld[i]&&!mat[i+n])findpath(i+n); for(int i=1;i<=n;i++)if(vld[i]&&!vis[i]&&vis[i+n])s[i]='1';else s[i]='0'; } int main(){ scanf("%d%d",&n,&m),S=2*n+1,T=2*n+2; for(int i=1,u,v;i<=m;i++)scanf("%d%d",&u,&v),trans[u][v]=true; for(int k=1;k<=n;k++)for(int i=1;i<=n;i++)for(int j=1;j<=n;j++)trans[i][j]|=trans[i][k]&&trans[k][j]; // for(int i=1;i<=n;i++){for(int j=1;j<=n;j++)printf("%d ",trans[i][j]);puts("");}puts(""); int mx=MaximalAntiChain(0);printf("%d\n",mx); GetScheme(),printf("%s\n",s+1); for(int i=1;i<=n;i++)t[i]=(MaximalAntiChain(i)==mx-1?'1':'0'); printf("%s\n",t+1); return 0; } ``` # CVIII.[[CTSC2001]终极情报网](https://www.luogu.com.cn/problem/P5814) [题解](https://www.luogu.com.cn/blog/Troverld/solution-p5814) # CIX.[[SDOI2011]保密](https://www.luogu.com.cn/problem/P2494) 经 典 多 合 一 ”总时间和总安全系数的比值“,条件反射**0/1分数规划**。 然后,因为要求出所有点的值,条件反射用**整体二分**(虽然实际上这是我第一次写整体二分,但写起来还是挺简单的)。二分过程中要求出全体点的距离,因为保证DAG,直接拓扑一下即可。 于是我们求出了所有点的值。 于是问题转换为二分图最小权点覆盖问题。 二分图最小权点覆盖问题可以使用最小割解决。因为每条边的两个端点 $u,v$ 中至少有一个在选取的点集内,抽象到最小割上就是 $S\rightarrow u,u\rightarrow v,v\rightarrow T$ 三条边中至少断一条。我们又不想让 $u\rightarrow v$ 这条边断掉,所以将其赋为 $\infty$,另外两条边边权赋作该点点权即可。 时间复杂度 $O\Big((m+n)n'\log val+m'\sqrt{n'}\Big)$,其中 $val$ 是第一问中边权大小,加 $'$ 的来自于二分图,不加 $'$ 的来自于原图。 代码: ```cpp #include<bits/stdc++.h> using namespace std; const double eps=1e-5;//eps of binary search const double EPS=1e-8;//eps of comparasion between doubles double mn[210]; int nbi,mbi; namespace FS{ int n,m; double dis[710]; int arr[210],head[710],cnt,in[710]; struct node{int to,next;double val;}edge[200100]; struct EDGE{ int u,v,s,t; void read(){scanf("%d%d%d%d",&u,&v,&t,&s);} }e[100100]; void ae(int u,int v,double w){edge[cnt].next=head[u],edge[cnt].to=v,edge[cnt].val=w,head[u]=cnt++;} bool vis[710]; void dfs(int x){ if(vis[x])return;vis[x]=true; for(int i=head[x];i!=-1;i=edge[i].next)dfs(edge[i].to); } queue<int>q; void bfs(){ for(int i=1;i<=n;i++)dis[i]=0x3f3f3f3f;dis[n]=0,q.push(n); while(!q.empty()){ int x=q.front();q.pop(); for(int i=head[x];i!=-1;i=edge[i].next){ dis[edge[i].to]=min(dis[edge[i].to],dis[x]+edge[i].val); if(!--in[edge[i].to])q.push(edge[i].to); } } } void solve(int L,int R,double l,double r){ if(L>R)return; if(r-l<eps){for(int i=L;i<=R;i++)mn[arr[i]]=l;return;} double mid=(l+r)/2; memset(head,-1,sizeof(head)),cnt=0; for(int i=1;i<=m;i++)if(vis[e[i].u]&&vis[e[i].v])ae(e[i].u,e[i].v,e[i].t-mid*e[i].s),in[e[i].v]++; bfs(); // printf("%lf:\n",mid); // for(int i=1;i<=n;i++)printf("%lf ",dis[i]);puts(""); sort(arr+L,arr+R+1,[](int u,int v){return dis[u]<dis[v];}); // for(int i=L;i<=R;i++)printf("%d ",arr[i]);puts(""); int pos=L;while(pos<=R&&dis[arr[pos]]<-EPS)pos++; // printf("%d\n",pos); solve(L,pos-1,l,mid),solve(pos,R,mid,r); } void init(){ scanf("%d%d",&n,&m),memset(head,-1,sizeof(head)); for(int i=1;i<=m;i++)e[i].read(),ae(e[i].u,e[i].v,0); dfs(n); // for(int i=1;i<=m;i++)printf("%d %d (%d/%d)\n",e[i].u,e[i].v,e[i].t,e[i].s); scanf("%d%d",&mbi,&nbi); int tot=0; for(int i=1;i<=nbi;i++)if(vis[i])arr[++tot]=i;else mn[i]=-1; solve(1,tot,0,10); } } namespace DD{ const int N=210,M=400000; int head[N],cur[N],dep[N],cnt,S,T; double res; struct node{ int to,next; double val; }edge[M]; void ae(int u,int v,double w){ edge[cnt].next=head[u],edge[cnt].to=v,edge[cnt].val=w,head[u]=cnt++; edge[cnt].next=head[v],edge[cnt].to=u,edge[cnt].val=0,head[v]=cnt++; } queue<int>q; inline bool bfs(){ memset(dep,0,sizeof(dep)),q.push(S),dep[S]=1; while(!q.empty()){ register int x=q.front();q.pop(); for(register int i=cur[x]=head[x];i!=-1;i=edge[i].next)if(edge[i].val>EPS&&!dep[edge[i].to])dep[edge[i].to]=dep[x]+1,q.push(edge[i].to); } return dep[T]>0; } bool reach; inline double dfs(int x,double flow){ if(x==T){ res+=flow; reach=true; return flow; } double used=0; for(register int &i=cur[x];i!=-1;i=edge[i].next){ if(edge[i].val<EPS||dep[edge[i].to]!=dep[x]+1)continue; register double ff=dfs(edge[i].to,min(edge[i].val,flow-used)); if(ff){ edge[i].val-=ff; edge[i^1].val+=ff; used+=ff; if(used==flow)break; } } return used; } inline void Dinic(){ while(bfs()){ reach=true; while(reach)reach=false,dfs(S,1e9); } } } using namespace DD; int main(){ FS::init(),S=nbi+1,T=nbi+2,memset(head,-1,sizeof(head)); // for(int i=1;i<=nbi;i++)printf("%.1lf ",mn[i]);puts(""); for(int i=1,x,y;i<=mbi;i++){ scanf("%d%d",&x,&y); if(mn[x]<-EPS&&mn[y]<-EPS){puts("-1");return 0;} ae(x,y,0x3f3f3f3f); } for(int i=1;i<=nbi;i++){ if(mn[i]<-EPS)mn[i]=0x3f3f3f3f; if(i&1)ae(S,i,mn[i]); else ae(i,T,mn[i]); } Dinic(); printf("%.1lf\n",res); return 0; } ``` # CX.[[JLOI2014]镜面通道](https://www.luogu.com.cn/problem/P3260) 凭直觉可以发现,若两端间有一条可以通过的路径,则光一定可以射过去。 ~~证明?OI不需要证明~~ 于是我们把所有相交的图形间连边,就得到了一张无向图,而我们需要删去一些边保证无向图中上边和下边所对应的点不连通。 于是拆点跑最小割即可。 代码: ```cpp #include<bits/stdc++.h> using namespace std; typedef long long ll; #define y1 __17680321 namespace MaxFlow{ const int N=610,M=2000000; int head[N],cur[N],dep[N],cnt,S,T,res; struct node{int to,next,val;}edge[M]; void ae(int u,int v,int w){ edge[cnt].next=head[u],edge[cnt].to=v,edge[cnt].val=w,head[u]=cnt++; edge[cnt].next=head[v],edge[cnt].to=u,edge[cnt].val=0,head[v]=cnt++; } queue<int>q; inline bool bfs(){ memset(dep,0,sizeof(dep)),q.push(S),dep[S]=1; while(!q.empty()){ register int x=q.front();q.pop(); for(register int i=cur[x]=head[x];i!=-1;i=edge[i].next)if(edge[i].val&&!dep[edge[i].to])dep[edge[i].to]=dep[x]+1,q.push(edge[i].to); } return dep[T]>0; } bool reach; inline int dfs(int x,int flow){ if(x==T){res+=flow,reach=true;return flow;} int used=0; for(register int &i=cur[x];i!=-1;i=edge[i].next){ if(!edge[i].val||dep[edge[i].to]!=dep[x]+1)continue; register int ff=dfs(edge[i].to,min(edge[i].val,flow-used)); if(ff){edge[i].val-=ff,edge[i^1].val+=ff,used+=ff;if(used==flow)break;} } return used; } inline void Dinic(){while(bfs()){reach=true;while(reach)reach=false,dfs(S,0x3f3f3f3f);}} } using namespace MaxFlow; struct Vector{ int x,y; Vector(){} Vector(int X,int Y){x=X,y=Y;} friend Vector operator +(const Vector &u,const Vector &v){return Vector(u.x+v.x,u.y+v.y);} friend Vector operator -(const Vector &u,const Vector &v){return Vector(u.x-v.x,u.y-v.y);} ll operator ~()const{return 1ll*x*x+1ll*y*y;}//the modulo of a vector void read(){scanf("%d%d",&x,&y);} void print(){printf("(%d,%d)",x,y);} }; int X,Y,n,tp[310]; typedef Vector Point; struct Rect{ int x1,y1,x2,y2; Point LD,LU,RD,RU; void read(){ scanf("%d%d%d%d",&x1,&y1,&x2,&y2); LD=Point(x1,y1),LU=Point(x1,y2),RD=Point(x2,y1),RU=Point(x2,y2); } }r[310]; struct Circ{ int x,y,r; Point O; void read(){scanf("%d%d%d",&x,&y,&r);O=Point(x,y);} }c[310]; bool Inter(Rect u,Rect v){ return !(u.x2<v.x1||u.x1>v.x2||u.y2<v.y1||u.y1>v.y2); } bool Inter(Circ u,Circ v){ return ~(u.O-v.O)<=1ll*(u.r+v.r)*(u.r+v.r); } bool Inter(Rect u,Circ v){ if(~(u.LD-v.O)<=1ll*v.r*v.r)return true; if(~(u.LU-v.O)<=1ll*v.r*v.r)return true; if(~(u.RD-v.O)<=1ll*v.r*v.r)return true; if(~(u.RU-v.O)<=1ll*v.r*v.r)return true; return !(u.x1>v.x+v.r||u.x2<v.x-v.r||u.y1>v.y+v.r||u.y2<v.y-v.r); } bool Inter(Rect u,int ip){ if(ip==1)return u.y2>=Y; if(ip==-1)return u.y1<=0; } bool Inter(Circ u,int ip){ if(ip==1)return u.y+u.r>=Y; if(ip==-1)return u.y-u.r<=0; } int main(){ scanf("%d%d%d",&X,&Y,&n),S=2*n+1,T=2*n+2,memset(head,-1,sizeof(head)); for(int i=1;i<=n;i++){ ae(i,i+n,1); scanf("%d",&tp[i]); if(tp[i]==1){ c[i].read(); if(Inter(c[i],1))ae(i+n,T,0x3f3f3f3f); if(Inter(c[i],-1))ae(S,i,0x3f3f3f3f); }else{ r[i].read(); if(Inter(r[i],1))ae(i+n,T,0x3f3f3f3f); if(Inter(r[i],-1))ae(S,i,0x3f3f3f3f); } } for(int i=1;i<=n;i++)for(int j=i+1;j<=n;j++){ if(tp[i]==1&&tp[j]==1&&Inter(c[i],c[j]))ae(i+n,j,0x3f3f3f3f),ae(j+n,i,0x3f3f3f3f); if(tp[i]==2&&tp[j]==2&&Inter(r[i],r[j]))ae(i+n,j,0x3f3f3f3f),ae(j+n,i,0x3f3f3f3f); if(tp[i]==1&&tp[j]==2&&Inter(r[j],c[i]))ae(i+n,j,0x3f3f3f3f),ae(j+n,i,0x3f3f3f3f); if(tp[i]==2&&tp[j]==1&&Inter(r[i],c[j]))ae(i+n,j,0x3f3f3f3f),ae(j+n,i,0x3f3f3f3f); } Dinic(); printf("%d\n",res); return 0; } ``` # CXI.[[POI2010]MOS-Bridges](https://www.luogu.com.cn/problem/P3511) “最大值最小”先套个二分罢。问题转换为判断一张有向图上是否存在欧拉回路。 有向图上存在欧拉回路,当且仅当所有点的入度等于出度。但是本题因为有无向边的存在,不能简单判断。 但我们可以先强制定下无向边的方向,同时给它反悔的机会。 具体而言,我们先强制定方向得到一张有向图,此时可能有点的出入度不平衡。对于入度多的点,我们从 $S$ 连来多的流量;对于出度多的点,我们连到 $T$ 相应的流量;然后可以反悔的边,就相当于连接两点的度数为 $2$ 的有向边。(消除原本的度数影响+其本身的度数影响,一共是 $2$ 点) 于是按照这种建图方式,我们发现若一个点的出入度奇偶不同,则一定不合法(因为反悔不会改变出入度的奇偶性);然后,我们就可以为了避免边的流量不跑满而将所有边权除以 $2$。然后判断这张图的最大流是否满流即可。 当二分结束求出答案后,每条边的方向也就可以确定了,我们便得到了一张有向欧拉图。求出上面的欧拉回路即可。 因为本人当初学欧拉回路时就没有好好学,所以这里额外扯一句欧拉回路——它可以通过以**链表**记录边,然后合并大回路和小回路得到(这就是必须用链表的原因)。 代码: ```cpp #include<bits/stdc++.h> using namespace std; int n,m; struct EDGE{ int u,v,s,t; void read(){scanf("%d%d%d%d",&u,&v,&s,&t);} }e[20100]; namespace MaxFlow{ const int N=1010,M=200000; int head[N],cur[N],dep[N],cnt,S,T,res; struct node{int to,next,val;}edge[M]; void ae(int u,int v,int w){ edge[cnt].next=head[u],edge[cnt].to=v,edge[cnt].val=w,head[u]=cnt++; edge[cnt].next=head[v],edge[cnt].to=u,edge[cnt].val=0,head[v]=cnt++; } queue<int>q; inline bool bfs(){ memset(dep,0,sizeof(dep)),q.push(S),dep[S]=1; while(!q.empty()){ register int x=q.front();q.pop(); for(register int i=cur[x]=head[x];i!=-1;i=edge[i].next)if(edge[i].val&&!dep[edge[i].to])dep[edge[i].to]=dep[x]+1,q.push(edge[i].to); } return dep[T]>0; } bool reach; inline int dfs(int x,int flow){ if(x==T){res+=flow,reach=true;return flow;} int used=0; for(register int &i=cur[x];i!=-1;i=edge[i].next){ if(!edge[i].val||dep[edge[i].to]!=dep[x]+1)continue; register int ff=dfs(edge[i].to,min(edge[i].val,flow-used)); if(ff){edge[i].val-=ff,edge[i^1].val+=ff,used+=ff;if(used==flow)break;} } return used; } inline void Dinic(){while(bfs()){reach=true;while(reach)reach=false,dfs(S,0x3f3f3f3f);}} } using namespace MaxFlow; int in[N]; bool che(int ip){ memset(head,-1,sizeof(head)),cnt=res=0; for(int i=1;i<=n;i++)in[i]=0; for(int i=1;i<=m;i++){ if(e[i].s<=ip&&e[i].t<=ip)in[e[i].u]++,in[e[i].v]--,ae(e[i].u,e[i].v,1); else if(e[i].s<=ip)in[e[i].u]++,in[e[i].v]--; else if(e[i].t<=ip)in[e[i].u]--,in[e[i].v]++; else return false; } for(int i=1;i<=n;i++)if(in[i]&1)return false; int sum=0; for(int i=1;i<=n;i++){ in[i]/=2; if(in[i]>0)ae(S,i,in[i]),sum+=in[i]; if(in[i]<0)ae(i,T,-in[i]); } Dinic(); return sum==res; } vector<pair<int,int> >v[N]; set<pair<int,int> >s; int nex[20100]; pair<int,int>dfs(int x){ if(v[x].empty())return make_pair(-1,-1); int id=v[x].back().first,y=v[x].back().second;v[x].pop_back(); pair<int,int>tmp=dfs(y); nex[id]=tmp.first; if(tmp.second==-1)tmp.second=id; while(!v[x].empty()){ int nid=v[x].back().first,ny=v[x].back().second;v[x].pop_back(); pair<int,int>ntmp=dfs(ny); if(ntmp.second==-1)ntmp.second=nid; nex[ntmp.second]=id; nex[nid]=ntmp.first; id=nid; } return make_pair(id,tmp.second); } int main(){ scanf("%d%d",&n,&m),S=n+1,T=n+2; for(int i=1;i<=m;i++)e[i].read(),in[e[i].u]++,in[e[i].v]++; for(int i=1;i<=n;i++)if(in[i]&1){puts("NIE");return 0;} int l=1,r=1000; while(l<r){ int mid=(l+r)>>1; if(che(mid))r=mid; else l=mid+1; } printf("%d\n",r); che(r); for(int x=1;x<=n;x++)for(int i=head[x];i!=-1;i=edge[i].next)if(edge[i].to<=n&&edge[i].val)s.insert(make_pair(x,edge[i].to)); for(int i=1;i<=m;i++){ if(e[i].s<=r&&e[i].t<=r){ if(s.find(make_pair(e[i].u,e[i].v))!=s.end())v[e[i].u].push_back(make_pair(i,e[i].v)); if(s.find(make_pair(e[i].v,e[i].u))!=s.end())v[e[i].v].push_back(make_pair(i,e[i].u)); } else if(e[i].s<=r)v[e[i].u].push_back(make_pair(i,e[i].v)); else if(e[i].t<=r)v[e[i].v].push_back(make_pair(i,e[i].u)); } int x=dfs(1).first; while(x!=-1)printf("%d ",x),x=nex[x];puts(""); return 0; } ``` # CXII.[[BJWC2018]Kakuro](https://www.luogu.com.cn/problem/P4486) 首先模型是可以建出来的:源点连到所有“右上角的数字”,右上角的数字连到所有与它有交的左下角数字,左下角数字再连到汇点。 我们来进一步完善这个系统。首先,上述三条边应该全部强制让它满流。同时,为了使收支平衡,我们还可以连如下一些边: 1. 源点到右上角及其反边,对应着修改右上角的值。需要注意的是,反边的权值上限小于右上角的数字,因为须保证非负。 2. 汇点到左下角及其反边。同上。 3. 右上到左下及其反边。同上。 代码中已经手动合并了源点和汇点,因此只能见到一个合并后的点 $O$。 代码: ```cpp #include<bits/stdc++.h> using namespace std; typedef long long ll; int n,m,num,tp[50][50],id[50][50][2],val[50][50][3],cst[50][50][3];//0:downwards 1:rightwards namespace MCMF{ const int N=1010,M=2000000; int head[N],cnt,dis[N],fr[N],S,T,deg[N],flow; ll cost; struct node{ int to,next,val,cost; }edge[M]; void ae(int u,int v,int w,int c){//add a single directed edge, without lower limit edge[cnt].cost=c,edge[cnt].next=head[u],edge[cnt].to=v,edge[cnt].val=w,head[u]=cnt++; edge[cnt].cost=-c,edge[cnt].next=head[v],edge[cnt].to=u,edge[cnt].val=0,head[v]=cnt++; } void AE(int u,int v,int w){//add a must-full directed edge deg[u]-=w,deg[v]+=w; } queue<int>q; bool in[N]; bool SPFA(){ memset(dis,0x3f,sizeof(dis)),dis[S]=0,q.push(S),in[S]=true; while(!q.empty()){ int x=q.front();q.pop(),in[x]=false; for(int i=head[x];i!=-1;i=edge[i].next){ if(!edge[i].val)continue; if(dis[edge[i].to]>dis[x]+edge[i].cost){ dis[edge[i].to]=dis[x]+edge[i].cost,fr[edge[i].to]=i; if(!in[edge[i].to])in[edge[i].to]=true,q.push(edge[i].to); } } } if(dis[T]==dis[T+1])return false; int x=T,mn=0x3f3f3f3f; while(x!=S)mn=min(mn,edge[fr[x]].val),x=edge[fr[x]^1].to; cost+=1ll*dis[T]*mn,flow+=mn,x=T; while(x!=S)edge[fr[x]].val-=mn,edge[fr[x]^1].val+=mn,x=edge[fr[x]^1].to; return true; } } using namespace MCMF; int O; int main(){ scanf("%d%d",&n,&m),memset(head,-1,sizeof(head)); for(int i=1;i<=n;i++)for(int j=1;j<=m;j++){ scanf("%d",&tp[i][j]); if(tp[i][j]==1||tp[i][j]==3)id[i][j][0]=++num; if(tp[i][j]==2||tp[i][j]==3)id[i][j][1]=++num; } for(int i=1;i<=n;i++)for(int j=1;j<=m;j++){ if(tp[i][j]==1||tp[i][j]==3)scanf("%d",&val[i][j][0]); if(tp[i][j]==2||tp[i][j]==3)scanf("%d",&val[i][j][1]); if(tp[i][j]==4)scanf("%d",&val[i][j][2]); } for(int i=1;i<=n;i++)for(int j=1;j<=m;j++){ if(tp[i][j]==1||tp[i][j]==3)scanf("%d",&cst[i][j][0]); if(tp[i][j]==2||tp[i][j]==3)scanf("%d",&cst[i][j][1]); if(tp[i][j]==4)scanf("%d",&cst[i][j][2]); } // for(int i=1;i<=n;i++){for(int j=1;j<=m;j++)printf("(%d,%d)",id[i][j][0],id[i][j][1]);puts("");}puts(""); // for(int i=1;i<=n;i++){for(int j=1;j<=m;j++)printf("(%d,%d)",val[i][j][0],val[i][j][1]);puts("");}puts(""); // for(int i=1;i<=n;i++){for(int j=1;j<=m;j++)printf("(%d,%d)",cst[i][j][0],cst[i][j][1]);puts("");}puts(""); O=num+1,S=num+2,T=num+3; for(int i=1;i<=n;i++)for(int j=1;j<=m;j++){ if(id[i][j][0]){ AE(O,id[i][j][0],val[i][j][0]); if(cst[i][j][0]!=-1)ae(O,id[i][j][0],0x3f3f3f3f,cst[i][j][0]),ae(id[i][j][0],O,val[i][j][0]-1,cst[i][j][0]); } if(id[i][j][1]){ AE(id[i][j][1],O,val[i][j][1]); if(cst[i][j][1]!=-1)ae(id[i][j][1],O,0x3f3f3f3f,cst[i][j][1]),ae(O,id[i][j][1],val[i][j][1]-1,cst[i][j][1]); } if(tp[i][j]==4){ int I=i,J=j; while(!id[I][j][0])I--; while(!id[i][J][1])J--; AE(id[I][j][0],id[i][J][1],val[i][j][2]); if(cst[i][j][2]!=-1)ae(id[I][j][0],id[i][J][1],0x3f3f3f3f,cst[i][j][2]),ae(id[i][J][1],id[I][j][0],val[i][j][2]-1,cst[i][j][2]); } } int sum=0; for(int i=1;i<=O;i++){ if(deg[i]>0)ae(S,i,deg[i],0),sum+=deg[i]; if(deg[i]<0)ae(i,T,-deg[i],0); } while(SPFA()); if(sum!=flow)puts("-1");else printf("%lld\n",cost); return 0; } ``` # CXIII.[[北京省选集训2019]图的难题](https://www.luogu.com.cn/problem/P5295)/[[UOJ#168][UR #11]元旦老人与丛林](https://uoj.ac/contest/23/problem/168) 两题是重题,第二题数据范围更大。 首先,我们发现,如果原图的 $|E|>2|V|-2$,显然是不合法的; 再进一步思考,如果原图存在一个**导出子图**(包含一些点及所有两端均在该点集内的边的子图)$G'=\{V',E'\}$ 使得 $|E'|>2|V'|-2$,则也一定不合法。 我们称“不存在上述导出子图”为“**符合性质**”。那么,如果原图 $G$ 符合性质,是否就合法了呢?我们考虑归纳证明。 首先,当 $n=1$ 时显然成立。否则,假设对于所有 $n'<n$ 均成立,考虑构造出 $n$ 时的分配方案。 考虑 $n$ 个节点中度数最少的那个,不妨设为 $x$。当度数 $\geq4$ 时,显然总边数不小于 $\dfrac{4n}{2}=2n$,则此时整张图不符合上述 $m\leq2n-2$ 的前提条件,可以舍去。 则此度数只能为 $0,1,2,3$。前三者时,我们一定可以把任一条边分到 $n-1$ 条边时的白图里,另一条分到黑图里,并且两张图均仍是森林。 于是现在就只剩下度数为 $3$ 的情形。 因为 $G$ 符合性质,所以其任意子图 $G'$ 均有 $|E'|\leq2|V'|-2$。故我们定义一张子图 $G'$ 是“满”的当且仅当 $|E'|=2|V'|-2$。 则若两张子图 $G_1,G_2$ 都是满的,则它们的并也是满的。因为 $G_1\cup G_2=G_1+G_2-G_1\cap G_2$,而因为 $G$ 符合性质,所以 $|E_1\cap E_2|\leq2|V_1\cap V_2|-2$;但是因为在上式中其前面是负号,所以有 $|E_1\cup E_2|\geq2|V_1\cup V_2|-2$。但是 $G_1\cup G_2$ 也是 $G$ 的子图,所以两者结合起来就只能有 $|E_1\cup E_2|=2|V_1\cup V_2|-2$,即二者之并是满的。 有了上述结论,我们再设上述度数为 $3$ 的点连到了 $a,b,c$ 三个点。则在删去 $x$ 及其连边后,在剩余的 $n-1$ 个点的图中,同时包含 $(a,b)$ 的满子图、同时包含 $(b,c)$ 的满子图、同时包含 $(a,c)$ 的满子图,这三者不可能同时出现,不然就可以把它们求并得到同时包含 $(a,b,c)$ 的满子图。得到这张图后,再加入 $x$ 及其连边,便得到了一张 $|E|=2|V|-1$ 的不合条件的图,与所有子图都符合条件矛盾。 于是便证明了至少有一张上述满子图不存在,不妨设为 $(a,b)$ 的满子图不存在。而这时,如果我们往 $n-1$ 个点的图中添加一条额外的 $(a,b)$ 边,显然这时整张图依旧符合性质,且点数为 $n-1$,存在至少一种划分方案。在任一方案中,总有一棵森林包含 $(a,b)$,于是删去 $(a,b)$,连接 $(a,x)$,$(x,b)$,同时把 $(x,c)$ 加入另一棵森林。 于是便构造出一种合法的分配方案,则 $G$ 符合性质是充要条件。 要判断是否存在不符条件的导出子图,等价于求 $|E|-2|V|$ 的 $\max$,并观察其是否有 $\leq-2$。我们发现,若我们选择了一条边,则其两端的节点也必须被选择——这看上去像一个最大权闭合子图问题。我们化边为点,其中边的权值为 $1$,点的权值为 $-2$,然后求其最大权导出子图即可。 需要注意的是,该导出子图必须非空。所以,我们必须枚举一个点,强制其在导出子图中(即将其权值设作 $0$,然后最终求出来的最大权再减去 $2$)。 对于第一题,枚举点并暴力建图是可以通过的;然而对于第二题,暴力建图会喜获TLE,需要手动退流以保证复杂度。 因为没退好流,所以最终还是T掉了。 T掉的UOJ#168的代码: ```cpp #include<bits/stdc++.h> using namespace std; int T,n,m; namespace MaxFlow{ const int N=6010,M=2000000; int head[N],cur[N],dep[N],cnt,S,T,res; struct node{int to,next,val;}edge[M]; void ae(int u,int v,int w){ edge[cnt].next=head[u],edge[cnt].to=v,edge[cnt].val=w,head[u]=cnt++; edge[cnt].next=head[v],edge[cnt].to=u,edge[cnt].val=0,head[v]=cnt++; } queue<int>q; inline bool bfs(){ memset(dep,0,sizeof(dep)),q.push(S),dep[S]=1; while(!q.empty()){ register int x=q.front();q.pop(); for(register int i=cur[x]=head[x];i!=-1;i=edge[i].next)if(edge[i].val&&!dep[edge[i].to])dep[edge[i].to]=dep[x]+1,q.push(edge[i].to); } return dep[T]>0; } bool reach; inline int dfs(int x,int flow){ if(x==T){res+=flow,reach=true;return flow;} int used=0; for(register int &i=cur[x];i!=-1;i=edge[i].next){ if(!edge[i].val||dep[edge[i].to]!=dep[x]+1)continue; register int ff=dfs(edge[i].to,min(edge[i].val,flow-used)); if(ff){edge[i].val-=ff,edge[i^1].val+=ff,used+=ff;if(used==flow)break;} } return used; } inline int Dinic(int s,int t){S=s,T=t,res=0;while(bfs()){reach=true;while(reach)reach=false,dfs(S,0x3f3f3f3f);}return res;} } using MaxFlow::Dinic; using MaxFlow::edge; using MaxFlow::ae; using MaxFlow::cnt; using MaxFlow::head; int tmp[2010],id[2010]; void read(int &x){ x=0; char c=getchar(); while(c>'9'||c<'0')c=getchar(); while(c>='0'&&c<='9')x=(x<<3)+(x<<1)+(c^48),c=getchar(); } int main(){ read(n),read(m),memset(head,-1,sizeof(head)); int S=n+m+1,T=n+m+2,sum=0; for(int i=1,x,y;i<=m;i++)read(x),read(y),ae(S,n+i,1),ae(n+i,x,0x3f3f3f3f),ae(n+i,y,0x3f3f3f3f),sum+=1; for(int i=1;i<=n;i++)id[i]=cnt,ae(i,T,2); for(int i=1;i<=n;i++){ edge[id[i]].val=edge[id[i]^1].val=0; sum+=Dinic(i,S); sum-=Dinic(S,T); if(sum>0){puts("No");return 0;} edge[id[i]].val=2,edge[id[i]^1].val=0; } puts("Yes"); return 0; } ``` # CXIV.[[TJOI2013]循环格](https://www.luogu.com.cn/problem/P3965) 明显我们可以把“箭头”看作一个格子向另一个格子的有向边,则整张图构成一个内向树森林。 而“完美循环格”,则相当于内向树全部退化为环。 考虑环的特征,发现是所有点的入度和出度都为 $1$。 于是其可以被抽象为两个点匹配,于是连出边来,需要旋转得到的边费用为 $1$,不需旋转的边费用为 $0$,然后跑最小费用二分图匹配即可。 代码: ```cpp #include<bits/stdc++.h> using namespace std; int n,m,dx[4]={1,0,-1,0},dy[4]={0,1,0,-1}; char s[20][20]; namespace MCMF{ const int N=1000,M=20000; int head[N],cnt,dis[N],fr[N],id[N],S,T,cost; struct node{ int to,next,val,cost; }edge[M]; void ae(int u,int v,int w,int c){ edge[cnt].cost=c,edge[cnt].next=head[u],edge[cnt].to=v,edge[cnt].val=w,head[u]=cnt++; edge[cnt].cost=-c,edge[cnt].next=head[v],edge[cnt].to=u,edge[cnt].val=0,head[v]=cnt++; } queue<int>q; bool in[N]; bool SPFA(){ memset(dis,0x3f,sizeof(dis)),dis[S]=0,q.push(S),in[S]=true; while(!q.empty()){ int x=q.front();q.pop(),in[x]=false; // printf("%d\n",x); for(int i=head[x];i!=-1;i=edge[i].next){ if(!edge[i].val)continue; if(dis[edge[i].to]>dis[x]+edge[i].cost){ dis[edge[i].to]=dis[x]+edge[i].cost,fr[edge[i].to]=x,id[edge[i].to]=i; if(!in[edge[i].to])in[edge[i].to]=true,q.push(edge[i].to); } } } if(dis[T]==dis[T+1])return false; int x=T,mn=0x3f3f3f3f; while(x!=S)mn=min(mn,edge[id[x]].val),x=fr[x]; cost+=dis[T]*mn,x=T; while(x!=S)edge[id[x]].val-=mn,edge[id[x]^1].val+=mn,x=fr[x]; return true; } } using namespace MCMF; int main(){ scanf("%d%d",&n,&m),S=2*n*m,T=2*n*m+1,memset(head,-1,sizeof(head)); for(int i=0;i<n;i++)scanf("%s",s[i]); for(int i=0;i<n;i++)for(int j=0;j<m;j++){ ae(S,i*m+j,1,0),ae(n*m+i*m+j,T,1,0); int dr; if(s[i][j]=='D')dr=0; if(s[i][j]=='R')dr=1; if(s[i][j]=='U')dr=2; if(s[i][j]=='L')dr=3; for(int k=0;k<4;k++)ae(i*m+j,n*m+(i+dx[k]+n)%n*m+(j+dy[k]+m)%m,1,k!=dr); } while(SPFA()); printf("%d\n",cost); return 0; } ``` # CXV.[[CEOI2008]order](https://www.luogu.com.cn/problem/P4177) 我们如果考虑只有一件任务,一台机器,一道工序,应该怎样建图? 我们首先考虑源点连到机器,任务连到汇点——因为这两个东西都是多对一的。然后就把工序看作是连接二者的边。 考虑用最小割解决问题,则问题转换为三段中必须切断一个——源点到机器,意味着买了台机器;机器到任务,意味着租了台机器;任务到汇点,意味着放弃该任务。 代码: ```cpp #include<bits/stdc++.h> using namespace std; namespace MaxFlow{ const int N=2500,M=5000000; int head[N],cur[N],dep[N],cnt,S,T,res; struct node{int to,next,val;}edge[M]; void ae(int u,int v,int w){ edge[cnt].next=head[u],edge[cnt].to=v,edge[cnt].val=w,head[u]=cnt++; edge[cnt].next=head[v],edge[cnt].to=u,edge[cnt].val=0,head[v]=cnt++; } queue<int>q; inline bool bfs(){ memset(dep,0,sizeof(dep)),q.push(S),dep[S]=1; while(!q.empty()){ register int x=q.front();q.pop(); for(register int i=cur[x]=head[x];i!=-1;i=edge[i].next)if(edge[i].val&&!dep[edge[i].to])dep[edge[i].to]=dep[x]+1,q.push(edge[i].to); } return dep[T]>0; } bool reach; inline int dfs(int x,int flow){ if(x==T){res+=flow,reach=true;return flow;} int used=0; for(register int &i=cur[x];i!=-1;i=edge[i].next){ if(!edge[i].val||dep[edge[i].to]!=dep[x]+1)continue; register int ff=dfs(edge[i].to,min(edge[i].val,flow-used)); if(ff){edge[i].val-=ff,edge[i^1].val+=ff,used+=ff;if(used==flow)break;} } return used; } inline void Dinic(){while(bfs()){reach=true;while(reach)reach=false,dfs(S,0x3f3f3f3f);}} } using namespace MaxFlow; int n,m; int main(){ scanf("%d%d",&n,&m),S=n+m+1,T=n+m+2,memset(head,-1,sizeof(head)); int sum=0; for(int i=1,a,b,c,d;i<=n;i++){ scanf("%d%d",&a,&b),sum+=a; ae(m+i,T,a); while(b--)scanf("%d%d",&c,&d),ae(c,m+i,d); } for(int i=1,x;i<=m;i++)scanf("%d",&x),ae(S,i,x); Dinic(); printf("%d\n",sum-res); return 0; } ``` # CXVI.[CF802C Heidi and Library (hard)](https://www.luogu.com.cn/problem/CF802C) 建图随便建,拆个点完事。 限制每次请求的书直接上下界网络流随便搞一下就行。 代码: ```cpp #include<bits/stdc++.h> using namespace std; int n,m,s,t,c[20100]; namespace MCMF{ int head[20100],cnt,dis[20100],fr[20100],id[20100],S,T,cost,deg[20100]; struct node{ int to,next,val,cost; }edge[4010000]; void ae(int u,int v,int w,int c){ // printf("%d %d (%d,%d)\n",u,v,w,c); edge[cnt].cost=c,edge[cnt].next=head[u],edge[cnt].to=v,edge[cnt].val=w,head[u]=cnt++; edge[cnt].cost=-c,edge[cnt].next=head[v],edge[cnt].to=u,edge[cnt].val=0,head[v]=cnt++; } void ae(int u,int v){deg[v]++,deg[u]--;} queue<int>q; bool in[20100]; bool SPFA(){ memset(dis,0x3f,sizeof(dis)),dis[S]=0,q.push(S),in[S]=true; while(!q.empty()){ int x=q.front();q.pop(),in[x]=false; // printf("%d\n",x); for(int i=head[x];i!=-1;i=edge[i].next){ if(!edge[i].val)continue; if(dis[edge[i].to]>dis[x]+edge[i].cost){ dis[edge[i].to]=dis[x]+edge[i].cost,fr[edge[i].to]=x,id[edge[i].to]=i; if(!in[edge[i].to])in[edge[i].to]=true,q.push(edge[i].to); } } } if(dis[T]==dis[T+1])return false; int x=T,mn=0x3f3f3f3f; while(x!=S)mn=min(mn,edge[id[x]].val),x=fr[x]; cost+=dis[T]*mn,x=T; while(x!=S)edge[id[x]].val-=mn,edge[id[x]^1].val+=mn,x=fr[x]; return true; } } using namespace MCMF; int main(){ scanf("%d%d",&n,&m),s=2*n*n+n,t=2*n*n+n+1,S=2*n*n+n+2,T=2*n*n+n+3,memset(head,-1,sizeof(head)); for(int i=0,x;i<n;i++){ scanf("%d",&x),x--; for(int j=0;j<n;j++)if(j==x)ae(i*n+j,n*n+i*n+j);else ae(i*n+j,n*n+i*n+j,1,0); for(int j=0;j<n;j++)if(i+1<n)ae(n*n+i*n+j,(i+1)*n+j,1,0);else ae(n*n+i*n+j,t,1,0); } for(int i=0;i<n;i++)scanf("%d",&c[i]); ae(s,2*n*n,m,0); for(int i=0;i<n;i++){ for(int j=0;j<n;j++)ae(2*n*n+i,i*n+j,1,c[j]); if(i)for(int j=0;j<n;j++)ae(n*n+(i-1)*n+j,2*n*n+i,1,0); } ae(t,s,0x3f3f3f3f,0); for(int i=0;i<=t;i++){ if(deg[i]>0)ae(S,i,deg[i],0); if(deg[i]<0)ae(i,T,-deg[i],0); } while(SPFA()); printf("%d\n",cost); return 0; } ``` # CXVII.[CF491C Deciphering](https://www.luogu.com.cn/problem/CF491C) 随便建图跑费用流即可。 代码: ```cpp #include<bits/stdc++.h> using namespace std; int n,m,g[52][52]; int HASH(char i){ if('a'<=i&&i<='z')return i-'a'; if('A'<=i&&i<='Z')return 26+i-'A'; } char HASH(int i){ if(i<26)return 'a'+i; if(i<52)return 'A'+i-26; } char s[2001000],t[2001000]; namespace MCMF{ const int N=200,M=200000; int head[N],cnt,dis[N],fr[N],id[N],S,T,cost; struct node{ int to,next,val,cost; }edge[M]; void ae(int u,int v,int w,int c){ // printf("%d %d %d %d\n",u,v,w,c); edge[cnt].cost=c,edge[cnt].next=head[u],edge[cnt].to=v,edge[cnt].val=w,head[u]=cnt++; edge[cnt].cost=-c,edge[cnt].next=head[v],edge[cnt].to=u,edge[cnt].val=0,head[v]=cnt++; } queue<int>q; bool in[N]; bool SPFA(){ memset(dis,0xcf,sizeof(dis)),dis[S]=0,q.push(S),in[S]=true; while(!q.empty()){ int x=q.front();q.pop(),in[x]=false; // printf("%d\n",x); for(int i=head[x];i!=-1;i=edge[i].next){ if(!edge[i].val)continue; if(dis[edge[i].to]<dis[x]+edge[i].cost){ dis[edge[i].to]=dis[x]+edge[i].cost,fr[edge[i].to]=x,id[edge[i].to]=i; if(!in[edge[i].to])in[edge[i].to]=true,q.push(edge[i].to); } } } if(dis[T]==dis[T+1])return false; int x=T,mn=0x3f3f3f3f; while(x!=S)mn=min(mn,edge[id[x]].val),x=fr[x]; cost+=dis[T]*mn,x=T; while(x!=S)edge[id[x]].val-=mn,edge[id[x]^1].val+=mn,x=fr[x]; return true; } } using namespace MCMF; int main(){ scanf("%d%d%s%s",&n,&m,s,t),S=2*m,T=2*m+1,memset(head,-1,sizeof(head)); for(int i=0;i<n;i++)g[HASH(s[i])][HASH(t[i])]++; for(int i=0;i<m;i++)ae(S,i,1,0),ae(i+m,T,1,0); for(int i=0;i<m;i++)for(int j=0;j<m;j++)ae(i,j+m,1,g[i][j]); while(SPFA()); printf("%d\n",cost); for(int i=0;i<m;i++)for(int j=head[i];j!=-1;j=edge[j].next)if(edge[j].to>=m&&edge[j].to<2*m&&!edge[j].val)putchar(HASH(edge[j].to-m)); return 0; } ``` # CXVIII.[CF513F2 Scaygerboss](https://www.luogu.com.cn/problem/CF513F2) 首先boss的性别可以根据男女的数量直接确定出来。之后套个二分,然后就是三分图匹配(一个男配一个女再配一个位置),随便拆个点然后最大流check就行了。 需要注意的是本题比较卡常,不能把东西开得太大。 代码: ```cpp #include<bits/stdc++.h> using namespace std; typedef long long ll; int n,m,s,t,X[1010],Y[1010],Z[1010],dis[50][50][50][50],dx[4]={1,0,-1,0},dy[4]={0,1,0,-1}; char g[50][50]; queue<pair<int,int> >q; void bfs(int xx,int yy){ dis[xx][yy][xx][yy]=0,q.push(make_pair(xx,yy)); while(!q.empty()){ int x=q.front().first,y=q.front().second;q.pop(); for(int i=0;i<4;i++){ if(x+dx[i]>n||y+dy[i]>m||x+dx[i]<1||y+dy[i]<1||g[x+dx[i]][y+dy[i]]=='#'||dis[xx][yy][x+dx[i]][y+dy[i]]!=-1)continue; dis[xx][yy][x+dx[i]][y+dy[i]]=dis[xx][yy][x][y]+1,q.push(make_pair(x+dx[i],y+dy[i])); } } } namespace MaxFlow{ const int N=2010,M=2001000; int head[N],cur[N],dep[N],cnt,S,T,res; struct node{int to,next,val;}edge[M]; void ae(int u,int v,int w){ // printf("%d %d %d\n",u,v,w); edge[cnt].next=head[u],edge[cnt].to=v,edge[cnt].val=w,head[u]=cnt++; edge[cnt].next=head[v],edge[cnt].to=u,edge[cnt].val=0,head[v]=cnt++; } queue<int>q; inline bool bfs(){ memset(dep,0,sizeof(dep)),q.push(S),dep[S]=1; while(!q.empty()){ register int x=q.front();q.pop(); for(register int i=cur[x]=head[x];i!=-1;i=edge[i].next)if(edge[i].val&&!dep[edge[i].to])dep[edge[i].to]=dep[x]+1,q.push(edge[i].to); } return dep[T]>0; } bool reach; inline int dfs(int x,int flow){ if(x==T){res+=flow,reach=true;return flow;} int used=0; for(register int &i=cur[x];i!=-1;i=edge[i].next){ if(!edge[i].val||dep[edge[i].to]!=dep[x]+1)continue; register int ff=dfs(edge[i].to,min(edge[i].val,flow-used)); if(ff){edge[i].val-=ff,edge[i^1].val+=ff,used+=ff;if(used==flow)break;} } return used; } inline void Dinic(){while(bfs()){reach=true;while(reach)reach=false,dfs(S,0x3f3f3f3f);}} } using namespace MaxFlow; bool che(ll ip){ memset(head,-1,sizeof(head)),cnt=res=0; for(int x=1;x<=n;x++)for(int y=1;y<=m;y++){ for(int i=1;i<=s;i++){ if(dis[X[i]][Y[i]][x][y]==-1)continue; if(1ll*dis[X[i]][Y[i]][x][y]*Z[i]>ip)continue; ae(2*n*m+i,(x-1)*m+y,1); } for(int j=1;j<=t;j++){ if(dis[X[s+j]][Y[s+j]][x][y]==-1)continue; if(1ll*dis[X[s+j]][Y[s+j]][x][y]*Z[s+j]>ip)continue; ae(n*m+(x-1)*m+y,2*n*m+s+j,1); } } for(int i=1;i<=s;i++)ae(S,2*n*m+i,1); for(int i=1;i<=t;i++)ae(2*n*m+s+i,T,1); for(int x=1;x<=n;x++)for(int y=1;y<=m;y++)if(g[x][y]!='#')ae((x-1)*m+y,n*m+(x-1)*m+y,1); Dinic(); return res==s; } int main(){ scanf("%d%d%d%d",&n,&m,&s,&t),memset(dis,-1,sizeof(dis)); for(int i=1;i<=n;i++)scanf("%s",g[i]+1); for(int i=1;i<=n;i++)for(int j=1;j<=m;j++)if(g[i][j]!='#')bfs(i,j); if(abs(s-t)!=1){puts("-1");return 0;} if(s<t)scanf("%d%d%d",&X[s+1],&Y[s+1],&Z[s+1]); else scanf("%d%d%d",&X[s+t+1],&Y[s+t+1],&Z[s+t+1]); for(int i=1;i<=s;i++)scanf("%d%d%d",&X[i],&Y[i],&Z[i]);if(s<=t)s++; for(int i=1;i<=t;i++)scanf("%d%d%d",&X[s+i],&Y[s+i],&Z[s+i]);if(t<=s)t++; S=2*n*m+s+t+1,T=2*n*m+s+t+2; ll l=0,r=1e12; while(l<r){ ll mid=(l+r)>>1; if(che(mid))r=mid; else l=mid+1; } if(!che(r))puts("-1"); else printf("%lld\n",r); return 0; } ``` # CXIX.[CF434D Nanami's Power Plant](https://www.luogu.com.cn/problem/CF434D) [题解](https://www.luogu.com.cn/blog/Troverld/solution-cf434d) # CXX.[CF786E ALT](https://www.luogu.com.cn/problem/CF786E) [题解](https://www.luogu.com.cn/blog/Troverld/solution-cf786e) # CXXI.[CF903G Yet Another Maxflow Problem](https://www.luogu.com.cn/problem/CF903G) 在做完CXIX.[CF434D Nanami's Power Plant](https://www.luogu.com.cn/problem/CF434D)后,我对这题也立刻来了灵感。 首先,题目要求最大流,想想看发现可以把它变成最小割。 然后,同Nanami那题一样,我们敏锐地发现 $A_1\rightarrow A_n,B_1\rightarrow B_n$ 这两条路径上,最多只会被各割 $1$ 条边。 于是我们考虑假如我们割掉了 $A_i\rightarrow A_{i+1},B_{j}\rightarrow B_{j+1}$ 这两条边,还需要割掉什么边。 发现就是所有的 $A_x\rightarrow B_y$,其中 $x\geq i\land y<j$。 明显这是个二维数点问题。 因为对于每个 $A_i$,不论 $A_i\rightarrow A_{i+1}$ 的边权怎么变,能使其取得 $\min$ 的 $B_j$ 是唯一的。于是我们可以直接使用线段树处理二维数点问题,然后就可以预处理出来上述最优的 $j$ 的费用。然后使用一个 `multiset` 之类随便维护 $A_i\rightarrow A_{i+1}\text{ 的边的边权}+\text{上述最优费用}$ 的 $\min$ 即可。 需要注意的是可能有不割 $A$ 边或不割 $B$ 边的情形。这时就可以看作割掉了 $A_{n}\rightarrow A_{n+1}$ 及 $B_0\rightarrow B_1$ 的边,同上一致处理即可。 时间复杂度 $O(n\log n)$。 代码: ```cpp #include<bits/stdc++.h> using namespace std; typedef long long ll; int n,m,q,a[200100],b[200100]; ll mn[200100]; #define lson x<<1 #define rson x<<1|1 #define mid ((l+r)>>1) struct SegTree{ll mn,tag;}seg[800100]; void ADD(int x,ll y){seg[x].mn+=y,seg[x].tag+=y;} void pushup(int x){seg[x].mn=min(seg[lson].mn,seg[rson].mn);} void pushdown(int x){ADD(lson,seg[x].tag),ADD(rson,seg[x].tag),seg[x].tag=0;} void build(int x,int l,int r){ if(l==r){seg[x].mn=b[l];return;} build(lson,l,mid),build(rson,mid+1,r),pushup(x); } void modify(int x,int l,int r,int P,int val){ if(l>=P)return; if(r<P){ADD(x,val);return;} pushdown(x),modify(lson,l,mid,P,val),modify(rson,mid+1,r,P,val),pushup(x); } vector<pair<int,int> >v[200100]; multiset<ll>s; int main(){ scanf("%d%d%d",&n,&m,&q); for(int i=1;i<n;i++)scanf("%d%d",&a[i],&b[i]); for(int i=1,x,y,z;i<=m;i++)scanf("%d%d%d",&x,&y,&z),v[x].push_back(make_pair(y,z)); build(1,0,n-1); for(int i=1;i<=n;i++){ for(auto j:v[i])modify(1,0,n-1,j.first,j.second); mn[i]=seg[1].mn,s.insert(mn[i]+a[i]); } printf("%lld\n",*s.begin()); for(int i=1,x,y;i<=q;i++)scanf("%d%d",&x,&y),s.erase(s.find(mn[x]+a[x])),a[x]=y,s.insert(mn[x]+a[x]),printf("%lld\n",*s.begin()); return 0; } ``` # CXXII.[CF1430G Yet Another DAG Problem](https://www.luogu.com.cn/problem/CF1430G) Nanami永远的神(震声)! 因为模型也是一样的(有大小限制的变量取值),所以就直接考虑像Nanami一样做。 明显每个点的值域只要取在 $[0,n-1]$ 中即可一定满足条件。 于是首先先把每个点代表的链建出来。 一开始想的是,对于原图中连边的两个点,它们每一组取值的代价仅在连接两条链上点的边处计算,使用某些方法解出每条边的取值即可。但是这样发现,两条链上的取值对共有 $\dfrac{n(n-1)}{2}$ 种,但是我们一共却仅有 $\dfrac{n(n-2)}{2}$ 条可以用来组合的边,该线性方程组不一定有解。 然后不得不转换思路。发现收益 $w_ib_i=w_i(a_u-a_v)=w_ia_u-w_ia_v$,然后就可以关于 $u$ 求出所有上述的 $w_i$ 或 $-w_i$ 之和,设为 $s_u$,这样 $u$ 总共的收益就是函数 $a_us_u$,然后再加上那些限制,问题就被我们转换成了Nanami。 代码: ```cpp #include<bits/stdc++.h> using namespace std; const int lim=50000000; int n,m,a[20]; namespace MaxFlow{ const int N=201000,M=2001000; int head[N],cur[N],dep[N],cnt,S,T,res; struct node{int to,next,val;}edge[M]; void ae(int u,int v,int w){ // printf("%d %d %d\n",u,v,w); edge[cnt].next=head[u],edge[cnt].to=v,edge[cnt].val=w,head[u]=cnt++; edge[cnt].next=head[v],edge[cnt].to=u,edge[cnt].val=0,head[v]=cnt++; } queue<int>q; inline bool bfs(){ memset(dep,0,sizeof(dep)),q.push(S),dep[S]=1; while(!q.empty()){ register int x=q.front();q.pop(); for(register int i=cur[x]=head[x];i!=-1;i=edge[i].next)if(edge[i].val&&!dep[edge[i].to])dep[edge[i].to]=dep[x]+1,q.push(edge[i].to); } return dep[T]>0; } bool reach; inline int dfs(int x,int flow){ if(x==T){res+=flow,reach=true;return flow;} int used=0; for(register int &i=cur[x];i!=-1;i=edge[i].next){ if(!edge[i].val||dep[edge[i].to]!=dep[x]+1)continue; register int ff=dfs(edge[i].to,min(edge[i].val,flow-used)); if(ff){edge[i].val-=ff,edge[i^1].val+=ff,used+=ff;if(used==flow)break;} } return used; } inline void Dinic(){while(bfs()){reach=true;while(reach)reach=false,dfs(S,0x3f3f3f3f);}} } using namespace MaxFlow; int main(){ scanf("%d%d",&n,&m),memset(head,-1,sizeof(head)),S=n*n,T=n*n+1; for(int i=1,x,y,z;i<=m;i++){ scanf("%d%d%d",&x,&y,&z),a[--x]+=z,a[--y]-=z; for(int j=1;j<n;j++)ae(y*n+(j-1),x*n+j,0x3f3f3f3f); ae(S,x*n,0x3f3f3f3f); } for(int i=0;i<n;i++){ ae(S,i*n,lim); for(int j=1;j<n;j++)ae(i*n+(j-1),i*n+j,lim+a[i]*j); ae(i*n+n-1,T,0x3f3f3f3f); } Dinic(); // printf("%d\n",res-lim*n); for(int i=0;i<n;i++)for(int j=0;j<n;j++)if(!dep[i*n+j]){printf("%d ",j);break;} return 0; } ``` # CXXIII.[[NOI2019] 序列](https://www.luogu.com.cn/problem/P5470) ~~最近的NOI咋都喜欢玩模拟费用流这种阴间玩意~~ 首先,这题不难看出是个费用流模型。~~但是我想的多数奇奇怪怪的网络流都是带上下界的,因此没有任何优化前景~~ 正解是说,这个东西可以被看作是二分图最大权匹配的模型,其中任意匹配的点对不超过 $K-L$ 对。任意匹配,可以通过建一个中间点,然后左部点全部连到该点,该点再连到全部右部节点,这样就能处理;而现在有了匹配上界的限制,就把该中间点拆点,使得只有一条流量为 $K-L$ 的边。 因为这张图比较别致,所以可以考虑用**模拟费用流**优化。 什么是模拟费用流呢?就是用人类智慧手动跑费用流。 首先,二分图匹配问题要手动实现不是不可能的,只需要找到你匹配的点对即可。 于是我们考虑一点点地往图上添加流量,直到流量到达 $K$。 我们记“**任意流**”为经由前述 $K-L$ 的边的流量,并设 $fre$ 表示该边目前剩余的流量;再记“**绑定流**”为左部右部的同一位置配对的流量。 显然,任意时刻,任意流总是不小于绑定流。于是,若 $fre$ 非零,就流任意流即可。流任意流的方法也很简单,找到左右部权值最大的点,连一块即可。 否则,就只能流绑定流了。绑定流有如下流法: 1. 直接流 $i,i'$(令带 $'$ 的表示右部节点)。显然此时取所有双方都未配对的点对中最大的那个即可。 2. 考虑现行有一条任意流 $i\rightarrow j'$。我们可以找到任意一个未配对节点 $k$,并将其修改成 $i\rightarrow i',k\rightarrow j$,这样子就让 $i',k$ 这两个点参与了匹配。 3. 同上,只不过这里是找到 $k'$,然后修改为 $j\rightarrow j',i\rightarrow k'$。 每次流绑定流就从三者中找到最优的那种流即可。 可以发现,二三两种操作本质上是先退流再增广,因此替代了费用流的操作,也因此成功模拟了费用流。 我们考虑要维护什么才能支持查询。 首先,左右部未匹配的点显然是要维护的。我们各开一个大根堆维护其中权值最大的。 然后,$i+i'$ 这种直接流的也要开一个堆维护。 再后,因为 $i\rightarrow j'$ 中,$i'$ 与 $j$ 两个点都分别在二、三两种方案中参与了匹配,所以我们还需要分别维护 $i',j$ 的最大值。准确地说,$i'$ 就是所有**自身尚未匹配,但其左部对应节点已经匹配的右部节点**,而 $j$ 同理是所有**自身尚未匹配,右部对应节点已经匹配的左部节点**。继续开堆维护即可。 至于 $k$ 和 $k'$,因为也没有特别的限制,所以就选择左右部未匹配的点中权值最大的即可,我们上文已经维护过了。 现在,就是一些具体的细节了。举例来说,因为随着某些操作的执行,另一些堆中的某些点可能已经无效了,但我们又不太能把它从堆里掏出来,因此在堆顶出堆的时候,先判断一下堆顶的元素是否合法即可,如果不合法就一直弹,直到堆顶合法,或是堆空掉。 再举例来说,当我们流任意流时,可能会出现任意流流成了绑定流,也即两端位置相同的情形。这时候,要记得把它算成绑定流,也即回退给 $fre$ 一点流量。 还有一个例子,就是若我们有任意流匹配 $i\rightarrow j',k\rightarrow i'$,完全可以把它修改成 $i\rightarrow i',k\rightarrow j'$,使得 $fre$ 凭空多了一点流量,且每个节点的匹配与否的状态无变化。因此,每当连边 $i\rightarrow j'$ 时,都要判断 $i'$ 是否已经匹配,或者 $j$ 是否已经匹配,如果是的话就可以调整了。 并且,调整可能还不是一次性的。还是上文的例子,当连边 $k\rightarrow j'$ 时,仍有可能出现 $k'$ 匹配或是 $j$ 匹配的情形,也要递归下去处理;甚至,还会出现 $k\rightarrow j'$ 任意流流成了绑定流的例子,也要判掉。 于是综上起来,这里的 `Adjust` 函数就可以递归地处理上述操作,其中 `mat,mbt` 分别是左部的匹配对象,右部的匹配对象。 ```cpp void Adjust(int x,int y){//try to adjust the FREE-FLOW (x,y) if(x==y){fre++;return;}//no need to adjust. if(mbt[x]){mat[mbt[x]]=y,mbt[y]=mbt[x],mat[x]=mbt[x]=x,fre++,Adjust(mbt[y],y);return;} if(mat[y]){mbt[mat[y]]=x,mat[x]=mat[y],mat[y]=mbt[y]=y,fre++,Adjust(x,mat[x]);return;} } ``` 最后的一个例子,是某些堆空掉的情形。此时,对应的方案就不合法,不能予以选择。 时间复杂度 $O(n\log n)$。 代码: ```cpp #include<bits/stdc++.h> using namespace std; typedef long long ll; int T,n,m,fre,a[200100],b[200100],mat[200100],mbt[200100]; ll res; priority_queue<pair<int,int> >as,bs,am,bm,o;//s:spare nodes; m:parterner matched,self spare nodes; o:pairs. void Adjust(int x,int y){//try to adjust the FREE-FLOW (x,y) if(x==y){fre++;return;}//no need to adjust. if(mbt[x]){mat[mbt[x]]=y,mbt[y]=mbt[x],mat[x]=mbt[x]=x,fre++,Adjust(mbt[y],y);return;} if(mat[y]){mbt[mat[y]]=x,mat[x]=mat[y],mat[y]=mbt[y]=y,fre++,Adjust(x,mat[x]);return;} } void clear(priority_queue<pair<int,int> >&q){while(!q.empty())q.pop();} int main(){ scanf("%d",&T); while(T--){ scanf("%d%d%d",&n,&m,&fre),fre=m-fre,res=0; clear(as),clear(bs),clear(am),clear(bm),clear(o); for(int i=1;i<=n;i++)scanf("%d",&a[i]),as.push(make_pair(a[i],i)),mat[i]=0; for(int i=1;i<=n;i++)scanf("%d",&b[i]),bs.push(make_pair(b[i],i)),mbt[i]=0; for(int i=1;i<=n;i++)o.push(make_pair(a[i]+b[i],i)); // printf("%d %d %d %d %d\n",as.size(),bs.size(),am.size(),bm.size(),o.size()); while(m--){ // printf("%d:%d\n",m,fre); // for(int i=1;i<=n;i++)printf("%d ",mat[i]);puts(""); // for(int i=1;i<=n;i++)printf("%d ",mbt[i]);puts(""); while(!o.empty()&&(mat[o.top().second]||mbt[o.top().second]))o.pop(); while(mat[as.top().second])as.pop(); while(mbt[bs.top().second])bs.pop(); while(!am.empty()&&(mat[am.top().second]||!mbt[am.top().second]))am.pop(); while(!bm.empty()&&(!mat[bm.top().second]||mbt[bm.top().second]))bm.pop(); if(fre){ int x=as.top().second,y=bs.top().second;as.pop(),bs.pop(); res+=a[x]+b[y]; mat[x]=y,mbt[y]=x,fre--; if(!mbt[x])bm.push(make_pair(b[x],x)); if(!mat[y])am.push(make_pair(a[y],y)); // printf("FRE:%d %d\n",x,y); Adjust(x,y); continue; }else{ int t1=0;if(!o.empty())t1=o.top().first; int t2=0;if(!am.empty())t2=am.top().first+bs.top().first; int t3=0;if(!bm.empty())t3=bm.top().first+as.top().first; // printf("SIT:%d %d %d\n",t1,t2,t3); if(t1>=t2&&t1>=t3){ // printf("OO:%d\n",o.top().second); res+=t1; mat[o.top().second]=mbt[o.top().second]=o.top().second; o.pop(); continue; } if(t2>=t3){ int x=am.top().second,y=bs.top().second;am.pop(),bs.pop(); // printf("AM:%d %d\n",x,y); res+=t2; int z=mbt[x]; mat[z]=y,mbt[y]=z; mat[x]=mbt[x]=x; if(!mat[y])am.push(make_pair(a[y],y)); Adjust(z,y); }else{ int x=as.top().second,y=bm.top().second;as.pop(),bm.pop(); // printf("BM:%d %d\n",x,y); res+=t3; int z=mat[y]; mbt[z]=x,mat[x]=z; mat[y]=mbt[y]=y; if(!mbt[x])bm.push(make_pair(b[x],x)); Adjust(x,z); } } } // for(int i=1;i<=n;i++)printf("%d ",mat[i]);puts(""); // for(int i=1;i<=n;i++)printf("%d ",mbt[i]);puts(""); printf("%lld\n",res); } return 0; } ``` # CXXIV.[CF724E Goods transportation](https://www.luogu.com.cn/problem/CF724E) 继续模拟网络流。 首先,一个最大流的模型是显然的:源点向每个点连边,每个点向之后的点连边,每个点再向汇点连边。但是显然这么做复杂度会炸。 考虑将最大流转为最小割。我们考虑手动分配一个点是割掉源点边还是汇点边。假如割掉汇点边,显然其费用就是 $b_i$,即汇点流量;假如割掉源点边,则费用是在源点流量 $a_i$ 的基础上,还要割掉每一条连到该点的边。假如到 $i$ 为止共有 $j$ 个点被割掉了源点边(包括 $i$),则剩余的 $i-j$ 个点到 $i$ 的边也必须被割掉,于是这时的代价就是 $a_i+m(i-j)$,其中 $m$ 是边权。因为状态仅与 $i,j$ 有关,所以直接DP即可。时间复杂度 $O(n^2)$。 代码: ```cpp #include<bits/stdc++.h> using namespace std; typedef long long ll; int n,m; ll f[2][10100],a[10100],b[10100],res=0x3f3f3f3f3f3f3f3f; int main(){ scanf("%d%d",&n,&m); for(int i=1;i<=n;i++)scanf("%d",&a[i]); for(int i=1;i<=n;i++)scanf("%d",&b[i]); memset(f,0x3f,sizeof(f)),f[0][0]=0; for(int i=1;i<=n;i++){ memset(f[i&1],0x3f,sizeof(f[i&1])); for(int j=0;j<=n;j++){ if(j)f[i&1][j]=min(f[i&1][j],f[!(i&1)][j-1]+a[i]+1ll*m*(i-j)); f[i&1][j]=min(f[i&1][j],f[!(i&1)][j]+b[i]); } } for(int i=0;i<=n;i++)res=min(res,f[n&1][i]); printf("%lld\n",res); return 0; } ``` 还可以优化。考虑初始令全部点都断去源点边,仅保留汇点边。然后,考虑每次将一个点的汇点边断开,源点边连上。 显然,当我们翻转第一个点的时候,若其位置是 $i$,则额外代价为 $b_i-a_i+m(n-i)$,因为其与后缀所有节点间连边皆须断开。 当我们翻转第二个点(设为 $j$)时,我们发现其额外代价为 $b_j-a_j+m(n-j)-m$,因为边 $(i,j)$ 在 $i,j$ 之一处被计算了,但实际上其不应考虑,故应该减去。 然后,第三个点的额外代价为 $b_k-a_k+m(n-k)-2m$。通过找规律,我们发现若第 $i$ 个位置翻转了位置 $j$,其代价为 $b_j-a_j+m(n-j)-(i-1)m$。 因为 $i,j$ 代价独立,所以我们可以分开考虑。对于 $j$ 这维,我们可以贪心地依次选取 $b_j-a_j+m(n-j)$ 最小的,那么就将所有东西关于该值排序。之后枚举 $i$ 即可。 (如果 $n$ 出到 $10^6$ 大概就可以当NOID1T2难度的题了) 时间复杂度 $O(n\log n)$。 代码: ```cpp #include<bits/stdc++.h> using namespace std; typedef long long ll; int n,m,a[10100],b[10100]; ll o[10100],res,sum; int main(){ scanf("%d%d",&n,&m); for(int i=1;i<=n;i++)scanf("%d",&a[i]),res+=a[i]; for(int i=1;i<=n;i++)scanf("%d",&b[i]),o[i]=1ll*m*(n-i)+b[i]-a[i]; sort(o+1,o+n+1),sum=res; for(int i=1;i<=n;i++)res=min(res,sum+=o[i]-1ll*m*(i-1)); printf("%lld\n",res); return 0; } ``` # CXXV.[UOJ#455. 【UER #8】雪灾与外卖](https://uoj.ac/problem/455) 题意:有 $n$ 只老鼠和 $m$ 个洞,老鼠分别位于 $x_i$,洞分别位于 $y_j$,最多能塞下 $c_j$ 只老鼠,且塞一只老鼠的代价是 $w_j$。一只老鼠从位置 $i$ 移动到位置 $j$ 需要花费额外的代价 $|i-j|$。求使所有老鼠有洞进的最小代价。 考虑从左往右遍历每只老鼠和每个洞,计算匹配。 对于老鼠: 1. 与之前的一个洞匹配。 2. 抢走之前某只老鼠的洞。 自然,老鼠也可以留着匹配以后的洞。但是为了避免出现老鼠配不上洞的情形,我们强制老鼠必须配上一个洞。但万一真的没有剩的洞了 呢?那就让它配上一个代价为 $\infty$ 的虚洞,这样子就一定会被接下来的某个洞抢走。 对于洞: 1. 抢走之前某只洞的老鼠。 2. 等待后面的某只老鼠与其匹配。 这里发现洞的操作中没有直接与老鼠匹配的选项,因为老鼠都已经有匹配了(可能匹配的是虚洞)。 具体而言,我们分别对老鼠和洞各维护一个小根堆。 对于老鼠操作1和2,都直接与洞堆顶匹配即可。若洞堆顶的元素是 $cost$,则答案增加 $cost+x$,然后洞堆 `pop`。同时,为了方便退流,要同时再往老鼠堆中扔入一个 $-cost-x$。退流操作支持了洞操作1。 可能有人会问,对于老鼠操作2,那个被抢走洞的老鼠不需要配对吗?但是我们发现,有一个显然的性质是老鼠与洞间的连线不可能交叉(不然可以把叉打开使得答案严格不增),因此一对老鼠与洞不会同时被抢走。故这个情形只需在洞的操作中处理即可。 对于洞操作1,就直接与老鼠堆顶匹配即可。若老鼠堆顶的元素是 $cost$,则答案增加 $cost+w+y$。但是需要注意的是,当 $cost+w+y\geq0$ 时,抢了还不如不抢,因此此时就可以直接跳过操作1,执行操作2。同时,为了方便退流,还要往洞堆里扔入 $-(cost+w+y)+w-y$。退流操作支持了老鼠操作2。 同时,洞操作1还要兼顾我们上文没有进行的操作,即,那个被抢走老鼠的洞还需要配对。我们不选择在抢的洞处统计,而选择在被抢的洞处统计。因此,每执行一个洞操作1,我们同时还要往老鼠堆中扔入 $-w-y$,用来表示这个洞的老鼠被抢走了。 对于洞操作2,就直接往洞堆里扔入 $w-y$ 即可。洞操作2支持了老鼠操作1。 但是我们发现这样搞总操作数是 $O(\sum c)$ 级别的,不可能通过。但是,发现因为有着“一对老鼠与洞不会同时被抢走”的条件,所以如果能够使得每对只会被处理一次,复杂度就是正确的 $O(n)$。 于是,我们发现对于 $-w-y$ 的项与 $w-y$ 的项,其对于同一个洞的所有位置都是相等的。于是我们堆中元素需要两维,一维是值,一维是出现次数,即可将复杂度优化到均摊 $O(n)$。总复杂度 $O(n\log n)$。 如果要修改堆顶的元素的值可以使用 `mutable` 关键字。 代码: ```cpp #include<bits/stdc++.h> using namespace std; typedef long long ll; int n,m,px[100100],py[100100],fod[100100],cst[100100],all; ll res; struct dat{ll v;mutable int t;dat(ll V,int T){v=V,t=T;}friend bool operator<(const dat&x,const dat&y){return x.v>y.v;}}; priority_queue<dat>mou,hol; int main(){ scanf("%d%d",&n,&m); for(int i=1;i<=n;i++)scanf("%d",&px[i]); for(int i=1;i<=m;i++)scanf("%d%d%d",&py[i],&cst[i],&fod[i]),fod[i]=min(fod[i],n),all+=fod[i],all=min(all,n); if(all<n){puts("-1");return 0;} hol.emplace(0x3f3f3f3f3f,0x3f3f3f3f); for(int i=1,j=1;i<=n||j<=m;){ if(i<=n&&(px[i]<=py[j]||j>m)){ ll c=hol.top().v+px[i]; if(!--hol.top().t)hol.pop(); res+=c,mou.emplace(-c-px[i],1); i++; }else{ int tot=0; while(fod[j]&&!mou.empty()){ ll c=(mou.top().v+cst[j]+py[j]); if(c>=0)break; int mn=min(mou.top().t,fod[j]); res+=c*mn,hol.emplace(-c+cst[j]-py[j],mn); fod[j]-=mn,tot+=mn; if(!(mou.top().t-=mn))mou.pop(); } if(fod[j])hol.emplace(cst[j]-py[j],fod[j]); if(tot)mou.emplace(-cst[j]-py[j],tot); j++; } } printf("%lld\n",res); return 0; } ``` # CXXVI.[LOJ#6405. 「ICPC World Finals 2018」征服世界](https://loj.ac/p/6405) 依旧把“军队”叫做老鼠,“目的地”叫做洞。 其实在模拟费用流的题中还是比较套路的( 我们考虑在每个节点上开两个小根堆维护所有的老鼠和洞。众所周知的是,位置 $x$ 与 $y$ 之间路径长度是 $dis=dep_x+dep_y-2dep_{lca}$。于是我们考虑在一个点 $x$ 处合并子树中的路径。注意到路径的两个端点**可以**来自于 $x$ 的同一个儿子,因为这样就相当于多往 $x$ 绕了一圈,不如不绕。 在合并 $x,y$ 后,往 $x$ 堆中扔 $dep_x-dis$,$y$ 堆中扔 $dep_y-dis$,表示反悔。同时,为了强制所有的洞都被匹配,我们预先令所有的 $dep_y$ 全部减去一个 $\infty$,最后再加上即可。 合并当前节点与子树的堆可以使用启发式合并,或者使用可并堆~~虽然我不会~~。 前者时间复杂度 $O(n\log^2n)$,后者一个 $\log$。 代码: ```cpp #include<bits/stdc++.h> using namespace std; typedef long long ll; int n,head[250100],cnt,a[250100],b[250100]; ll dep[250100],res; struct edge{int to,next,val;}edge[500100]; void ae(int u,int v,int w){ edge[cnt].next=head[u],edge[cnt].to=v,edge[cnt].val=w,head[u]=cnt++; edge[cnt].next=head[v],edge[cnt].to=u,edge[cnt].val=w,head[v]=cnt++; } struct dat{ll v;mutable int t;dat(ll V,int T){v=V,t=T;}friend bool operator<(const dat&x,const dat&y){return x.v>y.v;}}; void merge(priority_queue<dat>&p,priority_queue<dat>&q){ if(p.size()<q.size())swap(p,q); while(!q.empty())p.push(q.top()),q.pop(); } priority_queue<dat>p[250100],q[250100]; void dfs(int x,int fa){ if(a[x])p[x].emplace(dep[x],a[x]); if(b[x])q[x].emplace(dep[x]-0x3f3f3f3f3f3f,b[x]); for(int i=head[x],y;i!=-1;i=edge[i].next){ if((y=edge[i].to)==fa)continue; dep[y]=dep[x]+edge[i].val,dfs(y,x); merge(p[x],p[y]); merge(q[x],q[y]); } while(!p[x].empty()&&!q[x].empty()){ ll X=p[x].top().v,Y=q[x].top().v; ll val=X+Y-2*dep[x]; if(val>=0)break; int mn=min(p[x].top().t,q[x].top().t); res+=val*mn; p[x].emplace(X-val,mn); q[x].emplace(Y-val,mn); if(!(p[x].top().t-=mn))p[x].pop(); if(!(q[x].top().t-=mn))q[x].pop(); } } int main(){ scanf("%d",&n),memset(head,-1,sizeof(head)); for(int i=1,x,y,z;i<n;i++)scanf("%d%d%d",&x,&y,&z),ae(x,y,z); for(int i=1;i<=n;i++){ scanf("%d%d",&a[i],&b[i]); if(a[i]>=b[i])a[i]-=b[i],b[i]=0;else b[i]-=a[i],a[i]=0; res+=0x3f3f3f3f3f3f*b[i]; } dfs(1,0),printf("%lld\n",res); return 0; } ``` # CXXVII.[[TJOI2010]电影迷](https://www.luogu.com.cn/problem/P3872) 拿一道水题练练板子。 随便建图然后跑最小割即可,事老套路。 代码: ```cpp #include<bits/stdc++.h> using namespace std; const int N=110; int n,m,sum; namespace NetworkFlows{ const int M=100100; int head[N],cur[N],cnt,dep[N],S,T,res; struct node{int to,next,val;}edge[M]; void ae(int u,int v,int w){ edge[cnt].next=head[u],edge[cnt].to=v,edge[cnt].val=w,head[u]=cnt++; edge[cnt].next=head[v],edge[cnt].to=u,edge[cnt].val=0,head[v]=cnt++; } queue<int>q; bool bfs(){ q.push(S),memset(dep,-1,sizeof(dep)),dep[S]=0; while(!q.empty()){int x=q.front();q.pop();for(int i=cur[x]=head[x],y;i!=-1;i=edge[i].next)if(dep[y=edge[i].to]==-1&&edge[i].val)dep[y]=dep[x]+1,q.push(y);} return dep[T]!=-1; } bool reach; int dfs(int x,int flow){ if(x==T){res+=flow,reach=true;return flow;} int used=0; for(int &i=cur[x];i!=-1;i=edge[i].next){ if(dep[edge[i].to]!=dep[x]+1||!edge[i].val)continue; int ff=dfs(edge[i].to,min(flow-used,edge[i].val)); if(ff){used+=ff,edge[i].val-=ff,edge[i^1].val+=ff;if(used==flow)return used;} } return used; } void Dinic(){while(bfs()){reach=true;while(reach)reach=false,dfs(S,0x3f3f3f3f);}} } using namespace NetworkFlows; int main(){ scanf("%d%d",&n,&m),memset(head,-1,sizeof(head)),S=n+1,T=n+2; for(int i=1,x;i<=n;i++){ scanf("%d",&x); if(x>0)sum+=x,ae(S,i,x); if(x<0)ae(i,T,-x); } for(int i=1,x,y,z;i<=m;i++)scanf("%d%d%d",&x,&y,&z),ae(x,y,z); Dinic(),printf("%d\n",sum-res); return 0; } ``` # CXXVIII.[CF1510B Button Lock](https://www.luogu.com.cn/problem/CF1510B) [题解](https://www.luogu.com.cn/blog/Troverld/solution-cf1510b) # CXXIX.[[ZJOI2011]营救皮卡丘](https://www.luogu.com.cn/problem/P4542) [题解](https://www.luogu.com.cn/blog/Troverld/solution-p4542) # CXXX.[CF1519F Chests and Keys](https://codeforces.ml/problemset/problem/1519/F) 我们不妨考虑,假如Alice已经决定了上锁方案,Bob的最优抉择是什么? 我们发现这长得很像最大权闭合子图问题。于是建图,从源点连向每个箱子权值单位流量,箱子流向其上的锁无穷大单位流量,锁流向汇点锁权值单位流量。则Bob最优抉择时的收益即为箱子权值和减去最小割。 Alice的上锁操作本质是决定连接箱子与锁的无穷大边的方案。而其目标是收益为 $0$(显然Bob不可能让收益为负,因为不进行任何操作时的收益即为 $0$),也即最小割是割去所有箱子边。 或许会想到这本质是一个带权二分图完美匹配问题,进而往Hall定理去想。但是这并非一个有出路的解法。更好的想法是将最小割转成最大流后,发现应满足最大流时所有箱边满流。于是考虑用DP模拟最大流。我们考虑依次决定每个箱子上哪些锁。设 $f_{i,j,k,l}$ 表示第 $i$ 个箱子,当前考虑了第 $j$ 把钥匙,第 $i$ 个箱子的剩余流量是 $k$,且所有钥匙的剩余流量状态是 $l$ 时的最小代价。直接枚举 $(i,j)$ 连不连边、如果连边流几点流量即可。 时间复杂度 $O(nma^2(b+1)^m)$。轻松跑过。 代码: ```cpp #include<bits/stdc++.h> using namespace std; int n,m,f[6][6][5][15625],a[7],b[6],c[7][6],pov[7],res=0x3f3f3f3f; void chmn(int&x,int y){if(x>y)x=y;} int main(){ scanf("%d%d",&n,&m),memset(f,0x3f,sizeof(f)); for(int i=0;i<n;i++)scanf("%d",&a[i]); int sta=0; pov[0]=1;for(int i=1;i<=m;i++)pov[i]=pov[i-1]*5; for(int i=0;i<m;i++)scanf("%d",&b[i]),sta+=b[i]*pov[i]; for(int i=0;i<n;i++)for(int j=0;j<m;j++)scanf("%d",&c[i][j]); f[0][0][a[0]][sta]=0; for(int i=0;i<n;i++)for(int j=0;j<m;j++)for(int k=0;k<=a[i];k++)for(int l=0;l<pov[m];l++)for(int r=0;r<=min((l/pov[j])%5,k);r++){ if(j==m-1){ if(r!=k)continue; if(i==n-1)chmn(res,f[i][j][k][l]+(!!r)*c[i][j]); else chmn(f[i+1][0][a[i+1]][l-pov[j]*r],f[i][j][k][l]+(!!r)*c[i][j]); }else chmn(f[i][j+1][k-r][l-pov[j]*r],f[i][j][k][l]+(!!r)*c[i][j]); } printf("%d\n",res==0x3f3f3f3f?-1:res); return 0; } ``` # CXXXI.[[CTSC2010]星际旅行](https://www.luogu.com.cn/problem/P4189) 假如只想知道从原点出发回到原点的最长路径应该怎么办? 显然,因为这是树,所以一条上述路径如果经过一条边,则必然经过其偶数遍。 而因为题目非常贴心地让一个点的离开次数不小于其度数,所以第一遍显然可以遍历整棵树。 这样之后,假如一条边两端点都还有次数没用完,显然我们可以来回一次使得两端点次数各减一,且路径长度加二。 然后我们发现这是多重匹配问题。 因为这是在树上,所以我们显然可以贪心,即自儿子向父亲转移,每次只要能匹配就匹配。显然在此种情形下不可能出现退流,也即答案最优。 现在考虑我们已经知道了自原点出发到某点 $x$ 的最长路径,现在想递推出到 $x$ 的儿子 $y$ 的最长路径。 显然,如果 $x$ 有次数没用光,则可以直接令 $x$ 的次数减一,答案加一,这样就能转移到 $y$; 否则,则 $x$ 的次数用光,我们需要退一次流,令 $x,y$ 次数各加一,答案减二,这样 $x$ 就有次数,可以减了。 当然,在退流后,$y$ 也有可能找到一个儿子匹配。那就找一下就行了。 时间复杂度 $O(n)$。 代码: ```cpp #include<bits/stdc++.h> using namespace std; int n,a[50100],res[50100]; vector<int>v[50100]; void dfs1(int x,int fa){ for(auto y:v[x])if(y!=fa){ dfs1(y,x); int k=min(a[x],a[y]); res[1]+=2*k,a[x]-=k,a[y]-=k; } } void dfs2(int x,int fa){ for(auto y:v[x])if(y!=fa){ res[y]=res[x]+1; if(a[x])a[x]--,dfs2(y,x),a[x]++; else{ a[y]++,res[y]-=2; int Z=-1; for(auto z:v[y])if(z!=x&&a[z]){a[y]--,a[z]--,res[y]+=2,Z=z;break;} dfs2(y,x); a[y]--; if(Z!=-1)a[y]++,a[Z]++; } } } int main(){ scanf("%d",&n); for(int i=1;i<=n;i++)scanf("%d",&a[i]); for(int i=1,x,y;i<n;i++)scanf("%d%d",&x,&y),res[1]+=2,a[++x]--,a[++y]--,v[x].push_back(y),v[y].push_back(x); dfs1(1,0),dfs2(1,0); for(int i=1;i<=n;i++)printf("%d\n",res[i]); return 0; } ``` # CXXXII.[[Aizu1410]Draw in Straight Lines](https://vjudge.net/problem/Aizu-1410) 我们可以将每个格子分成4种类型: - 被横着的黑色颜料涂了,记作A。 - 被横着的白色颜料涂了,记作B。 - 被竖着的黑色颜料涂了,记作C。 - 被竖着的白色颜料涂了,记作D。 当然,一个格子也有可能作为一个单独的点被上色,但这里我们把它当作补救的措施,也即如果上述操作不能满足要求或者代价过大,就使用补救措施。 现在考虑有什么性质可以被得出: - A,B同时在一个格子上染色是不合法的。因为白格不能再被涂黑,所以操作顺序必然是先涂A再涂B;但是这样的话: - 如果B仅仅覆盖了A的一段前后缀,那还不如各退一步,让这段被覆盖两次的部分一次也不被覆盖。 - 如果B覆盖了A中间的一段,那倒不如将A分两次涂,这样还是涂两次,不过总计上色的格子数显然减少了。 同理,C,D同时出现也是不合法的。 - 如果一个格子是黑格的话: 其A,C中至少得有一个,且B,D中任何一个都不能出现。 前半句话如果不成立就有 $c$ 的代价手动上色,后半句话必须成立。 - 如果一个格子是白格的话: 其要么A且D,要么B且C。 AB和CD的组合已经被我们否决过了。 AC不能同时出现。且如果AC中出现了一个但BD中却没有再出现一个,也要花费 $c$ 的代价。 发现这个“如果?且?,则代价是?”的模型非常像最小割。尝试建图。 我们对于每个格子开A,B,C,D四个点。经过手动试验,发现B,C在S侧而A,D在T侧是一个合法的选择。如果一个元素与其所在侧的边被割断就表示这个点出现,反之则不出现。 现在考虑连边。 - $(S,B,a),(S,C,a),(A,T,a),(D,T,a)$,表示被选择就要花 $a$ 的代价。 - $(A,B,\infty),(D,C,\infty)$。如果 $A,B$ 同时被选,等价于存在 $S\rightarrow A$ 且 $T\rightarrow B$,就会使得此种情形不合法。 - 假如我们设带 $'$ 的变量表示在当前格**右侧**的格子,那么连边 $(A,A',b)$,其它变量同理向其它方向对应连边。(例如,连边 $(B',B,b)$) 我们考虑这条边何时会被删去。显然,是在 $S,A$ 联通且 $A',T$ 联通的情形。$A',T$ 联通就说明 $A'$ 没被选,$S,A$ 联通就等价于 $A,T$ 的边被断(不然就出现了 $S$ 到 $T$ 的通路),进而等价于 $A$ 被选。相邻格子出现了一个选上一个没被选上,就表明新建了一段连续的A。 需要注意的是,如果当前格子是最右一行的格子(这就意味着不存在 $A'$),我们应该将边 $(A,T)$ 的代价换成 $a+b$。 - 假如当前格是黑格: - 限制“A,C至少有一个,否则代价为 $c$”:连边 $(C,A,c)$。这条边会在A,C的边都未被断去(即A,C都没有)的情形下被断去,代价为 $c$。 - 限制“B,D一个都不能有”:连边 $(S,B,\infty),(D,T,\infty)$,这样便不可能切断它们与对应侧的边。 - 假如当前格是白格: - 限制“A,C不同时出现”:连边 $(A,C,\infty)$。这条边会联通 $S,T$,当且仅当 $S\rightarrow A$ 且 $C\rightarrow T$。而这两个条件各自等价于 $(A,T)$ 边被断和 $(S,C)$ 边被断,也即 $A,C$ 同时被选。 - 限制“如果A那么D否则代价 $c$,如果C那么B否则代价 $c$”:连边 $(A,D,c),(B,C,c)$。以前者为例,其会被断当且仅当 $S,A$ 联通即A被选,$D,T$ 联通即D不被选。 跑就完事了。 代码: ```cpp #include<bits/stdc++.h> using namespace std; int n,m,a,b,c; char s[50][50]; namespace MaxFlow{ const int N=7000,M=500000; int head[N],cur[N],dep[N],cnt,S,T,res; struct node{int to,next,val;}edge[M]; void ae(int u,int v,int w){ edge[cnt].next=head[u],edge[cnt].to=v,edge[cnt].val=w,head[u]=cnt++; edge[cnt].next=head[v],edge[cnt].to=u,edge[cnt].val=0,head[v]=cnt++; } queue<int>q; inline bool bfs(){ memset(dep,0,sizeof(dep)),q.push(S),dep[S]=1; while(!q.empty()){ register int x=q.front();q.pop(); for(register int i=cur[x]=head[x];i!=-1;i=edge[i].next)if(edge[i].val&&!dep[edge[i].to])dep[edge[i].to]=dep[x]+1,q.push(edge[i].to); } return dep[T]>0; } bool reach; inline int dfs(int x,int flow){ if(x==T){res+=flow,reach=true;return flow;} int used=0; for(register int &i=cur[x];i!=-1;i=edge[i].next){ if(!edge[i].val||dep[edge[i].to]!=dep[x]+1)continue; register int ff=dfs(edge[i].to,min(edge[i].val,flow-used)); if(ff){edge[i].val-=ff,edge[i^1].val+=ff,used+=ff;if(used==flow)break;} } return used; } inline void Dinic(){while(bfs()){reach=true;while(reach)reach=false,dfs(S,0x3f3f3f3f);}} } using namespace MaxFlow; int main(){ scanf("%d%d%d%d%d",&n,&m,&a,&b,&c),S=4*n*m,T=4*n*m+1,memset(head,-1,sizeof(head)); for(int i=0;i<n;i++)scanf("%s",s[i]); for(int i=0;i<n;i++)for(int j=0;j<m;j++){ ae(i*m+j,j+1<m?i*m+(j+1):T,b); ae(j+1<m?n*m+i*m+(j+1):S,n*m+i*m+j,b); ae(i+1<n?2*n*m+(i+1)*m+j:S,2*n*m+i*m+j,b); ae(3*n*m+i*m+j,i+1<n?3*n*m+(i+1)*m+j:T,b); } for(int i=0;i<n;i++)for(int j=0;j<m;j++){ int A=i*m+j,B=n*m+A,C=n*m+B,D=n*m+C; ae(S,B,a),ae(S,C,a),ae(A,T,a),ae(D,T,a); ae(A,B,0x3f3f3f3f),ae(D,C,0x3f3f3f3f); if(s[i][j]=='#')ae(C,A,c),ae(S,B,0x3f3f3f3f),ae(D,T,0x3f3f3f3f); else ae(A,C,0x3f3f3f3f),ae(A,D,c),ae(B,C,c); } Dinic(); printf("%d\n",res); return 0; } ``` # CXXXIII.[[CTSC2010]产品销售](https://www.luogu.com.cn/problem/P4217) 首先,一个暴力的建图是可以得到的: - 建立 $n$ 个点表示每个季度。 - 源点向每个点连边 $(U_i,P_i)$,表示生产。 - 每个点向汇点连边 $(D_i,0)$,表示销售。 - 每个点向下一个点连边 $(\infty,M_i)$,表示储存产品。 - 下一个点向每个点连边 $(\infty,C_i)$,表示延期订单。 暴力跑费用流,TLE30。 代码: ```cpp #include<bits/stdc++.h> using namespace std; int n; typedef long long ll; namespace MCMF{ const int N=100100,M=1000000; int head[N],cnt,dis[N],S,T; ll cost; struct node{ int to,next,val,cost; }edge[M]; void ae(int u,int v,int w,int c){ edge[cnt].cost=c,edge[cnt].next=head[u],edge[cnt].to=v,edge[cnt].val=w,head[u]=cnt++; edge[cnt].cost=-c,edge[cnt].next=head[v],edge[cnt].to=u,edge[cnt].val=0,head[v]=cnt++; } queue<int>q; bool in[N],reach; bool SPFA(){ q.push(S),memset(dis,0x3f,sizeof(dis)),dis[S]=0,in[S]=true; while(!q.empty()){ int x=q.front();q.pop(),in[x]=false; for(int i=head[x];i!=-1;i=edge[i].next)if(edge[i].val&&dis[edge[i].to]>dis[x]+edge[i].cost){ dis[edge[i].to]=dis[x]+edge[i].cost; if(!in[edge[i].to])q.push(edge[i].to),in[edge[i].to]=true; } } return dis[T]!=0x3f3f3f3f; } int dfs(int x,int flow){ if(x==T){cost+=1ll*flow*dis[T],reach=true;return flow;} int used=0; for(int i=head[x];i!=-1;i=edge[i].next){ if(dis[edge[i].to]!=dis[x]+edge[i].cost||!edge[i].val||in[edge[i].to])continue; in[edge[i].to]=true; int ff=dfs(edge[i].to,min(flow-used,edge[i].val)); in[edge[i].to]=false; if(ff){ edge[i].val-=ff; edge[i^1].val+=ff; used+=ff; if(used==flow)return used; } } return used; } void Dinic(){while(SPFA()){reach=true;while(reach)reach=false,in[S]=true,dfs(S,0x3f3f3f3f),in[S]=false;}} } using namespace MCMF; int u[100100]; int main(){ scanf("%d",&n),S=n+1,T=n+2,memset(head,-1,sizeof(head)); for(int i=1,x;i<=n;i++)scanf("%d",&x),ae(i,T,x,0); for(int i=1;i<=n;i++)scanf("%d",&u[i]); for(int i=1,x;i<=n;i++)scanf("%d",&x),ae(S,i,u[i],x); for(int i=1,x;i<n;i++)scanf("%d",&x),ae(i,i+1,0x3f3f3f3f,x); for(int i=1,x;i<n;i++)scanf("%d",&x),ae(i+1,i,0x3f3f3f3f,x); Dinic(); printf("%lld\n",cost); return 0; } ``` 考虑模拟费用流。 首先,终态时所有连到汇点的边总是满流。这就意味着其不可能出现退流。这也意味着我们可以从左到右枚举每个点连到汇点的边,判断其到底是由左方的点增广还是右方的点增广更优。 因为我们是从左往右枚举每个点的,所以就能发现从左往右的增广路不会出现退流——按照此种方案其反悔边不会被任何增广路通过。这会使得操作简便。 先考虑其被右方点增广。显然这种增广不可能遇到反悔边。那么此次增广就找到代价最小的点,流来 $\min(\text{当前点剩余容量},\text{代价最小点剩余流量})$ 即可。 再考虑其被左方点增广。则这种增广可能遇到反悔边。在流量不超过反悔边的容量之前,该边的权值是负的(即为反悔边的权值);在流量超过之后,该边的权值是正的。 显然我们单次增广时应保证单位流量的代价始终相同,不然就不好计算。那么我们仍然找到代价最小点,此次流量就应该是 $\min(\text{当前点剩余容量},\text{代价最小点剩余流量},\text{路径上反悔边容量最小值})$。 其被右方点增广时,需要建立反悔边,那么就把路径上所有边的返回边权值增加此次增广的流量即可。 其被左方点增广时,按照我们之前的分析反悔边不可能被经过,那么就不建立反悔边辣。 现在考虑分析复杂度。 显然对于被右方点增广的情形,每次增广点与被增广点必须死一个,那么这个复杂度肯定没问题。 而对于被左方点增广的情形,如果此次增广的流量是增广点流量上限或者被增广点流量上限那么肯定没问题;而如果流量是某条反悔边的流量上限的话——显然对于一条反悔边,当被增广点在其左侧时权值不降,而当被增广点在其右侧时权值不增,那么每条反悔边也只能死一次,不会复活。所以总增广数是 $O(n)$ 级别的。 那么就用数据结构随便维护就行了。右方点增广显然可以线段树,左方点增广也可以线段树,但是需要在上面二分出所有在此次增广中死掉的反悔边,之后修改其权值为正常边。 时间复杂度 $O(n\log n)$。 写起来非常恶心,因为你需要实现三棵不同的线段树——一棵处理右方点的增广,一棵处理被右方点增广时建立的反悔边的权值,一棵处理左方点的增广。细节多得要命,码量也大得要命。 代码: ```cpp #include<bits/stdc++.h> using namespace std; typedef long long ll; int n,D[100100],U[100100],Pr[100100],R[100100],L[100100]; #define lson x<<1 #define rson x<<1|1 #define mid ((l+r)>>1) namespace RS{//rightward segtree struct SegTree{ int mn,mnp; SegTree(){mn=0x7f7f7f7f,mnp=0;} SegTree(int MN,int MNP){mn=MN,mnp=MNP;} friend SegTree operator+(const SegTree&u,const SegTree&v){return u.mn<v.mn?u:v;} }seg[400100]; void build(int x,int l,int r){ if(l==r)seg[x]=SegTree(Pr[l]+L[l],l); else build(lson,l,mid),build(rson,mid+1,r),seg[x]=seg[lson]+seg[rson]; } void kill(int x,int l,int r,int P){ if(l==r)return void(seg[x]=SegTree()); if(P<=mid)kill(lson,l,mid,P);else kill(rson,mid+1,r,P); seg[x]=seg[lson]+seg[rson]; } SegTree query(int x,int l,int r,int P){ if(r<P)return SegTree(); if(l>=P)return seg[x]; return query(lson,l,mid,P)+query(rson,mid+1,r,P); } } namespace LS{//leftward segtree namespace OS{//the segtree of occurances struct SegTree{ int mn,mnp,tag; SegTree(){mn=mnp=tag=0;} SegTree(int MN,int MNP){mn=MN,mnp=MNP,tag=0;} friend SegTree operator+(const SegTree&u,const SegTree&v){ SegTree w=u.mn<v.mn?u:v; w.tag=0; return w; } }seg[400100]; void build(int x,int l,int r){ if(l==r)seg[x].mnp=l; else build(lson,l,mid),build(rson,mid+1,r),seg[x]=seg[lson]+seg[rson]; } void ADD(int x,int y){seg[x].mn+=y,seg[x].tag+=y;} void pushdown(int x){ADD(lson,seg[x].tag),ADD(rson,seg[x].tag),seg[x].tag=0;} void modify(int x,int l,int r,int L,int R,int val){ if(l>R||r<L)return; if(L<=l&&r<=R){ADD(x,val);return;} pushdown(x),modify(lson,l,mid,L,R,val),modify(rson,mid+1,r,L,R,val),seg[x]=seg[lson]+seg[rson]; } void kill(int x,int l,int r,int P){ if(l==r)return seg[x]=SegTree(0x3f3f3f3f,0),void(); pushdown(x); if(P<=mid)kill(lson,l,mid,P);else kill(rson,mid+1,r,P); seg[x]=seg[lson]+seg[rson]; } SegTree query(int x,int l,int r,int L,int R){ if(l>R||r<L)return SegTree(0x3f3f3f3f,0); if(L<=l&&r<=R)return seg[x]; pushdown(x);return query(lson,l,mid,L,R)+query(rson,mid+1,r,L,R); } } namespace TS{//the segtree of transformation struct SegTree{ int mn,sum,mnp; SegTree(){mn=0x3f3f3f3f,sum=mnp=0;} SegTree(int MN,int SUM,int MNP){mn=MN+SUM,sum=SUM,mnp=MNP;} friend SegTree operator+(SegTree u,SegTree v){ u.mn+=v.sum; SegTree w=(u.mn<v.mn?u:v); w.sum=u.sum+v.sum; return w; } }seg[400100]; void turnon(int x,int l,int r,int P,int tp){ if(l==r)return seg[x]=SegTree(Pr[P],tp==1?R[P]:-(L[P+1]-L[P]),P),void(); if(P<=mid)turnon(lson,l,mid,P,tp);else turnon(rson,mid+1,r,P,tp); seg[x]=seg[lson]+seg[rson]; } void turnoff(int x,int l,int r,int P){ if(l==r)return seg[x].mn=0x3f3f3f3f,seg[x].mnp=0,void(); if(P<=mid)turnoff(lson,l,mid,P);else turnoff(rson,mid+1,r,P); seg[x]=seg[lson]+seg[rson]; } SegTree query(int x,int l,int r,int P){ if(l>=P)return SegTree(); if(r<P)return seg[x]; return query(lson,l,mid,P)+query(rson,mid+1,r,P); } } } ll res; int main(){ scanf("%d",&n); for(int i=1;i<=n;i++)scanf("%d",&D[i]); for(int i=1;i<=n;i++)scanf("%d",&U[i]); for(int i=1;i<=n;i++)scanf("%d",&Pr[i]); for(int i=1;i<n;i++)scanf("%d",&R[i]); for(int i=2;i<=n;i++)scanf("%d",&L[i]),L[i]+=L[i-1]; RS::build(1,1,n),LS::OS::build(1,1,n); for(int i=1;i<=n;i++){ if(i>1){ int tms=LS::OS::query(1,1,n,i-1,i-1).mn; if(!tms)LS::OS::kill(1,1,n,i-1); LS::TS::turnon(1,1,n,i-1,!tms); if(!U[i-1])LS::TS::turnoff(1,1,n,i-1); } while(D[i]){ auto Pl=LS::TS::query(1,1,n,i); auto Pr=RS::query(1,1,n,i); Pr.mn-=L[i]; if(Pl.mn<=Pr.mn){ auto Ps=LS::OS::query(1,1,n,Pl.mnp,i-1); int flow=min(min(D[i],U[Pl.mnp]),Ps.mn); D[i]-=flow,U[Pl.mnp]-=flow,LS::OS::modify(1,1,n,Pl.mnp,i-1,-flow),Ps.mn-=flow; if(!U[Pl.mnp])LS::TS::turnoff(1,1,n,Pl.mnp); while(!Ps.mn){ LS::OS::kill(1,1,n,Ps.mnp); LS::TS::turnon(1,1,n,Ps.mnp,1); if(!U[Ps.mnp])LS::TS::turnoff(1,1,n,Ps.mnp); Ps=LS::OS::query(1,1,n,Pl.mnp,i-1); } res+=1ll*flow*Pl.mn; }else{ int flow=min(D[i],U[Pr.mnp]); D[i]-=flow,U[Pr.mnp]-=flow,LS::OS::modify(1,1,n,i,Pr.mnp-1,flow); if(!U[Pr.mnp])RS::kill(1,1,n,Pr.mnp); res+=1ll*flow*Pr.mn; } } } printf("%lld\n",res); return 0; } ``` # CXXXIV.[CF1307G Cow and Exercise](https://www.luogu.com.cn/problem/CF1307G) 我们考虑逐渐增大边权和的增量。 显然,在最初的时候,我们会不断让最短路集合中所有路径长度全部增加相同的值,直到最短路长度与次短路相同;然后,我们就会把次短路加入最短路集合,继续每次往其中增加相同值,直到最短路长度与三短路长度相同…… 于是我们考虑所有最短路上的边构成的子图。显然我们增加边的时候要选择最少的覆盖住了所有路径的边。 然后我们发现该子图,如果忽略边权,则其**最小割**是一种符合条件的边集。 这时候,我们考虑在边的容量全为 $1$、费用为给定边权的图上,运行费用流的EK算法。 显然,EK算法中,我们先增广出一条最短路并删除。然后找到一条与其无交(假如不存在退流的话)的最短路,继续删除之。 显然每条最短路都ban掉了一些交集非空的最短路,换言之就是确立了最小割上一条边,而这条边显然就可以当作我们增加权值的对象。 那么我们每次把增广结束后的流量与权值打包扔到 `vector` 中。回答询问时,就在 `vector` 里找到 $\dfrac{cost+x}{flow}$ 的 $\min$ 作为答案即可——因为显然,我们肯定要使得 $flow$ 中每条路径的 $cost$ 全部相等。 问题来了,为什么是那些东西的 $\min$ 呢? 显然,如果我们选择了 $flow$ 过小的状态,则其实际意义是我们的权值只使最短路集合中部分路径的权值增加了,显然大于使全部路径增加的答案;而如果我们选择了 $flow$ 过大的状态,则其实际意义是我们把一些非最短路也拉过来平均了,显然要大于只平均最短路的答案。 代码: ```cpp #include<bits/stdc++.h> using namespace std; int n,m,Q; vector<pair<int,int> >v; namespace MCMF{ int S,T,dis[60],fr[60],res,cost,cnt,head[60]; struct node{int to,next,val,cost;}edge[5010]; void ae(int u,int v,int w){ edge[cnt].cost=w,edge[cnt].next=head[u],edge[cnt].to=v,edge[cnt].val=1,head[u]=cnt++; edge[cnt].cost=-w,edge[cnt].next=head[v],edge[cnt].to=u,edge[cnt].val=0,head[v]=cnt++; } bool in[60]; queue<int>q; bool SPFA(){ memset(dis,0x3f,sizeof(dis)),in[S]=true,dis[S]=0,q.push(S); while(!q.empty()){ int x=q.front();q.pop(),in[x]=false; for(int i=head[x];i!=-1;i=edge[i].next)if(edge[i].val&&dis[edge[i].to]>dis[x]+edge[i].cost){ dis[edge[i].to]=dis[x]+edge[i].cost,fr[edge[i].to]=i; if(!in[edge[i].to])in[edge[i].to]=true,q.push(edge[i].to); } } // for(int i=1;i<=n;i++)printf("%d ",dis[i]);puts(""); if(dis[T]==0x3f3f3f3f)return false; int mn=0x3f3f3f3f; for(int x=T;x!=S;x=edge[fr[x]^1].to)mn=min(mn,edge[fr[x]].val); res+=mn,cost+=dis[T]*mn; // printf("%d %d\n",res,cost); v.emplace_back(res,cost); for(int x=T;x!=S;x=edge[fr[x]^1].to)edge[fr[x]].val-=mn,edge[fr[x]^1].val+=mn; return true; } } using namespace MCMF; int main(){ scanf("%d%d",&n,&m),S=1,T=n,memset(head,-1,sizeof(head)); for(int i=1,x,y,z;i<=m;i++)scanf("%d%d%d",&x,&y,&z),ae(x,y,z); while(SPFA()); scanf("%d",&Q); for(int i=1,x;i<=Q;i++){ scanf("%d",&x); double mx=0x3f3f3f3f; for(auto p:v)mx=min(mx,1.0*(p.second+x)/p.first); printf("%lf\n",mx); } return 0; } ``` # CXXXV.[[ARC107F]Sum of Abs](https://atcoder.jp/contests/arc107/tasks/arc107_f) 考虑每个 $B_i$,其系数要么是 $1$,要么是 $0$,要么是 $-1$。 并且,两个被同一条边连着的点的系数不应该一个是 $1$,一个是 $-1$。 模型应该很鲜明了。拆点。 连边 $(S,i,B_i),(i,i',A_i),(i',T,-B_i)$,断掉就表示了各自被选。 对于有边的 $(i,j)$,连边 $(i',j,\infty),(j',i,\infty)$。 显然其最小割的相反数即为答案。 但是有负权边。那么就把所有边的边权统一加上 $10^6$ 即可。 代码: ```cpp #include<bits/stdc++.h> using namespace std; const int inf=1e6; int n,m; namespace MaxFlow{ const int N=610,M=200000; int head[N],cur[N],dep[N],cnt,S,T,res; struct node{int to,next,val;}edge[M]; void ae(int u,int v,int w){ // printf("%d %d %d\n",u,v,w); edge[cnt].next=head[u],edge[cnt].to=v,edge[cnt].val=w,head[u]=cnt++; edge[cnt].next=head[v],edge[cnt].to=u,edge[cnt].val=0,head[v]=cnt++; } queue<int>q; inline bool bfs(){ memset(dep,0,sizeof(dep)),q.push(S),dep[S]=1; while(!q.empty()){ register int x=q.front();q.pop(); for(register int i=cur[x]=head[x];i!=-1;i=edge[i].next)if(edge[i].val&&!dep[edge[i].to])dep[edge[i].to]=dep[x]+1,q.push(edge[i].to); } return dep[T]>0; } bool reach; inline int dfs(int x,int flow){ if(x==T){res+=flow,reach=true;return flow;} int used=0; for(register int &i=cur[x];i!=-1;i=edge[i].next){ if(!edge[i].val||dep[edge[i].to]!=dep[x]+1)continue; register int ff=dfs(edge[i].to,min(edge[i].val,flow-used)); if(ff){edge[i].val-=ff,edge[i^1].val+=ff,used+=ff;if(used==flow)break;} } return used; } inline void Dinic(){while(bfs()){reach=true;while(reach)reach=false,dfs(S,0x3f3f3f3f);}} } using namespace MaxFlow; int main(){ scanf("%d%d",&n,&m),S=2*n+1,T=2*n+2,memset(head,-1,sizeof(head)); for(int i=1,x;i<=n;i++)scanf("%d",&x),ae(i,i+n,x+inf); for(int i=1,x;i<=n;i++)scanf("%d",&x),ae(S,i,x+inf),ae(i+n,T,-x+inf); for(int i=1,x,y;i<=m;i++)scanf("%d%d",&x,&y),ae(x+n,y,0x3f3f3f3f),ae(y+n,x,0x3f3f3f3f); Dinic(); printf("%d\n",n*inf-res); return 0; } ``` # CXXXVI.[CF1383F Special Edges](https://www.luogu.com.cn/problem/CF1383F) 最大流不好搞,考虑最小割。 最小割就可以枚举前 $k$ 条边中有哪些在割集内。 于是我们预处理出 $f(i)$ 表示 $i$ 中的边不在割集内时的答案。(为什么是“不在”呢?因为我代码就是这么写的) 假设我们现在已经得到了 $f$,那么我们直接对于每个询问 $2^k$ 枚举割集求 $\min$ 即可。这部分可以用 `lowbit` 计算子集和来做到 $q2^k$。 那么我们现在考虑求出 $f$。有个显然的想法是对于每个 $f(i)$,找到其中的某个元素 $j$,在 $f(i\setminus j)$(其中 $\setminus$ 是从集合中删除某数的意思)的残量网络上令 $j$ 这条边满流,即可得到 $f(i)$ 的网络,那么再跑一遍网络流就能得到 $f(i)$。 假如跑常规 `Dinic` 的话复杂度很玄学,不一定过得去;但是我们可以**爆搜**。显然每遍爆搜至少搜出一点流量,而新增的边最大权值也不过 $25$,那么至多搜 $25$ 轮就能增广完毕。这样,求出 $f(i)$ 就只需要 $O(\text{Dinic}+2^kmw)$ 就能解决(那个 `Dinic` 是因为要求出 $f(0)$ 需要一遍网络流)。 听说这种爆搜还有名字叫做 `FF` 算法。 一些卡常小技巧: - 预处理。包括但不限于 $2^k$、`lowbit` 等。 - 在求出 $f$ 时不使用递归搜索。(可以参见代码) - `FF` 爆搜时使用 `bfs` 而非 `dfs`。(我先写的 `dfs` 然后 `TLE on 89`,换成 `bfs` 却是 `TLE on 87`。但是想了想把 `bfs` 换成搜到 `T` 就结束,然后就过了) 最终 `4.93s` 惊险卡过。 代码: ```cpp #include<bits/stdc++.h> using namespace std; const int lim=25; const int N=10003,M=20000; struct MaxFlow{ int head[N],cur[N],dep[N],cnt,S,T,res; MaxFlow(){memset(head,-1,sizeof(head));} struct node{int to,next,val;}edge[M]; void ae(int u,int v,int w){ edge[cnt].next=head[u],edge[cnt].to=v,edge[cnt].val=w,head[u]=cnt++; edge[cnt].next=head[v],edge[cnt].to=u,edge[cnt].val=0,head[v]=cnt++; } queue<int>q; inline bool bfs(){ memset(dep,0,sizeof(dep)),q.push(S),dep[S]=1; while(!q.empty()){ register int x=q.front();q.pop(); for(register int i=cur[x]=head[x];i!=-1;i=edge[i].next)if(edge[i].val&&!dep[edge[i].to])dep[edge[i].to]=dep[x]+1,q.push(edge[i].to); } return dep[T]>0; } bool reach; inline int dfs(int x,int flow){ if(x==T){res+=flow,reach=true;return flow;} int used=0; for(register int &i=cur[x];i!=-1;i=edge[i].next){ if(!edge[i].val||dep[edge[i].to]!=dep[x]+1)continue; register int ff=dfs(edge[i].to,min(edge[i].val,flow-used)); if(ff){edge[i].val-=ff,edge[i^1].val+=ff,used+=ff;if(used==flow)break;} } return used; } inline void Dinic(){while(bfs()){reach=true;while(reach)reach=false,dfs(S,0x3f3f3f3f);}} int fr[N]; inline bool Augment(){ memset(dep,0,sizeof(dep)); while(!q.empty())q.pop(); q.push(S),dep[S]=0x3f3f3f3f; while(!q.empty()){ register int x=q.front();q.pop(); if(x==T)break; for(register int i=head[x];i!=-1;i=edge[i].next)if(edge[i].val&&!dep[edge[i].to])dep[edge[i].to]=min(dep[x],edge[i].val),fr[edge[i].to]=i,q.push(edge[i].to); } if(!dep[T])return false; res+=dep[T]; for(int x=T;x!=S;x=edge[fr[x]^1].to)edge[fr[x]].val-=dep[T],edge[fr[x]^1].val+=dep[T]; return true; } inline void FF(){while(Augment());} }G[11]; int cnt,bin[20],tp,id[1<<10]; int n,m,k,q,a[10]; int f[1<<10]; inline void read(int&x){ x=0; char c=getchar(); while(c>'9'||c<'0')c=getchar(); while(c>='0'&&c<='9')x=(x<<3)+(x<<1)+(c^48),c=getchar(); } inline void print(int x){if(x<=9)putchar('0'+x);else print(x/10),putchar('0'+x%10);} int g[1<<10],lb[1<<10],num; int main(){ read(n),read(m),read(k),read(q),G[0].S=1,G[0].T=n,num=1<<k; for(register int i=0,x,y,z;i<m;i++)read(x),read(y),read(z),G[0].ae(x,y,i<k?0:z); G[0].Dinic(); for(int i=0;i<num;i++){ f[i]=G[id[i]].res; for(int j=0;j<k;j++){ if(i&(1<<j))break; G[id[i|(1<<j)]=(tp?bin[tp--]:++cnt)]=G[id[i]]; G[id[i|(1<<j)]].edge[j<<1].val=lim; G[id[i|(1<<j)]].FF(); } bin[++tp]=id[i]; } // for(int i=0;i<(1<<k);i++)printf("%d ",f[i]);puts(""); for(int i=1;i<num;i++)for(lb[i]=0;;lb[i]++)if(i&(1<<lb[i]))break; for(register int i=1;i<=q;i++){ register int ans=0x3f3f3f3f; for(register int j=0;j<k;j++)read(a[j]); for(register int msk=1;msk<num;msk++)g[msk]=g[msk^(1<<lb[msk])]+a[lb[msk]]; for(register int msk=0;msk<num;msk++)ans=min(ans,f[msk]+g[(num-1)^msk]); print(ans),putchar('\n'); } return 0; } ``` # CXXXVII.[[ICPC2017 WF]Son of Pipe Stream](https://www.luogu.com.cn/problem/P6938) > Translation 1.我们将 `Flubber` 译作“亚水”。 这个翻译怎么来的呢?我也不造。 > Observation 1.假如我们取消“亚水”的粘度限制 $v$,然后就能发现对于任何结果只需要将“亚水”的流量除以 $v$ 就能得到考虑粘度时的流量。于是只需要将答案除以 $v^a$ 即可。 > Algorithm 1.建立伪源点 $S$,并向 $1$(亚水厂)和 $2$(水厂)连边,边权为正无穷。令汇点为 $3$ 号点(混合物的终点)。设最大流是 $A$。 > Algorithm 2.求出只考虑水时的最大流量 $F$ 以及只考虑亚水时的最大流量 $W$。 > Observation 2.考虑令某个极大情况(指水和亚水在另一种液体不退流的前提下都已经满流)下水的流量是 $w$,亚水的流量是 $f$。则必有 $w\leq W,f\leq F,w+f=A$。 前两条显然成立。考虑证明最后一条性质。 显然,必有 $W+F\geq A$,因为 $A$ 时水和亚水的流量肯定分别小于 $W$ 和 $F$。 然后,当水的流量取到最大值 $W$ 时,我们可以证明此时的 $f=A-W$。具体而言,我们可以先仅连接 $(S,1)$ 的边,然后流光 $W$;接着,连接 $(S,2)$,在残量网络上继续流 $A-W$。因为任意增广总能增广出最大流,所以必然能够流出 $A-W$;然后,因为显然我们不会向 $S$ 退流,所以流完的时候 $w=W$。因此,$(W,A-W)$ 必然可以流出。同理,$(A-F,F)$ 必然亦可以流出。 我们考虑将 $(W,A-W)$ 时的残量网络的所有边上流量全部乘以一个 $\alpha\in(0,1)$;同理,在 $F$ 的所有边上流量全部乘以 $1-\alpha$;合并两者,显然仍然得到一张合法的网络,因为就算某条边在 $(W,A-W)$ 与 $(A-F,F)$ 中均满流,在新图中也不过是满流罢了。新图中流量为 $\Big(\alpha(W+F)+A-F-\alpha A,\alpha A+F-\alpha(W+F)\Big)$,加一块就会发现等于 $A$。 并且,因为 $\alpha$ 的取值是连续的,所以任意 $(w,f)$ 满足 $w\in[A-F,W]\land w+f=A$ 均是可以拼凑出的。 现在考虑 $f^aw^{1-a}$ 的贡献式。求导得出其是单峰的,且极值在 $f=aA$ 处取得。 但是有可能 $f\notin[A-W,F]$。但是因为其单峰,是容易解决的。 但是有可能叠加 $(W,A-W)$ 与 $(A-F,F)$ 时出现一根水管里水和亚水的方向相反。 但我们可以这样构造:令极值时的流量是 $(w,f)$。连边 $(S,1,f),(S,2,w)$,跑最大流。新建图,图中所有边的容量为旧图中流量。连边 $(S,1,f)$,则每条边此时流量即为亚水流量,容量减流量即为水流量。 别忘记亚水流量要除以 $v$。 代码: ```cpp #include<bits/stdc++.h> using namespace std; const int N=310,M=90100; const double EPS=1e-8; int n,m; double V,a; namespace MF{ int head[N],cur[N],dep[N],cnt,S,T,res; struct node{int to,next,val;}edge[M]; void ae(int u,int v,int w){ edge[cnt].next=head[u],edge[cnt].to=v,edge[cnt].val=w,head[u]=cnt++; edge[cnt].next=head[v],edge[cnt].to=u,edge[cnt].val=0,head[v]=cnt++; } void AE(int u,int v,int w){ edge[cnt].next=head[u],edge[cnt].to=v,edge[cnt].val=w,head[u]=cnt++; edge[cnt].next=head[v],edge[cnt].to=u,edge[cnt].val=w,head[v]=cnt++; } queue<int>q; inline bool bfs(){ memset(dep,0,sizeof(dep)),q.push(S),dep[S]=1; while(!q.empty()){ register int x=q.front();q.pop(); for(register int i=cur[x]=head[x];i!=-1;i=edge[i].next)if(edge[i].val&&!dep[edge[i].to])dep[edge[i].to]=dep[x]+1,q.push(edge[i].to); } return dep[T]>0; } bool reach; inline int dfs(int x,int flow){ if(x==T){ res+=flow; reach=true; return flow; } int used=0; for(register int &i=cur[x];i!=-1;i=edge[i].next){ if(!edge[i].val||dep[edge[i].to]!=dep[x]+1)continue; register int ff=dfs(edge[i].to,min(edge[i].val,flow-used)); if(ff){ edge[i].val-=ff; edge[i^1].val+=ff; used+=ff; if(used==flow)break; } } return used; } inline void Dinic(){ while(bfs()){ reach=true; while(reach)reach=false,dfs(S,0x3f3f3f3f); } } } namespace DD{ int head[N],cur[N],dep[N],cnt,S,T; double res; struct node{ int to,next; double val; }edge[M]; void ae(int u,int v,double w){ edge[cnt].next=head[u],edge[cnt].to=v,edge[cnt].val=w,head[u]=cnt++; edge[cnt].next=head[v],edge[cnt].to=u,edge[cnt].val=0,head[v]=cnt++; } void AE(int u,int v,double w){ edge[cnt].next=head[u],edge[cnt].to=v,edge[cnt].val=w,head[u]=cnt++; edge[cnt].next=head[v],edge[cnt].to=u,edge[cnt].val=w,head[v]=cnt++; } queue<int>q; inline bool bfs(){ memset(dep,0,sizeof(dep)),q.push(S),dep[S]=1; while(!q.empty()){ register int x=q.front();q.pop(); for(register int i=cur[x]=head[x];i!=-1;i=edge[i].next)if(edge[i].val>EPS&&!dep[edge[i].to])dep[edge[i].to]=dep[x]+1,q.push(edge[i].to); } return dep[T]>0; } bool reach; inline double dfs(int x,double flow){ if(x==T){ res+=flow; reach=true; return flow; } double used=0; for(register int &i=cur[x];i!=-1;i=edge[i].next){ if(edge[i].val<EPS||dep[edge[i].to]!=dep[x]+1)continue; register double ff=dfs(edge[i].to,min(edge[i].val,flow-used)); if(ff){ edge[i].val-=ff; edge[i^1].val+=ff; used+=ff; if(used==flow)break; } } return used; } inline void Dinic(){ while(bfs()){ reach=true; while(reach)reach=false,dfs(S,1e9); } } } int W,F,A; double w,f; int tp[90100]; int main(){ scanf("%d%d%lf%lf",&n,&m,&V,&a),memset(MF::head,-1,sizeof(MF::head)),memset(DD::head,-1,sizeof(DD::head)); for(int i=1,x,y,z;i<=m;i++)scanf("%d%d%d",&x,&y,&z),MF::AE(x,y,z),DD::AE(x,y,z); MF::S=0,MF::T=3,MF::ae(0,1,0x3f3f3f3f),MF::ae(0,2,0x3f3f3f3f),MF::Dinic(),A=MF::res,swap(MF::S,MF::T),MF::Dinic(),MF::res=0; MF::S=1,MF::T=3,MF::Dinic(),F=MF::res,swap(MF::S,MF::T),MF::Dinic(),MF::res=0; MF::S=2,MF::T=3,MF::Dinic(),W=MF::res,swap(MF::S,MF::T),MF::Dinic(),MF::res=0; f=a*A; f=max(f,0.0+A-W); f=min(f,0.0+F); w=A-f; DD::S=0,DD::T=3,DD::ae(0,1,f),DD::ae(0,2,w),DD::Dinic(); for(int i=0;i+4<DD::cnt;i+=2){ double tmp=(DD::edge[i^1].val-DD::edge[i].val)/2; if(tmp>EPS)tp[i]=1; if(tmp<-EPS)tp[i]=-1; DD::edge[i].val=max(tmp,0.0); DD::edge[i^1].val=max(-tmp,0.0); } DD::edge[DD::cnt-4].val=f,DD::edge[DD::cnt-3].val=DD::edge[DD::cnt-2].val=DD::edge[DD::cnt-1].val=0; DD::Dinic(); for(int i=0;i+4<DD::cnt;i+=2){ if(tp[i]==1)printf("%lf %lf\n",DD::edge[i^1].val/V,DD::edge[i].val); else printf("%lf %lf\n",-DD::edge[i].val/V,-DD::edge[i^1].val); } printf("%lf\n",pow(f/V,a)*pow(w,1-a)); return 0; } ```