网络流学习笔记

前言

本文初稿于 2023 年 12 月,当月根据 gyy 的博客 和 yny 的讲座初次强化网络流,感觉挺抽象的。

后在 2024 年 3 月,由 gyy 的网络流专题周,再一次对网络流研究,补充了一些习题。

本人认为其实有些板子是没有用的。。。

有些题的代码是洛谷提交记录,或者没放。

感觉基本上每一道题的板子都是当时打的,并没有贺,所以在某一天打了 \(9\) 次 Simplex。。。

再一次研究了幻想乡,理解也比之前深刻了许多。


  • 对于网格相关的题目,我们可以考虑 黑白染色,或许这可以使流量平衡。
  • 上下界网络流是好的,有时也可以进行一些优化,如 P4542 中。
  • 区间选择 非常重要,我们同样有一个模型去适用于它,这是基于上下界网络流的。
  • 最小积费用流 本质上和 dinic 的费用流相关,会有一些细节,具体见该板块。
  • 条件的递归可以往 分层图 方向想,但分层图的点数有时会比较多,需要注意。
  • 不要动不动就拆点,但不得不承认 拆点 确实在非常多的题目上面都有用。
  • 模拟费用流 非常迷人,用其他的方法去实现费用流所做的。
  • 动态加边 最好用 Simplex 实现,如果用 dinic 要想清楚正确性。

板子

最大流

Dinic

关于最大流直接 Dinic 的时间复杂度是 \(\mathcal O(n^2m)\) 的,并不算快,

而这是 Dinic 的板子。

struct edge{
  int v,nxt;
  ll w;
}e[N<<1];

void add(int u,int v,ll w){
  e[++tot]=(edge){v,head[u],w};head[u]=tot;
  e[++tot]=(edge){u,head[v],0};head[v]=tot;
}

bool bfs(){
  queue<int> q;
  memset(lv,-1,sizeof(lv));
  lv[s]=0;q.push(s);vis[s]=true;
  memcpy(cur,head,sizeof(cur));
  while(!q.empty()){
  	int u=q.front();q.pop();vis[u]=false;
  	for(int i=head[u];i!=-1;i=e[i].nxt){
  	  int v=e[i].v;ll w=e[i].w;
  	  if(w>0&&lv[v]==-1){
  	  	lv[v]=lv[u]+1;
  	  	if(!vis[v]) q.push(v),vis[v]=true;
  	  }
  	}
  }
  return lv[t]!=-1;
}

ll dfs(int u,int flow){
  if(u==t) return flow;
  ll res=flow;
  for(int i=cur[u];i!=-1;i=e[i].nxt){
  	int v=e[i].v;cur[u]=i;
  	if(lv[v]==lv[u]+1&&e[i].w){
  	  ll c=dfs(v,min(res,e[i].w));
  	  e[i].w-=c;e[i^1].w+=c;res-=c;
  	}
  }
  return flow-res;
}

void dinic(){
  ans=0;
  while(bfs()) ans+=dfs(s,inf);
}

ISAP

还是看 gyy 的博客 好用些。

其实就是去减少 bfs 的次数,从而做到优化。

namespace _ISAP{
  const ll inf=1e12;
  int lv[N],gap[N],head[N],tot=1,cur[N],S,T;
  struct edge{int v,nxt;ll w;}e[N<<1];
  
  void add(int u,int v,int w){
  	e[++tot]=(edge){v,head[u],w};head[u]=tot;
  	e[++tot]=(edge){u,head[v],0};head[v]=tot;
  }
  
  void bfs(){
    memset(lv,-1,sizeof(lv));
    memset(gap,0,sizeof(gap));
    queue<int> q;
    q.push(T);lv[T]=0;gap[0]=1;
    while(!q.empty()){
      int u=q.front();q.pop();
      for(int i=head[u];i;i=e[i].nxt){
      	int v=e[i].v;
      	if(lv[v]!=-1) continue;
      	q.push(v),lv[v]=lv[u]+1,gap[lv[v]]++;
      }
    }
  }
  
  ll dfs(int u,ll flow){
  	if(u==T) return flow;
  	ll res=flow;
  	for(int &i=cur[u];i;i=e[i].nxt){
  	  int v=e[i].v;ll w=e[i].w;
  	  if(lv[v]+1==lv[u]&&w){
  	  	ll c=dfs(v,min(res,w));
  	  	res-=c;e[i].w-=c;e[i^1].w+=c;
  	  	if(!res) return flow-res;
  	  }
  	}
  	gap[lv[u]]--;
  	if(!gap[lv[u]]) lv[S]=N;
  	gap[++lv[u]]++;
  	return flow-res;
  }
  
  ll ISAP(){
  	ll ans=0;
  	bfs();
  	while(lv[S]<n) memcpy(cur,head,sizeof(cur)),ans+=dfs(S,inf);
    return ans;
  }
}

yny 的神秘优化

可以轻松跑过最大流预留推进。


其实本质上就是将流量进行分块,块长每次 \(\div 20\) 较优。

这样玄学的优化可以轻松跑过 P4722 代码是好写的——调的过程中发现我网络流假了好多个地方啊。

namespace Dinic{
  const ll inf=1e18;
  int cur[N],lv[N],S,T,nw[N];
  struct edge{int v,id;ll w;};
  struct node{
  	int u,v;ll w;
    bool operator <(const node &rhs) const{return w>rhs.w;}
  };
  
  vector<node> g;
  vector<edge> E[N];
  
  void add_edge(int u,int v,int w){g.pb((node){u,v,w});}
  
  void add(int u,int v,ll w){
    E[u].pb((edge){v,nw[v]++,w});
    E[v].pb((edge){u,nw[u]++,0});
  }
  
  bool bfs(){
    memset(lv,0,sizeof(lv));
    memset(cur,0,sizeof(cur));
    queue<int> q;
    q.push(S);lv[S]=1;
    while(!q.empty()){
      int u=q.front();q.pop();
      for(auto i:E[u]){
      	int v=i.v;
      	if(!lv[v]&&i.w){
      	  lv[v]=lv[u]+1,q.push(v);
          if(v==T) return true;
        }
      }
    }
    return false;
  }
  
  ll dfs(int u,ll flow){
  	if(u==T||!flow) return flow;
  	ll res=flow;
  	for(int i=cur[u];i<(int)E[u].size()&&res>0;++i){
  	  cur[u]=i;
  	  int v=E[u][i].v;ll w=E[u][i].w;
  	  if(lv[v]==lv[u]+1&&w){
  	  	ll c=dfs(v,min(w,res));
  	  	if(!c) lv[v]=0;
  	  	res-=c;E[u][i].w-=c;E[v][E[u][i].id].w+=c;
  	  }
  	}
  	return flow-res;
  }
  
  ll sol(){
  	ll ans=0;
  	while(bfs()) ans+=dfs(S,inf);
  	return ans;
  }
	
  ll dinic(){
  	ll ans=0;sort(g.begin(),g.end());
  	for(int i=1e9,j=0;j<(int)g.size();i/=20){
  	  while(g[j].w>=i&&j<(int)g.size()) add(g[j].u,g[j].v,g[j].w),++j;
  	  ans+=sol();
  	}
  	return ans;
  }
}

最大流易错点:

  1. 一定要判 res>0 的情况。

  2. 直接用 vector 正向存图会快很多。

    似乎 vector 会 快上 \(4\),为什么呢?

    经过 gyy 的验证,我们发现当加入的次数比查询的次数不成正比时,也就是次数少很多,

    dfs 中的 ++ii=e[i].nxt 快很多。

  3. 注意当前弧优化的写法,不能把当前弧优化和 \(res\gt 0\) 都放在 for 循环里面写。


费用流

费用流很慢啊,所以要用一些其他算法。

Dinic

普通的 dinic 跑费用流真的很慢很慢。

\(\mathcal O(n^2m)\)

bool spfa(){
  for(int i=1;i<=n;i++) dis[i]=inf,vis[i]=false;
  queue<int> q;vis[s]=true;
  q.push(s);dis[s]=0;cur[s]=head[s];
  while(!q.empty()){
  	int u=q.front();q.pop();vis[u]=false;
  	for(int i=head[u];i!=-1;i=e[i].nxt){
  	  int v=e[i].v;ll val=e[i].val,cost=e[i].cost;
  	  if(val&&dis[v]>dis[u]+cost){
  	  	dis[v]=dis[u]+cost;
  	  	cur[v]=head[v];
  	  	if(!vis[v]) q.push(v),vis[v]=true;
  	  }
  	}
  }
  return dis[t]!=inf;
}

ll dfs(int u,ll flow){
  if(u==t) return flow;
  ll res=flow;vis[u]=true;
  for(int i=cur[u];i!=-1;i=e[i].nxt){
  	int v=e[i].v;ll val=e[i].val,cost=e[i].cost;cur[u]=i;
  	if(val>0&&dis[v]==dis[u]+cost&&!vis[v]){
  	  ll c=dfs(v,min(res,val));
  	  e[i].val-=c;e[i^1].val+=c;res-=c;
  	}
  }
  return flow-res;
}

void dinic(){
  ans=anscost=0;
  while(spfa()){
  	ll flow=dfs(s,inf);
  	ans+=flow;anscost+=dis[t]*flow;
  }
}

单纯形网络流

很有用的优化,代码也不是很难写,

似乎可以被 卡成指数级,但是也不知道怎么卡啊。


而这东西的平均复杂度是 \(\mathcal O(nm)\) 的,实测非常快,很好用。

具体的可以看 gyy 的博客,讲得非常好,表示好评。🚀🚀🚀


简单来说,就是在图中不断找到负环然后将负环跑满,

它和线性代数中的单纯形法类似,这里简单写一下实现的步骤。


  1. 为了满足 线性规划的标准型,我们需要构建出 循环流,就是建立一条 \(T \to S\) 的边,流量为 \(\infty\),费用为 \(- \infty\)

    这样保证了图中一定存在 负环,并且也是正确性的保证(当没有负环的时候一定是跑满流了)。

  2. 我们找到一棵 支撑树,任意一棵都行。

    支撑树 (机房文化),说人话就是 生成树

  3. 枚举每一条边,判断它加入之后是否会存在 负环,如果有负环就将负环消掉。

    消掉的做法就是下面几步,加入一条边后,原本的 支撑树 就一定会变成基环树,而存在的负环一定包含这条边。

  4. 加入这条边(入基边),找到树上的 LCA,并且找出 环上面流量最小 的边,存下整个环。

    实现的时候还要记录下这个边在新加入的边的左边还是右边,会涉及到 边反向 的问题。

  5. 推流,再 构建新的支撑树

    画图可以发现其实就是 翻转当前边到删除的边路径上的所有边

  6. 回到 Step 3. 知道没有负环为止。

而我们最后的最大流就是新建的 \(T \to S\) 的反向边的流量,费用在每一次推流的过程中记录即可(和普通 dinic 的记录方法一样)。

注意过程中我们算了 \(T \to S\) 这条边的费用的贡献,最后减去就好了。


接下来放上代码。

namespace MCMF{
  const ll inf=1e9;
  
  int head[N],tot=1,fa[N],fe[N],cir[N],tag[N],nw=0,S,T;
  ll pre[N];
  //fa[i] 记录 i 的父亲,fe[i] 记录 i 到父亲的边的编号,cir 记录环上的边,tag 是每一条边的标记,相当于不清空的 vis 数组
  
  struct edge{//邻接表存边,w 是流量,c 是费用
  	int u,v,nxt;
  	ll w,c;
  }e[N<<1];
  
  void add(int u,int v,int w,int c){//正常的存边方式
    e[++tot]=(edge){u,v,head[u],w,c};head[u]=tot;
    e[++tot]=(edge){v,u,head[v],0,-c};head[v]=tot;
  }
  
  void init_ZCT(int u,int lst,int col=1){//找一棵支撑树
  	fa[u]=e[lst].u,fe[u]=lst;tag[u]=col;//打标记
  	for(int i=head[u];i;i=e[i].nxt)
  	  if(tag[e[i].v]!=col&&e[i].w) init_ZCT(e[i].v,i,col);
  }
  
  ll sum(int u){//求 u 到根的和,从而可以判断负环
  	if(tag[u]==nw) return pre[u];
  	tag[u]=nw,pre[u]=sum(fa[u])+e[fe[u]].c;
    return pre[u];
  }
  
  ll push_flow(int x){//推流和重构支撑树的过程
  	int rt=e[x].u,lca=e[x].v,cnt=0,del=0,P=2;//P=2 表示最小流量的边就是当前新加入的这条边
  	ll cost=0,F=e[x].w;//cost 计算费用,F 是最小的流量
  	++nw;//新的标记,非常有必要
    
    //找 LCA 过程,不断往上跳并且打标记(似乎可以用 LCT 实现,但是常数巨大)
    while(rt) tag[rt]=nw,rt=fa[rt];
    while(tag[lca]!=nw) tag[lca]=nw,lca=fa[lca];
    
    //往这条边的左边找,P=0
    for(int u=e[x].u;u!=lca;u=fa[u]){
      cir[++cnt]=fe[u];
      if(F>e[fe[u]].w) F=e[fe[u]].w,del=u,P=0;
    }
    
    //往这条边的右边找,注意每一条边都要反向
    for(int u=e[x].v;u!=lca;u=fa[u]){
      cir[++cnt]=fe[u]^1;
      if(F>e[fe[u]^1].w) F=e[fe[u]^1].w,del=u,P=1;
    }
    
    cir[++cnt]=x;//加上自己
    
    //计算费用
    for(int i=1;i<=cnt;i++)
      cost+=F*e[cir[i]].c,e[cir[i]].w-=F,e[cir[i]^1].w+=F;
      
    if(P==2) return cost;
    
    //重构支撑树的过程,我们钦定了 v 是起点
    int u=e[x].u,v=e[x].v;
    if(P==1) swap(u,v);
    int lste=x^P,lstu=v,tmp;
    
    while(lstu!=del){//不断往上跳和反向
      lste^=1;--tag[u];
      swap(fe[u],lste);
      tmp=fa[u],fa[u]=lstu,lstu=u,u=tmp;
    }
    
    return cost;
  }
  
  ll MinC=0;
  
  ll Simplex(){
  	add(T,S,inf,-inf);//建边
  	init_ZCT(T,0,++nw);//找支撑树
  	tag[T]=++nw;fa[T]=0;
  	
  	bool fl=1;
  	while(fl){
  	  fl=0;
  	  for(int i=2;i<=tot;i++)
  	  	if(e[i].w&&e[i].c+sum(e[i].u)-sum(e[i].v)<0)//判断是否有负环 
  	  	  MinC+=push_flow(i),fl=1;
  	}
  	MinC+=e[tot].w*inf;//减去 T -> S 的贡献
  	return e[tot].w;
  }
}

关于单纯形的网络流的拓展应用非常多啊,

它既可以跑掉 带负环的费用流

还可以 支持动态加边。(下面的美食节就是应用)

dinic 的费用流很明显是不支持动态加边的,

因为每一次找的是最短路,有可能你新加入的边是最短路,

这样在残图上面跑容易证明是错误的。


单纯形的费用流还 跑得飞快

在费用为 \(0\) 的时候还可以轻松跑过 P4722 【模板】最大流 加强版 / 预流推进

但实测还是会比上面的 yny 的神秘优化慢一些的,但是还是很快。

非常强大!


习题

20231210

不太想写,但是还是写一下吧。

早上被喊起来上课/kk

不愧是 yny,最后 5 分钟不知道讲了多少道题。

最大流

前面没听/kk


Dinic 算法的时间复杂度是是 \(\mathcal O(n^2 m)\),而在二分图上面可以变成 \(\mathcal O(m \sqrt n)\)

P3163 [CQOI2014] 危桥

Alice 和 Bob 居住在一个由 \(N\) 座岛屿组成的国家,岛屿被编号为 \(0\)\(N-1\)。某些岛屿之间有桥相连,桥上的道路是双向的,但一次只能供一人通行。其中一些桥由于年久失修成为危桥,最多只能通行两次。

Alice 希望在岛屿 \(a_1\)\(a_2\) 之间往返 \(a_n\) 次(从 \(a1\)\(a2\) 再从 \(a2\)\(a1\) 算一次往返)。同时,Bob 希望在岛屿 \(b_1\)\(b_2\) 之间往返 \(b_n\) 次。这个过程中,所有危桥最多通行两次,其余的桥可以无限次通行。请问 Alice 和 Bob 能完成他们的愿望吗?

\(4 \leq N\leq 50,\ 0 \leq a_1, a_2, b_1, b_2 \leq N-1,\ 1 \leq a_n, b_n \leq 50\)

首先没有什么思路——感觉可以跑 \(50\) 次网络流。


考虑直接建图,按照很传统的方式,\(s \to a_1,s \to b_1,a_2 \to t,b_2 \to t\)

这是容易理解的,而流量就是 \(a_n,b_n\)

于是跑一次最大流,如果满流,就满足条件?


发现显然是不正确的,对于危桥而言,我们钦定的流量是 \(1\),但是不排除有可能这个危桥被来回走了两次,

而还有可能 \(a_1 \to b_2,b_1 \to a_2\),这种也是没有办法排除的。


所以我们现在考虑再构造一种方法,把 \(b_1,b_2\) 交换之后再跑一次网络流,

这样是不是就避免了上面不满足条件的情况?


显然是肯定的,因为路是双向的,如果两者都满流,我们就可以把一些路翻转,

已达到 \(a_1 \to a_2,b_1 \to b_2\) 的目的。

而对于危桥走两次的情况也是显然的,反向之后就不成立了。

于是这样就做完了。

#include <bits/stdc++.h>
using namespace std;

const int N=1e5+5,inf=1e9;
int n,s,t,a1,a2,an,b1,b2,bn,head[N],tot=-1,cur[N],lv[N],ans=0;
struct edge{int v,nxt,w;}e[N<<1];
bool vis[N];
char mp[55][55];

void add(int u,int v,int w){
  e[++tot]=(edge){v,head[u],w};head[u]=tot;
  e[++tot]=(edge){u,head[v],w};head[v]=tot;
}

bool bfs(){
  memset(lv,-1,sizeof(lv));
  queue<int> q;
  cur[s]=head[s],lv[s]=0,vis[s]=true,q.push(s);
  while(!q.empty()){
  	int u=q.front();q.pop();vis[u]=false;
  	for(int i=head[u];i!=-1;i=e[i].nxt){
  	  int v=e[i].v,w=e[i].w;
	  if(w>0&&lv[v]==-1){
	  	lv[v]=lv[u]+1;cur[v]=head[v];
	  	if(!vis[v]) q.push(v),vis[v]=true;
	  }	
	}
  }
  return lv[t]!=-1;
}

int dfs(int u,int flow){
  if(u==t) return flow;
  int res=flow;
  for(int i=cur[u];i!=-1;i=e[i].nxt){
  	cur[u]=i;int v=e[i].v,w=e[i].w;
  	if(w&&lv[v]==lv[u]+1){
  	  int c=dfs(v,min(w,res));
	  e[i].w-=c;
	  e[i^1].w+=c;
	  res-=c;	
	}
  }
  return flow-res;
}

void dinic(){
  ans=0;
  while(bfs()) ans+=dfs(s,inf);
}

bool chk(int a1,int a2,int an,int b1,int b2,int bn){
  memset(head,-1,sizeof(head));tot=-1;
  s=0,t=n+1;
  add(s,a1,an);add(s,b1,bn);
  add(a2,t,an);add(b2,t,bn);
  for(int i=1;i<=n;i++)
    for(int j=i+1;j<=n;j++){
      if(mp[i][j]=='O') add(i,j,1);
      else if(mp[i][j]=='N') add(i,j,inf);
    }
  dinic();
  return (ans==an+bn);
}

void sol(){
  scanf("%d%d%d%d%d%d",&a1,&a2,&an,&b1,&b2,&bn);
  ++a1,++a2,++b1,++b2;
  for(int i=1;i<=n;i++) scanf("%s",mp[i]+1);
  if(chk(a1,a2,an,b1,b2,bn)&&chk(a1,a2,an,b2,b1,bn)) puts("Yes");
  else puts("No");
}

int main(){
  /*2023.12.10 H_W_Y P3163 [CQOI2014] 危桥 网络流*/ 
  while(scanf("%d",&n)!=EOF) sol();
  return 0;
}

CF1592F2 Alice and Recoloring 2 - 好题

CF1592F2 Alice and Recoloring 2

给定一个 \(n\)\(m\) 列的目标矩阵,矩阵元素只有 W 或 B ,并且你有一个初始矩阵,元素全为 W 。

现在你可以矩阵实施以下操作:

使用一块钱,选定一个包含 \((1,1)\) 的子矩阵,把矩阵中的元素全部反转( W 变 B , B 变 W )。

使用三块钱,选定一个包含 \((n,1)\) 的子矩阵,把矩阵中的元素全部反转。

使用四块钱,选定一个包含 \((1,m)\) 的子矩阵,把矩阵中的元素全部反转。

使用两块钱,选定一个包含 \((n,m)\) 的子矩阵,把矩阵中的元素全部反转。

现在需要你求出把初始矩阵变为目标矩阵最少需要几块钱。

\(1 \le n,m \le 500\)

感觉很多操作不知道如何下手。


首先需要发现第 \(2,3\) 种操作是完全没有意义的,

因为可以直接用两次 \(1\) 操作代替,而且代价还会更小。


我们现在想把每次翻转一个区间改成单点修改。

而反转很容易想到 异或 ,那么如果一个点表示的值是一个区间的异或值,只有奇数个点改变时异或值才会变。

于是我们考虑人类智慧构造一下,得到数组 \(a\),使得

\[a_{i,j} = s_{i,j} \oplus s_{i+1,j} \oplus s_{i,j+1} \oplus s_{i+1,j+1} \]


把它画在图上面,我们可以很容易的发现:

  1. 对于操作 \(1\),只会改变 \(a_{x,y}\) 的值;
  2. 对于操作 \(2\),只会改变形如 \((x,y),(x,m),(n,y),(n,m)\) 这四个值,那么我们把这样的一次操作记作 \(op(x,y)\)

发现对于操作 \(2\),只有在 \((x,y),(x,m),(n,y)\) 的值都是 \(1\) 的时候我们才会用,

反之直接用操作 \(1\),是一定不劣的。

而如果两次连续操作 \(op(x,y1),op(x,y2)\) 是不优的,因为我们可以直接用操作 \(1\) 完成。


所以对于每一个 \(x,y\),它最多只会被涉及一次操作,

那么这时就想到了矩形的常见建图方法,我们把每一个 \(x\) 连到 \(s\)\(y\) 连到 \(t\)

于是跑一次二分图最大匹配即可,最后的答案也是好处理的。


P4474 王者之剑

P4474 王者之剑 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

一下子没反应过来。


模拟一下发现相邻的一定选不了,所以就变成了求最大独立集。

黑白染色之后直接跑二分图最大匹配即可。代码


最小割最大流定理

由最小割定理可以得到最小割等于最大流,

感觉感性理解还是很好理解的。

P4313 文理分科

典题。

文理分科是一件很纠结的事情!(虽然看到这个题目的人肯定都没有纠结过)

小 P 所在的班级要进行文理分科。他的班级可以用一个 \(n\times m\) 的矩阵进行描述,每个格子代表一个同学的座位。每位同学必须从文科和理科中选择一科。同学们在选择科目的时候会获得一个满意值。满意值按如下的方式得到:

  • 如果第 \(i\) 行第 \(j\) 列的同学选择了文科,则他将获得 \(art_{i,j}\) 的满意值,如果选择理科,将得到 \(science_{i,j}\) 的满意值。

  • 如果第 \(i\) 行第 \(j\) 列的同学选择了文科,并且他相邻(两个格子相邻当且仅当它们拥有一条相同的边)的同学全部选择了文科,则他会更开心,所以会增加 \(same\_art_{i,j}\) 的满意值。

  • 如果第 \(i\) 行第 \(j\) 列的同学选择了理科,并且他相邻的同学全部选择了理科,则增加 \(same \_ science_{i,j}\) 的满意值。

小 P 想知道,大家应该如何选择,才能使所有人的满意值之和最大。请告诉他这个最大值。

\(n,m\leq 100\),读入数据均 \(\leq 500\)

同样是类似于分成两个集合,求最小割。


而最小割如何构造,首先对于单个点的贡献是好构造的,

而对于每一个多个点同时选择得到的权值也是好计算的,

直接新建一个节点向这些点连边,于是再从 \(s,t\) 连一下边于是就可以跑最小割了。

#include <bits/stdc++.h>
using namespace std;

const int N=3e5+5,inf=1e9;
int n,m,cnt=0,head[N],tot=-1,cur[N],lv[N],s,t,ans=0,sum=0;
struct edge{int v,nxt,w;}e[N<<1];
bool vis[N];

void add(int u,int v,int w){
  e[++tot]=(edge){v,head[u],w};head[u]=tot;
  e[++tot]=(edge){u,head[v],0};head[v]=tot;
}

bool bfs(){
  memset(lv,-1,sizeof(lv));
  queue<int> q;
  lv[s]=0;q.push(s);
  cur[s]=head[s];vis[s]=true;
  while(!q.empty()){
  	int u=q.front();q.pop();vis[u]=false;
  	for(int i=head[u];i!=-1;i=e[i].nxt){
  	  int v=e[i].v,w=e[i].w;
      if(w>0&&lv[v]==-1){
      	lv[v]=lv[u]+1;cur[v]=head[v];
	    if(!vis[v]) q.push(v),vis[v]=true;
	  }
	}
  }
  return lv[t]!=-1;
}

int dfs(int u,int flow){
  if(u==t) return flow;
  int res=flow;
  for(int i=cur[u];i!=-1;i=e[i].nxt){
  	cur[u]=i;
  	int v=e[i].v,w=e[i].w;
  	if(w&&lv[v]==lv[u]+1){
  	  int c=dfs(v,min(w,res));
	  res-=c;
	  e[i].w-=c;
	  e[i^1].w+=c;	
	}
  }
  return flow-res;
}

void dinic(){
  ans=0;
  while(bfs()) ans+=dfs(s,inf); 
}

int id(int i,int j){return (i-1)*m+j;}

const int fx[5]={0,0,0,-1,1},fy[5]={0,1,-1,0,0};

void ins1(int i,int j,int v){
  ++cnt;
  add(s,cnt,v);
  for(int k=0;k<5;k++){
  	int x=i+fx[k],y=j+fy[k];
  	if(x>0&&y>0&&x<=n&&y<=m) add(cnt,id(x,y),inf);
  }
}

void ins2(int i,int j,int v){
  ++cnt;
  add(cnt,t,v);
  for(int k=0;k<5;k++){
  	int x=i+fx[k],y=j+fy[k];
  	if(x>0&&y>0&&x<=n&&y<=m) add(id(x,y),cnt,inf);
  }
}

int main(){
  /*2023.12.10 H_W_Y P4313 文理分科 最小割*/ 
  ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
  cin>>n>>m;
  memset(head,-1,sizeof(head));
  s=0;cnt=n*m+1;t=cnt;
  for(int i=1;i<=n;i++)
    for(int j=1,x;j<=m;j++)
	  cin>>x,add(s,id(i,j),x),sum+=x;
  for(int i=1;i<=n;i++)
    for(int j=1,x;j<=m;j++)
	  cin>>x,add(id(i,j),t,x),sum+=x;
  for(int i=1;i<=n;i++)
    for(int j=1,x;j<=m;j++)
      cin>>x,ins1(i,j,x),sum+=x;
  for(int i=1;i<=n;i++)
    for(int j=1,x;j<=m;j++)
      cin>>x,ins2(i,j,x),sum+=x;
  dinic();
  cout<<(sum-ans)<<'\n';
  return 0;
}

P3227 [HNOI2013] 切糕

P3227 [HNOI2013] 切糕

经过千辛万苦小 A 得到了一块切糕,切糕的形状是长方体,小 A 打算拦腰将切糕切成两半分给小 B。出于美观考虑,小 A 希望切面能尽量光滑且和谐。于是她找到你,希望你能帮她找出最好的切割方案。

出于简便考虑,我们将切糕视作一个长 \(P\)、宽 \(Q\)、高 \(R\) 的长方体点阵。我们将位于第 \(z\) 层中第 \(x\) 行、第 \(y\) 列上的点称 \((x,y,z)\),它有一个非负的不和谐值 \(v(x,y,z)\)。一个合法的切面满足以下两个条件:

  • 与每个纵轴(一共有 \(P\times Q\) 个纵轴)有且仅有一个交点。即切面是一个函数 \(f(x,y)\),对于所有 \((x,y)(x\in [1,P],y\in[1,Q])\),我们需指定一个切割点 \(f(x,y)\),且 \(1\le f(x,y)\le R\)

  • 切面需要满足一定的光滑性要求,即相邻纵轴上的切割点不能相距太远。对于所有的 \(1\le x,x'\le P\)\(1\le y,y'\le Q\),若 \(|x-x'|+|y-y'|=1\),则 \(|f(x,y)-f(x',y')| \le D\),其中 \(D\) 是给定的一个非负整数。

可能有许多切面 \(f\) 满足上面的条件,小 A 希望找出总的切割点上的不和谐值最小的那个。

对于 \(100\%\) 的数据,\(1 \leq P,Q,R\leq 40,0\le D\le R\),且给出的所有的不和谐值不超过 \(1000\)

题目就感觉比较抽象。/kk


考虑我们直接建图,也就是将每一层穿起来,

但是发现这样很难满足 \(d\) 的性质,

\(d\) 的性质满足的要素其实就是在答案为选两个差为 \(d\) 以上的点的时候,

这个答案会被覆盖掉,也就是根本不存在了。


那么我们最开始都是从 \((i,j,k) \to (i,j,k+1),w=a[i][j][k]\) 的边,

而为了满足 \(d\) 的条件,我们再建 \((i,j,k) \to (x,y,k-d),w=\inf\) 的边,其中 \((x,y)\)\((i,j)\) 相邻,

这样建图之后我们发现不合法的条件一定不会成为答案,所以直接跑最小割即可。

#include <bits/stdc++.h>
using namespace std;

int read(){
  int x=0,f=1;char ch=getchar();
  while(!isdigit(ch)){if(ch=='-') f=-1;ch=getchar();}
  while(isdigit(ch)){x=(x<<1)+(x<<3)+ch-'0';ch=getchar();}
  return x*f;
}

const int N=55,fx[4]={-1,1,0,0},fy[4]={0,0,-1,1},P=1e5+5,M=5e6+5,inf=1e9;
int n,m,h,d,a[N][N][N],head[P],tot=-1,lv[P],cur[P],s,t;
struct edge{int v,nxt,w;}e[M];
bool vis[P];

void add(int u,int v,int w){
  e[++tot]=(edge){v,head[u],w};head[u]=tot;
  e[++tot]=(edge){u,head[v],0};head[v]=tot;
}

bool bfs(){
  memset(lv,-1,sizeof(lv));
  queue<int> q;q.push(s);
  vis[s]=true;cur[s]=head[s];lv[s]=0;
  while(!q.empty()){
  	int u=q.front();q.pop();vis[u]=false;
  	for(int i=head[u];i!=-1;i=e[i].nxt){
  	  int v=e[i].v,w=e[i].w;
  	  if(w&&lv[v]==-1){
  	  	lv[v]=lv[u]+1;cur[v]=head[v];
  	  	if(!vis[v]) q.push(v),vis[v]=true;
  	  }
  	}
  }
  return lv[t]!=-1;
}

int dfs(int u,int flow){
  if(u==t) return flow;
  int res=flow;
  for(int i=cur[u];i!=-1;i=e[i].nxt){
  	int v=e[i].v,w=e[i].w;cur[u]=i;
  	if(w&&lv[v]==lv[u]+1){
  	  int c=dfs(v,min(w,res));
  	  res-=c;e[i].w-=c,e[i^1].w+=c;
  	}
  	if(!res) break;
  }
  return flow-res;
}

int dinic(){
  int ans=0;
  while(bfs()) ans+=dfs(s,inf);
  return ans;
}

int id(int i,int j,int k){return (k-1)*n*m+(i-1)*m+j;}

int main(){
  /*2023.12.11 H_W_Y P3227 [HNOI2013] 切糕 最小割最大流*/
  n=read();m=read();h=read();d=read();
  memset(head,-1,sizeof(head));tot=-1;
  s=0,t=n*m*h+1;
  for(int k=1;k<=h;++k)
    for(int i=1;i<=n;++i)
      for(int j=1;j<=m;++j) 
        a[k][i][j]=read();
  for(int i=1;i<=n;++i)
    for(int j=1;j<=m;++j){
    	
      add(s,id(i,j,1),inf);
      
      for(int k=1;k<h;++k) add(id(i,j,k),id(i,j,k+1),a[k][i][j]);
      add(id(i,j,h),t,a[h][i][j]);
      
      for(int p=0;p<4;++p){
      	int x=i+fx[p],y=j+fy[p];
      	if(x>0&&y>0&&x<=n&&y<=m)
      	  for(int k=d+1;k<=h;++k) add(id(i,j,k),id(x,y,k-d),inf);
      }
    }
  printf("%d\n",dinic());
  return 0;
}

CF1383F Special Edges

Problem - 1383F - Codeforces

好题啊。


首先每次跑一定会爆炸,然而你把它想成最大流固然没有任何思路。

于是此时此刻我们就考虑把最大流想成最小割?


这样每一条边其实就只会有两种情形,割和不割,也就是是否满流!

那么我们就可以枚举哪些边满流了,哪些边并没有满流,

对于每一种情况,我们把满流的特殊边设成 \(0\),反之设成 \(25\),再重新跑。

发现这样一共 \(2^{k}\) 种情况其实每一种可以从 \(s \oplus lowbit(s)\) 转移过来,于是这样就变成了尝试增广出一条路径。

而这种东西是 dinic 不擅长的,所以单纯形是更好的选择!!!


这样我们就可以预处理出来,理论上复杂度 \(\mathcal O(2^kwm)\) 但是单纯形会很快。

而每次询问的时候,其实我们就变成了枚举哪些边被割了哪些边没有就行了,

用割掉的特殊边流量之和加上预处理出的剩余边流量之和即可,取 \(\min\) 就是答案。

这样我们就做完了,非常好的思路!!!代码

int main(){
  /*2024.3.18 H_W_Y CF1383F Special Edges MCMF*/
  ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
  cin>>n>>m>>K>>Q;
  
  S=n+1,T=S+1;
  add(S,1,inf);
  add(n,T,inf);
  
  for(int i=1,u,v,w;i<=K;i++){
    cin>>u>>v>>w;
    pos[i]=tot+1;
    add(u,v,w);
  }
  
  for(int i=K+1,u,v,w;i<=m;i++) cin>>u>>v>>w,add(u,v,w);
  
  for(int i=2;i<=tot;i++) val[0][i]=e[i].w;
  
  for(int i=1;i<(1<<K);i++) low[i]=i&(-i),lg[i]=lg[i/2]+1;
  
  for(int s=0;s<(1<<K);s++){
    for(int i=2;i<=tot;i++) e[i].w=val[s^low[s]][i];
    e[pos[lg[low[s]]]].w=25;
    ans[s]=ans[s^low[s]]+Simplex();
    for(int i=2;i<=tot;i++) val[s][i]=e[i].w;
  }
  
  while(Q--){
    for(int i=1;i<=K;i++) cin>>t[i];
    p[0]=0,res=1e9;
    for(int s=1;s<(1<<K);s++) p[s]=p[s^low[s]]+t[lg[low[s]]];
    for(int s=0;s<(1<<K);s++) res=min(res,p[s]+ans[s^((1<<K)-1)]);
    cout<<res<<'\n';
  }
  
  return 0;
}

为什么 Simplex 不删边多次跑的正确性是对的?

发现最后建的 \(T \to S\) 的边一定是在生成树上面的第一条边,于是之前的 \(T \to S\) 的没流完的边一定不在生成树上面,而分析以下发现一定不会构成负环,于是就不会对新一次的 Simplex 造成任何贡献,是非常正确的。


最小割树(实际上是等价流树)

最小割树其实就是用最小割构成的一棵树(废话)。

它可以解决一个集合与一个集合中每一对点的最小割问题。

这网上的文章一般都写的等价流树,包括下面我写的。


P4897 【模板】最小割树(Gomory-Hu Tree)

P4897 【模板】最小割树(Gomory-Hu Tree)

求最小割树。

这里只是想放一个板子。/kk

#include <bits/stdc++.h>
using namespace std;

const int N=505,M=1e5+5,inf=1e9;
int n,m,Q,head[N],tot=-1,cur[N],lv[N],ans[N][N],s,t,a[N],tmp1[N],tmp2[N];
struct edge{int v,nxt,w;}e[M<<1];
bool vis[N];

void add(int u,int v,int w){
  e[++tot]=(edge){v,head[u],w};head[u]=tot;
  e[++tot]=(edge){u,head[v],0};head[v]=tot;
}

bool bfs(){
  memset(lv,-1,sizeof(lv));
  queue<int> q;q.push(s);
  vis[s]=true;cur[s]=head[s],lv[s]=0;
  while(!q.empty()){
  	int u=q.front();q.pop();vis[u]=false;
  	for(int i=head[u];i!=-1;i=e[i].nxt){
  	  int v=e[i].v,w=e[i].w;
  	  if(w&&lv[v]==-1){
  	  	lv[v]=lv[u]+1;cur[v]=head[v];
  	  	if(!vis[v]) q.push(v),vis[v]=true;
  	  }
  	}
  }
  return lv[t]!=-1;
}

int dfs(int u,int flow){
  if(u==t) return flow;
  int res=flow;
  for(int &i=cur[u];i!=-1;i=e[i].nxt){
  	int v=e[i].v,w=e[i].w;
  	if(w&&lv[v]==lv[u]+1){
  	  int c=dfs(v,min(res,w));
  	  res-=c;e[i].w-=c;e[i^1].w+=c;
  	}
  	if(!res) break;
  }
  return flow-res;
}

void init(){
  for(int i=0;i<tot;i+=2)
    e[i].w+=e[i^1].w,e[i^1].w=0;
}

int dinic(){
  int res=0;init();
  while(bfs()) res+=dfs(s,inf);
  return res;
}

void wrk(int l,int r){
  if(l==r) return ;
  s=a[l],t=a[l+1];
  int res=dinic(),S=a[l],T=a[l+1],cnt1=0,cnt2=0;
  ans[S][T]=ans[T][S]=res;
  
  for(int i=l;i<=r;i++){
    if(lv[a[i]]!=-1) tmp1[++cnt1]=a[i];
    else tmp2[++cnt2]=a[i];
  }
  
  for(int i=1;i<=cnt1;i++) a[l+i-1]=tmp1[i];
  for(int i=1;i<=cnt2;i++) a[l+cnt1+i-1]=tmp2[i];
  
  wrk(l,l+cnt1-1);wrk(l+cnt1,r);
  
  for(int i=1;i<=cnt1;i++)
    for(int j=1;j<=cnt2;j++){
      int x=a[l+i-1],y=a[l+cnt1+j-1];
      ans[x][y]=ans[y][x]=min(min(ans[S][x],ans[y][T]),ans[S][T]);
    }
}

int main(){
  /*2023.12.11 H_W_Y P4897 【模板】最小割树(Gomory-Hu Tree) 最小割*/
  ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
  cin>>n>>m;
  
  memset(ans,0x3f,sizeof(ans));
  memset(head,-1,sizeof(head));tot=-1;
  
  for(int i=0;i<=n;i++) a[i]=i;
  
  for(int i=1,u,v,w;i<=m;i++) cin>>u>>v>>w,add(u,v,w),add(v,u,w);
  
  wrk(0,n);
  cin>>Q;
  while(Q--){
  	int u,v;cin>>u>>v;
  	cout<<ans[u][v]<<'\n';
  }
  return 0;
}

P4123 [CQOI2016] 不同的最小割 - 最小割树

P4123 [CQOI2016] 不同的最小割

一张 \(n\) 个点 \(m\) 条边的图,每一对点的最小割中,有多少个互不相同。

\(1 \le n \le 1000,1 \le m \le 10000\)

首先引入一个东西叫做最小割树,顾名思义,就是一棵由最小割建出来的树。


最小割树的每一条边就代表着这两边两个集合的最小割,这是好理解的,

于是任意两个点的最小割就是树上的最小边的长度。


而这道题就是问树边有多少个不同的点,用 set 维护一下即可。

#include <bits/stdc++.h>
using namespace std;

const int N=1e3+5,M=5e4+5,inf=1e9;
int n,m,head[N],tot=-1,cur[N],lv[N],s,t,tmp1[N],tmp2[N],a[N];
struct edge{int v,nxt,w;}e[M];
bool vis[N];

void add(int u,int v,int w){
  e[++tot]=(edge){v,head[u],w};head[u]=tot;
  e[++tot]=(edge){u,head[v],0};head[v]=tot;
}

bool bfs(){
  memset(lv,-1,sizeof(lv));
  queue<int> q;q.push(s);
  cur[s]=head[s],lv[s]=0,vis[s]=true;
  while(!q.empty()){
  	int u=q.front();q.pop();vis[u]=false;
  	for(int i=head[u];i!=-1;i=e[i].nxt){
  	  int v=e[i].v,w=e[i].w;
  	  if(w&&lv[v]==-1){
  	  	lv[v]=lv[u]+1;cur[v]=head[v];
  	  	if(!vis[v]) q.push(v),vis[v]=true;
  	  }
  	}
  }
  return lv[t]!=-1;
}

int dfs(int u,int flow){
  if(u==t) return flow;
  int res=flow;
  for(int &i=cur[u];i!=-1;i=e[i].nxt){
  	int v=e[i].v,w=e[i].w;
  	if(w&&lv[v]==lv[u]+1){
  	  int c=dfs(v,min(res,w));
  	  res-=c;e[i].w-=c;e[i^1].w+=c;
  	}
  	if(!res) break;
  }
  return flow-res;
}

void init(){
  for(int i=0;i<tot;i+=2) e[i].w+=e[i^1].w,e[i^1].w=0;
}

int dinic(){
  int res=0;init();
  while(bfs()) res+=dfs(s,inf);
  return res;
}

set<int> st;

void wrk(int l,int r){
  if(l==r) return;
  s=a[l];t=a[l+1];
  int res=dinic(),cnt1=0,cnt2=0;
  for(int i=l;i<=r;i++)
    if(lv[a[i]]!=-1) tmp1[++cnt1]=a[i];
    else tmp2[++cnt2]=a[i];
  st.insert(res);
  for(int i=1;i<=cnt1;i++) a[l+i-1]=tmp1[i];
  for(int i=1;i<=cnt2;i++) a[l+cnt1+i-1]=tmp2[i];
  wrk(l,l+cnt1-1);wrk(l+cnt1,r);
}

int main(){
  /*2023.12.11 H_W_Y P4123 [CQOI2016] 不同的最小割 最小割树*/
  ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
  cin>>n>>m;memset(head,-1,sizeof(head));tot=-1;
  for(int i=1;i<=n;i++) a[i]=i;
  for(int i=1,u,v,w;i<=m;i++) cin>>u>>v>>w,add(u,v,w),add(v,u,w);
  wrk(1,n);cout<<st.size()<<'\n';
  return 0;
}

然而 loj 需要把 \(a\) rand 一下才可以过。


最大权闭合子图

给定一个有向图,点有点权。

如果一个点 \(u\) 被选了,所有 \(u\) 的出边指向的点 \(v\) 也必须选。

求最大收益。(点权可以为负数)


利用最小割来解决。先假设所有正点权都选。

正点权连到 \(st\),表示放弃这个点,负点权连到 \(ed\),表示选择这个点。

原图中所有 \((u, v)\) 连接一条 \((u, v, \inf)\) 的边。


感觉还比较好理解

P4177 [CEOI2008] order

P4177 [CEOI2008] order

\(m\) 个工作,\(m\) 种机器,每种机器你可以租或者买过来. 每个工作包括若干道工序, 每道工序需要某种机器来完成,你可以通过购买或租用机器来完成。

租用的机器只能用一次,可以多次租用。现在给出这些参数,求最大利润。

\(1 \le n \le 1200, 1 \le m \le 1200\)

同样是最大权最小子图,只不过多了一个租用的操作。

租用也是比较好处理的。

我们直接把中间的边的容量 \(inf\) 改成租用的费用即可。

网络流一定要剪枝!!!

#include <bits/stdc++.h>
using namespace std;

const int N=3e6+5,inf=0x3f3f3f3f;
int n,m,head[N],st,ed,tot=-1,sum=0,cur[N],lv[N],l,r,q[N];
struct edge{
  int v,nxt,val;
}e[N<<2];

int read(){
  int x=0,f=1;char ch=getchar();
  while(!isdigit(ch)){if(ch=='-') f=-1;ch=getchar();}
  while(isdigit(ch)){x=(x<<1)+(x<<3)+ch-'0';ch=getchar();}
  return x*f;
}

void add(int u,int v,int w){
  e[++tot]=(edge){v,head[u],w};
  head[u]=tot;
  e[++tot]=(edge){u,head[v],0};
  head[v]=tot;
}

bool bfs(){
  for(int i=0;i<=ed;i++) lv[i]=-1;
  lv[st]=1;
  cur[st]=head[st];
  q[l=1]=st;r=1;
  while(l<=r){
  	int u=q[l++];
  	for(int i=head[u];i!=-1;i=e[i].nxt){
  	  int v=e[i].v,val=e[i].val;
	  if(val>0&&lv[v]==-1){
	  	lv[v]=lv[u]+1;
	  	cur[v]=head[v];
	  	q[++r]=v;
	  	if(v==ed) return true; 
	  }	
	}
  }
  return lv[ed]!=-1; 
}

int dfs(int u,int flow){
  if(u==ed) return flow;
  int res=flow;
  for(int i=cur[u];i!=-1;i=e[i].nxt){
  	cur[u]=i;
  	int v=e[i].v,val=e[i].val;
  	if(val>0&&lv[v]==lv[u]+1){
  	  int c=dfs(v,min(res,val));
  	  if(!c) lv[v]=-1;
	  res-=c;
	  e[i].val-=c;
	  e[i^1].val+=c;	
	}
	if(res==0) break;//网络流剪枝很有必要!!! 
  }
  if(res!=0) lv[u]=-1;
  return flow-res;
}

int dinic(){
  int res=0;
  while(bfs()) res+=dfs(st,inf);
  return res;
}

int main(){
  /*2023.8.23 H_W_Y P4177 [CEOI2008] order 网络流+最大权闭合子图*/ 
  memset(head,-1,sizeof(head));tot=-1;
  n=read();m=read();
  st=0,ed=n+m+1;
  for(int i=1,x,t;i<=n;i++){
  	x=read();add(st,i,x);sum+=x;
  	t=read();
  	for(int j=1,a,b;j<=t;j++){
  	  a=read();b=read();
	  add(i,a+n,b);	
	}
  }
  for(int i=1,x;i<=m;i++){x=read();add(i+n,ed,x);}
  printf("%d\n",sum-dinic());
  return 0;
}

P4174 [NOI2006] 最大获利

P4174 [NOI2006] 最大获利 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

很讨厌这种有关两个的东西。

看了题解才发现其实就是一个最大权闭合子图。

代码


上下界网络流

以前不会,听了还是不会。


学习了一会儿,感觉也没有那么难。

首先无源汇上下界可行流是简单的,

我们就假设都跑满了下界建图之后跑网络流。


转到有源汇上面,

就是先跑一次无源汇使得流量平衡之后再在残图上面跑一次网络流即可。

感觉挺好懂的。

注意我们为了保证流量平衡,所以还需建立超级源点和超级汇点来平衡流量,也就是补流。

思路与 Simplex 其实相类似,并且上下界的费用流还是可以用 Simplex 来跑。

下面会有一个板子。


CF1416F Showing Off - 黑白染色

CF1416F Showing Off

对于大小为 \(n\cdot m\) 的矩阵 \(A\)\(B\),其中 \(A\) 的每个元素为一个权值 \(w(i,j)\)\(B\) 的每个元素为一个方向 L/R/D/U

初始你在 \((i,j)\),若 \(B_{i,j}=L\),你可以走到 \((i,j-1)\) 处,依次类推。

定义 \(S_{i,j}\) 表示从 \((i,j)\) 出发能够到达的点的 \(A_{i,j}\) 的和。

给定矩阵 \(S\),构造 \(A\)\(B\) 使得其生成的矩阵为 \(S\)

\(A\) 的每个元素均为正整数。

\(1\le n\cdot m\le 10^5,S_{i,j}\in [2,10^9]\)

很妙的一道题啊。


首先观察样例可以得到,一条路径的端点一定是一个环,

也就是说构成了一个基环树森林。

而在这个环上面的每一个点 \(s\) 值是一样的。


现在来考虑如何构造,

对于一个点 \((i,j)\),如果它四周没有比它小的点,那么它一定是在环上面的,

反之,它有可能在环上有可能不在环上。


而由于整个图是一个四联通性的,

所以每一个环一定是一个偶环,于是我们可以直接把偶环拆成很多个二元环,

而这种情况和之前是类似的,构造也是好构造的。


对于非环的点,我们可以钦定它的下一个点是什么,

于是直接连一条边即可,而点权则是两点之差。

对于环,我们就只希望构造出这些合法的二元组使得两两不交,

而这个就启发我们用黑白染色去解决。


于是这个问题就变成了在一张图上面构造一些合法的不交二元环,

而允许存在一些点即可选择成为环上的点,又可以不成为环上的点,

这是用上下界最大流完成的(对于非环上的点我们连到旁边的点的流量范围是 \([0,1]\))。

无解的情况就是没有可行流。

#include <bits/stdc++.h>
using namespace std;

const int N=1e5+5,inf=1e9;
int n,m,Q,head[N],tot=-1,cur[N],lv[N],s,t,S,T,dt[N],cnt,a[N],b[N];
char c[N];
bool vis[N],fl[N];
struct edge{int v,nxt,w;}e[N<<4];
const int fx[4]={0,0,1,-1},fy[4]={1,-1,0,0};
const char pos[4]={'R','L','D','U'},rev[4]={'L','R','U','D'};

void add(int u,int v,int w){
  e[++tot]=(edge){v,head[u],w};head[u]=tot;
  e[++tot]=(edge){u,head[v],0};head[v]=tot;
}

void addlr(int u,int v,int l,int r){
  add(u,v,r-l);
  dt[u]-=l,dt[v]+=l;
}

bool bfs(){
  memset(lv,-1,sizeof(lv));
  queue<int> q;q.push(s);
  lv[s]=0;cur[s]=head[s],vis[s]=true;
  while(!q.empty()){
  	int u=q.front();q.pop();vis[u]=false;
  	for(int i=head[u];i!=-1;i=e[i].nxt){
  	  int v=e[i].v,w=e[i].w;
	  if(w&&lv[v]==-1){
	  	lv[v]=lv[u]+1;cur[v]=head[v];
	  	if(!vis[v]) q.push(v),vis[v]=true;
	  }	
	}
  }  
  return lv[t]!=-1;
}

int dfs(int u,int flow){
  if(u==t) return flow;
  int res=flow;
  for(int &i=cur[u];i!=-1;i=e[i].nxt){
  	int v=e[i].v,w=e[i].w;
  	if(w&&lv[v]==lv[u]+1){
  	  int nw=dfs(v,min(w,res));
	  res-=nw;e[i].w-=nw;e[i^1].w+=nw;	
	}
	if(!res) break;
  }
  return flow-res;
}

int dinic(){
  int res=0;
  while(bfs()) res+=dfs(s,inf);
  return res;
} 

int id(int i,int j){return (i-1)*m+j;}
bool chk(int i,int j){return (i>0&&j>0&&i<=n&&j<=m);}
void init(){
  for(int i=1;i<=n*m;i++) vis[i]=fl[i]=false,lv[i]=b[i]=0;
  for(int i=s;i<=t;i++) head[i]=-1,dt[i]=0;
  cnt=0,tot=-1;
}

void sol(){
  cin>>n>>m;
  s=0;t=n*m+3;S=n*m+1,T=n*m+2;
  for(int i=1;i<=n*m;i++) cin>>a[i];
  
  for(int i=1;i<=n;i++)
    for(int j=1;j<=m;j++){
      bool flg=true;
      for(int k=0;k<4;k++){
      	int x=fx[k]+i,y=fy[k]+j;
      	if(chk(x,y)&&a[id(x,y)]<a[id(i,j)]) flg=false;
	  }
	  fl[id(i,j)]=flg;
	}
  for(int i=1;i<=n;i++)
    for(int j=1;j<=m;j++){
      if((i+j)&1){
      	addlr(S,id(i,j),fl[id(i,j)],1);
      	for(int k=0;k<4;k++){
      	  int x=fx[k]+i,y=fy[k]+j;
		  if(chk(x,y)&&a[id(x,y)]==a[id(i,j)]) addlr(id(i,j),id(x,y),0,1);	
		}
	  }else addlr(id(i,j),T,fl[id(i,j)],1);
	}
  addlr(T,S,0,inf);
  for(int i=1;i<=n*m+2;i++){
  	if(dt[i]>0) add(s,i,dt[i]),cnt+=dt[i];
  	else add(i,t,-dt[i]);
  }
  if(dinic()!=cnt){cout<<"NO\n";return;}
  else cout<<"YES\n";
  
  for(int i=1;i<=n;i++)
    for(int j=1;j<=m;j++){
      if((i+j)&1){
      	for(int k=head[id(i,j)];k!=-1;k=e[k].nxt){
      	  int v=e[k].v;
		  if(v==S||v==s||e[k^1].w==0) continue;
		  b[id(i,j)]=1,b[v]=a[v]-1;
		  for(int l=0;l<4;l++){
		  	int x=fx[l]+i,y=fy[l]+j;
		  	if(chk(x,y)&&id(x,y)==v) c[id(i,j)]=pos[l],c[v]=rev[l];
		  }	
		}
	  }
	}
  
  for(int i=1;i<=n;i++)
    for(int j=1;j<=m;j++){
      if(b[id(i,j)]) continue;
      for(int k=0;k<4;k++){
      	int x=i+fx[k],y=fy[k]+j;
      	if(chk(x,y)&&a[id(x,y)]<a[id(i,j)]){
      	  b[id(i,j)]=a[id(i,j)]-a[id(x,y)];
		  c[id(i,j)]=pos[k];	
		}
	  }
	}
	
  for(int i=1;i<=n;cout<<'\n',i++) for(int j=1;j<=m;j++) cout<<b[id(i,j)]<<' ';
  for(int i=1;i<=n;cout<<'\n',i++) for(int j=1;j<=m;j++) cout<<c[id(i,j)]<<' ';
}

int main(){
  /*2023.12.12 H_W_Y CF1416 Showing Off 上下界可行流*/
  ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
  memset(head,-1,sizeof(head));tot=-1;
  cin>>Q;
  while(Q--) sol(),init();
  return 0; 
}


P4043 [AHOI2014/JSOI2014] 支线剧情

P4043 [AHOI2014/JSOI2014] 支线剧情 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

图是很显然的,也就相当于题目中就给出来了。

但是这玩意和上下界有关?

仔细一想,发现每一条边的流量是 \([1,\infty]\),那么这就是上下界了。


而发现做这个东西是简单的,直接跑上下界费用流就可以了。

下面是一个板子。

如果流入量多就需要从超级源点连向它,

反之从他连到超级汇点,这是好想的,实在不行可以看 gyy 博客。代码

  cin>>n;
  S=114514,T=S+1;
  s=999,t=s+1;
  
  add(s,1,inf,0);
  
  for(int i=1,k;i<=n;i++){
    cin>>k;
    a[i]-=k;
    for(int j=1,v,c;j<=k;j++){
      cin>>v>>c;
      ++a[v];
      Sum+=c;
      add(i,v,inf,c);
    }
    if(i!=1) add(i,t,inf,0);
  }
  add(t,s,inf,0);
  for(int i=1;i<=n;i++){
    if(a[i]>0) add(S,i,a[i],0);//补流
    else add(i,T,-a[i],0);
  }
  cerr<<Simplex()<<'\n';
  cout<<mc+Sum<<'\n';

P5192 Zoj3229 Shoot the Bullet|东方文花帖|【模板】有源汇上下界最大流

P5192 Zoj3229 Shoot the Bullet|东方文花帖|【模板】有源汇上下界最大流 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

非常不好的板。

不如上一道题,看看就行了。代码

void sol(){
  s=n+m+1,t=s+1,ss=t+1,tt=ss+1;
  sum=0;memset(d,0,sizeof(d));
  tot=1;memset(head,0,sizeof(head));
  
  for(int i=1,x;i<=m;i++){
    cin>>x;
    d[i+n]-=x,d[t]+=x;
    add(i+n,t,inf);
  }
  for(int i=1,k,x;i<=n;i++){
    cin>>k>>x;
    add(s,i,x);
    for(int j=1,z,l,r;j<=k;j++){
      cin>>z>>l>>r;++z;
      add(i,z+n,r-l);
      d[i]-=l,d[z+n]+=l;
    }
  }
  for(int i=1;i<=t;i++){
    if(d[i]>0) add(ss,i,d[i]),sum+=d[i];
    else if(d[i]<0) add(i,tt,-d[i]);
  }
  add2(t,s,inf);
  res=dinic(ss,tt);
    
  if(res!=sum) return cout<<"-1"<<endl<<endl,void();
  tot-=2,head[t]=h1,head[s]=h2;
  cout<<res+dinic(s,t)<<endl<<endl;
}

最小费用最大流

嗯。

P2053 [SCOI2007] 修车

P2053 [SCOI2007] 修车

同一时刻有 \(N\) 位车主带着他们的爱车来到了汽车维修中心。

维修中心共有 \(M\) 位技术人员,不同的技术人员对不同的车进行维修所用的时间是不同的。

现在需要安排这 \(M\) 位技术人员所维修的车及顺序,使得顾客平均等待的时间最小。

说明:顾客的等待时间是指从他把车送至维修中心到维修完毕所用的时间。

\(2\le M\le 9\)\(1\le N\le 60\)\(1\le T\le 10^3\)

感觉就很想网络流,但是又很难去排顺序。


首先构成一个二分图,左边是车,右边是人,

那么如何做到车辆按照顺序呢?

我们考虑 拆点,把每一个工人拆成 \(n\) 个点,每一个点表示他修他的倒数第 \(i\) 辆车所用的时间。

于是费用是好计算的,直接 \(i \times t_j\) 即可,这个推一推就可以知道。

直接跑一次费用流就可以了。

#include <bits/stdc++.h>
using namespace std;

const int N=1e3+5,inf=1e9;
int n,m,head[N],tot=-1,cur[N],dis[N],s,t,ans=0,ansc=0;
struct edge{int v,nxt,w,c;}e[N*N];
bool vis[N];

void add(int u,int v,int w,int c){
  e[++tot]=(edge){v,head[u],w,c};head[u]=tot;
  e[++tot]=(edge){u,head[v],0,-c};head[v]=tot;
}

bool spfa(){
  for(int i=0;i<=t;i++) dis[i]=inf,vis[i]=false;
  queue<int> q;
  q.push(s);vis[s]=true;
  cur[s]=head[s];dis[s]=0;
  while(!q.empty()){
    int u=q.front();q.pop();vis[u]=false;
    for(int i=head[u];i!=-1;i=e[i].nxt){
      int v=e[i].v,w=e[i].w,c=e[i].c;
      if(w&&dis[v]>dis[u]+c){
      	dis[v]=dis[u]+c;cur[v]=head[v];
      	if(!vis[v]) q.push(v),vis[v]=true;
      }
    }
  }
  return dis[t]!=inf;
}

int dfs(int u,int flow){
  vis[u]=true;
  if(u==t) return flow;
  int res=flow;
  for(int &i=cur[u];i!=-1;i=e[i].nxt){
  	int v=e[i].v,w=e[i].w,c=e[i].c;
  	if(w&&!vis[v]&&dis[v]==dis[u]+c){
  	  int nw=dfs(v,min(res,w));
  	  res-=nw;e[i].w-=nw;e[i^1].w+=nw;
  	}
  	if(!res) break;
  }
  return flow-res;
}

void dinic(){
  while(spfa()){
  	int nw=dfs(s,inf);
    ans+=nw,ansc+=dis[t]*nw;
  }
}

int main(){
  /*2023.12.12 H_W_Y P2053 [SCOI2007] 修车 最小费用最大流*/
  scanf("%d%d",&m,&n);
  memset(head,-1,sizeof(head));tot=-1;
  for(int i=1;i<=n;i++)
    for(int j=1,x;j<=m;j++){
      scanf("%d",&x);
      for(int k=1;k<=n;k++) add(i,j*n+k,1,x*k);
    }
  s=0,t=n*(m+1)+1;
  for(int i=1;i<=n;i++) add(s,i,1,0);
  for(int i=1;i<=n*m;i++) add(i+n,t,1,0);
  dinic();
  printf("%.2lf\n",1.0*ansc/n);
  return 0;
}

P3705 [SDOI2017] 新生舞会

P3705 [SDOI2017] 新生舞会

学校组织了一次新生舞会,Cathy 作为经验丰富的老学姐,负责为同学们安排舞伴。

\(n\) 个男生和 \(n\) 个女生参加舞会,一个男生和一个女生一起跳舞,互为舞伴。

Cathy 收集了这些同学之间的关系,比如两个人之前认识没,计算得出 \(a_{i,j}\)

Cathy 还需要考虑两个人一起跳舞是否方便,比如身高体重差别会不会太大,计算得出 \(b_{i,j}\),表示第 \(i\) 个男生和第 \(j\) 个女生一起跳舞时的不协调程度。

当然,还需要考虑很多其他问题。

Cathy 想先用一个程序通过 \(a_{i,j}\)\(b_{i,j}\) 求出一种方案,再手动对方案进行微调。

Cathy 找到你,希望你帮她写那个程序。

一个方案中有 n 对舞伴,假设没对舞伴的喜悦程度分别是 \(a'_1,a'_2,...,a'_n\),假设每对舞伴的不协调程度分别是 \(b'_1,b'_2,...,b'_n\)。令

\(C=\frac {a'_1+a'_2+...+a'_n}{b'_1+b'_2+...+b'_n}\)

Cathy 希望 \(C\) 值最大。

\(1\le n\le 100,1\le a_{i,j},b_{i,j}\le10^4\)

发现直接求 \(C\) 是非常困难的。


我们做一些分数规划,

发现 \(\sum a_i - \sum{b_i} \times C = 0\)

那么容易想到去二分 \(C\),于是我们就是要求 \(a_i-b_i \times C\) 的最大值,

这个可以直接用网络流完成了,

具体就是分成二分图,每一条边的费用就是 \(a_i - b_i \times C\),我们只需要求出最大费用最大流即可。

#include <bits/stdc++.h>
using namespace std;
#define eps 1e-8
#define db double

const int N=1e5+5,M=105,inf=1e9;
int n,head[N],tot=-1,cur[N],s,t,a[105][105],b[105][105];
bool vis[N];
db ansc=0,dis[N];
struct edge{int v,nxt,w;db c;}e[N<<1];

void add(int u,int v,int w,db c){
  e[++tot]=(edge){v,head[u],w,c};head[u]=tot;
  e[++tot]=(edge){u,head[v],0,-c};head[v]=tot;
}

bool spfa(){
  for(int i=s;i<=t;i++) dis[i]=-1.0*inf,vis[i]=false;
  queue<int> q;q.push(s);
  cur[s]=head[s],vis[s]=true,dis[s]=0;
  while(!q.empty()){
  	int u=q.front();q.pop();vis[u]=false;
  	for(int i=head[u];i!=-1;i=e[i].nxt){
  	  int v=e[i].v,w=e[i].w;db c=e[i].c;
  	  while(w&&dis[v]<dis[u]+c){
  	    dis[v]=dis[u]+c;cur[v]=head[v];
  	    if(!vis[v]) q.push(v),vis[v]=true;
  	  }
  	}
  }
  return dis[t]!=-1.0*inf;
}

int dfs(int u,int flow){
  vis[u]=true;
  if(u==t) return flow;
  int res=flow;
  for(int &i=cur[u];i!=-1;i=e[i].nxt){
  	int v=e[i].v,w=e[i].w;db c=e[i].c;
  	if(!vis[v]&&w&&dis[v]==dis[u]+c){
  	  int nw=dfs(v,min(res,w));
  	  if(nw<=0) continue;
  	  res-=nw;e[i].w-=nw;e[i^1].w+=nw;
  	}
  	if(!res) break;
  }
  return flow-res;
}

void dinic(){
  ansc=0;
  while(spfa()){
    vis[t]=true;
    while(vis[t]){
      for(int i=0;i<=t;i++) vis[i]=false;
      db nw=dfs(s,inf);ansc+=1.0*nw*dis[t];
    }
  }
}

bool chk(db x){
  s=0,t=2*n+1;
  for(int i=0;i<=t;i++) head[i]=-1,vis[i]=false;tot=-1;
  for(int i=1;i<=n;i++) add(s,i,1,0),add(i+n,t,1,0);
  for(int i=1;i<=n;i++)
    for(int j=1;j<=n;j++)
      add(i,j+n,1,1.0*a[i][j]-1.0*x*b[i][j]);
  dinic();
  return ansc>=0;
}

void sol(){
  db r=10005,l=0,ans=0;
  while(r-l>eps){
  	db mid=(l+r)/2.0;
  	if(chk(mid)) ans=mid,l=mid+eps;
  	else r=mid-eps;
  }
  printf("%.6lf\n",ans);
}

int main(){
  /*2023.12.12 H_W_Y P3705 [SDOI2017] 新生舞会 最小费用最大流*/
  scanf("%d",&n);
  for(int i=1;i<=n;i++) for(int j=1;j<=n;j++) scanf("%d",&a[i][j]);
  for(int i=1;i<=n;i++) for(int j=1;j<=n;j++) scanf("%d",&b[i][j]);
  sol();return 0;
}

注意 eps 一定要取小一点。


P3980 [NOI2008] 志愿者招募

P3980 [NOI2008] 志愿者招募

申奥成功后,布布经过不懈努力,终于成为奥组委下属公司人力资源部门的主管。布布刚上任就遇到了一个难题:为即将启动的奥运新项目招募一批短期志愿者。经过估算,这个项目需要 \(n\) 天才能完成,其中第 \(i\) 天至少需要 \(a_i\) 个人。布布通过了解得知,一共有 \(m\) 类志愿者可以招募。其中第 \(i\) 类可以从第 \(s_i\) 天工作到第 \(t_i\) 天,招募费用是每人 \(c_i\) 元。新官上任三把火,为了出色地完成自己的工作,布布希望用尽量少的费用招募足够的志愿者,但这并不是他的特长!于是布布找到了你,希望你帮他设计一种最优的招募方案。

\(1\leq n\leq 1000\)\(1\leq m\leq 10000\),题目中其他所涉及的数据均不超过 \(2^{31}-1\)

发现图中的每一类志愿者是只要你选了它就会给你干 \(s \sim t\) 天,这并不是好处理的。


既然上一天的志愿者要延续到下一天,而这道题读起来很想最小费用最大流,我把人数变成流量。

于是我们考虑从 \(i \to i+1\) 建立一条流量为 \(-a[i]\) 的边,比较好理解。

那么如何去满足人数的限制条件呢?


发现每一类志愿者干都会一直干到 \(t\) 天,

也就是说在 \(s\) 天开始这些志愿者人数时不变的,直到 \(t+1\) 天他们就不干了,

于是我们可以建立一条 \(s \to t+1\) 的边,流量为 \(\inf\),那么这样就表示到 \(t+1\) 那天往下流的时候,这一天对下一天的贡献会减小。

而费用就是我们题目中的 \(c_i\) 即可,最后我们希望总的流量为 \(0\)


但是考虑网络流的流量不能是负数,所以直接把 \(-a[i]\) 变成 \(\inf - a[i]\),这样流量最终就是 \(\inf\) 即可。

#include <bits/stdc++.h>
using namespace std;

const int N=1e3+5,inf=1e9;
int n,m,head[N],tot=-1,dis[N],cur[N],s,t;
bool vis[N];
struct edge{int v,nxt,w,c;}e[N*N];

void add(int u,int v,int w,int c){
  e[++tot]=(edge){v,head[u],w,c};head[u]=tot;
  e[++tot]=(edge){u,head[v],0,-c};head[v]=tot;
}

bool spfa(){
  for(int i=0;i<=t;i++) dis[i]=inf,vis[i]=false;
  queue<int> q;q.push(s);
  cur[s]=head[s],vis[s]=true,dis[s]=0;
  while(!q.empty()){
  	int u=q.front();q.pop();vis[u]=false;
  	for(int i=head[u];i!=-1;i=e[i].nxt){
  	  int v=e[i].v,w=e[i].w,c=e[i].c;
  	  if(w&&dis[v]>dis[u]+c){
  	  	dis[v]=dis[u]+c;cur[v]=head[v];
  	  	if(!vis[v]) q.push(v),vis[v]=true;
  	  }
  	}
  }
  return dis[t]!=inf;
}

int dfs(int u,int flow){
  vis[u]=true;
  if(u==t) return flow;
  int res=flow;
  for(int &i=cur[u];i!=-1;i=e[i].nxt){
  	int v=e[i].v,w=e[i].w,c=e[i].c;
  	if(!vis[v]&&w&&dis[v]==dis[u]+c){
  	  int nw=dfs(v,min(w,res));
  	  res-=nw,e[i].w-=nw,e[i^1].w+=nw;
  	} 
  	if(!res) break;
  }
  return flow-res;
}

int dinic(){
  int res=0;
  while(spfa()){
  	vis[t]=true;
  	while(vis[t]){
  	  for(int i=s;i<=t;i++) vis[i]=false;
  	  int nw=dfs(s,inf);res+=nw*dis[t];
  	}
  }
  return res;
}

int main(){
  /*2023.12.12 H_W_Y P3980 [NOI2008] 志愿者招募 最小费用最大流*/
  ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
  cin>>n>>m;memset(head,-1,sizeof(head));tot=-1;
  s=0,t=n+2;
  for(int i=1,x;i<=n;i++) cin>>x,add(i,i+1,inf-x,0);
  for(int i=1,si,ti,x;i<=m;i++) cin>>si>>ti>>x,add(si,ti+1,inf,x);
  add(s,1,inf,0);add(n+1,t,inf,0);
  cout<<dinic()<<'\n';
  return 0;
}

[AGC031E] Snuke the Phantom Thief

[AGC031E] Snuke the Phantom Thief

在二维平面上,有 \(n\) 颗珠宝,第\(i\)颗珠宝在 \((x_i,y_i)\) 的位置,价值为 \(v_i\)

现在有一个盗贼想要偷这些珠宝。

现在给出 \(m\) 个限制约束偷的珠宝,约束有以下四种:

  • 横坐标小于等于 \(a_i\) 的珠宝最多偷 \(b_i\) 颗。
  • 横坐标大于等于 \(a_i\) 的珠宝最多偷 \(b_i\) 颗。
  • 纵坐标小于等于 \(a_i\) 的珠宝最多偷 \(b_i\) 颗。
  • 纵坐标大于等于 \(a_i\) 的珠宝最多偷 \(b_i\) 颗。

这四个限制输入的时候分别用LRDU四个字母来区分。

现在问你在满足这些约束的条件下,盗贼偷的珠宝的最大价值和是多少。

\(1 \le n \le 80, 1\le m \le 320\)

挺有意思的一道题。


首先感觉可以去网络流,但是最多的限制就非常难搞,所以我们考虑把它变成最小。

于是不难想到去枚举答案,这样被选的每一颗宝石就有了要求,即 \([lx,rx],[ly,ry]\)

这是很好求出来的,于是直接建图跑最大费用最大流即可。

#include <bits/stdc++.h>
using namespace std;
#define ll long long

const int N=1e5+5,inf=1e9,M=105;
const ll INF=1e18;
int n,m,head[N],s,t,fl,tot=-1,cur[N],Lx[M],Ly[M],Rx[M],Ry[M];
ll dis[N],ans,sum=0;
bool vis[N];
struct pnt{int x,y;ll v;}a[M];
struct qry{char ch;int x,y;}p[505];
struct edge{int v,nxt,w;ll c;}e[N<<1];

void add(int u,int v,int w,ll c){
  e[++tot]=(edge){v,head[u],w,c};head[u]=tot;
  e[++tot]=(edge){u,head[v],0,-c};head[v]=tot;
}

bool spfa(){
  for(int i=s;i<=t;i++) dis[i]=-INF,vis[i]=false;
  queue<int> q;q.push(s);
  dis[s]=0,cur[s]=head[s],vis[s]=true;
  while(!q.empty()){
  	int u=q.front();q.pop();vis[u]=false;
  	for(int i=head[u];i!=-1;i=e[i].nxt){
  	  int v=e[i].v,w=e[i].w;ll c=e[i].c;
  	  if(w&&dis[v]<dis[u]+c){
  	  	dis[v]=dis[u]+c;cur[v]=head[v];
  	  	if(!vis[v]) q.push(v),vis[v]=true;
  	  }
  	}
  }
  return dis[t]!=-INF;
}

int dfs(int u,int flow){
  vis[u]=true;
  if(u==t) return flow;
  int res=flow;
  for(int &i=cur[u];i!=-1;i=e[i].nxt){
  	int v=e[i].v,w=e[i].w;ll c=e[i].c;
  	if(w&&!vis[v]&&dis[v]==dis[u]+c){
  	  int nw=dfs(v,min(res,w));
  	  res-=nw;e[i].w-=nw,e[i^1].w+=nw;
  	}
  	if(!res) break;
  }
  return flow-res;
}

void dinic(){
  fl=0,ans=0;
  while(spfa()){
  	vis[t]=true;
  	while(vis[t]){
  	  for(int i=s;i<=t;i++) vis[i]=false;
  	  int nw=dfs(s,inf);fl+=nw,ans+=1ll*nw*dis[t];
  	}
  }
}

int main(){
  /*2023.12.12 H_W_Y [AGC031E] Snuke the Phantom Thief 最大费用最大流*/
  ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
  cin>>n;
  for(int i=1;i<=n;i++) cin>>a[i].x>>a[i].y>>a[i].v;
  cin>>m;
  for(int i=1;i<=m;i++) cin>>p[i].ch>>p[i].x>>p[i].y;
  for(int k=1;k<=n;k++){
  	s=0,t=2*k+2*n+1;
  	for(int i=s;i<=t;i++) head[i]=-1,vis[i]=false;tot=-1;
  	for(int i=1;i<=k;i++) add(s,i,1,0);
  	for(int i=2*n+k+1;i<t;i++) add(i,t,1,0);
  	for(int i=k+1;i<=k+n;i++) add(i,i+n,1,a[i-k].v);
  	for(int i=1;i<=k;i++) Lx[i]=Ly[i]=0,Rx[i]=Ry[i]=inf;
  	
  	for(int i=1;i<=m;i++){
  	  if(p[i].ch=='L') for(int j=p[i].y+1;j<=k;j++) Lx[j]=max(Lx[j],p[i].x+1);
  	  if(p[i].ch=='R') for(int j=1;j<=k-p[i].y;j++) Rx[j]=min(Rx[j],p[i].x-1);
  	  if(p[i].ch=='U') for(int j=1;j<=k-p[i].y;j++) Ry[j]=min(Ry[j],p[i].x-1);
  	  if(p[i].ch=='D') for(int j=p[i].y+1;j<=k;j++) Ly[j]=max(Ly[j],p[i].x+1);
  	}
  	
  	for(int i=1;i<=k;i++)
  	  for(int j=1;j<=n;j++)
  	    if(Lx[i]<=a[j].x&&a[j].x<=Rx[i]) add(i,j+k,1,0);
  	for(int i=1;i<=k;i++)
  	  for(int j=1;j<=n;j++)
  	    if(Ly[i]<=a[j].y&&a[j].y<=Ry[i]) add(j+k+n,i+2*n+k,1,0);
  	dinic();
  	sum=max(sum,ans);
  }
  cout<<sum<<'\n';
  return 0;
}

CF1178H Stock Exchange - 前缀和优化建图 - 好题!

CF1178H Stock Exchange

股票交易所里有 \(2n\) 种股票,每种股票有两个属性 \(a_i,b_i\),在时刻 \(t\ge 0\),第 \(i\) 种股票的价格为 \(a_i*\lfloor t\rfloor+b_i\)

每个时刻可以进行任意次股票交易,在时刻 \(t\) 时能够把股票 \(i\) 换成股票 \(j\) 当且仅当股票 \(i\) 在时刻 \(t\) 的价格不小于股票 \(j\) 在时刻 \(t\) 的价格。

现在你手上有 \(1\)\(n\) 号股票各一张,现在要求的是把这些股票换成 \(n+1\)\(2n\) 号股票各一张的最早时刻,以及在最早换完股票前提下的最少交易次数。

\(1\le n\le 2200,0\le a_i,b_i\le 10^9\)

好题!


首先很容易想到二分答案,发现如果答案是 \(T\),那么其实交换的时刻也就是 \(0,T\),这样一定是不劣的。

于是我们就可以在 \(0\) 把每个股票换成 \(T\) 时刻最大的股票从而在 \(T\) 时刻任意交换,

而这样的贪心一定是不劣的,而实现起来直接用两个排序去解决,时间复杂度 \(\mathcal O(n \log^2n)\)


于是乎第一问就解决了,现在来考虑第二问。

用同样的思路,我们很容易想到把一个点拆成两个,分别表示 \(0\)\(T\) 时刻,

于是按照可以交换的关系建图,这样直接跑费用流即可。

但是发现这道题的空间是 \(16MB\),显然是不行的。

考虑优化,

不难想到可以用前缀和优化建图,于是边数就变成了 \(O(n)\) 级别的,

最后跑一个费用流即可。(由于每次增广的流量一定是 \(1\),所以可以做一些小优化,见代码)

#include <bits/stdc++.h>
using namespace std;
#define ll long long 

const int N=1e5+5,inf=1e9;
int n,s,t,cnt=0,head[N],tot=-1,id1[N],id2[N],l,r,T,dis[N],lst[N],f[N],in[N],out[N],sum[N];
ll k[N],b[N],ans=0,pos[N];
struct edge{int v,nxt,w,c;}e[N<<1];
bool vis[N];

ll calc(int i,int x){return 1ll*x*k[i]+b[i];}
void add(int u,int v,int w,int c){
  e[++tot]=(edge){v,head[u],w,c};head[u]=tot;
  e[++tot]=(edge){u,head[v],0,-c};head[v]=tot;
}

bool spfa(){
  for(int i=0;i<=cnt;i++) dis[i]=inf,vis[i]=false;
  queue<int> q;q.push(s);
  vis[s]=true;dis[s]=0;
  while(!q.empty()){
  	int u=q.front();q.pop();vis[u]=false;
  	for(int i=head[u];i!=-1;i=e[i].nxt){
  	  int v=e[i].v,w=e[i].w,c=e[i].c;
  	  if(w&&dis[v]>dis[u]+c){
  	  	dis[v]=dis[u]+c;lst[v]=i;f[v]=u;
  	  	if(!vis[v]) q.push(v),vis[v]=true;
  	  }
  	}
  }
  return dis[t]!=inf;
}

void dinic(){
  ans=0;
  while(spfa()){
  	ans+=dis[t];
  	int u=t;
  	while(u!=s) --e[lst[u]].w,++e[lst[u]^1].w,u=f[u];
  }
}

bool cmp1(const int &x,const int &y){return (b[x]==b[y])?(k[x]>k[y]):(b[x]<b[y]);}

bool chk(int T){
  for(int i=1;i<=(n<<1);i++) id1[i]=id2[i]=i;
  sort(id1+1,id1+(n<<1)+1,cmp1);
  sort(id2+n+1,id2+(n<<1)+1,[&](int x,int y){return calc(x,T)<calc(y,T);});
  ll mx=0;
  for(int i=1;i<=(n<<1);i++){
  	mx=max(mx,calc(id1[i],T));
  	if(id1[i]<=n) pos[id1[i]]=mx;
  }
  sort(pos+1,pos+n+1);
  for(int i=n;i>=1;i--)
    if(pos[i]<calc(id2[i+n],T)) return false;
  return true;
}

void sol(int T){
  s=++cnt;t=++cnt;
  for(int i=1;i<=(n<<1);i++){
  	id1[i]=id2[i]=i;
    in[i]=++cnt;out[i]=++cnt;
    if(i<=n) add(s,in[i],1,0);
    else add(out[i],t,1,0);
    add(in[i],out[i],inf,0);
  }
  sort(id1+1,id1+(n<<1)+1,cmp1);
  sum[1]=in[id1[1]];
  for(int i=2;i<=(n<<1);i++){
  	sum[i]=++cnt;
  	add(sum[i],sum[i-1],inf,0);
  	add(sum[i],in[id1[i]],inf,0);
  	add(in[id1[i]],sum[i-1],inf,1);
  }
  sort(id2+1,id2+(n<<1)+1,[&](int x,int y){return (calc(x,T)==calc(y,T))?x>y:calc(x,T)<calc(y,T);});
  sum[1]=out[id2[1]];
  for(int i=2;i<=(n<<1);i++){
  	sum[i]=++cnt;
  	add(sum[i],sum[i-1],inf,0);
  	add(sum[i],out[id2[i]],inf,0);
  	add(out[id2[i]],sum[i-1],inf,1);
  }
  dinic();
}

int main(){
  /*2023.12.12 H_W_Y CF1178H Stock Exchange 二分+最小费用最大流*/
  ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
  l=0,r=1e9+1;memset(head,-1,sizeof(head));tot=-1;
  cin>>n;
  for(int i=1;i<=(n<<1);i++) cin>>k[i]>>b[i];
  while(l<r){
  	int mid=(l+r)/2;
  	if(chk(mid)) r=mid;
  	else l=mid+1;
  }
  if(l==1e9+1) cout<<"-1",exit(0);
  sol(l);cout<<l<<' '<<ans<<'\n';
  return 0;
}

P4249 [WC2007] 剪刀石头布 - 好题

P4249 [WC2007] 剪刀石头布

在一些一对一游戏的比赛(如下棋、乒乓球和羽毛球的单打)中,我们经常会遇到 \(A\) 胜过 \(B\)\(B\) 胜过 \(C\)\(C\) 又胜过 \(A\) 的有趣情况,不妨形象的称之为剪刀石头布情况。有的时候,无聊的人们会津津乐道于统计有多少这样的剪刀石头布情况发生,即有多少对无序三元组 \((A,B,C)\),满足其中的一个人在比赛中赢了另一个人,另一个人赢了第三个人而第三个人又胜过了第一个人。注意这里无序的意思是说三元组中元素的顺序并不重要,将 \((A, B, C)\)\((A, C, B)\)\((B, A, C)\)\((B, C, A)\)\((C, A, B)\)\((C, B, A)\) 视为相同的情况。

\(N\) 个人参加一场这样的游戏的比赛,赛程规定任意两个人之间都要进行一场比赛:这样总共有 \(\frac{N*(N-1)}{2}\) 场比赛。比赛已经进行了一部分,我们想知道在极端情况下,比赛结束后最多会发生多少剪刀石头布情况。即给出已经发生的比赛结果,而你可以任意安排剩下的比赛的结果,以得到尽量多的剪刀石头布情况。

\(N \leq 100\)

发现正着真的非常不好做啊。


题意就是一个三元环计数问题,但是正着一点也不好做。

所以现在我们考虑倒着来做,容易计算出 \(n\) 个点构成三元环的总个数是

\[\binom{n}{3}=\frac{n(n-1)(n-2)}{6} \]

而当三个元素不能组成三元环时,一定是有一个点的 入度/出度\(2\) 了。

那么我们现在只考虑入度(这样不会算重),

发现设一个点的入度时 \(x\),那么它所在的三元组有 \(\binom{x}{2}\) 是不合法的。

于是答案就是

\[ans=\frac{n(n-1)(n-2)}{6} - \sum_{i=1}^n \binom{degree(i)}{2} \]

可是现在又怎么求呢?


我们考虑差分一下,也就是考虑入度 \(+1\) 所造成的贡献,设当前入度为 \(x\),那么

\[\binom{x+1}{2} - \binom{x}{2} = x \]

于是我们把每一个的 \(x\) 作为费用,也就是 \(0,1,2,\dots\),跑一次费用流就可以了。


如何建图?

由于一条边对应的两点只有一个点的入度会 \(+1\),于是我们把每一条边也看作一个点。

  1. \(s\) 到边对应的点连一条容量为 \(1\),费用为 \(0\) 的边。
  2. 边对应的点向它所连接的节点分别连容量为 \(1\),费用为 \(0\) 的边,从而保证了只选一个。
  3. 每一个图中的节点向 \(t\)\(n\) 条边,容量为 \(1\),费用为 \(0,1,2,\dots\)

于是就做完了。


#include <bits/stdc++.h>
using namespace std;

const int N=1e5+5,M=105,inf=1e9;
int n,head[N],tot=-1,cur[N],dis[N],ans=0,cnt=0,s,t,a[M][M],id[M][M],d[M];
bool vis[N];
struct edge{int v,nxt,w,c;}e[N<<1];

void add(int u,int v,int w,int c){
  e[++tot]=(edge){v,head[u],w,c};head[u]=tot;
  e[++tot]=(edge){u,head[v],0,-c};head[v]=tot;
}

bool spfa(){
  for(int i=s;i<=t;i++) dis[i]=inf,vis[i]=false;
  queue<int> q;q.push(s);
  dis[s]=0;cur[s]=head[s],vis[s]=true;
  while(!q.empty()){
  	int u=q.front();q.pop();vis[u]=false;
  	for(int i=head[u];i!=-1;i=e[i].nxt){
  	  int v=e[i].v,w=e[i].w,c=e[i].c;
  	  if(w&&dis[v]>dis[u]+c){
  	  	dis[v]=dis[u]+c;cur[v]=head[v];
  	  	if(!vis[v]) q.push(v),vis[v]=true;
  	  }
  	}
  }
  return dis[t]!=inf;
}

int dfs(int u,int flow){
  vis[u]=true;
  if(u==t) return flow;
  int res=flow;
  for(int &i=cur[u];i!=-1;i=e[i].nxt){
  	int v=e[i].v,w=e[i].w,c=e[i].c;
  	if(w&&!vis[v]&&dis[v]==dis[u]+c){
  	  int nw=dfs(v,min(w,res));
  	  res-=nw;e[i].w-=nw;e[i^1].w+=nw;
  	}
  	if(!res) break;
  }
  return flow-res;
}

void dinic(){
  while(spfa()){
  	int nw=dfs(s,inf);
  	ans+=nw*dis[t];
  }
}

int main(){
  /*2023.12.12 H_W_Y P4249 [WC2007] 剪刀石头布 最小费用最大流*/
  ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
  cin>>n;s=0,cnt=n;
  memset(head,-1,sizeof(head));tot=-1;
  for(int i=1;i<=n;i++)
    for(int j=1;j<=n;j++){
      cin>>a[i][j];
      if(j<=i) continue;
      if(a[i][j]==1) ++d[i];
      else if(a[i][j]==0) ++d[j];
      else{
      	id[i][j]=++cnt;
        add(cnt,i,1,0);add(cnt,j,1,0);add(s,cnt,1,0);
      }
    }
  t=++cnt;ans=0;
  for(int i=1;i<=n;i++){
  	if(d[i]>=2) ans+=(d[i]-1)*d[i]/2;
  	for(int j=d[i];j<n-1;j++) add(i,t,1,j);
  }
  dinic();
  ans=n*(n-1)*(n-2)/6-ans;
  cout<<ans<<'\n';
  for(int i=1;i<=n;i++)
    for(int j=i+1;j<=n;j++)
      if(a[i][j]==2)
      	for(int k=head[id[i][j]];k!=-1;k=e[k].nxt){
      	  int v=e[k].v;
      	  if(e[k].w==0){
      	  	if(v==i) a[i][j]=1,a[j][i]=0;
      	  	else a[i][j]=0,a[j][i]=1;
      	  	break;
      	  }
      	}

  for(int i=1;i<=n;cout<<'\n',i++) for(int j=1;j<=n;j++) cout<<a[i][j]<<' ';
  return 0;
}

黑白染色

这种问题一般与网格有关。


P7231 [COCI2015-2016#3] DOMINO

P7231 [COCI2015-2016#3] DOMINO

感觉想到黑白染色就做完了。

直接 simplex 跑过去,注意要卡空间,所以有些地方开 short 就可以了。

short 的范围是 \((-32767,32767)\)代码


P4701 粘骨牌

P4701 粘骨牌

首先对于这种网格问题,我们想到了 黑白染色

由于 \(n,m\) 都是奇数,所以我们假设四个角都是黑色,并且空白点一定是黑色的,因为黑色会比白色多一个。

而白色的点一定会被一个骨牌覆盖,所以白色的关键点也一定是露不出来的。


发现我们把棋子的移动看成空白点的移动,那么我们就可以得到一些边,

如果移动到了一个关键点,那么就失败了。

发现这样把图建出来,每一个关键点连到汇点,那么就变成了求 最小割 了。

所以直接用最大流=最小割去求就可以了。代码


注意有 \(n=1/m=1\) 的情况——有些人一直卡在 \(92\) 就是因为建边建出界了。


P4003 无限之环

P4003 无限之环 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

根本想不到是网络流。


首先,我们容易想到 流量平衡 一是,你从原点流出去多少流量,在汇点就有多少流量,

但是哪里是原点和汇点呢?

想一想发现每一个流量的连动块大小至少为 \(2\),那么我们只要 黑白染色,让原点和汇点相邻就可以了。

那么这样一想,我们就顺利地想到了 黑白染色,把黑点看作是原点,白点看作是汇点,每个点从中心连到四个方向,

这样就完成了建图。

判断是否成立其实就是判断流量是否相等。


接着发现,其实费用是可以去引流完成的,如果边数为 \(2\) 我们就从上到下/从左到右建边,费用为 \(1\) 就可以了。

最后跑一个最小费用最大流即可得到答案,具体可以看洛谷的第一篇题解。


流量平衡 想到 黑白染色 是非常妙的。代码

下面是建边部分。

void add_1(int x,int y){//从中心连到两边 
  for(int i=1;i<5;i++){
    if(!((a[x][y]>>(i-1))&1)) continue;
    if((x+y)&1) add(id(x,y,0),id(x,y,i),1,0);
    else add(id(x,y,i),id(x,y,0),1,0);
  }
}

void add_2(int x,int y){//与旁边的点连边 
  if(!((x+y)&1)) return;
  if(x>1) add(id(x,y,1),id(x-1,y,3),1,0);
  if(y<m) add(id(x,y,2),id(x,y+1,4),1,0);
  if(x<n) add(id(x,y,3),id(x+1,y,1),1,0);
  if(y>1) add(id(x,y,4),id(x,y-1,2),1,0);
}

void add_3(int x,int y){//有费用的边
  if(a[x][y]==5||a[x][y]==10) return ;
  if(num[x][y]==2){
    add(id(x,y,2),id(x,y,4),1,1);
    add(id(x,y,4),id(x,y,2),1,1);
    add(id(x,y,1),id(x,y,3),1,1);
    add(id(x,y,3),id(x,y,1),1,1);
  }else{
    add(id(x,y,1),id(x,y,2),1,1);
    add(id(x,y,2),id(x,y,3),1,1);
    add(id(x,y,3),id(x,y,4),1,1);
    add(id(x,y,4),id(x,y,1),1,1);
    
    add(id(x,y,2),id(x,y,1),1,1);
    add(id(x,y,3),id(x,y,2),1,1);
    add(id(x,y,4),id(x,y,3),1,1);
    add(id(x,y,1),id(x,y,4),1,1);
  }
}

出入度平衡

as the name implies.

每个点的出入度平衡,所以很明显容易想到 拆点


P3965 [TJOI2013] 循环格

P3965 [TJOI2013] 循环格 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

典。


容易发现满足条件当且仅当每个节点的 出度入度 都为 \(1\)

由于鸽巢原理,如果一个入度 \(\gt 1\),那么就一定有一个点没有入度了。

所以我们考虑把每个点拆成两个点,出点和入点,

建边有些费用为 \(1\),其他费用为 \(0\) 即可,这样就做完了。

只要想到出入度都为 \(1\),然后直接拆点就可以了。代码


P2469 [SDOI2010] 星际竞速

P2469 [SDOI2010] 星际竞速 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

感觉思路挺自然的,但是最后一步没想到。


首先拆点是显然的,每个点的入点表示从这个点连出去,反之出点表示到达了这个点。

那么点与点之间的道路是很好表示的,现在问题就在于如何表示能量爆发?

发现能量爆发的意思其实就是从任意一个入点直接到这个点,再说白一点就是从起点直接过来,

所以直接从原点连到这个点的出点就可以了。


这样就做完了,要把两个点的意义想清楚就是好做的了。代码


P4553 80人环游世界

P4553 80人环游世界 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

和上一题非常像,但是没有了跳跃操作。


由于起点是任意的,也就是从原点连到出点的每条边流量是任意的。

所以为了保证是 \(M\) 个人,所以我们直接建立一个虚拟点,与源点连一条流量为 \(m\) 的边就可以了,

连到右部点其实就相当于作为起点。

于是类似于上一道题我们就做完了。代码


区间选择

非常好的模型啊。


给定 \([1,m]\)\(n\) 个区间 \([L_i,R_i]\),每个区间 选择一次 的代价是 \(w_i\),每个区间最多选 \(p_i\) 次,要求使 任一点 \(j\) 被选的次数在 \([a_j,b_j]\) 中间,求代价最值。

这个模型的边数是 \(m+n\) 级别的。


首先,构造出一条链,长度为 \(m\),链上的每一条边 \((i,i+1)\) 记录点 \(i\) 的选择次数。

对于每一个区间 \([L,R]\),我们连一条边 \((L,R+1)\) 表示选择这个区间,流一个流量表示选了一次。

那么对于这样的边,它的流量是 \(p_i\) 而费用是 \(w_i\)

而我们如何限制每个点的 \([a_j,b_j]\) 呢?

发现假设现在的流量为 \(F\)(这里的 \(F\) 是足够大的),那么 \((i,i+1)\) 这条边的流量区间其实是 \([F-b_i,F-a_i]\)

因为选了这个点才不会从下面流过。

所以这本质上是一个上下界网络流,直接跑似乎就可以了。


P3358 最长k可重区间集问题

P3358 最长k可重区间集问题 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

很形式化的题意。

转化到模型上面就是 \(w=R-L+1,p=1,a=0,b=k\) 的东西。

而由于 \((i,i+1)\) 的流量的下届是定值,所以我们可以把总流量就直接减去 \(k\),这样就可以直接满足条件,

于是跑最小费用最大流就可以了。代码

  cin>>n>>m;
  S=114514,T=S+1;
  
  add(S,1,n-m,0);
  add(mx+1,T,n-m,0);
  
  for(int i=1;i<=mx;i++) add(i,i+1,m,0);
  
  for(int i=1,l,r;i<=n;i++){
    cin>>l>>r;--r;
    add(l,r+1,1,-r+l-1);
  }
  
  cerr<<Simplex()<<'\n';
  cout<<-MinC<<'\n';

P6967 [NEERC2016] Delight for a Cat

P6967 [NEERC2016] Delight for a Cat

判断一个区间是否合法是一个非常恼怒的事情,

那么其实我们可以转化成看一个点对哪一个区间有贡献。


我们把每一个区间看成了点,

讨论第 \(i\) 个时刻是否选择睡觉。

那么第 \(i\) 个时刻的决定相当于是把 \([i-k+1,i]\) 选了一次,而每个区间最多选一次,每次代价为 \(s_i-e_i\)

而对于每一个点,它至少被选 \(m_s\) 次,至多被选 \(k-m_e\) 次,于是上下界就有了。

这样就成功地转化成了区间选择问题,

而由于每一个的下界其实是一样的,所以我们可以把流量设为 \(k-m_e\) 从而消去了下界,

这样就直接用费用流做完了。代码

int main(){
  /*2024.3.18 H_W_Y P6967 [NEERC2016] Delight for a Cat MCMF*/
  ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
  cin>>n>>K>>Ms>>Me;
  for(int i=1;i<=n;i++) cin>>s[i];
  for(int i=1;i<=n;i++) cin>>E[i],Sum+=E[i];
  
  S=n+2,T=S+1;
  for(int i=1;i<=n-K+1;i++) add(i,i+1,K-Ms-Me,0);
  
  add(S,1,K-Me,0);
  add(n-K+2,T,K-Me,0);
  
  int pos=tot+1;
  
  for(int i=1;i<K;i++) add(1,min(i+1,n-K+2),1,-s[i]+E[i]);
  for(int i=K;i<=n;i++) add(i-K+1,min(i+1,n-K+2),1,-s[i]+E[i]);
  
  cerr<<Simplex()<<'\n';
  cout<<Sum-MinC<<'\n';
  
  for(int i=pos;i<tot-1;i+=2){
    if(e[i].w==0) cout<<"S";
    else cout<<"E";
  }
  
  return 0;
}

最小积费用流

相当于把普通费用流中的求和变成了求乘积。

于是这个似乎最好直接用 dinic 的费用流完成,

其实与其他没什么区别,大致的思路是完全一致的。


实现可以看第一题的代码,非常典。


P4329 [COCI2006-2007#1] Bond

P4329 [COCI2006-2007#1] Bond - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

典。


好久不写 dinic 费用流记得打标记啊!!!代码

#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define db double

const int N=1e4+3;
const db eps=1e-8;
int n,m;

db qpow(db a,ll b){
  db res=1.0;
  while(b){if(b&1) res=res*a;a=a*a;b>>=1;}
  return res;
}

namespace MCMF{
  const int inf=1e9;
  int head[N],tot=1,cur[N],S,T;
  db dis[N];
  bool vis[N];
  struct edge{int v,nxt,w;db c;}e[N<<1];
  
  void add(int u,int v,int w,db c){
    e[++tot]=(edge){v,head[u],w,c};head[u]=tot;
    e[++tot]=(edge){u,head[v],0,1.0/c};head[v]=tot;
  }
  
  bool spfa(){
    memset(dis,0,sizeof(dis));
    memset(vis,0,sizeof(vis));
    queue<int> Q;
    Q.push(S);dis[S]=1.0;
    cur[S]=head[S],vis[S]=1;
    while(!Q.empty()){
      int u=Q.front();Q.pop();
      vis[u]=0;
      for(int i=head[u];i;i=e[i].nxt){
        int v=e[i].v,w=e[i].w;db c=e[i].c;
        if(c!=0&&w&&dis[v]+eps<dis[u]*c){
          dis[v]=dis[u]*c;
          cur[v]=head[v];
          if(!vis[v]) vis[v]=1,Q.push(v);
        }
      }
    }
    return dis[T]!=0;
  }
  
  int dfs(int u,int flow){
    if(u==T||!flow) return flow;
    int res=flow;
    vis[u]=1;
    for(int i=cur[u];i&&res>0;i=e[i].nxt){
      cur[u]=i;
      int v=e[i].v,w=e[i].w;db c=e[i].c;
      if(!vis[v]&&c!=0&&w&&dis[v]-dis[u]*c<=eps){
        int f=dfs(v,min(w,res));
        if(!f) dis[v]=0;
        res-=f,e[i].w-=f,e[i^1].w+=f;
      }
    }
    vis[u]=0;
    return flow-res;
  }
  
  db MinC=1.0;
  
  int dinic(){
    int res=0;
    while(spfa()){
      int flow=dfs(S,inf);res+=flow;MinC*=qpow(dis[T],flow);
    }
    return res;
  }
}
using namespace MCMF;

int main(){
  /*2024.3.15 H_W_Y P4329 [COCI2006-2007#1] Bond MCMF*/
  ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
  cin>>n;
  S=2*n+1,T=S+1;
  
  for(int i=1;i<=n;i++){
    add(S,i,1,1.0);
    add(i+n,T,1,1.0);
  }
  
  for(int i=1;i<=n;i++)
    for(int j=1;j<=n;j++){
      db x;cin>>x;
      x=x/100.0;
      add(i,j+n,1,x);
    }
  
  int ans=dinic();
  if(ans!=n) cout<<"0\n";
  else cout<<fixed<<setprecision(7)<<MinC*100.0<<'\n';
  return 0;
}

P5814 [CTSC2001] 终极情报网

P5814 [CTSC2001] 终极情报网 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

和上一道题基本一致,只是输出方式比较恶心。

不需要拆点!!!

不要动不动就拆点啊。代码


分层图问题

一般来解决一些匀速递减的东西。

感觉非常有用。


P4009 汽车加油行驶问题

P4009 汽车加油行驶问题 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

考虑把油量分层,每一次走到下一层去,

具体可以看代码。


注意这道题其实跑最短路就可以了,

所以如果直接用 Simplex 会 TLE,非常不友好。代码

int main(){
  /*2024.3.15 H_W_Y P4009 汽车加油行驶问题 分层图问题*/ 
  ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
  cin>>n>>m>>A>>B>>C;++m;
  S=n*n*m+1,T=S+1;
  
  for(int i=1;i<=n;i++)
    for(int j=1;j<=n;j++)
      cin>>a[i][j];
  
  add(S,id(1,1,1),1,0);
  for(int k=1;k<=m;k++)
    for(int i=1;i<=n;i++)
      for(int j=1;j<=n;j++){
        if(a[i][j]==0&&k!=m){
          if(i>1) add(id(i,j,k),id(i-1,j,k+1),1,B);
          if(j>1) add(id(i,j,k),id(i,j-1,k+1),1,B);
          if(i<n) add(id(i,j,k),id(i+1,j,k+1),1,0);
          if(j<n) add(id(i,j,k),id(i,j+1,k+1),1,0);
        }
        if(a[i][j]==1&&k!=1){
          add(id(i,j,k),id(i,j,1),1,A);
          if(i>1) add(id(i,j,k),id(i-1,j,2),1,A+B);
          if(j>1) add(id(i,j,k),id(i,j-1,2),1,A+B);
          if(i<n) add(id(i,j,k),id(i+1,j,2),1,A);
          if(j<n) add(id(i,j,k),id(i,j+1,2),1,A);
        }
        if(a[i][j]==0&&k==m)
          add(id(i,j,k),id(i,j,1),1,C+A);
      }
  for(int i=1;i<=m;i++) add(id(n,n,i),T,1,0);
  
  cerr<<dinic()<<'\n';
  cout<<MinC<<'\n';
  
  return 0;
}

P4542 [ZJOI2011] 营救皮卡丘

P4542 [ZJOI2011] 营救皮卡丘

不太能救得了皮卡丘,必须经过 \(k-1\) 是一个非常烦人的限制。

其实这道题也不那么算成分层图。


考虑转化条件!

何必看成有条件顺序?发现直接可以转化成每个点经过一次的路径总和,

那么这个问题就变成了最小路径覆盖的问题,于是我们想到了费用流。


考虑拆点,把每一个点拆成入点和出点,

那么由于必须要经过这个点,所以流量至少为 \(1\)

现在考虑一种转化方式,我们把每一对入点和出点之间建立一条流量为 \(1\),费用为 \(-\infty\) 的边,

再去建立流量为 \(k\),费用为 \(0\) 的边,这样就保证了至少流一次。


而如何保证先经过 \(k-1\) 再经过 \(k\) 呢?

发现其实我们可以统一处理一下,先跑一个 Floyd 求一个最短路,

然后直接对于每一个 \(u<v\) 的边,从 \(u\) 的出点向 \(v\) 的入点连边即可。

这样跑费用流就可以求出最优答案了。

于是我们就做完了。代码

int main(){
  /*2024.3.18 H_W_Y P4542 [ZJOI2011] 营救皮卡丘 MCMF*/
  ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
  
  cin>>n>>m>>K;++n;
  S=2*n+1,T=S+1;
  
  for(int i=1;i<=n;i++){
    add(id(i,0),id(i,1),1,-1e6);
    add(id(i,0),id(i,1),K,0);
    add(id(i,1),T,K,0);
  }
  add(S,id(1,0),K,0);
  
  memset(dis,0x3f,sizeof(dis));
  
  for(int i=1,u,v,w;i<=m;i++){
    cin>>u>>v>>w,++u,++v;
    dis[u][v]=dis[v][u]=min(dis[u][v],w);  
  }
  
  for(int i=1;i<=n;i++)
    for(int j=i;j<=n;j++)
      for(int k=1;k<j;k++)
        dis[i][j]=dis[j][i]=min(dis[i][j],dis[i][k]+dis[k][j]);

  for(int i=1;i<=n;i++)
    for(int j=i+1;j<=n;j++)
      add(id(i,1),id(j,0),K,dis[i][j]);
      
  int res=Simplex();
  cerr<<res<<'\n';
  cout<<(MinC+n*1000000)<<'\n';

  return 0;
}

同时,gyy 提供了一个分层图的做法,即用分层图的方式满足上面说的第二个条件递推,

但是这样的实现是较劣的,具体可以看 gyy 的博客。


动态加边费用流

有些时候直接一次性把边加完是非常劣的,

因为边数会很大,而很多时候又用不到。


所以这个时候我们考虑动态加边,

一个点把之前的边都用完了之后再给他加新边。


而 Simplex 的加边也是简单的,

就是每一次跑完之后看看是不是需要再加一条边,

注意每次要统计流量,并且把 \(T \to S\) 的边删掉,

加完之后再重新加进去才行,每次相当于一次初始化。


很丑啊,但是跑得快啊。


P2050 [NOI2012] 美食节

P2050 [NOI2012] 美食节

一共有 \(n\) 道菜和 \(m\) 个厨师,第 \(i\) 个厨师做第 \(j\) 道菜需要 \(t_{i,j}\) 的时间,

需要做 \(p_i\) 道第 \(i\) 道菜,问最小等待时间。

\(1 \le n \le 40,1 \le m \le 100,\sum p_i \le 800\)

典。

其实就是上面 P2053 [SCOI2007] 修车 的加强版,

直接按照相同的方法做可以那到 70 分(用了 Simplex),

于是现在我们考虑如何优化。


容易发现后面的很多点都是没有意义的,所以我们直接按照前面说的动态加点的方法做即可。

代码非常好懂。

#include <bits/stdc++.h>
using namespace std;
#define ll long long

const int N=1e5+5,M=4e6+5;
int n,m,num=0,t[45][105],c[105];

int id(int i,int j){return n+(i-1)*num+j;}

namespace MCMF{
  const ll inf=1e9;
  int head[N],tot=1,cir[N],fa[N],fe[N],tag[N],nw=0,S,T;
  ll pre[N];
  
  struct edge{
  	int u,v,nxt;
  	ll w,c;
  }e[M<<1];
  
  void add(int u,int v,int w,int c){
    e[++tot]=(edge){u,v,head[u],w,c};head[u]=tot;
    e[++tot]=(edge){v,u,head[v],0,-c};head[v]=tot;
  }
  
  void init_ZCT(int u,int lst,int col=1){
  	fa[u]=e[lst].u,fe[u]=lst,tag[u]=col;
  	for(int i=head[u];i;i=e[i].nxt) if(e[i].w&&tag[e[i].v]!=col) init_ZCT(e[i].v,i,col);
  }
  
  ll sum(int u){
  	if(tag[u]==nw) return pre[u];
  	tag[u]=nw;pre[u]=sum(fa[u])+e[fe[u]].c;
  	return pre[u];
  }
  
  ll push_flow(int x){
  	int rt=e[x].u,lca=e[x].v,cnt=0,del=0,P=2;
  	ll F=e[x].w,cost=0;
  	
  	++nw;
  	while(rt) tag[rt]=nw,rt=fa[rt];
  	while(tag[lca]!=nw) tag[lca]=nw,lca=fa[lca];
  	
  	for(int u=e[x].u;u!=lca;u=fa[u]){
  	  cir[++cnt]=fe[u];
  	  if(F>e[fe[u]].w) F=e[fe[u]].w,del=u,P=0;
  	}
  	for(int u=e[x].v;u!=lca;u=fa[u]){
  	  cir[++cnt]=fe[u]^1;
  	  if(F>e[fe[u]^1].w) F=e[fe[u]^1].w,del=u,P=1;
  	}
  	cir[++cnt]=x;
  	
  	for(int i=1;i<=cnt;i++) cost+=F*e[cir[i]].c,e[cir[i]].w-=F,e[cir[i]^1].w+=F;
  	
  	if(P==2) return cost;
  	
  	int u=e[x].u,v=e[x].v;
  	if(P==1) swap(u,v);
  	int lste=x^P,lstu=v,tmp=0;
  	while(lstu!=del){
  	  lste^=1;--tag[u];
  	  swap(fe[u],lste);
  	  tmp=fa[u],fa[u]=lstu,lstu=u,u=tmp;
  	} 
  	return cost;
  }
  
  void clear(int x){e[x]=e[x^1]={0,0,0,0,0};}
  
  ll Simplex(){
  	add(T,S,inf,-inf);
  	init_ZCT(T,0,++nw);
  	tag[T]=++nw;fa[T]=0;
  	
  	ll MinC=0,F=0;
  	
  	bool fl=1;
  	while(fl){
  	  fl=0;
  	  for(int i=2;i<=tot;i++)
  	    if(e[i].w&&sum(e[i].u)-sum(e[i].v)+e[i].c<0)
  	      MinC+=push_flow(i),fl=1;
  	  
  	  F+=e[tot].w;clear(tot);
  	  
  	  for(int i=1;i<=m;i++)
  	    if(e[head[id(i,c[i])]].w==0){
  	      ++c[i];
  	      for(int j=1;j<=n;j++) add(j,id(i,c[i]),1,t[j][i]*c[i]);
  	      add(id(i,c[i]),T,1,0);
  	    }
  	  add(T,S,inf,-inf);init_ZCT(T,0,++nw);tag[T]=++nw;fa[T]=0;
  	}
  	
  	MinC+=F*inf;
  	return MinC;
  }
}
using namespace MCMF;

int main(){
  /*2024.1.2 H_W_Y P2050 [NOI2012] 美食节 网络流*/
  ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
  cin>>n>>m;
  S=1e5,T=S+1;
  for(int i=1,x;i<=n;i++) cin>>x,add(S,i,x,0),num+=x;
  for(int i=1;i<=n;i++)
    for(int j=1;j<=m;j++)
      cin>>t[i][j];
  for(int i=1;i<=m;i++){
    for(int j=1;j<=n;j++)
      add(j,id(i,1),1,t[j][i]);
    add(id(i,1),T,1,0),c[i]=1;
  }
  cout<<Simplex();
  return 0;
}

P3529 [POI2011] PRO-Programming Contest

P3529 [POI2011] PRO-Programming Contest

和上一道题很像啊。


但是直接建边的边数可以达到 \(500^3\)!!!

那怎么办呢?


发现一下这道题的性质,发现费用是一定的,所以其实并不需要每次都向能到的所有点重新建边,我们考虑直接从 \(S\) 向每一个小师傅建立一条容量为 \(1\),费用为 \(r\) 的边,那么每一次给小师傅加流量就只会改一条边。

于是对于每一个小师傅到任务的边都是容量为 \(1\) 的免费边。

所以每一次动态加边只会改变从源点到小师傅边,这样的边数就有保证了。代码

#include <bits/stdc++.h>
using namespace std;
#define ll long long

const int N=1e6+5;
int n,m,R,len,K;

namespace MCMF{
  const ll inf=1e12;
  int head[N],tot=1,cur[N],S,T,F[N];
  ll dis[N],ans,anscost;
  bool vis[N];
  
  struct edge{
  	int v,nxt;
  	ll w,c;
  }e[N<<1];
  
  void add(int u,int v,ll w,ll c){
  	e[++tot]=(edge){v,head[u],w,c};
  	head[u]=tot;
  	e[++tot]=(edge){u,head[v],0,-c};
  	head[v]=tot;
  }
  
  bool spfa(){
  	for(int i=0;i<=T;i++) dis[i]=inf,vis[i]=false;
    queue<int> Q;
    Q.push(S),dis[S]=0,vis[S]=1,cur[S]=head[S];
    while(!Q.empty()){
      int u=Q.front();Q.pop();
      vis[u]=0;
      for(int i=head[u];i;i=e[i].nxt){
      	int v=e[i].v;ll w=e[i].w,c=e[i].c;
      	if(w&&dis[v]>dis[u]+c){
      	  dis[v]=dis[u]+c;
		  cur[v]=head[v];
		  if(!vis[v]) vis[v]=1,Q.push(v);	
		}
	  }
	}
	return dis[T]!=inf;
  }
  
  ll dfs(int u,ll flow){
  	if(u==T||!flow) return flow;
  	ll res=flow;vis[u]=1;
  	for(int i=cur[u];i&&res;i=e[i].nxt){
  	  cur[u]=i;
	  int v=e[i].v;ll w=e[i].w,c=e[i].c;
	  if(w&&!vis[v]&&dis[v]==dis[u]+c){
	  	ll f=dfs(v,min(w,res));
	  	if(!f) dis[v]=inf;
	  	res-=f,e[i].w-=f,e[i^1].w+=f;
	  }	
	}
	return flow-res;
  }
  
  void dinic(){
  	ans=anscost=0;
  	
  	while(spfa()){
  	  ll f=dfs(S,inf);
	  ans+=f;
	  anscost+=f*dis[T];
	  
	  if(f){
	  	for(int i=1;i<=n;i++)
	  	  if(e[head[i]^1].w==0&&F[i]+1<=len/R)
	  	    ++F[i],add(S,i,1,F[i]*R);
	  }	
	}
  }
}

using namespace MCMF;

int main(){
  /*2024.3.17 H_W_Y P3529 [POI2011] PRO-Programming Contest MCMF*/
  ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
  cin>>n>>m>>R>>len>>K;
  
  S=n+m+1,T=S+1;
  
  for(int i=1,a,b;i<=K;i++){
  	cin>>a>>b;
  	add(a,b+n,1,0);
  }
  for(int i=1;i<=m;i++) add(i+n,T,1,0);
  
  for(int i=1;i<=n;i++){
  	if(len/R>=1){
      add(S,i,1,R);
  	  F[i]=1;
	}
  }
  
  dinic();
  
  cout<<ans<<' '<<anscost<<'\n';
  
  for(int u=1;u<=n;u++){
    int nw=0;
    for(int i=head[u];i;i=e[i].nxt){
      int v=e[i].v;
      if(v>n&&v<=n+m&&e[i].w==0){
      	cout<<u<<' '<<v-n<<' '<<nw<<'\n';
      	nw+=R;
	  }
	}
  }
  
  return 0;
}

模拟费用流

as the name implies

模拟还是要分场合看情况的。


P6122 [NEERC2016] Mole Tunnels

P6122 [NEERC2016] Mole Tunnels

首先,直接每次跑费用流是正确的。

为什么呢?

在这道题可以看成贪心,因为每次只会多一个鼹鼠,而食物是不变的,所以每一次找一次增广路是正确的。

而如果食物也会增加就不对了,因为这样之前的最短路有可能就不对了。


那么其实这就变成了费用流,我们考虑直接在完全二叉树上面模拟一下,

每一个节点记录一下它子树里面离他最近的食物地点,每次 log 次往上面跳就可以了。

而代码中的 flow 就是模拟的费用流,因为我们有反向边就一定走反向边,

如果 \(flow \gt 0\) 那么存在从儿子 \(\to\) 父亲的反向边;

反之 \(flow \lt 0\) 那么存在从父亲 \(\to\) 儿子的反向边。

于是模拟费用流即可。代码

#include <bits/stdc++.h>
using namespace std;
#define ll long long 

const int N=5e5+5,inf=1e9;
int n,m,p[N],c[N],f[N],g[N];
ll flow[N],ans=0;

void upd(int u){
  f[u]=inf;
  if(c[u]) f[u]=0,g[u]=u;
  if(f[u<<1]+(flow[u<<1]<0?-1:1)<f[u]) f[u]=f[u<<1]+(flow[u<<1]<0?-1:1),g[u]=g[u<<1];
  if(f[u<<1|1]+(flow[u<<1|1]<0?-1:1)<f[u]) f[u]=f[u<<1|1]+(flow[u<<1|1]<0?-1:1),g[u]=g[u<<1|1];
}

int main(){
  /*2024.3.15 H_W_Y P6122 [NEERC2016] Mole Tunnels 模拟费用流*/ 
  ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
  cin>>n>>m;
  for(int i=1;i<=n;i++) cin>>c[i];
  for(int i=1;i<=m;i++) cin>>p[i];
  memset(f,0x3f,sizeof(f));
  for(int i=n;i>0;i--) upd(i);
  
  for(int i=1;i<=m;i++){
    int x=inf,u=p[i],t=0,v=0,lca=0;
    while(u){
      if(x>f[u]+t) x=f[u]+t,lca=u,v=g[u];
      t+=(flow[u]>0?-1:1),u>>=1;
    }
    u=p[i],ans+=x;
    while(u!=lca) flow[u]--,u>>=1,upd(u);
    c[v]--,upd(v);
    while(v!=lca) flow[v]++,v>>=1,upd(v);
    while(v) upd(v),v>>=1;
    cout<<ans<<' ';
  }
  
  return 0;
}

P5470 [NOI2019] 序列

P5470 [NOI2019] 序列 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

好题啊。让你彻底理解模拟费用流。


首先,考虑如何费用流?

发现对于每一个 \(i\) 我们都从 \(a_i \to b_i\) 连边,容量为 \(1\),费用为 \(0\)

\(S\) 连向所有 \(a_i\),容量为 \(1\),费用为 \(-a_i\)

所有 \(b_i\) 连向 \(T\),容量为 \(1\),费用为 \(-b_i\)

那么这样跑出来的就是两者同时选择。


而有些组是不需要 \(a_i,b_i\) 同时选择的,

所以我们新建两个虚拟节点 \(C\)\(D\)

将所有 \(a_i\)\(C\) 连容量为 \(1\) 的免费边,\(D\) 向所有 \(b_i\) 连容量为 \(1\) 的免费边。

\(C,D\) 中间连容量为 \(K-L\) 的免费边,这样跑费用流就可以得到答案。

用 Simplex 实现以下可以拿到 \(64\) 分的好成绩。代码


发现图是简单的,真的有必要去跑费用流吗?

能不能直接模拟费用流?


发现是可以的,我们贪心地想,如果 \(C \to D\) 没有流满,那么我们一定选 \(a,b\) 最大的去流,

反之流满了,我们就尝试两种方法:

  1. 选剩下 \(a_i+b_i\) 最大的。
  2. 尝试选一个 \(a/b\) 和已经选过的配对。

发现这两种情况稍微想一下就可以用 priority_queue 直接完成,还是非常不错的。

这样我们就用 \(\mathcal O(n \log n)\) 的时间复杂度模拟了费用流,非常成功。

代码还是比较好写的。

const int N=1e6+3;
int n,m,K,L,a[N],b[N],nw,vis[N];
ll ans=0;

priority_queue<pii> Fa,Fb,Ca,Cb,Cab;

void init(){
  while(!Fa.empty()) Fa.pop();
  while(!Fb.empty()) Fb.pop();
  while(!Ca.empty()) Ca.pop();
  while(!Cb.empty()) Cb.pop();
  while(!Cab.empty()) Cab.pop();
  
  ans=nw=0;
  
  for(int i=1;i<=n;i++){
    vis[i]=0;
    Ca.push({a[i],i});
    Cb.push({b[i],i});
  }
  
  for(int k=1;k<=K-L;k++){
    int i=Ca.top().se,j=Cb.top().se;
    Ca.pop(),Cb.pop();
    ans+=a[i]+b[j];
    vis[i]|=1,vis[j]|=2;
  }
  
  for(int i=1;i<=n;i++){
  	if(!vis[i]) Cab.push({a[i]+b[i],i});
    else if(vis[i]==1) Fb.push({b[i],i});
    else if(vis[i]==2) Fa.push({a[i],i});
    else ++nw;
  }
}

void chs1(){
  int i=Fa.top().se,j=Cb.top().se;
  Fa.pop(),Cb.pop();
  vis[i]|=1,vis[j]|=2;
  if(vis[j]==3) ++nw;
  else Fa.push({a[j],j});
}

void chs2(){
  int i=Ca.top().se,j=Fb.top().se;
  Ca.pop(),Fb.pop();
  vis[i]|=1,vis[j]|=2;
  if(vis[i]==3) ++nw;
  else Fb.push({b[i],i});
}

void chs3(){
  int i=Cab.top().se;
  Cab.pop();
  vis[i]=3;
}

void sol(){
  cin>>n>>K>>L;
  for(int i=1;i<=n;i++) cin>>a[i];
  for(int i=1;i<=n;i++) cin>>b[i];
  
  init();
  while(L--){
  	while(!Ca.empty()&&(vis[Ca.top().se]&1)) Ca.pop();
  	while(!Cb.empty()&&(vis[Cb.top().se]&2)) Cb.pop();
  	while(!Fa.empty()&&(vis[Fa.top().se]^2)) Fa.pop();
  	while(!Fb.empty()&&(vis[Fb.top().se]^1)) Fb.pop();
  	while(!Cab.empty()&&vis[Cab.top().se]) Cab.pop();
  	
  	int i,j;
  	ll v1,v2,v3,c1,c2,vmx;
  	v1=v2=v3=c1=c2=vmx=0;
  	
  	if(nw){
  	  --nw;
	  i=Ca.top().se,j=Cb.top().se;
	  Ca.pop(),Cb.pop();
	  ans+=a[i]+b[j];
	  vis[i]|=1,vis[j]|=2;
	  if(i==j){++nw;continue;}
	  if(vis[i]==3) ++nw;
	  else Fb.push({b[i],i});
	  if(vis[j]==3) ++nw;
	  else Fa.push({a[j],j});
	  continue;
	}
	
	if(!Fa.empty()&&!Cb.empty()){
	  i=Fa.top().se,j=Cb.top().se;
	  v1=a[i]+b[j];
	  if(vis[j]&1) c1=1;
	}
	
	if(!Fb.empty()&&!Ca.empty()){
	  i=Ca.top().se,j=Fb.top().se;
	  v2=a[i]+b[j];
	  if(vis[i]&2) c2=1;
	}
	
	if(!Cab.empty()) v3=Cab.top().fi;
	
	vmx=max(v1,max(v2,v3));
	ans+=vmx;
	
	if(v1==v2&&v1==vmx){
	  if(c1>=c2) chs1();
	  else chs2();
	}
	else if(v1==vmx) chs1();
	else if(v2==vmx) chs2();
    else chs3();
  }
  
  cout<<ans<<'\n';
}

int main(){
  /*2024.3.15 H_W_Y P5470 [NOI2019] 序列 MCMF*/
  ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
  int _;cin>>_;
  while(_--) sol();
  return 0;
}

构造调优

最后一道题哩。


这是一类构造问题,我们考虑先钦定出一种初始状态,然后不断调整变得优秀的思路。


P4486 [BJWC2018] Kakuro

P4486 [BJWC2018] Kakuro

思路似乎是简单的,分析一下。

发现我们先钦定每一个的空格的值都是 \(1\),这样就没有了减操作,

我们希望答案尽可能优。


容易想到用网络流。

同样拆点,因为每一个空格的改变会与两个线索有关系,

我们将源点连向向下的线索,向右的线索直接连到汇点。

由于对于每一个点的贡献我们是先减再加,所以这会涉及到两条边,

而对于空格,我们直接向他所影响到的两个线索连边就可以了。


这样似乎就做完了,

由于我们找的是最小值,所以每一次只需要保证 \(dis[T]\gt 0\)

注意负流量的边不能加啊。代码

int main(){
  /*2024.3.18 H_W_Y P4486 [BJWC2018] Kakuro MCMF*/
  ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
  cin>>n>>m;
  
  for(int i=1;i<=n;i++)
    for(int j=1;j<=m;j++)
      cin>>op[i][j];
      
  for(int i=1;i<=n;i++)
    for(int j=1;j<=m;j++){
      if(!op[i][j]) continue;
      cin>>a[i][j][0];
      if(op[i][j]==3) cin>>a[i][j][1];
    }
  
  for(int i=1;i<=n;i++)
    for(int j=1;j<=m;j++){
      if(!op[i][j]) continue;
      cin>>b[i][j][0];
      if(b[i][j][0]==-1) b[i][j][0]=inf;
      if(op[i][j]==3){
        cin>>b[i][j][1];
        if(b[i][j][1]==-1) b[i][j][1]=inf;
      }
    }

  S=2*n*m+1,T=S+1;
  
  for(int i=1;i<=n;i++)
    for(int j=1;j<=m;j++){
      ll nw=0;
      if(op[i][j]==1||op[i][j]==3){
        nw=gd(i,j);
        pre+=abs(nw-a[i][j][0])*b[i][j][0];
        add(S,id(i,j,0),a[i][j][0]-nw,-b[i][j][0]);
        add(S,id(i,j,0),inf,b[i][j][0]);
      }
      if(op[i][j]==2){
        nw=gr(i,j);
        pre+=abs(nw-a[i][j][0])*b[i][j][0];
        add(id(i,j,1),T,a[i][j][0]-nw,-b[i][j][0]);
        add(id(i,j,1),T,inf,b[i][j][0]);
      }
      if(op[i][j]==3){
        nw=gr(i,j);
        pre+=abs(nw-a[i][j][1])*b[i][j][1];
        add(id(i,j,1),T,a[i][j][1]-nw,-b[i][j][1]);
        add(id(i,j,1),T,inf,b[i][j][1]);
      }
      if(op[i][j]==4){
        int u=gu(i,j),l=gl(i,j);
        pre+=abs(a[i][j][0]-1)*b[i][j][0];
        add(id(u,j,0),id(i,l,1),a[i][j][0]-1,-b[i][j][0]);
        add(id(u,j,0),id(i,l,1),inf,b[i][j][0]);
      }
    }

  ll res=dinic();
  
  if(chk()) cout<<pre+res;
  else cout<<-1;
  return 0;
}

Conclusion

  1. 网络流可以通过 \(res=0\) 的剪枝大大减少复杂度。(P4177 [CEOI2008] order)
  2. 数组开小了也会 TLE。(P3227 [HNOI2013] 切糕)
  3. 网络流建图时我们可以考虑通过加一些边使得一些不合法的情况不会称为答案。(P3227 [HNOI2013] 切糕)
  4. 一些点强制匹配,一些点可以匹配的问题可以 黑白染色 之后直接跑 有源汇上下界可行流 判断即可。(CF1416F Showing Off)
  5. 对于有关矩形的问题,我们可以考虑 黑白染色 分组后解决或者 \(x,y\) 坐标分别作为左右部点 建图。(CF1416F + CF1592F2)
  6. 多种情况的问题我们可以先考虑那些情况不会用到。(CF1592F2 Alice and Recoloring 2)
  7. 注意精度问题,double 的二分的 eps 至少往下取两位。(P3705 [SDOI2017] 新生舞会)
  8. 有关最大的限制我们可以通过枚举答案转化到最小的限制来做。([AGC031E] Snuke the Phantom Thief)
  9. 做题的时候要常常想到把 区间/矩形 的修改/权值通过某些方法转化到 一个点 上。(CF1178H Stock Exchange)
  10. 前缀和优化建图,可以大大减小空间复杂度。(CF1178H Stock Exchange)
  11. 一条边的两个节点只选一个的情况可以考虑将 边也看作点,分别向两个节点连边,容量 \(1\),而 \(s\) 到边对于节点同样容量为 \(1\)。(P4249 [WC2007] 剪刀石头布)
  12. 计数问题要经常考虑容斥,将总方案数减去不合法方案数。(P4249 [WC2007] 剪刀石头布)
  13. 在有很多点都没有用上的时候,我们考虑 动态加点 从而 优化 时间复杂度。(P2050 [NOI2012] 美食节)
  14. 有关棋子的移动可以看成 空白位置 的移动去实现。(P4701 粘骨牌)
  15. 流量平衡 可以想到相邻点变成源汇 \(\to\) 黑白染色。(P4003 无限之环)
  16. 出入度平衡想到拆点,拆点需要想清楚入点和出点在题目中的意义,建边时就会相对容易。(P2469 [SDOI2010] 星际竞速)
  17. 拆点后直接从原点连到出点相当于把这个点作为起点。(P4553 80人环游世界)
  18. 拆点前分析好,不要动不动就拆点!!!(P5814 [CTSC2001] 终极情报网)
  19. 在边不够特殊,流量几乎相等,可以用最短路代替网络流时,单纯形 Simplex 一般会跑得很慢。(P4009 汽车加油行驶问题)
  20. 比较简单的费用流可以考虑直接 模拟费用流。(P5470 [NOI2019] 序列)
  21. 善于发现题目中的性质,有些相同的费用的边其实可以合并在一起。(P3529 [POI2011] PRO-Programming Contest)
  22. 判断一个区间是否满足条件时,尝试把区间看成点,而点的改变变成一个区间的贡献。(P6967 [NEERC2016] Delight for a Cat)
  23. 最大流的问题有时可以想成 最小割 去分析,或许会简单很多。(CF1383F Special Edges)
  24. 建边时不要加入流量为负的边!!!(P4486 [BJWC2018] Kakuro)

P3347 [ZJOI2015] 醉熏熏的幻想乡

P3347 [ZJOI2015] 醉熏熏的幻想乡

傲娇少女幽香是一个很萌很萌的妹子,这些天幻想乡的大家都不知道为何还是拼命喝酒。很快酒就供不应求了,为了满足大家的需求,幽香决定在森林里酿酒。

经过调查,幽香发现森林里面有一些地方非常适合酿酒,有一些地方则非常适合存酒。幽香把这些适合酿酒的地方成为酿酒点,不妨认为有 \(n\) 个酿酒点,从 \(1\)\(n\) 标号。同时也有 \(m\) 个适合存酒的地方,幽香将它们成为存酒点,从 \(1\)\(m\) 标号。在一些酿酒点和存酒点之间存在通道,如果酿酒点 \(i\) 到存酒点 \(j\) 之间存在通道,那么 \(i\) 生产的酒就可以被运输到 \(j\)

但是在一个地方酿酒是需要消耗幽香的魔力的,由于存在管理上的因素,在酿酒点 \(i\),制造 \(x\) 升的酒,需要花费 \(a_i\cdot x^2+b_i\cdot x\) 的魔力,注意 \(x\) 不一定是一个非负整数,也可以是一个非负实数,同时在这个点最多只能制造 \(c_i\) 升的酒。每个存酒点 \(j\) 有一个容量 \(d_j\),表示这个存酒点最多能存多少升的酒。

幽香打算存尽量多的酒,那么她需要再一些酿酒点生产一些酒并且通过通r道将酒运送到存酒点。当然幽香想要节省自己的魔力,所以想让你帮忙算出在满足要求的情况下,最少花费的魔力是多少?

\(1\leq n\leq100\)\(1\leq m\leq100\)

不能要了,已经把前面严格偏序了。(所以它与 网络流学习笔记 是并列关系了)

花了整整一个晚上+上午,但是真的好妙。


首先乍一看好像是费用流的板子,但是发现费用是二次函数。

而第一问是很好解决的,直接跑一次最大流即可,但是怎么算费用呢?


二次函数首先是不好处理的,所以我们考虑把它分成很多个区间,

也就说想要把流量的精度变得尽可能小,假设每一次增广的流量是 \(\Delta t\)

于是我们就可以将一个点向外连 \(\frac{c_i}{\Delta t}\) 条边,每条边的费用分别是每个 \(\Delta t\) 区间中所增加的费用,

只要我们把 \(\Delta t\) 分得足够小,那么精度是没有问题的,

也就说 \(\Delta t \to 0^+\) 时,我们可以得到答案,而此时每一次的费用就是原函数的导函数 \(f'(x)=2ax+b\)

但是显然时间复杂度是不允许的,不过理论上是可以拿不少分的。


那么我们现在考虑如何优化?

显然按照上面的思路是不好做的,于是我们考虑费用流的本质。


发现我们每次是找一条最短路,然后增广过去,也就是把最短路流满。

而在这个图中,发现只有 \(s\) 和酿酒点之间是有费用的,所以我们每一次其实是找了一个瞬间费用最小的酿酒点流下去。

所以对于每一个酿酒点,它的费用会从某一费用开始增加,而会在某一费用停止,

不从 \(0\) 开始是因为最初 \(b_i\) 的值,而停止增加是因为流量已经流满了 \(c_i\),或者造的就运不出去了。

而对于 \(a_i=0\) 的情况我们需要特殊考虑(后面再分析)。


现在我们记每一条边在 瞬间 的费用为 瞬时费用,也就是我们的导函数 \(f_i'(x)=2 a_i x+b_i\)

发现这是一次函数关系,那么如果我们求出来 瞬时费用与流量 的关系,那么它与 \(y\) 轴围成的面积就是答案。


现在我们考虑当前的 瞬时费用\(\lambda\),于是对于每一条边,有了流量的上界:\(L_i= \min(c_i,\frac{\lambda-b_i}{2a_i})\)

\(\frac{\lambda -b_i}{2a_i}\) 的由来:

我们假设此时流量 \(x\) 最大,那么满足

\[\begin{align} 2a_ix+b_i & =\lambda\\ x& = \frac{\lambda -b_i}{2a_i} \end{align} \]

注意要特判 \(a_i = 0\) 的情况。


于是现在我们考虑求出 流量与瞬时费用 的折线 \(T\)

可以理解成我们假设瞬时费用最大为 \(\lambda\) 时,整个图的最大流量。

而这是可以通过每条边是否满流和与 \(b_i\) 的关系就可以求出,也就是对于每一条边,他有如下几种情况:

  1. \(\lambda \le b_i\),那么根本不存在这条边,不产生贡献。
  2. 当前边满流且 \(\lambda \gt b_i\)
    • \(L_i \le c_i\),那么流量会随 \(\lambda\) 增大而增大,而当 \(\lambda\) 更小的时候也会满流。
    • \(L_i \gt c_i\),那么 \(L_i\) 会一直 \(= c_i\),此时 \(x_i =c_i\)
  3. 当前边不满流直接加上贡献即可。

这我们就可以求出 \(T\)\(\lambda\)\(\sum x_i\) 关于 \(\lambda\) 的函数。


分析到这里我们发现,

函数变换的时候当且仅当一条边启用,即 \(\lambda = b_i\) 时,

或者一条边满流/顶到上界的时候,而由于 \(b_i \le 3\),所以我们得到的折线 \(T\) 最多只有 \(2n\) 个拐点。

而为了放防止每次枚举到的 \(\lambda\) 不是拐点,

我们对 \(\lambda\) 进行随机的扰动即可。


于是我们最终想得到的时折线 \(T\) 的左右拐点,设他们的横坐标(也就是瞬间费用)是 \({\lambda_1,\lambda_2 \dots}\)

那么整个折线的图像是这样的(图来自题解)

image

于是黄色的面积就是答案。(也就是瞬时费用和流量的乘积)


发现 \(0\) 一定是一个拐点,

而如果有一个 \(a_i=0\) 的酿酒点,那么这条线会直线上升,也就是与 \(y\) 轴平行,这是需要直接提出来讨论的。

求这个折线,我们发现它是一个上凸的形式,

于是我们直接分治就可以找到这些横坐标了。

具体实现是这样的:

void sol(auto l,auto r){
  if(l==r) return;
  frac lambda=(r.se-l.se)/(l.fi-r.fi);
  auto mid=dinic(lambda.val()+eps);
  if(mid==r) return p.pb(lambda),void();
  sol(l,mid);sol(mid,r);
}

找到所有的 \(\lambda\) 之后,求面积就简单很多了,

只需要考虑两种情况:\(0,1,2,3\) 时的与 \(y\) 轴平行的矩形面积,和后面的梯形面积。

直接用公式算就可以了。

  for(int i=1;i<(int)p.size();i++){
  	auto l=dinic(p[i].val()-eps),r=dinic(p[i].val()+eps);
  	ans=ans+p[i]*(r.se-l.se+(r.fi-l.fi)*p[i]);//矩形面积
  	ans=ans+(p[i]+p[i-1])*(p[i]-p[i-1])*l.fi*frac(1,2);//梯形面积
  }

具体实现中就直接写一个分数类从而尽量减小误差,

函数 dinic 就是求当瞬时费用为 \(\lambda\) 时我们在折线上面的函数表达式。

#include <bits/stdc++.h>
using namespace std;
#define ll long long 
#define db double
#define fi first
#define se second
#define pb push_back

ll gcd(ll a,ll b){return (b==0)?a:gcd(b,a%b);}

 struct frac{
  ll a,b;
  frac(ll x=0,ll y=1){ll g=gcd(x,y);a=x/g,b=y/g;}
  bool operator == (const frac &x)const {return a*x.b==b*x.a;}
  frac operator + (const frac &x)const {return frac(a*x.b+x.a*b,b*x.b);}
  frac operator - (const frac &x)const {return frac(a*x.b-x.a*b,b*x.b);}
  frac operator * (const frac &x)const {return frac(a*x.a,b*x.b);}
  frac operator / (const frac &x)const {return frac(a*x.b,b*x.a);}
  db val(){return 1.0*a/b;}
  void prt(){cout<<a<<"/"<<b<<'\n';}
}ans;

const int inf=1e9,N=205,M=2e3+5;
const db eps=1e-7,feps=1e-8;

struct flow{
  int head[N],tot=-1,cur[N],lv[N],t;
  struct edge{int v,nxt;db w;}e[M<<1];
  bool vis[N];
  
  void init(){memset(head,-1,sizeof(head));tot=-1;}
  
  void add(int u,int v,db w){
  	e[++tot]=(edge){v,head[u],w};head[u]=tot;
  	e[++tot]=(edge){u,head[v],0};head[v]=tot;
  }
  
  db dfs(int u,db fl){
  	if(u==t) return fl;
  	db res=fl;
    
    for(int &i=cur[u];i!=-1;i=e[i].nxt){
      int v=e[i].v;db w=e[i].w;
      if(w>=feps&&lv[v]==lv[u]+1){
      	db c=dfs(v,min(res,w));
      	res-=c;e[i].w-=c;e[i^1].w+=c;
      }
      if(res<feps) break;
    }
    
    return fl-res;
  }
  
  void wrk(int s,int T){
  	t=T;
  	while(1){
      memset(lv,-1,sizeof(lv));
      queue<int> q;q.push(s);
      cur[s]=head[s];lv[s]=0;vis[s]=true;
    
      while(!q.empty()){
        int u=q.front();q.pop();vis[u]=false;
        for(int i=head[u];i!=-1;i=e[i].nxt){
      	  int v=e[i].v;db w=e[i].w;
      	  if(w>=feps&&lv[v]==-1){
      	    lv[v]=lv[u]+1;cur[v]=head[v];
      	    if(!vis[v]) q.push(v),vis[v]=true;
      	  }
        }
      }
      
      if(lv[t]==-1) break;
      dfs(s,inf);
  	}
  }  
}g;

int n,m,cnt,a[N],b[N],c[N],d[N],e[N][N];
vector<frac> p;

pair<frac,frac> dinic(db lambda){
  g.init();
  
  for(int i=1;i<=n;i++)
    if(b[i]<lambda){
      if(!a[i]) g.add(0,i,c[i]);
      else g.add(0,i,min(1.0*c[i],1.0*(lambda-b[i])/(2*a[i])));
    }
    
  for(int i=1;i<=n;i++) for(int j=1;j<=m;j++) if(e[i][j]) g.add(i,j+n,inf);
  for(int i=1;i<=m;i++) g.add(i+n,cnt,d[i]); 
  g.wrk(0,cnt);
  frac K,B;
  for(int i=1;i<=n;i++)
  	if(b[i]<lambda&&g.lv[i]==-1){
  	  if(!a[i]) B=B+frac(c[i]);
  	  else if(2*a[i]*c[i]+b[i]<lambda) B=B+frac(c[i]);
  	  else K=K+frac(1,2*a[i]),B=B-frac(b[i],2*a[i]);
  	}
  for(int i=1;i<=m;i++) if(g.lv[i+n]!=-1) B=B+frac(d[i]);
  
  return make_pair(K,B);
}

void sol(auto l,auto r){
  if(l==r) return;
  frac lambda=(r.se-l.se)/(l.fi-r.fi);
  auto mid=dinic(lambda.val()+eps);
  if(mid==r) return p.pb(lambda),void();
  sol(l,mid);sol(mid,r);
}

int main(){
  /*2023.12.13 H_W_Y P3347 [ZJOI2015] 醉熏熏的幻想乡 最小费用最大流*/
  ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
  cin>>n>>m;cnt=n+m+1;
  for(int i=1;i<=n;i++) cin>>a[i]>>b[i]>>c[i];
  for(int i=1;i<=m;i++) cin>>d[i];
  for(int i=1;i<=n;i++) for(int j=1;j<=m;j++) cin>>e[i][j];
  
  p.pb(frac());
  for(int t=1;t<4;t++){
  	sol(dinic(t-1+eps),dinic(t-eps));
  	p.pb(frac(t));
  }
  sol(dinic(3+eps),dinic(inf));
  
  for(int i=1;i<(int)p.size();i++){
  	auto l=dinic(p[i].val()-eps),r=dinic(p[i].val()+eps);
  	ans=ans+p[i]*(r.se-l.se+(r.fi-l.fi)*p[i]);
  	ans=ans+(p[i]+p[i-1])*(p[i]-p[i-1])*l.fi*frac(1,2);
  }
  
  cout<<dinic(inf).se.val()<<'\n';ans.prt();
  return 0;
}
posted @ 2023-12-13 11:19  H_W_Y  阅读(62)  评论(0编辑  收藏  举报