网络流
- 网络流是求网络最大流的算法,看似没什么用,实际上很多题目都可以通过建图转化为网络最大流问题
模板
P3376 【模板】网络最大流
概念
- “网络最大流问题”本身是指从一个原点 \(s\) 往外流水,这个原点本身有无穷多水可以流,有 \(m\) 根双向管道连接 \(n\) 个节点,每个节点都有一个最大流量,指的是这根水管最大可以承载的水流量。有一个汇点 \(t\),问从原点向汇点流水的最大流量。
反向边
- 容易想到,如果现在图上还有路可以走,即还有一条路可以让水流到\(t\),我们将这种路称为增广路。那么将这条增广路流了一定不亏。
但这样并不能保证最优,那么通过什么方法来保证我流了这条路后面还有办法反悔呢?
这里有一种非常巧妙的方法。为每一条边建一条权值为零的反边。如果我们发现某条路不优,那么就可以流回去(毕竟流量可以随意分配)。
我们对于这种路并不需要特殊处理,就像dfs的回溯一样,是对最优解的自发寻找。只需要每次流过一条边时,使其反边的流量加上这次的流量(因为正边会减少这次的流量)。
![初始状态]()
- 假设这是初始的状态
![第一次dfs]()
- 假设经过了一次增广(就是把某条增广路走满)后的图是这样的(这里的蓝色值是反边的流量)
这里如果没有反向边,那么程序就已经结束了(因为找不出增广路了)。但显然这并不是最优解。因此反向边的作用就凸显出来了
![第二次dfs]()
- 在有反向边的情况下,显然还有一条绿色的增广路。这时才真正意义上找到了最优解。观察最终状态,发现中间那条路实际上是没走的。因此,反边的作用就是对可能不优的方案进行“反悔”操作。
dinic算法
-
综上,我们具体需要解决两个问题:
1.找到是否有增广路
2.对增广路进行修改并统计答案 -
对于是否有增广路的问题,可以直接用bfs来寻找。
但是,在bfs的过程中,我们还需要建立分层图,即给每个点维护其到 \(s\) 的最小边数。这里,还是以上图为例,给上图标上层数
![初始分层图]()
-
为什么要标上层数呢?因为如果我们发现原图有增广路,那么就可以 从 \(s\) 开始dfs,依次算增广路。这里的图不是很好,如果有一个点最大流量比较大,那其就可以给很多与其相连的边分流量。如果没标层数,那这个边与其反边就可能反复互相流,造成极大的时间浪费。如果我们按照层数一层一层流,就可以保证每次都能靠近 \(t\)。同时,由于层数的限制,我们可以一次增广多条增广路。
-
但是,这里还是有些问题。相邻两个点 \(u、v\) 间的层数差并不是1,就可能造成无法增广的情况。
事实上,这种情况是没有影响的。由于每次增广完后都会重新bfs,重新计算层数,因此上述情况证明 \(u\) 到 \(v\) 这条路并不是到 \(t\) 的最短路,就算这条路会在最终的答案中,也会在 \(u\) 多次被增广后与 \(v\) 层数相差一,即 \(u、v\) 两个点在到 \(t\) 的同一条相对短的路径上。
当前弧优化
- 这个算是比较好理解的一条优化。在同一次dfs中,对于其枚举的前几条边,其到 \(t\) 的路径一定是被流满了的。如果再次dfs到这个点,那就不用考虑这前几个已经被流满了的点了。
几个小细节
- 由于对于流了的每条边,我们都要对其反边加上其流量,因此这里考虑用链式前向星,点的编号从1开始,在加边的时候正反边一起相邻加,对于某边的反边,其编号就是边的编号异或1。
- 在dfs时,如果某个点无法流到 \(t\),即流量为0,就将其层数设为 \(inf\),让其不再被更新(注意改层数只是对于这一次dfs而言,对下一次没有影响)
- 注意每次bfs时将当前弧优化的数组以及层数初始化回去
时间复杂度
注意到 dinic 每次分层,汇点的层数是单调递增的,因此增广的轮数是 \(O(n)\) 的。
对于每一次增广,可以发现 dfs 找到一条增广路的时间不会超过 \(O(n)\),而当前弧的状态数不会超过 \(m\)。
因此正常 dinic 的时间复杂度是 \(O(n^2m)\) 的。
同时,一般而言 dinic 在 OI 中有点难卡满。有人说这东西随机的话甚至可以看做是线性的,也不知道是不是对的。
网络流在二分图中的应用
- 对于每个左边的点都向另一个点 \(s\) 连一条权值为0的边
右边的点也都向 \(t\) 连一条权值为0的边,中间的点照常连接,权值都为1
跑出来的最大流就是最大匹配,正确性其实相对显然
时间复杂度 \(O(\sqrt{n}m)\)
code
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll N=5e5+10;
const ll inf=1e18+10;
ll n,m,s,t,head[N],tot=1,dep[N],now[N],ans=0;
//now->当前弧优化数组 tot从1开始
struct node
{
ll nxt,to;
ll val;
}e[N];
void add(ll u,ll v,ll w)
{
e[++tot].nxt=head[u];
head[u]=tot;
e[tot].to=v;
e[tot].val=w;
}
bool bfs()
{
for(ll i=1;i<=n;i++) dep[i]=inf;//记得初始化
queue <ll> q;
q.push(s);dep[s]=1;
now[s]=head[s]; //同样得初始化
while(!q.empty())//广搜
{
ll u=q.front();q.pop();
for(ll i=head[u];i;i=e[i].nxt)
{
ll v=e[i].to;
if(e[i].val&&dep[v]==inf)
{
q.push(v);
now[v]=head[v];
dep[v]=dep[u]+1;
if(v==t) return 1;
}
}
}
return 0;
}
ll dfs(ll u,ll sum)
{
if(u==t) return sum;
ll k=0,res=0;
for(ll i=now[u];i;i=e[i].nxt)
{
now[u]=i;
ll v=e[i].to;
if(e[i].val&&dep[v]==dep[u]+1)
{
k=dfs(v,min(sum,e[i].val));//尽可能流最大流量
if(k==0) dep[v]=inf; //流不到汇点
e[i].val-=k;e[i^1].val+=k;//其反边加
res+=k,sum-=k;
if(!sum) break;
}
}
return res;
}
int main()
{
cin>>n>>m>>s>>t;
for(int i=1,u,v,w;i<=m;i++)
{
cin>>u>>v>>w;
add(u,v,w);add(v,u,0);//建权值为0的反向边
}
while(bfs())
{
ans+=dfs(s,inf);//不断找增广路
}
cout<<ans<<endl;
}
P3381 【模板】最小费用最大流
费用流的要求是在最大流的基础上给每条边加上了边权,同时要求流的边的边权和最大。
主要的思想就是在 bfs 的时候求出每个点的最短路,然后 dfs 的时候除了层数外还要求 \(dis_v=dis_u+w\) 即可。这个时候层数的限制就没有意义了,直接去掉即可。(即 \(dis\) 的限制是一个比 \(dep\) 更强的限制)
code
一些细节是,在 dfs 的时候需要额外增加一个 \(vis\) 来保证不死循环。具体可以看代码。在代码末尾加了一个在讨论区看到的样例。如果不标记就会死循环。
点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define pii pair <int,int>
#define mp make_pair
const int N=1e5+7,inf=1e18+7;
int n,m,s,t,hd[N],nxt[N],val[N],to[N],c[N],idcnt=1,dis[N],nw[N],vis[N];
void add(int u,int v,int w,int cst){nxt[++idcnt]=hd[u];hd[u]=idcnt,to[idcnt]=v,val[idcnt]=w,c[idcnt]=cst;swap(u,v);nxt[++idcnt]=hd[u];hd[u]=idcnt,to[idcnt]=v,val[idcnt]=0,c[idcnt]=-cst;}
bool spfa(){
bool sign=0;for(int i=1;i<=n;i++)dis[i]=inf,vis[i]=0;
queue <int> q;q.push(s);
dis[s]=0,nw[s]=hd[s];
while(!q.empty()){
int u=q.front();q.pop();
nw[u]=hd[u];if(u==t)sign=1;
for(int i=hd[u];i;i=nxt[i]){
int v=to[i];
if(dis[v]>dis[u]+c[i]&&val[i]){
dis[v]=dis[u]+c[i];
if(vis[v])continue;
q.push(v),vis[v]=1;
}
}vis[u]=0;//注意这里重置 vis 要放在最后面
}
return sign;
}
pii dfs(int u,int sum){
if(u==t)return mp(sum,0);
vis[u]=1;
pii res=mp(0,0),k;
for(int i=nw[u];i;i=nxt[i]){
int v=to[i];nw[u]=i;if(!val[i]||dis[v]!=dis[u]+c[i]||vis[v])continue; //注意这里要判断 vis。具体的,可能会出现 c_i=0 使得死循环的情况
k=dfs(v,min(sum,val[i]));if(!k.first){dis[v]=inf;continue;}
val[i]-=k.first,sum-=k.first,val[i^1]+=k.first,res.first+=k.first;res.second+=c[i]*k.first+k.second;
if(!sum)break;
}
vis[u]=0;return res;
}
signed main(){
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
cin>>n>>m>>s>>t;
for(int i=1,u,v,w,cst;i<=m;i++)cin>>u>>v>>w>>cst,add(u,v,w,cst);
pii ans=mp(0,0);while(spfa()){pii t=dfs(s,inf);ans.first+=t.first,ans.second+=t.second;}
cout<<ans.first<<' '<<ans.second;return 0;
}
/*
如果 dfs 的时候不标记,下面这组样例会导致死循环。
6 6 1 6
3 6 1 1
1 2 1 1
2 3 1 0
3 4 1 0
4 5 1 0
5 3 1 0
*/
自己的板子
个人感觉写的比较简洁同时符合自己的习惯。由于网络流板子本身一般是比较固定的,变化都在建模上,因此直接封装起来比较好看。
网络流
点击查看代码
int s,t;
namespace flow{
const int N=2e6+7,inf=1e9+7;
int hd[N],nxt[N],to[N],val[N],idcnt=1,dep[N],nw[N];
void add(int u,int v,int w){nxt[++idcnt]=hd[u],hd[u]=idcnt,to[idcnt]=v,val[idcnt]=w;swap(u,v),nxt[++idcnt]=hd[u],hd[u]=idcnt,to[idcnt]=v,val[idcnt]=0;}
bool bfs(){
for(int i=1;i<=t;i++)dep[i]=inf;
queue <int> q;dep[s]=1,nw[s]=hd[s];q.push(s);
while(!q.empty()){
int u=q.front();q.pop();
for(int i=hd[u];i;i=nxt[i]){
int v=to[i];if(dep[v]==inf&&val[i]){dep[v]=dep[u]+1,q.push(v);nw[v]=hd[v];if(v==t)return 1;}
}
}
return 0;
}
int dfs(int u,int sum){
if(u==t)return sum;
int k,res=0;
for(int i=nw[u];i;i=nxt[i]){
nw[u]=i;int v=to[i];if(!val[i]||dep[v]!=dep[u]+1)continue;
k=dfs(v,min(sum,val[i]));if(!k){dep[v]=inf;continue;}
sum-=k,val[i]-=k,val[i^1]+=k,res+=k;if(!sum)break;
}
return res;
}
int calc(){
int ans=0;while(bfs())ans+=dfs(s,inf);return ans;
}
}
using namespace flow;
费用流
点击查看代码
int s,t;
namespace flow_cost{
const int N=2e6+7,inf=1e9+7;
#define pii pair <int,int>
#define mp make_pair
int hd[N],nxt[N],val[N],to[N],c[N],idcnt=1,dis[N],nw[N],vis[N];
void init(){for(int i=1;i<=t;i++)hd[i]=nw[i]=0;idcnt=1;}
void add(int u,int v,int w,int cst){nxt[++idcnt]=hd[u];hd[u]=idcnt,to[idcnt]=v,val[idcnt]=w,c[idcnt]=cst;swap(u,v);nxt[++idcnt]=hd[u];hd[u]=idcnt,to[idcnt]=v,val[idcnt]=0,c[idcnt]=-cst;}
bool spfa(){ //注意这里是最大费用最大流板子
bool sign=0;for(int i=1;i<=t;i++)dis[i]=-inf,vis[i]=0; //如果要是最小费用的话这里要改成正的 inf
queue <int> q;q.push(s);
dis[s]=0,nw[s]=hd[s];
while(!q.empty()){
int u=q.front();q.pop();
nw[u]=hd[u];if(u==t)sign=1;
for(int i=hd[u];i;i=nxt[i]){
int v=to[i];
if(dis[v]<dis[u]+c[i]&&val[i]){ //最小费用要改成大于号
dis[v]=dis[u]+c[i];
if(vis[v])continue;
q.push(v),vis[v]=1;
}
}vis[u]=0;//注意这里重置 vis 要放在最后面
}
return sign;
}
pii dfs(int u,int sum){
if(u==t)return mp(sum,0);
vis[u]=1;
pii res=mp(0,0),k;
for(int i=nw[u];i;i=nxt[i]){
int v=to[i];nw[u]=i;if(!val[i]||dis[v]!=dis[u]+c[i]||vis[v])continue; //注意这里要判断 vis。具体的,可能会出现 c_i=0 使得死循环的情况
k=dfs(v,min(sum,val[i]));if(!k.first){dis[v]=inf;continue;}
val[i]-=k.first,sum-=k.first,val[i^1]+=k.first,res.first+=k.first;res.second+=c[i]*k.first+k.second;//
if(!sum)break;
}
vis[u]=0;return res;
}
int calc(){
pii ans=mp(0,0);while(spfa()){pii tmp=dfs(s,inf);ans.first+=tmp.first,ans.second+=tmp.second;}
return ans.second;
}
}
using namespace flow_cost;
原始对偶优化费用流
这里有一个额外的优化,就是原始对偶优化。由于在残量网络上跑最短路,这个图是会变化的,同时有负权边,因此我们原来是使用了重新 spfa 来更新最短路的。
但是我们可以通过原始对偶来优化这个过程。由于残量网络是在原图上变化的,因此可以通过一次 spfa 求出原始的最短路,然后直接用 dij 来跑最短路。具体的过程可以上网去搜一下。
同时细节很多,但确实可以优化一点速度。(可能并非一点)
点击查看代码
int s,t;
namespace flow_cost_opt{
const int N=2e6+7,inf=1e9+7;
#define pii pair <int,int>
#define mp make_pair
int hd[N],nxt[N],val[N],to[N],c[N],idcnt=1,dis[N],nw[N],vis[N],h[N];
void init(){for(int i=1;i<=t;i++)hd[i]=nw[i]=0;idcnt=1;}
void add(int u,int v,int w,int cst){nxt[++idcnt]=hd[u];hd[u]=idcnt,to[idcnt]=v,val[idcnt]=w,c[idcnt]=cst;swap(u,v);nxt[++idcnt]=hd[u];hd[u]=idcnt,to[idcnt]=v,val[idcnt]=0,c[idcnt]=-cst;}
void spfa(){
for(int i=1;i<=t;i++)h[i]=inf,vis[i]=0; //这样之后两点间的距离就变为 d(u,v)+h_u-h_v。显然有前式大于 0,因此可以用 dij 优化。
queue <int> q;q.push(s);
h[s]=0,nw[s]=hd[s];
while(!q.empty()){
int u=q.front();q.pop();
nw[u]=hd[u];
for(int i=hd[u];i;i=nxt[i]){
int v=to[i];
if(h[v]>h[u]+c[i]&&val[i]){ //注意这里写的是最小费用最大流。
h[v]=h[u]+c[i];
if(vis[v])continue;
q.push(v),vis[v]=1;
}
}vis[u]=0;
}
}
bool dij(){ //跑 dij 求最短路
for(int i=1;i<=t;i++) (h[i]+=dis[i])%=(inf+1),dis[i]=inf,vis[i]=0;//这里 += 的逻辑是在“有效”的 dij 之后再加。如果有 dis=inf,那么之后都一定是 inf 了,就不重要了。
priority_queue <pii> q;q.push(mp(0,s));
dis[s]=0,nw[s]=hd[s];bool sign=0;
while(!q.empty()){
int u=q.top().second;q.pop();
nw[u]=hd[u];if(u==t)sign=1;if(vis[u])continue;vis[u]=1;
for(int i=hd[u];i;i=nxt[i]){
int v=to[i],cst=c[i]+h[u]-h[v];
if(dis[v]>dis[u]+cst&&val[i]){
dis[v]=dis[u]+cst;q.push({-dis[v],v});
}
}
}
for(int i=1;i<=t;i++)vis[i]=0; //dij 完了以后不一定 vis 是空的,因此不能像 spfa 一样直接与 dfs 共用 vis,要清空。
return sign;
}
pii dfs(int u,int sum){
if(u==t)return mp(sum,0);
vis[u]=1;
pii res=mp(0,0),k;
for(int i=nw[u];i;i=nxt[i]){
int v=to[i],cst=c[i]+h[u]-h[v];nw[u]=i;if(!val[i]||dis[v]!=dis[u]+cst||vis[v])continue; //注意这里要判断 vis。具体的,可能会出现 c_i=0 使得死循环的情况。
k=dfs(v,min(sum,val[i]));if(!k.first){vis[v]=1;continue;} //注意这里不能加 dis,因为这个东西会加到势能里面去!!!这里如果不剪枝貌似和剪枝差不多。剪枝的话上面 dij 的开头要多加一个清空 vis,好像还会更慢。
val[i]-=k.first,sum-=k.first,val[i^1]+=k.first,res.first+=k.first;res.second+=c[i]*k.first+k.second;//
if(!sum)break;
}
vis[u]=0;return res;
}
int calc(){
pii ans=mp(0,0);spfa();while(dij()){pii tmp=dfs(s,inf);ans.first+=tmp.first,ans.second+=tmp.second;}
return ans.second;
}
}
using namespace flow_cost_opt;
应用
P1345 [USACO5.4] 奶牛的电信Telecowmunication
题意
- 在一个有 \(n\) 个点 \(m\) 条边的无向连通图中最少需要割几个点才能使给定的 \(s\) 与 \(t\) 不连通
\(n\le100,m\le500\)
解法
- 有一个定理:最小割=最大流。
- 这里的最小割指的是割最少的边数,但这里求的是最小割的点数。如果直接跑最大流,会出问题。如图
![初始图]()
- 这里跑最大流就显然不对。最大流是3,但最小割的点数应该是1。
- 虽然直接跑网络流不对,那能不能将分割点转化为最小割呢?答案是可以的,那就是:拆点
- 按上图中的下半部分举例(\(s\) 未画出)
![拆点图]()
- 将每个点 \(i\) 转化成 \(i\) 与 \(i+n\) 两个点,这两个点之间连一条流量为1的边
对于原图中的一条 \(u\) 到 \(v\) 的边,转化成 \(u+n\) 到 \(v\) 的一条流量为 \(inf\) 的边 - 这样连边的原因是因为这种方法将一个点变成了中间的那条流量为0的边,这样最终跑出来的最大流就是必须经过的点数,也就是答案
小细节
- 注意连的是双向边,同时双向边都要建反边
- 由于 \(s\) 与 \(t\) 本身不能被删掉,因此 \(s\) 到 \(s+n\) 与 \(t\) 到 \(t+n\) 的边的流量必须是 \(inf\)
- 最终的终点是 \(t+n\)
- 建的双向边注意是 \(u+n->v\) 和 \(v+n->u\),因为实际上原本的点还是 \(u\) 和 \(v\) 本身(也就是说上图只是一个形式化的图,并不标准,慎重借鉴)
code
点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N=60000+7;
int n,m,s,t,nxt[N],to[N],val[N],head[N],tot=1,dep[N],now[N];
void add(int u,int v,int w)
{
nxt[++tot]=head[u];
head[u]=tot;
to[tot]=v,val[tot]=w;
nxt[++tot]=head[v];
head[v]=tot;
to[tot]=u,val[tot]=0;
}
bool bfs()
{
for(int i=1;i<=n*5;i++) dep[i]=N;
queue <int> q;
q.push(s);now[s]=head[s],dep[s]=1;
while(!q.empty())
{
int u=q.front();q.pop();
for(int i=head[u];i;i=nxt[i])
{
int v=to[i];
if(dep[v]!=N||val[i]==0) continue;
dep[v]=dep[u]+1;now[v]=head[v];
q.push(v);
if(v==t) return true;
}
}
return false;
}
int dfs(int u,int sum)
{
int res=0;
if(u==t) return sum;
for(int i=now[u];i;i=nxt[i])
{
now[u]=i;
int v=to[i];
if(dep[v]!=dep[u]+1||val[i]==0) continue;
int k=dfs(v,min(sum,val[i]));
if(!k) dep[v]=N;
sum-=k,val[i]-=k,val[i^1]+=k,res+=k;
if(!sum) break;
}
return res;
}
int main()
{
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
cin>>n>>m>>s>>t;
for(int i=1;i<=n;i++)
if(i==s||i==t) add(i,i+n,N); //建反边在add中一并写了
else add(i,i+n,1);
for(int i=1,u,v;i<=m;i++)
{cin>>u>>v;add(u+n,v,N),add(v+n,u,N);}
int ans=0;
t=t+n;
while(bfs()) ans+=dfs(s,N); //板子
cout<<ans<<'\n';
return 0;
}
然后后面基本上都是建模之类的东西了。定义 \((u,v,w,c)\) 表示从 \(u\) 连向 \(v\) 的容量为 \(w\) 的费用为 \(c\) 的边。
P4016 负载平衡问题
考虑目标的个数是固定的,因此求出平均数以后比平均数小背 \(s\) 连向,比平均数大的连向 \(t\)。然后能够相邻点互相连费用为 1 的边。跑费用流即可。
code
点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int M=105;
int n,s,t,a[M];
namespace flow_cost{
const int N=2e6+7,inf=1e9+7;
#define pii pair <int,int>
#define mp make_pair
int hd[N],nxt[N],val[N],to[N],c[N],idcnt=1,dis[N],nw[N],vis[N];
void init(){for(int i=1;i<=t;i++)hd[i]=nw[i]=0;idcnt=1;}
void add(int u,int v,int w,int cst){nxt[++idcnt]=hd[u];hd[u]=idcnt,to[idcnt]=v,val[idcnt]=w,c[idcnt]=cst;swap(u,v);nxt[++idcnt]=hd[u];hd[u]=idcnt,to[idcnt]=v,val[idcnt]=0,c[idcnt]=-cst;}
bool spfa(){
bool sign=0;for(int i=1;i<=t;i++)dis[i]=inf,vis[i]=0;
queue <int> q;q.push(s);
dis[s]=0,nw[s]=hd[s];
while(!q.empty()){
int u=q.front();q.pop();
nw[u]=hd[u];if(u==t)sign=1;
for(int i=hd[u];i;i=nxt[i]){
int v=to[i];
if(dis[v]>dis[u]+c[i]&&val[i]){
dis[v]=dis[u]+c[i];
if(vis[v])continue;
q.push(v),vis[v]=1;
}
}vis[u]=0;
}
return sign;
}
pii dfs(int u,int sum){
if(u==t)return mp(sum,0);
vis[u]=1;
pii res=mp(0,0),k;
for(int i=nw[u];i;i=nxt[i]){
int v=to[i];nw[u]=i;if(!val[i]||dis[v]!=dis[u]+c[i]||vis[v])continue;
k=dfs(v,min(sum,val[i]));if(!k.first){dis[v]=inf;continue;}
val[i]-=k.first,sum-=k.first,val[i^1]+=k.first,res.first+=k.first;res.second+=c[i]*k.first+k.second;
if(!sum)break;
}
vis[u]=0;return res;
}
}
using namespace flow_cost;
signed main(){
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
cin>>n;s=n+1,t=s+1;int tmp=0;
for(int i=1;i<=n;i++) cin>>a[i],tmp+=a[i];
tmp/=n;
for(int i=1;i<=n;i++){int w=abs(tmp-a[i]);if(a[i]<tmp)add(s,i,w,0);if(a[i]>tmp)add(i,t,w,0);add(i,i%n+1,inf,1);add(i%n+1,i,inf,1);}
pii ans=mp(0,0);
while(spfa()){pii tmp=dfs(s,inf);ans.first+=tmp.first,ans.second+=tmp.second;}
cout<<ans.second<<'\n';return 0;
}
P2756 飞行员配对方案问题
二分图最大匹配板子。
code
点击查看代码
#include<bits/stdc++.h>
using namespace std;
int s,t,n,m;
namespace flow{
const int N=2e6+7,inf=1e9+7;
int hd[N],nxt[N],to[N],val[N],idcnt=1,tmp,ans,dep[N],nw[N];
void add(int u,int v,int w){nxt[++idcnt]=hd[u],hd[u]=idcnt,to[idcnt]=v,val[idcnt]=w;swap(u,v),nxt[++idcnt]=hd[u],hd[u]=idcnt,to[idcnt]=v,val[idcnt]=0;}
bool bfs(){
for(int i=1;i<=t;i++)dep[i]=inf;
queue <int> q;dep[s]=1,nw[s]=hd[s];q.push(s);
while(!q.empty()){
int u=q.front();q.pop();
for(int i=hd[u];i;i=nxt[i]){
int v=to[i];if(dep[v]==inf&&val[i]){dep[v]=dep[u]+1,q.push(v);nw[v]=hd[v];if(v==t)return 1;}
}
}
return 0;
}
int dfs(int u,int sum){
if(u==t)return sum;
int k,res=0;
for(int i=nw[u];i;i=nxt[i]){
nw[u]=i;int v=to[i];if(!val[i]||dep[v]!=dep[u]+1)continue;
k=dfs(v,min(sum,val[i]));if(!k){dep[v]=inf;continue;}
sum-=k,val[i]-=k,val[i^1]+=k,res+=k;if(!sum)break;
}
return res;
}
}
using namespace flow;
void solve(int u){
for(int i=hd[u];i;i=nxt[i]){
int v=to[i];if(v==s)continue;
if(!val[i]){cout<<u<<' '<<v<<'\n';return ;}
}
}
signed main(){
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
cin>>m>>n;int u=0,v=0;s=n+1,t=s+1;
while(cin>>u>>v&&u!=-1){add(u,v,1);}
for(int i=1;i<=m;i++)add(s,i,1);for(int i=m+1;i<=n;i++)add(i,t,1);
int ans=0;
while(bfs()){ans+=dfs(s,inf);}
cout<<ans<<'\n';
for(int i=1;i<=m;i++)solve(i);
}
P4015 运输问题
二分图最大/小权匹配板子。直接跑费用流即可。
code
点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int M=305,inf=1e9+7;
int s,t,n,m,a[M],b[M];
int getl(int x){return x;}
int getr(int x){return x+n;}
namespace flow_cost_opt1{
const int N=2e6+7,inf=1e9+7;
#define pii pair <int,int>
#define mp make_pair
int hd[N],nxt[N],val[N],to[N],c[N],idcnt=1,dis[N],nw[N],vis[N],h[N];
void init(){for(int i=1;i<=t;i++)hd[i]=nw[i]=0;idcnt=1;}
void add(int u,int v,int w,int cst){nxt[++idcnt]=hd[u];hd[u]=idcnt,to[idcnt]=v,val[idcnt]=w,c[idcnt]=cst;swap(u,v);nxt[++idcnt]=hd[u];hd[u]=idcnt,to[idcnt]=v,val[idcnt]=0,c[idcnt]=-cst;}
void spfa(){
for(int i=1;i<=t;i++)h[i]=inf,vis[i]=0;
queue <int> q;q.push(s);
h[s]=0,nw[s]=hd[s];
while(!q.empty()){
int u=q.front();q.pop();
nw[u]=hd[u];
for(int i=hd[u];i;i=nxt[i]){
int v=to[i];
if(h[v]>h[u]+c[i]&&val[i]){
h[v]=h[u]+c[i];
if(vis[v])continue;
q.push(v),vis[v]=1;
}
}vis[u]=0;
}
}
bool dij(){
for(int i=1;i<=t;i++) h[i]+=dis[i],dis[i]=inf,vis[i]=0;
priority_queue <pii> q;q.push(mp(0,s));
dis[s]=0,nw[s]=hd[s];bool sign=0;
while(!q.empty()){
int u=q.top().second;q.pop();
nw[u]=hd[u];if(u==t)sign=1;if(vis[u])continue;vis[u]=1;
for(int i=hd[u];i;i=nxt[i]){
int v=to[i],cst=c[i]+h[u]-h[v];
if(dis[v]>dis[u]+cst&&val[i]){
dis[v]=dis[u]+cst;q.push({-dis[v],v});
}
}
}
for(int i=1;i<=t;i++)vis[i]=0;
return sign;
}
pii dfs(int u,int sum){
if(u==t)return mp(sum,0);
vis[u]=1;
pii res=mp(0,0),k;
for(int i=nw[u];i;i=nxt[i]){
int v=to[i],cst=c[i]+h[u]-h[v];nw[u]=i;if(!val[i]||dis[v]!=dis[u]+cst||vis[v])continue;
k=dfs(v,min(sum,val[i]));if(!k.first){vis[v]=1;continue;}
val[i]-=k.first,sum-=k.first,val[i^1]+=k.first,res.first+=k.first;res.second+=c[i]*k.first+k.second;//
if(!sum)break;
}
vis[u]=0;return res;
}
}
namespace flow_cost_opt2{
const int N=2e6+7,inf=1e9+7;
#define pii pair <int,int>
#define mp make_pair
int hd[N],nxt[N],val[N],to[N],c[N],idcnt=1,dis[N],nw[N],vis[N],h[N];
void init(){for(int i=1;i<=t;i++)hd[i]=nw[i]=0;idcnt=1;}
void add(int u,int v,int w,int cst){nxt[++idcnt]=hd[u];hd[u]=idcnt,to[idcnt]=v,val[idcnt]=w,c[idcnt]=cst;swap(u,v);nxt[++idcnt]=hd[u];hd[u]=idcnt,to[idcnt]=v,val[idcnt]=0,c[idcnt]=-cst;}
void spfa(){
for(int i=1;i<=t;i++)h[i]=-inf,vis[i]=0;
queue <int> q;q.push(s);
h[s]=0,nw[s]=hd[s];
while(!q.empty()){
int u=q.front();q.pop();
nw[u]=hd[u];
for(int i=hd[u];i;i=nxt[i]){
int v=to[i];
if(h[v]<h[u]+c[i]&&val[i]){
h[v]=h[u]+c[i];
if(vis[v])continue;
q.push(v),vis[v]=1;
}
}vis[u]=0;
}
}
bool dij(){
for(int i=1;i<=t;i++) h[i]+=dis[i],dis[i]=-inf,vis[i]=0;
priority_queue <pii> q;q.push(mp(0,s));
dis[s]=0,nw[s]=hd[s];bool sign=0;
while(!q.empty()){
int u=q.top().second;q.pop();
nw[u]=hd[u];if(u==t)sign=1;if(vis[u])continue;vis[u]=1;
for(int i=hd[u];i;i=nxt[i]){
int v=to[i],cst=c[i]+h[u]-h[v];
if(dis[v]<dis[u]+cst&&val[i]){
dis[v]=dis[u]+cst;q.push({dis[v],v});
}
}
}
for(int i=1;i<=t;i++)vis[i]=0;
return sign;
}
pii dfs(int u,int sum){
if(u==t)return mp(sum,0);
vis[u]=1;
pii res=mp(0,0),k;
for(int i=nw[u];i;i=nxt[i]){
int v=to[i],cst=c[i]+h[u]-h[v];nw[u]=i;if(!val[i]||dis[v]!=dis[u]+cst||vis[v])continue;
k=dfs(v,min(sum,val[i]));if(!k.first){vis[v]=1;continue;}
val[i]-=k.first,sum-=k.first,val[i^1]+=k.first,res.first+=k.first;res.second+=c[i]*k.first+k.second;//
if(!sum)break;
}
vis[u]=0;return res;
}
}
signed main(){
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
cin>>n>>m;s=n+m+1,t=s+1;
for(int i=1;i<=n;i++)cin>>a[i],flow_cost_opt1::add(s,getl(i),a[i],0),flow_cost_opt2::add(s,getl(i),a[i],0);
for(int i=1;i<=m;i++)cin>>b[i],flow_cost_opt1::add(getr(i),t,b[i],0),flow_cost_opt2::add(getr(i),t,b[i],0);
for(int i=1;i<=n;i++)for(int j=1,cst;j<=m;j++) cin>>cst,flow_cost_opt1::add(getl(i),getr(j),inf,cst),flow_cost_opt2::add(getl(i),getr(j),inf,cst);
pii ans=mp(0,0);flow_cost_opt1::spfa();
while(flow_cost_opt1::dij()){pii tmp=flow_cost_opt1::dfs(s,inf);ans.first+=tmp.first,ans.second+=tmp.second;}
cout<<ans.second<<'\n';
ans=mp(0,0);flow_cost_opt2::spfa();
while(flow_cost_opt2::dij()){pii tmp=flow_cost_opt2::dfs(s,inf);ans.first+=tmp.first,ans.second+=tmp.second;}
cout<<ans.second<<'\n';ans=mp(0,0);
return 0;
}
P3254 圆桌问题
为什么一开始没想出来?
考虑一个单位的代表间是没有区别的,因此一个单位建一个点。一个单位只能有一个人到某张桌子,连流量 1 的边即可。然后判断是否满流。输出方案是平凡的。
code
点击查看代码
#include<bits/stdc++.h>
using namespace std;
int s,t,n,m;
namespace flow{
const int N=2e6+7,inf=1e9+7;
int hd[N],nxt[N],to[N],val[N],idcnt=1,tmp,ans,dep[N],nw[N];
int getl(int x){return x;}
int getr(int x){return x+n;}
void add(int u,int v,int w){nxt[++idcnt]=hd[u],hd[u]=idcnt,to[idcnt]=v,val[idcnt]=w;swap(u,v),nxt[++idcnt]=hd[u],hd[u]=idcnt,to[idcnt]=v,val[idcnt]=0;}
bool bfs(){
for(int i=1;i<=t;i++)dep[i]=inf;
queue <int> q;dep[s]=1,nw[s]=hd[s];q.push(s);
while(!q.empty()){
int u=q.front();q.pop();
for(int i=hd[u];i;i=nxt[i]){
int v=to[i];if(dep[v]==inf&&val[i]){dep[v]=dep[u]+1,q.push(v);nw[v]=hd[v];if(v==t)return 1;}
}
}
return 0;
}
int dfs(int u,int sum){
if(u==t)return sum;
int k,res=0;
for(int i=nw[u];i;i=nxt[i]){
nw[u]=i;int v=to[i];if(!val[i]||dep[v]!=dep[u]+1)continue;
k=dfs(v,min(sum,val[i]));if(!k){dep[v]=inf;continue;}
sum-=k,val[i]-=k,val[i^1]+=k,res+=k;if(!sum)break;
}
return res;
}
}
using namespace flow;
void solve(int u){
for(int i=hd[u];i;i=nxt[i]){
int v=to[i];if(v==s)continue;
if(!val[i]){cout<<v-n<<' ';continue;}
}
cout<<'\n';
}
signed main(){
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
cin>>n>>m;s=n+m+1,t=s+1;int tmp=0;
for(int i=1,x;i<=n;i++)cin>>x,add(s,getl(i),x),tmp+=x;
for(int i=1,x;i<=m;i++)cin>>x,add(getr(i),t,x);
for(int i=1;i<=n;i++)for(int j=1;j<=m;j++)add(getl(i),getr(j),1);
int ans=0;
while(bfs()){ans+=dfs(s,inf);}
if(ans!=tmp){cout<<'0';return 0;}
cout<<"1\n";
for(int i=1;i<=n;i++)solve(getl(i));
}
P3355 骑士共存问题
经典黑白染色+最小割问题。网格图黑白染色是非常常见的套路了。
将棋盘黑白交错染色。考虑马一定只能攻击到与其颜色不同的方格,因此颜色相同的马之间一定不能互相攻击,因此变为二分图。
将能够互相攻击的位置连边。由于最小割=最大流,因此直接跑了之后最大流代表的是最小的能够互相攻击的马的对数。直接用方格总数减这个东西即可。
code
点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int N=205,M=2e6+7,inf=2e9+7;
int n,m,hd[M],nxt[M],to[M],val[M],s,t,nw[M],dep[M],ans=0,tmp=0,idcnt=1,vis[N][N],k;
void add(int u,int v,int w){nxt[++idcnt]=hd[u];hd[u]=idcnt;to[idcnt]=v,val[idcnt]=w;nxt[++idcnt]=hd[v];hd[v]=idcnt;to[idcnt]=u,val[idcnt]=0;}
int get(int i,int j){return (i-1)*m+j;}
bool bfs(){
for(int i=1;i<=n*m+2;i++)dep[i]=inf;
queue <int> q;q.push(s);dep[s]=1,nw[s]=hd[s];
while(!q.empty()){
int u=q.front();q.pop();
for(int i=hd[u];i;i=nxt[i]){
int v=to[i];if(dep[v]==inf&&val[i]){q.push(v),dep[v]=dep[u]+1,nw[v]=hd[v];if(v==t)return 1;}
}
}
return 0;
}
int dfs(int u,int sum){
if(u==t) return sum;
int k=0,res=0;
for(int i=nw[u];i;i=nxt[i]){
nw[u]=i;int v=to[i];
if(!val[i]||dep[v]!=dep[u]+1)continue;
k=dfs(v,min(sum,val[i]));if(!k)dep[v]=inf;
val[i]-=k,val[i^1]+=k;sum-=k,res+=k;
if(!sum)break;
}
return res;
}
signed main(){
ios::sync_with_stdio(false),cin.tie(0),cin.tie(0);
cin>>n>>k;m=n;s=n*m+1,t=s+1;tmp=n*m;for(int i=1,x,y;i<=k;i++)cin>>x>>y,tmp-=vis[x][y]^1,vis[x][y]=1;
for(int i=1;i<=n;i++)for(int j=1;j<=m;j++){
if(vis[i][j])continue;
if((i+j)&1){
add(s,get(i,j),1);
if(j>2&&i>1)add(get(i,j),get(i-1,j-2),inf);
if(j>2&&i<n)add(get(i,j),get(i+1,j-2),inf);
if(j<m-1&&i>1)add(get(i,j),get(i-1,j+2),inf);
if(j<m-1&&i<n)add(get(i,j),get(i+1,j+2),inf);
if(i>2&&j>1)add(get(i,j),get(i-2,j-1),inf);
if(i>2&&j<m)add(get(i,j),get(i-2,j+1),inf);
if(i<n-1&&j>1)add(get(i,j),get(i+2,j-1),inf);
if(i<n-1&&j<m)add(get(i,j),get(i+2,j+1),inf);
}
else add(get(i,j),t,1);
}
while(bfs())ans+=dfs(s,inf);
cout<<tmp-ans<<'\n';return 0;
}
P2765 魔术球问题
个人感觉实际上是困难题。
考虑相加成为完全平方数实际上是图上连向的关系,因此转化为 DAG 上的最小链覆盖。(我们钦定大的边连向小的边)
由于经典结论(最下面),残量网络上动态加边跑的复杂度是对的,因此我们可以从小到大枚举点,然后考虑链覆盖条数是否大于柱子数量。
输出方案是平凡的。
code
点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int M=2e6+7;
int s,t,n,m,a[M],fa[M],bel[M],cnt=0;
namespace flow{
const int N=2e6+7,inf=1e9+7;
int hd[N],nxt[N],to[N],val[N],idcnt=1,tmp,ans,dep[N],nw[N];
int getl(int x){return x*2-1;}
int getr(int x){return x*2;}
void add(int u,int v,int w){nxt[++idcnt]=hd[u],hd[u]=idcnt,to[idcnt]=v,val[idcnt]=w;swap(u,v),nxt[++idcnt]=hd[u],hd[u]=idcnt,to[idcnt]=v,val[idcnt]=0;}
bool bfs(){
dep[t]=inf;for(int i=1;i<=2*(n+1);i++)dep[i]=inf;
queue <int> q;dep[s]=1,nw[s]=hd[s];q.push(s);
while(!q.empty()){
int u=q.front();q.pop();
for(int i=hd[u];i;i=nxt[i]){
int v=to[i];if(dep[v]==inf&&val[i]){dep[v]=dep[u]+1,q.push(v);nw[v]=hd[v];if(v==t)return 1;}
}
}
return 0;
}
int dfs(int u,int sum){
if(u==t)return sum;
int k,res=0;
for(int i=nw[u];i;i=nxt[i]){
nw[u]=i;int v=to[i];if(!val[i]||dep[v]!=dep[u]+1)continue;
k=dfs(v,min(sum,val[i]));if(!k){dep[v]=inf;continue;}
sum-=k,val[i]-=k,val[i^1]+=k,res+=k;if(!sum)break;
}
return res;
}
}
using namespace flow;
vector <int> res[N];
int find(int x){return fa[x]==x?x:fa[x]=find(fa[x]);}
void solve(int u){
for(int i=hd[u];i;i=nxt[i]){
int v=to[i];if(v==s)continue;
if(!val[i]){fa[(u+1)/2]=find(v/2);res[bel[fa[(u+1)/2]]].push_back((u+1)/2);return;}
}
bel[(u+1)/2]=++cnt;res[bel[fa[(u+1)/2]]].push_back((u+1)/2);
}
signed main(){
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
cin>>m;int ans=0;n=0;s=10001,t=s+1;for(int i=1;i<=5000;i++)a[i]=i*i,fa[i]=i;
while(n-ans<=m){
int u=n+1;
int loc=upper_bound(a+1,a+5001+1,u)-a;loc--;
for(int i=loc;i<=5000&&a[i]-u<u;i++)add(getl(u),getr(a[i]-u),1);
add(s,getl(u),1),add(getr(u),t,1);
while(bfs()){ans+=dfs(s,inf);}
n++;if(n-ans>m)break;
}
n--;cout<<n<<'\n';
for(int i=1;i<=n;i++)solve(getl(i));
for(int i=1;i<=m;i++){for(int u:res[i])cout<<u<<' ';cout<<'\n';}
return 0;
}
P2774 方格取数问题
同样黑白染色,然后考虑最小割。将相邻的点互相连边,成为二分图,然后用总贡献减去最小被割掉的贡献。
code
点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int N=105,M=2e6+7,inf=2e9+7;
int n,m,a[N][N],hd[M],nxt[M],to[M],val[M],s,t,nw[M],dep[M],ans=0,tmp=0,idcnt=1;
void add(int u,int v,int w){nxt[++idcnt]=hd[u];hd[u]=idcnt;to[idcnt]=v,val[idcnt]=w;nxt[++idcnt]=hd[v];hd[v]=idcnt;to[idcnt]=u,val[idcnt]=0;}
int get(int i,int j){return (i-1)*m+j;}
bool bfs(){
for(int i=1;i<=n*m+2;i++)dep[i]=inf;
queue <int> q;q.push(s);dep[s]=1,nw[s]=hd[s];
while(!q.empty()){
int u=q.front();q.pop();
for(int i=hd[u];i;i=nxt[i]){
int v=to[i];if(dep[v]==inf&&val[i]){q.push(v),dep[v]=dep[u]+1,nw[v]=hd[v];if(v==t)return 1;}
}
}
return 0;
}
int dfs(int u,int sum){
if(u==t) return sum;
int k=0,res=0;
for(int i=nw[u];i;i=nxt[i]){
nw[u]=i;int v=to[i];
if(!val[i]||dep[v]!=dep[u]+1)continue;
k=dfs(v,min(sum,val[i]));if(!k)dep[v]=inf;
val[i]-=k,val[i^1]+=k;sum-=k,res+=k;
if(!sum)break;
}
return res;
}
signed main(){
ios::sync_with_stdio(false),cin.tie(0),cin.tie(0);
cin>>n>>m;s=n*m+1,t=s+1;for(int i=1;i<=n;i++)for(int j=1;j<=m;j++)cin>>a[i][j],tmp+=a[i][j];
for(int i=1;i<=n;i++)for(int j=1;j<=m;j++){
if((i+j)&1){
add(s,get(i,j),a[i][j]);//cout<<"* "<<i<<' '<<j<<'\n';
if(i>1)add(get(i,j),get(i-1,j),inf);//,cout<<"1 "<<get(i-1,j)<<' ',cout<<"2 "<<get(i,j-1)<<' ',cout<<"3 "<<get(i+1,j)<<' ',cout<<"4 "<<get(i,j+1)<<' '
if(j>1)add(get(i,j),get(i,j-1),inf);
if(i<n)add(get(i,j),get(i+1,j),inf);
if(j<m)add(get(i,j),get(i,j+1),inf);
// cout<<'\n';
}
else add(get(i,j),t,a[i][j]);
}
while(bfs())ans+=dfs(s,inf);
cout<<tmp-ans<<'\n';return 0;
}
/*
4 3
63 49 82
16 27 92
27 95 3
51 54 13
*/
P2762 太空飞行计划问题
考虑实验与仪器间的关系正常连,然后连 \((s,E_i,p_i)\),\((I_j,t,c_j)\)。跑最小割,然后用实验的总收益减去最小割。
考虑为什么是对的。因为如果跑出来是左部点的边被割掉了,代表不选这个实验。右部点被割掉表示选这个仪器。因为仪器的价值是负的。我们改成正的就变成了选。
code
点击查看代码
#include<bits/stdc++.h>
using namespace std;
// #define int long long
const int N=1e6+7,inf=2e9+7;
int n,m,s,t,tmp,nxt[N],hd[N],to[N],val[N],dep[N],nw[N],sign[N],idcnt=1;
int getl(int i){return i;}
int getr(int i){return n+i;}
void add(int u,int v,int w){nxt[++idcnt]=hd[u],hd[u]=idcnt,to[idcnt]=v,val[idcnt]=w;swap(u,v),nxt[++idcnt]=hd[u],hd[u]=idcnt,to[idcnt]=v,val[idcnt]=0;}
bool bfs(){
for(int i=1;i<=t;i++)dep[i]=inf;
queue <int> q;dep[s]=1,nw[s]=hd[s];q.push(s);
while(!q.empty()){
int u=q.front();q.pop();
for(int i=hd[u];i;i=nxt[i]){
int v=to[i];if(dep[v]==inf&&val[i]){dep[v]=dep[u]+1,q.push(v);nw[v]=hd[v];if(v==t)return 1;}
}
}
return 0;
}
int dfs(int u,int sum){
if(u==t)return sum;
int k,res=0;
for(int i=nw[u];i;i=nxt[i]){
nw[u]=i;int v=to[i];if(!val[i]||dep[v]!=dep[u]+1)continue;
k=dfs(v,min(sum,val[i]));if(!k){dep[v]=inf;continue;}
sum-=k,val[i]-=k,val[i^1]+=k,res+=k;if(!sum)break;
}
return res;
}
int main(){
// ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
cin>>n>>m;s=n+m+1,t=s+1;
for(int i=1,w;i<=n;i++){
cin>>w;add(s,getl(i),w);tmp+=w;
char tools[10000];
memset(tools,0,sizeof tools);
cin.getline(tools,10000);
int ulen=0,tool;
while (sscanf(tools+ulen,"%d",&tool)==1)//之前已经用scanf读完了赞助商同意支付该实验的费用
{//tool是该实验所需仪器的其中一个
//这一行,你可以将读进来的编号进行储存、处理,如连边。
add(getl(i),getr(tool),inf);
if (tool==0)
ulen++;
else {
while (tool) {
tool/=10;
ulen++;
}
}
ulen++;
}
}
for(int i=1,w;i<=m;i++)cin>>w,add(getr(i),t,w);
int ans=0;while(bfs())ans+=dfs(s,inf);
for(int i=1;i<=n;i++)if(dep[getl(i)]!=inf)cout<<i<<' ';cout<<'\n';
for(int i=1;i<=m;i++)if(dep[getr(i)]!=inf)cout<<i<<' ';cout<<'\n';
cout<<tmp-ans<<'\n';
}
P1361 小M的作物
考虑这个东西有捆绑的收益。这个东西也很经典,同样属于最小割模型。
显然正常的收益就是直接跑二分图最大权匹配即可。
考虑一种额外收益不被贡献的原因是什么。显然是与其相关联的一种作物种在了另一块田里。
考虑如何表示这个限制。由于是最小割模型,我们发现可以新建一个虚空节点表示这个额外收益。
例如全部选左部点才有的额外收益就与右部点的对应的点连边。这样如果右部点有边不被割掉,那么这个额外收益的边一定会被割掉。
然后发现不拆成左右部点是一样的。于是代码实现里面没有拆点。
code
点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int M=2e6+7,inf=1e9+7;
int n,m,s,t,hd[M],nxt[M],to[M],val[M],idcnt=1,tmp,ans,dep[M],nw[M];
void add(int u,int v,int w){nxt[++idcnt]=hd[u],hd[u]=idcnt,to[idcnt]=v,val[idcnt]=w;swap(u,v),nxt[++idcnt]=hd[u],hd[u]=idcnt,to[idcnt]=v,val[idcnt]=0;}
int get(int i){return i;}
int geta(int i){return n+i+2;}
int getb(int i){return m+n+i+2;}
bool bfs(){
for(int i=1;i<=m+m+n+2;i++)dep[i]=inf;
queue <int> q;dep[s]=1,nw[s]=hd[s];q.push(s);
while(!q.empty()){
int u=q.front();q.pop();
for(int i=hd[u];i;i=nxt[i]){
int v=to[i];if(dep[v]==inf&&val[i]){dep[v]=dep[u]+1,q.push(v);nw[v]=hd[v];if(v==t)return 1;}
}
}
return 0;
}
int dfs(int u,int sum){
if(u==t)return sum;
int k,res=0;
for(int i=nw[u];i;i=nxt[i]){
nw[u]=i;int v=to[i];if(!val[i]||dep[v]!=dep[u]+1)continue;
k=dfs(v,min(sum,val[i]));if(!k){dep[v]=inf;continue;}
sum-=k,val[i]-=k,val[i^1]+=k,res+=k;if(!sum)break;
}
return res;
}
signed main(){
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
cin>>n;s=n+1,t=s+1;
for(int i=1,w;i<=n;i++)cin>>w,add(s,get(i),w),tmp+=w;
for(int i=1,w;i<=n;i++)cin>>w,add(get(i),t,w),tmp+=w;
cin>>m;
for(int i=1,k,c1,c2;i<=m;i++){
cin>>k;cin>>c1>>c2;tmp+=c1+c2;add(s,geta(i),c1),add(getb(i),t,c2);
for(int j=1,u;j<=k;j++)cin>>u,add(geta(i),get(u),inf),add(get(u),getb(i),inf);
}
while(bfs())ans+=dfs(s,inf);
cout<<tmp-ans<<'\n';return 0;
}
P2770 航空路线问题
考虑转化一下。实际上就是找到两条没有公共点的最长路径。
考虑经典套路。不共点的限制不好做就拆点。点之间连流量为 1 费用为 1 的边。
然后不同点之间的路径就正常连。
输出方案是平凡但复杂的。注意特判 1 与 \(n\) 有直接连边的情况。
code
点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N=2e6+7,inf=1e9+7;
#define pii pair <int,int>
#define mp make_pair
int n,m,s,t,hd[N],nxt[N],val[N],to[N],c[N],idcnt=1,dis[N],nw[N],vis[N],stk[3][N],stkcnt[3],pst[N];
map <string,int> h1;
string h2[N];
int getl(int x){return x;}
int getr(int x){return x+n;}
void add(int u,int v,int w,int cst){nxt[++idcnt]=hd[u];hd[u]=idcnt,to[idcnt]=v,val[idcnt]=w,pst[idcnt]=w,c[idcnt]=cst;swap(u,v);nxt[++idcnt]=hd[u];hd[u]=idcnt,to[idcnt]=v,val[idcnt]=0,pst[idcnt]=0,c[idcnt]=-cst;}
bool spfa(){
bool sign=0;for(int i=1;i<=t;i++)dis[i]=-inf,vis[i]=0;
queue <int> q;q.push(s);
dis[s]=0,nw[s]=hd[s];
while(!q.empty()){
int u=q.front();q.pop();
nw[u]=hd[u];if(u==t)sign=1;
for(int i=hd[u];i;i=nxt[i]){
int v=to[i];
if(dis[v]<dis[u]+c[i]&&val[i]){
dis[v]=dis[u]+c[i];
if(vis[v])continue;
q.push(v),vis[v]=1;
}
}vis[u]=0;
}
return sign;
}
pii dfs(int u,int sum){
if(u==t)return mp(sum,0);
vis[u]=1;
pii res=mp(0,0),k;
for(int i=nw[u];i;i=nxt[i]){
int v=to[i];nw[u]=i;if(!val[i]||dis[v]!=dis[u]+c[i]||vis[v])continue;
k=dfs(v,min(sum,val[i]));if(!k.first){dis[v]=inf;continue;}
val[i]-=k.first,sum-=k.first,val[i^1]+=k.first,res.first+=k.first;res.second+=c[i]*k.first+k.second;
if(!sum)break;
}
vis[u]=0;return res;
}
void solve(int u,int sign){
if(u>n&&u<s)stk[sign][++stkcnt[sign]]=u-n;
if(u==2*n)return;
vis[u]=1;
for(int i=hd[u];i;i=nxt[i]){
if(!val[i]&&!vis[to[i]+n]&&to[i]<=n&&(sign==1||to[i]!=n)) {solve(to[i]+n,sign);return;}
}
}
signed main(){
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
cin>>n>>m;s=2*n+1,t=s+1;
string tmp1,tmp2;int sign1=0;pii ans=mp(0,0);
for(int i=1;i<=n;i++)cin>>tmp1,h1[tmp1]=i,h2[i]=tmp1,add(getl(i),getr(i),1+(i==1||i==n),1);
for(int i=1;i<=m;i++){cin>>tmp1>>tmp2;if(h1[tmp1]>h1[tmp2])swap(tmp1,tmp2);sign1=(sign1||(h1[tmp1]==1&&h1[tmp2]==n));if(h1[tmp1]==1&&h1[tmp2]==n)continue;add(getr(h1[tmp1]),getl(h1[tmp2]),1,0);}
add(s,getl(1),2,0),add(getr(n),t,2,0);
while(spfa()){pii tmp=dfs(s,inf);ans.first+=tmp.first,ans.second+=tmp.second;}
if(ans.first==0){
if(sign1){
cout<<"2\n";cout<<h2[1]<<'\n'<<h2[n]<<'\n'<<h2[1];
return 0;
}
else{cout<<"No Solution!";return 0;}
}
if(ans.first==1){
if(sign1){
solve(getr(1),1);cout<<stkcnt[1]<<'\n';
for(int i=1;i<=stkcnt[1];i++) cout<<h2[stk[1][i]]<<'\n';
cout<<h2[1]<<'\n';return 0;
}
else{cout<<"No Solution!";return 0;}
}
solve(getr(1),1);solve(getr(1),2);
cout<<stkcnt[1]+stkcnt[2]-1<<'\n';
for(int i=1;i<=stkcnt[1];i++)cout<<h2[stk[1][i]]<<'\n';
for(int i=stkcnt[2];i>=1;i--)cout<<h2[stk[2][i]]<<'\n';
return 0;
}
P2766 最长不下降子序列问题
显然同样考虑拆点。
第一问是简单的。我们可以暴力预处理出从每个点开始的最长不下降子序列大小 \(f\)。
考虑第二问。然后由于后面的 \(s\) 是固定的,因此我们直接要求 \(f\) 连续同时大小满足条件的点之间才可以连边。然后 \(f=1\) 的点连 \(s\),\(f=s\) 的点连 \(t\),最后流量就是大小。
考虑第三问。可以发现这个东西就是将 \(s\) 或 \(t\) 连向 1 或被 \(n\) 连向的边的流量变成 \(\infty\) 即可。如果没有这种边就不管。
code
不知道为什么写的是费用流。
点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N=2e6+7,inf=1e9+7;
#define pii pair <int,int>
#define mp make_pair
int n,m,s,t,a[N],len,f[N];
namespace flow_cost{
int hd[N],nxt[N],val[N],to[N],c[N],idcnt=1,dis[N],nw[N],vis[N];
void init(){for(int i=1;i<=t;i++)hd[i]=nw[i]=0;idcnt=1;}
int getl(int x){return x;}
int getr(int x){return x+n;}
void add(int u,int v,int w,int cst){nxt[++idcnt]=hd[u];hd[u]=idcnt,to[idcnt]=v,val[idcnt]=w,c[idcnt]=cst;swap(u,v);nxt[++idcnt]=hd[u];hd[u]=idcnt,to[idcnt]=v,val[idcnt]=0,c[idcnt]=-cst;}
bool spfa(){
bool sign=0;for(int i=1;i<=t;i++)dis[i]=-inf,vis[i]=0;
queue <int> q;q.push(s);
dis[s]=0,nw[s]=hd[s];
while(!q.empty()){
int u=q.front();q.pop();
nw[u]=hd[u];if(u==t)sign=1;
for(int i=hd[u];i;i=nxt[i]){
int v=to[i];
if(dis[v]<dis[u]+c[i]&&val[i]){
dis[v]=dis[u]+c[i];
if(vis[v])continue;
q.push(v),vis[v]=1;
}
}vis[u]=0;
}
return sign;
}
pii dfs(int u,int sum){
if(u==t)return mp(sum,0);
vis[u]=1;
pii res=mp(0,0),k;
for(int i=nw[u];i;i=nxt[i]){
int v=to[i];nw[u]=i;if(!val[i]||dis[v]!=dis[u]+c[i]||vis[v])continue;
k=dfs(v,min(sum,val[i]));if(!k.first){dis[v]=inf;continue;}
val[i]-=k.first,sum-=k.first,val[i^1]+=k.first,res.first+=k.first;res.second+=c[i]*k.first+k.second;
if(!sum)break;
}
vis[u]=0;return res;
}
}
using namespace flow_cost;
void solve1(){
for(int i=n;i>=1;i--){
f[i]=1;
for(int j=i+1;j<=n;j++)if(a[i]<=a[j])f[i]=max(f[j]+1,f[i]);
len=max(len,f[i]);
}
cout<<len<<'\n';
}
void solve2(){
if(len==1){cout<<n<<'\n';return ;}
for(int i=1;i<=n;i++){add(getl(i),getr(i),1,1);for(int j=i+1;j<=n;j++){if(a[i]<=a[j]&&f[j]+1==f[i])add(getr(i),getl(j),1,0);}}
for(int i=1;i<=n;i++){if(f[i]==len)add(s,getl(i),1,0);if(f[i]==1)add(getr(i),t,1,0);}
pii ans=mp(0,0);
while(spfa()){pii tmp=dfs(s,inf);ans.first+=tmp.first;ans.second+=tmp.second;}
cout<<ans.first<<'\n';
}
void solve3(){
if(len==1){cout<<n<<'\n';return ;}
for(int i=1;i<=n;i++){add(getl(i),getr(i),i==1||i==n?inf:1,1);for(int j=i+1;j<=n;j++){if(a[i]<=a[j]&&f[j]+1==f[i])add(getr(i),getl(j),1,0);}}
for(int i=1;i<=n;i++){if(f[i]==len)add(s,getl(i),i==1||i==n?inf:1,0);if(f[i]==1)add(getr(i),t,i==1||i==n?inf:1,0);}
pii ans=mp(0,0);
while(spfa()){pii tmp=dfs(s,inf);ans.first+=tmp.first;ans.second+=tmp.second;}
cout<<ans.first<<'\n';
}
signed main(){
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++)cin>>a[i];
if(n==1){cout<<"1\n1\n1\n";return 0;}
solve1();solve2();init();solve3();
return 0;
}
P4012 深海机器人问题
由于一个标本只能被贡献一次,因此这个东西好像没有那么简单。
考虑一种比较直观的建图方法:将边拆成两条,\((u,v,1,cst),(u,v,\infty,0)\)。通过这样的方法就可以使得一条边上的贡献只被贡献一次。
而输入是略困难的,需要小心。
code
点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N=2e6+7,inf=1e9+7;
#define pii pair <int,int>
#define mp make_pair
int n,m,s,t,a,b;
namespace flow_cost{
int hd[N],nxt[N],val[N],to[N],c[N],idcnt=1,dis[N],nw[N],vis[N];
void init(){for(int i=1;i<=t;i++)hd[i]=nw[i]=0;idcnt=1;}
int get(int i,int j){return (i-1)*m+j;}
void add(int u,int v,int w,int cst){nxt[++idcnt]=hd[u];hd[u]=idcnt,to[idcnt]=v,val[idcnt]=w,c[idcnt]=cst;swap(u,v);nxt[++idcnt]=hd[u];hd[u]=idcnt,to[idcnt]=v,val[idcnt]=0,c[idcnt]=-cst;}
bool spfa(){
bool sign=0;for(int i=1;i<=t;i++)dis[i]=-inf,vis[i]=0;
queue <int> q;q.push(s);
dis[s]=0,nw[s]=hd[s];
while(!q.empty()){
int u=q.front();q.pop();
nw[u]=hd[u];if(u==t)sign=1;
for(int i=hd[u];i;i=nxt[i]){
int v=to[i];
if(dis[v]<dis[u]+c[i]&&val[i]){
dis[v]=dis[u]+c[i];
if(vis[v])continue;
q.push(v),vis[v]=1;
}
}vis[u]=0;
}
return sign;
}
pii dfs(int u,int sum){
if(u==t)return mp(sum,0);
vis[u]=1;
pii res=mp(0,0),k;
for(int i=nw[u];i;i=nxt[i]){
int v=to[i];nw[u]=i;if(!val[i]||dis[v]!=dis[u]+c[i]||vis[v])continue;
k=dfs(v,min(sum,val[i]));if(!k.first){dis[v]=inf;continue;}
val[i]-=k.first,sum-=k.first,val[i^1]+=k.first,res.first+=k.first;res.second+=c[i]*k.first+k.second;//
if(!sum)break;
}
vis[u]=0;return res;
}
}
using namespace flow_cost;
signed main(){
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
cin>>a>>b>>n>>m;n++,m++;s=n*m+1,t=s+1;
for(int i=1;i<=n;i++)for(int j=1,w;j<m;j++)cin>>w,add(get(i,j),get(i,j+1),1,w),add(get(i,j),get(i,j+1),inf,0);
for(int i=1;i<=m;i++)for(int j=1,w;j<n;j++)cin>>w,add(get(j,i),get(j+1,i),1,w),add(get(j,i),get(j+1,i),inf,0);//这种解决方式有点巧妙!!!
for(int i=1,x,y,k;i<=a;i++)cin>>k>>x>>y,add(s,get(x+1,y+1),k,0);
for(int i=1,x,y,k;i<=b;i++)cin>>k>>x>>y,add(get(x+1,y+1),t,k,0);
pii ans=mp(0,0);
while(spfa()){pii tmp=dfs(s,inf);ans.first+=tmp.first,ans.second+=tmp.second;}//cout<<tmp.first<<' '<<tmp.second<<'\n';
cout<<ans.second<<'\n';return 0;
}
P4013 数字梯形问题
拆点。三个问点内部流量分别为:1,2,2。边之间的流量分别为:1,1,\(\infty\)。
最大费用即为答案。
code
点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N=2e6+7,inf=1e9+7,M=75;
#define pii pair <int,int>
#define mp make_pair
int n,m,s,t;
namespace flow_cost{
int hd[N],nxt[N],val[N],to[N],c[N],idcnt=1,dis[N],nw[N],vis[N],a[M][M];
void init(){for(int i=1;i<=t;i++)hd[i]=nw[i]=0;idcnt=1;}
int getl(int i,int j){return (m+m+i-1-1)*(i-1)/2+j;}
int getr(int i,int j){return getl(n,n+m-1)+getl(i,j);}
void add(int u,int v,int w,int cst){nxt[++idcnt]=hd[u];hd[u]=idcnt,to[idcnt]=v,val[idcnt]=w,c[idcnt]=cst;swap(u,v);nxt[++idcnt]=hd[u];hd[u]=idcnt,to[idcnt]=v,val[idcnt]=0,c[idcnt]=-cst;}
bool spfa(){
bool sign=0;for(int i=1;i<=t;i++)dis[i]=-inf,vis[i]=0;
queue <int> q;q.push(s);
dis[s]=0,nw[s]=hd[s];
while(!q.empty()){
int u=q.front();q.pop();
nw[u]=hd[u];if(u==t)sign=1;
for(int i=hd[u];i;i=nxt[i]){
int v=to[i];
if(dis[v]<dis[u]+c[i]&&val[i]){
dis[v]=dis[u]+c[i];
if(vis[v])continue;
q.push(v),vis[v]=1;
}
}vis[u]=0;
}
return sign;
}
pii dfs(int u,int sum){
// cout<<"dfs "<<u<<' '<<sum<<'\n';
if(u==t)return mp(sum,0);
vis[u]=1;
pii res=mp(0,0),k;
for(int i=nw[u];i;i=nxt[i]){
int v=to[i];nw[u]=i;if(!val[i]||dis[v]!=dis[u]+c[i]||vis[v])continue;
k=dfs(v,min(sum,val[i]));if(!k.first){dis[v]=inf;continue;}
val[i]-=k.first,sum-=k.first,val[i^1]+=k.first,res.first+=k.first;res.second+=c[i]*k.first+k.second;//
if(!sum)break;
}
vis[u]=0;return res;
}
}
using namespace flow_cost;
void solve1(){
for(int i=1;i<=m;i++) add(s,getl(1,i),1,0);
for(int i=1;i<n;i++){
for(int j=1;j<=m+i-1;j++)
add(getl(i,j),getr(i,j),1,a[i][j]),add(getr(i,j),getl(i+1,j),1,0),add(getr(i,j),getl(i+1,j+1),1,0);
}
for(int i=1;i<=n+m-1;i++) add(getl(n,i),getr(n,i),1,a[n][i]),add(getr(n,i),t,1,0);
pii ans=mp(0,0);
while(spfa()){pii tmp=dfs(s,inf);ans.first+=tmp.first,ans.second+=tmp.second;}
cout<<ans.second<<'\n';return;
}
void solve2(){
for(int i=1;i<=m;i++) add(s,getl(1,i),1,0);
for(int i=1;i<n;i++){
for(int j=1;j<=m+i-1;j++)
add(getl(i,j),getr(i,j),2,a[i][j]),add(getr(i,j),getl(i+1,j),1,0),add(getr(i,j),getl(i+1,j+1),1,0);
}
for(int i=1;i<=n+m-1;i++) add(getl(n,i),getr(n,i),2,a[n][i]),add(getr(n,i),t,2,0);
pii ans=mp(0,0);
while(spfa()){pii tmp=dfs(s,inf);ans.first+=tmp.first,ans.second+=tmp.second;}
cout<<ans.second<<'\n';return;
}
void solve3(){
for(int i=1;i<=m;i++) add(s,getl(1,i),1,0);
for(int i=1;i<n;i++){
for(int j=1;j<=m+i-1;j++)
add(getl(i,j),getr(i,j),m,a[i][j]),add(getr(i,j),getl(i+1,j),m,0),add(getr(i,j),getl(i+1,j+1),m,0);
}
for(int i=1;i<=n+m-1;i++) add(getl(n,i),getr(n,i),m,a[n][i]),add(getr(n,i),t,m,0);
pii ans=mp(0,0);
while(spfa()){pii tmp=dfs(s,inf);ans.first+=tmp.first,ans.second+=tmp.second;}
cout<<ans.second<<'\n';return;
}
signed main(){
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
cin>>m>>n;s=getr(n,n+m-1)+1,t=s+1;
for(int i=1;i<=n;i++)for(int j=1;j<=m+i-1;j++)cin>>a[i][j];
solve1(),init(),solve2(),init(),solve3();
return 0;
}
P3356 火星探险问题
贡献在点上,拆点。只能贡献一次,拆掉点内部的边。处理方法与上面某道题类似。
最困难的是输出方案。确实有些细节,但也还是平凡的。
P2764 最小路径覆盖问题
考虑我们每将两个点间的边连起来,就可以令链的条数减少 1。
那这样就变成了二分图的最大匹配。将一个点拆开形成左部点和右部点。
如果一个点 \(u\) 连向另一个 \(v\),就将 \(u\) 的左部点连向 \(v\) 的右部点。跑最大流即可。
考虑为什么是对的。拆点就相当于将点拆成了入点与出点。而跑二分图最大匹配就是将点连成了一条链。
而输出方案是平凡的。精细实现可以极大的减小码量。
code
点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N=2e6+7,inf=1e9+7;
int n,m,idcnt=1,hd[N],nw[N],nxt[N],to[N],val[N],dep[N],s,t,fa[N];
vector <int> stk[N];
int getl(int x){return x;}
int getr(int x){return x+n;}
void add(int u,int v,int w){nxt[++idcnt]=hd[u];hd[u]=idcnt;to[idcnt]=v,val[idcnt]=w;swap(u,v);nxt[++idcnt]=hd[u];hd[u]=idcnt;to[idcnt]=v,val[idcnt]=0;}
bool bfs(){
for(int i=1;i<=t;i++)dep[i]=1e9+7;
queue <int> q;q.push(s);dep[s]=1;nw[s]=hd[s];
while(!q.empty()){
int u=q .front();q.pop();
for(int i=hd[u];i;i=nxt[i]){
int v=to[i];
if(val[i]&&dep[v]==inf){dep[v]=dep[u]+1,q.push(v);nw[v]=hd[v];if(v==t)return 1;}
}
}
return 0;
}
int dfs(int u,int sum){
if(u==t)return sum;
int k=0,res=0;
for(int i=nw[u];i;i=nxt[i]){
int v=to[i];nw[u]=i;
if(dep[v]!=dep[u]+1||!val[i])continue;
k=dfs(v,min(sum,val[i]));if(!k){dep[v]=inf;continue;}
res+=k,val[i^1]+=k,val[i]-=k,sum-=k;
if(!sum)break;
}
return res;
}
int find(int x){return fa[x]==x?x:fa[x]=find(fa[x]);}
void merge(int x,int y){
x=find(x),y=find(y);if(x==y)return;
for(int tmp:stk[x])stk[y].push_back(tmp);
fa[x]=y;
}
void solve(int u){
for(int i=hd[u];i;i=nxt[i]){
if(to[i]==s)continue;
if(!val[i]){merge(to[i]-n,u);return;}
}
}
signed main(){
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
cin>>n>>m;s=2*n+1,t=s+1;int ans=0;
for(int i=1,u,v;i<=m;i++)cin>>u>>v,add(getl(u),getr(v),1);
for(int i=1;i<=n;i++)add(s,getl(i),1),add(getr(i),t,1),fa[i]=i,stk[i].push_back(i);
while(bfs()){ans+=dfs(s,inf);}
for(int i=1;i<=n;i++)solve(getl(i));
for(int i=1;i<=n;i++){
if(fa[i]==i){
for(int tmp:stk[i]) cout<<tmp<<' ';
cout<<'\n';
}
}
cout<<n-ans<<'\n';
return 0;
}
P2153 [SDOI2009] 晨跑
费用流板子。
code
点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N=2e6+7,inf=1e9+7;
#define pii pair <int,int>
#define mp make_pair
int n,m,s,t,a,b;
namespace flow_cost{
int hd[N],nxt[N],val[N],to[N],c[N],idcnt=1,dis[N],nw[N],vis[N];
int getl(int x){return x;}
int getr(int x){return x+n;}
void init(){for(int i=1;i<=t;i++)hd[i]=nw[i]=0;idcnt=1;}
void add(int u,int v,int w,int cst){nxt[++idcnt]=hd[u];hd[u]=idcnt,to[idcnt]=v,val[idcnt]=w,c[idcnt]=cst;swap(u,v);nxt[++idcnt]=hd[u];hd[u]=idcnt,to[idcnt]=v,val[idcnt]=0,c[idcnt]=-cst;}
bool spfa(){
bool sign=0;for(int i=1;i<=t;i++)dis[i]=inf,vis[i]=0;
queue <int> q;q.push(s);
dis[s]=0,nw[s]=hd[s];
while(!q.empty()){
int u=q.front();q.pop();
nw[u]=hd[u];if(u==t)sign=1;
for(int i=hd[u];i;i=nxt[i]){
int v=to[i];
if(dis[v]>dis[u]+c[i]&&val[i]){
dis[v]=dis[u]+c[i];
if(vis[v])continue;
q.push(v),vis[v]=1;
}
}vis[u]=0;
}
return sign;
}
pii dfs(int u,int sum){
if(u==t)return mp(sum,0);
vis[u]=1;
pii res=mp(0,0),k;
for(int i=nw[u];i;i=nxt[i]){
int v=to[i];nw[u]=i;if(!val[i]||dis[v]!=dis[u]+c[i]||vis[v])continue;
k=dfs(v,min(sum,val[i]));if(!k.first){dis[v]=inf;continue;}
val[i]-=k.first,sum-=k.first,val[i^1]+=k.first,res.first+=k.first;res.second+=c[i]*k.first+k.second;//
if(!sum)break;
}
vis[u]=0;return res;
}
}
using namespace flow_cost;
signed main(){
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
cin>>n>>m;s=2*n+1,t=s+1;
pii ans=mp(0,0);
for(int i=1,u,v,cst;i<=m;i++){cin>>u>>v>>cst;if(u==1&&v==n){ans.first++,ans.second+=cst;continue;}add(getr(u),getl(v),1,cst);}
for(int i=1;i<=n;i++)add(getl(i),getr(i),1+inf*(i==1||i==n),0);
add(s,getl(1),inf,0);add(getr(n),t,inf,0);
while(spfa()){pii tmp=dfs(s,inf);ans.first+=tmp.first,ans.second+=tmp.second;}
cout<<ans.first<<' '<<ans.second<<'\n';
}
接下来就基本不是网络流 24 题里的题了,是一些经典的应用和 idea。
P2053 [SCOI2007] 修车
(修上一辆车的你和修这里一辆车的你不是同一个人)
考虑对于每个修每辆车的一个人对每一辆车连边,实际上就是枚举了每个人每辆车第几个修。
然后考虑每辆车第几个修对时间的代价。发现就是倒数第几个修的 $\times $ 当前车需要修的时间。
因此我们将每个人拆开,每个拆开的人向每辆车连 “倒数第几个修的 $\times $ 当前车需要修的时间” 为费用的边,然后每辆车向汇点连流量为 1 的边。
code
注意求的是平均值。
点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int N=2e6+7,inf=1e9+7,M=105;
int s,t,m,n,a[M][M];
int getcar(int i){return i;}
int getman(int i,int j){return n+(i-1)*n+j;}//第 i 个人修第 j 辆车
namespace flow_cost{
#define pii pair <int,int>
#define mp make_pair
int hd[N],nxt[N],to[N],val[N],c[N],idcnt=1,dis[N],nw[N],vis[N];
void add(int u,int v,int w,int cst){nxt[++idcnt]=hd[u];hd[u]=idcnt;to[idcnt]=v,val[idcnt]=w,c[idcnt]=cst;swap(u,v);nxt[++idcnt]=hd[u];hd[u]=idcnt;to[idcnt]=v,val[idcnt]=0,c[idcnt]=-cst;}
bool spfa(){
queue <int> q;q.push(s);
for(int i=1;i<=t;i++)dis[i]=inf,vis[i]=0;
dis[s]=0;int sign=0;nw[s]=hd[s];
while(!q.empty()){
int u=q.front();q.pop();nw[u]=hd[u];if(u==t)sign=1;
for(int i=hd[u];i;i=nxt[i]){
int v=to[i];
if(dis[v]>dis[u]+c[i]&&val[i]){
dis[v]=dis[u]+c[i];
if(!vis[v])q.push(v),vis[v]=1;
}
}vis[u]=0;
}
return sign;
}
pii dfs(int u,int sum){
if(u==t)return mp(sum,0);
pii k,res=mp(0,0);vis[u]=1;
for(int i=nw[u];i;i=nxt[i]){
nw[u]=i;int v=to[i];if(dis[v]!=dis[u]+c[i]||!val[i]||vis[v])continue;
k=dfs(v,min(sum,val[i]));if(k.first==0){dis[v]=inf;continue;}
val[i]-=k.first,val[i^1]+=k.first;res.first+=k.first,sum-=k.first;res.second+=k.first*c[i]+k.second;
if(!sum)break;
}
vis[u]=0;return res;
}
}
using namespace flow_cost;
signed main(){
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
cin>>m>>n;s=getman(m,n)+1,t=s+1;
for(int i=1;i<=n;i++)add(getcar(i),t,1,0);
for(int i=1;i<=n;i++)for(int j=1;j<=m;j++)cin>>a[j][i],add(s,getman(j,i),1,0);
for(int i=1;i<=m;i++){
for(int j=1;j<=n;j++){
for(int k=1;k<=n;k++)
add(getman(i,j),getcar(k),1,a[i][k]*j);
}
}
pii ans=mp(0,0);
while(spfa()){pii tmp=dfs(s,inf);ans.first+=tmp.first,ans.second+=tmp.second;}
double res=1.0*ans.second/n;
cout<<fixed<<setprecision(2)<<res<<'\n';
return 0;
}
P2050 [NOI2012] 美食节
上一道题的加强版本。
由于上一道题那样的暴力建边会 T 飞,因此考虑优化一下建图。
因为修车只会是一个一个修的,不会出现一个人修倒数第一辆车是 \(a\),倒数第二辆不修,倒数第三辆又去修 \(b\) 的情况发生。
所以我们可以一直跑,如果某个人已经修了一辆车,我们就让其他所有车都给这个人接下来修。然后就可以了。
code
点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int M=1e3+7;
int s,t,n,m,p[M],a[M][M],num,top[M*M];
int getl(int i){return i;}
int getr(int i,int j){return n+(i-1)*num+j;}
namespace flow_cost{
const int N=2e6+7,inf=1e9+7;
#define pii pair <int,int>
#define mp make_pair
int hd[N],nxt[N],val[N],to[N],c[N],idcnt=1,dis[N],nw[N],vis[N],mrk[N];
void init(){for(int i=1;i<=t;i++)hd[i]=nw[i]=0;idcnt=1;}
void add(int u,int v,int w,int cst){nxt[++idcnt]=hd[u];hd[u]=idcnt,to[idcnt]=v,val[idcnt]=w,c[idcnt]=cst;swap(u,v);nxt[++idcnt]=hd[u];hd[u]=idcnt,to[idcnt]=v,val[idcnt]=0,c[idcnt]=-cst;}
bool spfa(){
bool sign=0;for(int i=1;i<=t;i++)dis[i]=inf,vis[i]=0;
queue <int> q;q.push(s);
dis[s]=0,nw[s]=hd[s];
while(!q.empty()){
int u=q.front();q.pop();
nw[u]=hd[u];if(u==t)sign=1;
for(int i=hd[u];i;i=nxt[i]){
int v=to[i];
if(dis[v]>dis[u]+c[i]&&val[i]){
dis[v]=dis[u]+c[i];
if(vis[v])continue;
q.push(v),vis[v]=1;
}
}vis[u]=0;
}
return sign;
}
pii dfs(int u,int sum){
if(u==t)return mp(sum,0);
vis[u]=1;
pii res=mp(0,0),k;
for(int i=nw[u];i;i=nxt[i]){
int v=to[i];nw[u]=i;if(!val[i]||dis[v]!=dis[u]+c[i]||vis[v])continue;
k=dfs(v,min(sum,val[i]));if(!k.first){dis[v]=inf;continue;}
val[i]-=k.first,sum-=k.first,val[i^1]+=k.first,res.first+=k.first;res.second+=c[i]*k.first+k.second;
mrk[u]=1;
if(!sum)break;
}
vis[u]=0;return res;
}
}
using namespace flow_cost;
signed main(){
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
cin>>n>>m;for(int i=1;i<=n;i++)cin>>p[i],num+=p[i];s=getr(m,num)+1,t=s+1;
for(int i=1;i<=n;i++){for(int j=1;j<=m;j++){cin>>a[i][j];}}
for(int i=1;i<=n;i++){add(s,getl(i),p[i],0);}
for(int i=1;i<=n;i++)for(int j=1;j<=m;j++)add(getl(i),getr(j,1),1,a[i][j]);
for(int i=1;i<=m;i++)top[i]=1,add(getr(i,1),t,1,0);
pii ans=mp(0,0);
while(spfa()){
pii tmp=dfs(s,inf);ans.first+=tmp.first,ans.second+=tmp.second;
for(int i=1;i<=m;i++){
if(mrk[getr(i,top[i])]&&top[i]<num){
top[i]++;
for(int j=1;j<=n;j++) add(getl(j),getr(i,top[i]),1,a[j][i]*top[i]);
add(getr(i,top[i]),t,1,0);
}
}
}
cout<<ans.second<<'\n';return 0;
}
P2469 [SDOI2010] 星际竞速
考虑如果要跳到某个点一定是刚开始的时候直接跳。之后再跳一定是不优的。
发现这种意义下是一个类似于最小路径覆盖的问题。
所以拆点,直接跑费用流。
当然注意这个东西与最小路径覆盖还是有区别的。我们的限制要求的是以某个点为路径起点有一个代价,走边有一个代价,因此作为路径起点的代价是连到右部点上的。源点连向左部点的费用为 0 指代的是这个点不是路径的起点。
code
点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int M=2e6+7,inf=1e9+7,N=2e6+7;
int s,t,n,m,a[M];
int getl(int x){return x;}
int getr(int x){return x+n;}
namespace flow_cost{
#define pii pair <int,int>
#define mp make_pair
int hd[N],nxt[N],to[N],val[N],c[N],idcnt=1,dis[N],nw[N],vis[N];
void add(int u,int v,int w,int cst){nxt[++idcnt]=hd[u];hd[u]=idcnt;to[idcnt]=v,val[idcnt]=w,c[idcnt]=cst;swap(u,v);nxt[++idcnt]=hd[u];hd[u]=idcnt;to[idcnt]=v,val[idcnt]=0,c[idcnt]=-cst;}
bool spfa(){
queue <int> q;q.push(s);
for(int i=1;i<=t;i++)dis[i]=inf,vis[i]=0;
dis[s]=0;int sign=0;nw[s]=hd[s];
while(!q.empty()){
int u=q.front();q.pop();nw[u]=hd[u];if(u==t)sign=1;
for(int i=hd[u];i;i=nxt[i]){
int v=to[i];
if(dis[v]>dis[u]+c[i]&&val[i]){
dis[v]=dis[u]+c[i];
if(!vis[v])q.push(v),vis[v]=1;
}
}vis[u]=0;
}
return sign;
}
pii dfs(int u,int sum){
if(u==t)return mp(sum,0);
pii k,res=mp(0,0);vis[u]=1;
for(int i=nw[u];i;i=nxt[i]){
nw[u]=i;int v=to[i];if(dis[v]!=dis[u]+c[i]||!val[i]||vis[v])continue;
k=dfs(v,min(sum,val[i]));if(k.first==0){dis[v]=inf;continue;}
val[i]-=k.first,val[i^1]+=k.first;res.first+=k.first,sum-=k.first;res.second+=k.first*c[i]+k.second;
if(!sum)break;
}
vis[u]=0;return res;
}
}
using namespace flow_cost;
signed main(){
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
cin>>n>>m;s=2*n+1,t=s+1;for(int i=1;i<=n;i++)cin>>a[i],add(s,getr(i),1,a[i]),add(s,getl(i),1,0),add(getr(i),t,1,0);
for(int i=1,u,v,cst;i<=m;i++){cin>>u>>v>>cst;if(u>v)swap(u,v);add(getl(u),getr(v),1,cst);}
pii ans=mp(0,0);
while(spfa()){pii tmp=dfs(s,inf);ans.first+=tmp.first,ans.second+=tmp.second;}
cout<<ans.second<<'\n';return 0;
}
P3358 最长k可重区间集问题
还是一道 24 题。
实际上就是一个点可以被覆盖 \(k\) 次。仍然考虑拆点。
然后考虑如何表示某个点被覆盖这种限制。一种直观的方法是在相邻点之间连容量为 \(k\) 的边,在给出的区间的左右端点间连费用为长度的边。考虑为什么是对的。
这种“区间连边”相当于让流量少走了一段路,“让流量从点上飞过去”。这样就可以达到被区间覆盖的点内的流量的减少。这种减少是有上限的,因此就可以做到某个点被区间“飞走”的流量不多于 \(k\)。
code
注意离散化。
点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N=2e6+7,inf=1e9+7;
int s,t,n,k,tmp[N],l[N],r[N],len[N],tmpcnt=0;
namespace flow_cost{
#define pii pair <int,int>
#define mp make_pair
int hd[N],nxt[N],to[N],val[N],c[N],idcnt=1,dis[N],nw[N],vis[N];
void add(int u,int v,int w,int cst){nxt[++idcnt]=hd[u];hd[u]=idcnt;to[idcnt]=v,val[idcnt]=w,c[idcnt]=cst;swap(u,v);nxt[++idcnt]=hd[u];hd[u]=idcnt;to[idcnt]=v,val[idcnt]=0,c[idcnt]=-cst;}
bool spfa(){
queue <int> q;q.push(s);
for(int i=1;i<=t;i++)dis[i]=-inf,vis[i]=0;
dis[s]=0;int sign=0;nw[s]=hd[s];
while(!q.empty()){
int u=q.front();q.pop();nw[u]=hd[u];if(u==t)sign=1;
for(int i=hd[u];i;i=nxt[i]){
int v=to[i];
if(dis[v]<dis[u]+c[i]&&val[i]){
dis[v]=dis[u]+c[i];
if(!vis[v])q.push(v),vis[v]=1;
}
}vis[u]=0;
}
return sign;
}
pii dfs(int u,int sum){
if(u==t)return mp(sum,0);
pii k,res=mp(0,0);vis[u]=1;
for(int i=nw[u];i;i=nxt[i]){
nw[u]=i;int v=to[i];if(dis[v]!=dis[u]+c[i]||!val[i]||vis[v])continue;
k=dfs(v,min(sum,val[i]));if(k.first==0){dis[v]=inf;continue;}
val[i]-=k.first,val[i^1]+=k.first;res.first+=k.first,sum-=k.first;res.second+=k.first*c[i]+k.second;
if(!sum)break;
}
vis[u]=0;return res;
}
}
using namespace flow_cost;
signed main(){
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
cin>>n>>k;
for(int i=1;i<=n;i++)cin>>l[i]>>r[i],tmp[++tmpcnt]=l[i],tmp[++tmpcnt]=r[i],len[i]=r[i]-l[i];
sort(tmp+1,tmp+tmpcnt+1);tmpcnt=unique(tmp+1,tmp+tmpcnt+1)-(tmp+1);for(int i=1;i<=n;i++)l[i]=lower_bound(tmp+1,tmp+tmpcnt+1,l[i])-tmp,r[i]=lower_bound(tmp+1,tmp+tmpcnt+1,r[i])-tmp;
s=tmpcnt+1,t=s+1;
for(int i=1;i<=n;i++)add(l[i],r[i],1,len[i]);
for(int i=1;i<tmpcnt;i++)add(i,i+1,k,0);
add(s,1,k,0),add(tmpcnt,t,k,0);
pii ans=mp(0,0);
while(spfa()){pii tmp=dfs(s,inf);ans.first+=tmp.first,ans.second+=tmp.second;}
cout<<ans.second;return 0;
}
P3980 [NOI2008] 志愿者招募
经典题,但是第一次做确实想不出来。
这个题是要求了每个点被多少个区间覆盖。感觉和上一道题有点类似。考虑如何处理这个限制。
想一想最小费用最大流的性质。如果我们可以通过一种方式保证流量不影响答案性质,用费用来使得流量自然的去避开一些边来满足题目条件,就可以做了。
由于我们现在是想要“流量从点上飞过去”,因此我们考虑对于每个点之间连接一个流量为 \(\infty-a_i\) 的边。由于这条边费用为 0,那么流量肯定会尽量走这条边。但是由于需要优先保证流量最大(恰好等于 \(\infty\)),那么流量就只能被强迫着走区间的边,就会产生正确的费用。
code
点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int M=2e4+7;
int s,t,n,m,a[M];
namespace flow_cost{
const int N=2e6+7,inf=1e9+7;
#define pii pair <int,int>
#define mp make_pair
int hd[N],nxt[N],val[N],to[N],c[N],idcnt=1,dis[N],nw[N],vis[N];
void init(){for(int i=1;i<=t;i++)hd[i]=nw[i]=0;idcnt=1;}
void add(int u,int v,int w,int cst){nxt[++idcnt]=hd[u];hd[u]=idcnt,to[idcnt]=v,val[idcnt]=w,c[idcnt]=cst;swap(u,v);nxt[++idcnt]=hd[u];hd[u]=idcnt,to[idcnt]=v,val[idcnt]=0,c[idcnt]=-cst;}
bool spfa(){
bool sign=0;for(int i=1;i<=t;i++)dis[i]=inf,vis[i]=0;
queue <int> q;q.push(s);
dis[s]=0,nw[s]=hd[s];
while(!q.empty()){
int u=q.front();q.pop();
nw[u]=hd[u];if(u==t)sign=1;
for(int i=hd[u];i;i=nxt[i]){
int v=to[i];
if(dis[v]>dis[u]+c[i]&&val[i]){
dis[v]=dis[u]+c[i];
if(vis[v])continue;
q.push(v),vis[v]=1;
}
}vis[u]=0;
}
return sign;
}
pii dfs(int u,int sum){
if(u==t)return mp(sum,0);
vis[u]=1;
pii res=mp(0,0),k;
for(int i=nw[u];i;i=nxt[i]){
int v=to[i];nw[u]=i;if(!val[i]||dis[v]!=dis[u]+c[i]||vis[v])continue;
k=dfs(v,min(sum,val[i]));if(!k.first){dis[v]=inf;continue;}
val[i]-=k.first,sum-=k.first,val[i^1]+=k.first,res.first+=k.first;res.second+=c[i]*k.first+k.second;//
if(!sum)break;
}
vis[u]=0;return res;
}
}
using namespace flow_cost;
signed main(){
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
cin>>n>>m;s=n+2,t=s+1;for(int i=1;i<=n;i++)cin>>a[i];
for(int i=1;i<=n;i++)add(i,i+1,inf-a[i],0);
add(n+1,t,inf,0);add(s,1,inf,0);
for(int i=1,u,v,cst;i<=m;i++){cin>>u>>v>>cst;add(u,v+1,inf,cst);}
pii ans=mp(0,0);
while(spfa()){pii tmp=dfs(s,inf);ans.first+=tmp.first,ans.second+=tmp.second;}
cout<<ans.second<<'\n';return 0;
}
P3153 [CQOI2009] 跳舞
考虑最多能有多少场舞会这种东西网络流不是很好表示,然后数据范围又是小到离谱,因此可以直接考虑二分答案。
假设我们已经得到了舞会的场数,那么每个人应该与多少人跳舞就已经确定下来了。但是喜欢与不喜欢的连边方式是不一样的。
考虑不将“喜欢”和“不喜欢”完全分开,而是将与不喜欢的人跳舞的情况连在人身上,流量就是 \(k\)。这样就可以满足所有限制了。
然后判断就是是否流量恰好为 \(n\)。
code
点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int M=105;
int s,t,n,k,a[M][M];
int getll(int x){return x;}
int getlr(int x){return x+n;}
int getrl(int x){return x+n+n;}
int getrr(int x){return x+3*n;}
namespace flow{
const int N=2e6+7,inf=1e9+7;
int hd[N],nxt[N],to[N],val[N],idcnt=1,tmp,ans,dep[N],nw[N];
void add(int u,int v,int w){nxt[++idcnt]=hd[u],hd[u]=idcnt,to[idcnt]=v,val[idcnt]=w;swap(u,v),nxt[++idcnt]=hd[u],hd[u]=idcnt,to[idcnt]=v,val[idcnt]=0;}
void init(){for(int i=1;i<=t;i++)hd[i]=nw[i]=0;idcnt=1;}
bool bfs(){
for(int i=1;i<=t;i++)dep[i]=inf;
queue <int> q;dep[s]=1,nw[s]=hd[s];q.push(s);
while(!q.empty()){
int u=q.front();q.pop();
for(int i=hd[u];i;i=nxt[i]){
int v=to[i];if(dep[v]==inf&&val[i]){dep[v]=dep[u]+1,q.push(v);nw[v]=hd[v];if(v==t)return 1;}
}
}
return 0;
}
int dfs(int u,int sum){
if(u==t)return sum;
int k,res=0;
for(int i=nw[u];i;i=nxt[i]){
nw[u]=i;int v=to[i];if(!val[i]||dep[v]!=dep[u]+1)continue;
k=dfs(v,min(sum,val[i]));if(!k){dep[v]=inf;continue;}
sum-=k,val[i]-=k,val[i^1]+=k,res+=k;if(!sum)break;
}
return res;
}
}
using namespace flow;
int solve(int num){
s=4*n+1,t=s+1;init();
for(int i=1;i<=n;i++)add(s,getll(i),num),add(getll(i),getlr(i),k),add(getrr(i),getrl(i),k),add(getrl(i),t,num);
for(int i=1;i<=n;i++)for(int j=1;j<=n;j++){if(a[i][j])add(getll(i),getrl(j),1);else add(getlr(i),getrr(j),1);}
int ans=0;
while(bfs()){ans+=dfs(s,inf);}//cout<<"solve "<<num<<' '<<ans<<'\n';
return ans;
}
signed main(){
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
cin>>n>>k;for(int i=1;i<=n;i++)for(int j=1;j<=n;j++){char c;cin>>c;a[i][j]=(c=='Y');}
int l=0,r=n,ans=l;
while(l<=r){
int mid=(l+r)>>1;
if(solve(mid)==n*mid)l=mid+1,ans=mid;
else r=mid-1;
}
cout<<ans<<'\n';return 0;
}
P5458 [BJOI2016] 水晶
我们可以先将三维的坐标换成唯一的二维坐标。
发现三元环和链的限制是本质相同的。因为三元环中也一定有一个是能量源。
由于求的是最小剩余价值,因此向最小割的方向去想。显然拆点。
然后直接在跑到能量源的时候判断并连边即可。
code
就是分类讨论有点烦。
点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define pii pair <int,int>
#define mp make_pair
const int M=4005;
int s,t,n,a[M][M],id[M][M],cnt=0,nx[M*M],ny[M*M],sign[M*M],totalsum=0;
pii get(int x,int y,int z){return mp(x-z+2001,y-z+2001);}
int getl(int x){return x;}
int getr(int x){return x+cnt;}
namespace flow{
const int N=2e6+7,inf=1e9+7;
int hd[N],nxt[N],to[N],val[N],idcnt=1,tmp,ans,dep[N],nw[N];
void add(int u,int v,int w){nxt[++idcnt]=hd[u],hd[u]=idcnt,to[idcnt]=v,val[idcnt]=w;swap(u,v),nxt[++idcnt]=hd[u],hd[u]=idcnt,to[idcnt]=v,val[idcnt]=0;}
bool bfs(){
for(int i=1;i<=t;i++)dep[i]=inf;
queue <int> q;dep[s]=1,nw[s]=hd[s];q.push(s);
while(!q.empty()){
int u=q.front();q.pop();
for(int i=hd[u];i;i=nxt[i]){
int v=to[i];if(dep[v]==inf&&val[i]){dep[v]=dep[u]+1,q.push(v);nw[v]=hd[v];if(v==t)return 1;}
}
}
return 0;
}
int dfs(int u,int sum){
if(u==t)return sum;
int k,res=0;
for(int i=nw[u];i;i=nxt[i]){
nw[u]=i;int v=to[i];if(!val[i]||dep[v]!=dep[u]+1)continue;
k=dfs(v,min(sum,val[i]));if(!k){dep[v]=inf;continue;}
sum-=k,val[i]-=k,val[i^1]+=k,res+=k;if(!sum)break;
}
return res;
}
}
using namespace flow;
signed main(){
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
cin>>n;for(int i=1,x,y,z,w;i<=n;i++){
cin>>x>>y>>z>>w;pii tmp=get(x,y,z);a[tmp.first][tmp.second]+=w*10+w*((x+y+z)%3==0);id[tmp.first][tmp.second]=id[tmp.first][tmp.second]?id[tmp.first][tmp.second]:++cnt;
int u=tmp.first,v=tmp.second;nx[id[u][v]]=u,ny[id[u][v]]=v;sign[id[u][v]]=(x+y+z+6000)%3;
}
s=2*cnt+1,t=s+1;
for(int i=1;i<=cnt;i++){
int x=nx[i],y=ny[i],u=id[x][y],v;totalsum+=a[x][y];
add(getl(u),getr(u),a[x][y]);
if(sign[u]==0){
if(v=id[x][y-1])add(getr(u),v,inf);if(v=id[x-1][y])add(getr(u),v,inf);if(v=id[x+1][y+1])add(getr(u),v,inf);
if(v=id[x][y+1])add(getr(v),u,inf);if(v=id[x+1][y])add(getr(v),u,inf);if(v=id[x-1][y-1])add(getr(v),u,inf);
}
if(sign[u]==2){add(getr(u),t,inf);}
if(sign[u]==1){add(s,getl(u),inf);}
}
int ans=0;while(bfs()){ans+=dfs(s,inf);}
cout<<fixed<<setprecision(1)<<1.0*(totalsum-ans)/10.0;return 0; //记得除以 10
}
P2805 [NOI2009] 植物大战僵尸
一些植物可以保护另外一些植物,当然是一种连边关系。注意一棵植物在另外一颗植物前面也算是一种特殊的保护关系。
发现有一种可能是植物间互相保护导致金刚不坏,我们要把这种东西消除掉。拓扑排序即可。
然后发现这个限制就是如果选了某个点,那么就要选前面的所有点。因此我们将图反过来,就变成了求最大权闭合子图。
最大权闭合子图的连边方式就是正权的点连源点,点权为负的就连汇点。点之间的边就正常连,流量为 \(\infty\)。
最后的答案就是所有正权的和减去上图的最小割。
正确性的话就是如果一个正权点连向源点的边被割掉了,那么后面的点就与其无关了,那么就会导致这个点的贡献失去。
如果一个负权点连向汇点的边被割掉了,就代表这个点被选了。那么就会导致答案变小其权值。
code
点击查看代码
#include<bits/stdc++.h>
using namespace std;
// #define int long long
const int M=55;
int s,t,n,m,a[M][M],fan[M*M][M*M];
struct node{int id,du;}id[M*M];
int get(int x,int y){return (x-1)*m+y;}
namespace flow{
const int N=2e6+7,inf=1e9+7;
int hd[N],nxt[N],to[N],val[N],idcnt=1,dep[N],nw[N];
void add(int u,int v,int w){nxt[++idcnt]=hd[u],hd[u]=idcnt,to[idcnt]=v,val[idcnt]=w;swap(u,v),nxt[++idcnt]=hd[u],hd[u]=idcnt,to[idcnt]=v,val[idcnt]=0;}
bool bfs(){
for(int i=1;i<=t;i++)dep[i]=inf;
queue <int> q;dep[s]=1,nw[s]=hd[s];q.push(s);
while(!q.empty()){
int u=q.front();q.pop();
for(int i=hd[u];i;i=nxt[i]){
int v=to[i];if(dep[v]==inf&&val[i]){dep[v]=dep[u]+1,q.push(v);nw[v]=hd[v];if(v==t)return 1;}
}
}
return 0;
}
int dfs(int u,int sum){
if(u==t)return sum;
int k,res=0;
for(int i=nw[u];i;i=nxt[i]){
nw[u]=i;int v=to[i];if(!val[i]||dep[v]!=dep[u]+1)continue;
k=dfs(v,min(sum,val[i]));if(!k){dep[v]=inf;continue;}
sum-=k,val[i]-=k,val[i^1]+=k,res+=k;if(!sum)break;
}
return res;
}
}
using namespace flow;
signed main(){
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
cin>>n>>m;queue <int> q;s=get(n,m)+1,t=s+1;
for(int i=1;i<=n;i++){
for(int j=1,x,y;j<=m;j++){
cin>>a[i][j];int num;cin>>num;id[get(i,j)].id=get(i,j);//cout<<"fail "<<get(i,j)<<' '<<get(x,y)<<' '<<fan[get(i,j),get(x,y)]<<'\n';
while(num--){cin>>x>>y;x++,y++;if(fan[get(i,j)][get(x,y)]){continue;}fan[get(i,j)][get(x,y)]=1;id[get(x,y)].du++;}
if(j>1&&!fan[get(i,j)][get(i,j-1)])fan[get(i,j)][get(i,j-1)]=1,id[get(i,j-1)].du++;//,cout<<"insert "<<get(i,j)<<' '<<get(i,j-1)<<'\n'
}
}
for(int i=1;i<=get(n,m);i++){if(id[i].du==0)q.push(i);}//cout<<i<<' '<<id[i].du<<'\n';
while(!q.empty()){
int u=q.front();q.pop();//cout<<"Q "<<u<<'\n';
for(int v=1;v<=get(n,m);v++){if(fan[u][v]){id[v].du--;if(!id[v].du)q.push(v);}}
}
int ans=0;
for(int i=1;i<=get(n,m);i++){
if(id[i].du)continue;
int x=(i-1)/m+1,y=(i-1)%m+1;
if(a[x][y]>=0)add(s,i,a[x][y]),ans+=a[x][y];else add(i,t,-a[x][y]);
for(int j=1;j<=get(n,m);j++){
if(id[j].du)continue;
if(fan[i][j]) add(j,i,inf);
}
}
// for(int i=1;i<=n;i++){for(int j=1;j<m;j++){add(get(i,j),get(i,j+1),inf);}}
while(bfs()){ans-=dfs(s,inf);}
cout<<ans;return 0;
}
P3305 [SDOI2013] 费用流
考虑 Bob 的策略。可以发现其策略是简单的:找到一条流量最大的边然后把所有的单位花费全放上去即可。
因此 Alice 的策略就是要求流量最大的边最小。这个显然去二分答案。由于画图发现有可能出现流量为小数的情况(这样更平衡),因此要实数二分。
现在知道流量最大只能是多少,然后考虑是否也能跑到最大流。由于数据范围,直接暴力修改边权重新跑判断最大流是否是原始的最大流即可。
code
点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define ld double
const ld eps=1e-6;
int s,t,n,m,p;
namespace flow{
const int N=2e6+7,inf=1e9+7;
int hd[N],nxt[N],to[N],idcnt=1,tmp,ans,dep[N],nw[N];
ld val[N],c[N];
void add(int u,int v,int w){nxt[++idcnt]=hd[u],hd[u]=idcnt,to[idcnt]=v,val[idcnt]=c[idcnt]=w;swap(u,v),nxt[++idcnt]=hd[u],hd[u]=idcnt,to[idcnt]=v,val[idcnt]=c[idcnt]=0;}
bool bfs(){
for(int i=1;i<=t;i++)dep[i]=inf;
queue <int> q;dep[s]=1,nw[s]=hd[s];q.push(s);
while(!q.empty()){
int u=q.front();q.pop();
for(int i=hd[u];i;i=nxt[i]){
int v=to[i];if(dep[v]==inf&&val[i]){dep[v]=dep[u]+1,q.push(v);nw[v]=hd[v];if(v==t)return 1;}
}
}
return 0;
}
ld dfs(int u,ld sum){
if(u==t)return sum;
ld k,res=0;
for(int i=nw[u];i;i=nxt[i]){
nw[u]=i;int v=to[i];if(!val[i]||dep[v]!=dep[u]+1)continue;
k=dfs(v,min(sum,val[i]));if(k==0){dep[v]=inf;continue;}
sum-=k,val[i]-=k,val[i^1]+=k,res+=k;if(!sum)break;
}
return res;
}
}
using namespace flow;
ld solve(ld num){
for(int i=1;i<=idcnt;i++){if(c[i]>num)val[i]=num;else val[i]=c[i];}
ld tmp=0;while(bfs()){tmp+=dfs(s,inf);}
return tmp;
}
signed main(){
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
cin>>n>>m>>p;s=1,t=n;
for(int i=1,u,v,w;i<=m;i++)cin>>u>>v>>w,add(u,v,w);
ld l=0,r=5e4,ans=r,tmp=0;
while(bfs())tmp+=dfs(s,inf);
while(r-l>eps){
ld mid=1.0*(l+r)/2;
if(abs(solve(mid)-tmp)<=eps) ans=mid,r=mid;
else l=mid;
}
cout<<tmp<<'\n'<<fixed<<setprecision(4)<<1.0*ans*p<<'\n';return 0;
}
P4298 [CTSC2008] 祭祀
需要带一点脑子。
由于任意两个祭祀点之间不可以连通,因此我们可以暴力跑一个传递闭包。
然后发现这个东西就是一个最小点覆盖。跑一遍。答案就是第一问。然后就是构造以及判断可行性。
然后不会。然后看题解。
首先考虑第三问,及判断可行性。
一个点可行的条件是什么?一种比较不显然的方法是枚举每一个点,然后将当前点所有与这个点有连边的点全部删掉,然后再跑一边。判断这个点对答案的影响是什么。
如果当前点对于答案的影响恰好为 1,那么就代表当前点可以作为祭祀的点,否则一定不行。
然后考虑第二问怎么做。由于我们只需要输出一组可行解,因此我们可以通过第三问的答案,判断是否可以作为祭祀点,然后将所有与其相连的点染色使其不再能作为祭祀点即可。
code
注意传递闭包枚举中间点需要先枚举!
点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int M=2005;
int s,t,n,m,num,mp[M][M],id[M][M],ans2[M],ans3[M],col[M],colcnt=0;
int getl(int x){return x;}
int getr(int x){return x+n;}
namespace flow{
const int N=2e6+7,inf=1e9+7;
int hd[N],nxt[N],to[N],val[N],idcnt=1,dep[N],nw[N],c[N],frm[N],nd[N];
void add(int u,int v,int w){nxt[++idcnt]=hd[u],hd[u]=idcnt,to[idcnt]=v,val[idcnt]=c[idcnt]=w;swap(u,v),nxt[++idcnt]=hd[u],hd[u]=idcnt,to[idcnt]=v,val[idcnt]=c[idcnt]=0;}
void init(){for(int i=2;i<=idcnt;i++)val[i]=c[i];}
bool bfs(){
for(int i=1;i<=t;i++)dep[i]=inf;
queue <int> q;dep[s]=1,nw[s]=hd[s];q.push(s);
while(!q.empty()){
int u=q.front();q.pop();
for(int i=hd[u];i;i=nxt[i]){
int v=to[i];if(dep[v]==inf&&val[i]){dep[v]=dep[u]+1,q.push(v);nw[v]=hd[v];if(v==t)return 1;}
}
}
return 0;
}
int dfs(int u,int sum){
if(u==t)return sum;
int k,res=0;
for(int i=nw[u];i;i=nxt[i]){
nw[u]=i;int v=to[i];if(!val[i]||dep[v]!=dep[u]+1)continue;
k=dfs(v,min(sum,val[i]));if(!k){dep[v]=inf;continue;}
sum-=k,val[i]-=k,val[i^1]+=k,res+=k;if(!sum)break;
}
return res;
}
}
using namespace flow;
void solve1(){
for(int i=1;i<=n;i++)add(s,getl(i),1),frm[i]=idcnt-1,add(getr(i),t,1),nd[i]=idcnt-1;
for(int i=1,u,v;i<=m;i++)cin>>u>>v,mp[u][v]=1;
for(int k=1;k<=n;k++)for(int i=1;i<=n;i++)for(int j=1;j<=n;j++)mp[i][j]=(mp[i][j]|(mp[i][k]&&mp[k][j]));//注意传递闭包 k 的枚举是在前面的!!
for(int i=1;i<=n;i++)for(int j=1;j<=n;j++)if(mp[i][j])add(getl(i),getr(j),1),id[i][j]=idcnt-1;
while(bfs()){num+=dfs(s,inf);}cout<<n-num<<'\n';
}
void solve2(){
for(int i=1;i<=n;i++){
if(ans3[i]&&!col[i]){
ans2[i]=1;col[i]=++colcnt;
for(int j=1;j<=n;j++)if(mp[i][j]||mp[j][i])col[j]=col[i];
}
}
for(int i=1;i<=n;i++)cout<<ans2[i]; cout<<'\n';
}
void solve3(){
for(int u=1;u<=n;u++){
val[frm[u]]=val[nd[u]]=0;int tmp=1;
for(int v=1;v<=n;v++){
if(id[u][v]||id[v][u]) val[frm[v]]=val[nd[v]]=0,tmp++;
}
while(bfs()){tmp+=dfs(s,inf);}
ans3[u]=(n-tmp==n-num-1);init();
}
}
signed main(){
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
cin>>n>>m;s=2*n+1,t=s+1;
solve1();init();solve3();solve2();for(int i=1;i<=n;i++)cout<<ans3[i];
}
P6158 封锁
最小乘积模型和最小割的综合运用。
考虑此题就是求一个二维的最小割。按照最小乘积模型的板子直接修改边权发现就是对的。直接做就可以了。
code
细节并不多。但是不知道为什么写了相当久。
当然这个题用最短路的做法也是非常巧妙的。
点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define pii pair <int,int>
#define mp make_pair
const int M=405,N=2e6+7,inf=1e9+7;
int s,t,n,mpa[N],mpb[N],ansx=inf,ansy=inf;
int getid(int x,int y){return (x-1)*n+y;}
namespace flow{
int hd[N],nxt[N],to[N],val[N],idcnt=1,dep[N],nw[N];
void add(int u,int v,int w){nxt[++idcnt]=hd[u],hd[u]=idcnt,to[idcnt]=v,val[idcnt]=w;swap(u,v),nxt[++idcnt]=hd[u],hd[u]=idcnt,to[idcnt]=v,val[idcnt]=0;}
void init(int xa,int ya,int xb,int yb){for(int i=1;i<=idcnt;i++)val[i]=(xb-xa)*mpb[i]+(ya-yb)*mpa[i];}
bool bfs(){
for(int i=1;i<=t;i++)dep[i]=inf;
queue <int> q;dep[s]=1,nw[s]=hd[s];q.push(s);
while(!q.empty()){
int u=q.front();q.pop();
for(int i=hd[u];i;i=nxt[i]){
int v=to[i];if(dep[v]==inf&&val[i]){dep[v]=dep[u]+1,q.push(v);nw[v]=hd[v];if(v==t)return 1;}
}
}
return 0;
}
int dfs(int u,int sum){
if(u==t)return sum;
int k,res=0;
for(int i=nw[u];i;i=nxt[i]){
nw[u]=i;int v=to[i];if(!val[i]||dep[v]!=dep[u]+1)continue;
k=dfs(v,min(sum,val[i]));if(!k){dep[v]=inf;continue;}
sum-=k,val[i]-=k,val[i^1]+=k,res+=k;if(!sum)break;
}
return res;
}
pii calc(){
int tmp=0;while(bfs()){tmp+=dfs(s,inf);}
pii ans=mp(0,0);
for(int u=1;u<=n*n;u++) {
for(int i=hd[u];i;i=nxt[i]){
int v=to[i];if(dep[u]!=inf&&dep[v]==inf&&v!=t){ans.first+=mpa[i],ans.second+=mpb[i];}
}
}
return ans;
}
}
using namespace flow;
void solve(int xa,int ya,int xb,int yb){
if(1ll*xa*yb>=1ll*ansx*ansy||xa==xb||ya==yb)return;
init(xa,ya,xb,yb);pii C=calc();int xc=C.first,yc=C.second;
if(1ll*xc*yc<1ll*ansx*ansy)ansx=xc,ansy=yc;
if(1ll*(xb-xa)*(yc-ya)-1ll*(xc-xa)*(yb-ya)>=0ll)return;
solve(xa,ya,xc,ya);solve(xc,yc,xb,yb);
}
signed main(){
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
cin>>n;s=n*n+1,t=s+1;add(s,1,1);mpa[idcnt-1]=mpb[idcnt-1]=inf;add(n*n,t,1);mpa[idcnt-1]=mpb[idcnt-1]=inf;
for(int i=1;i<n;i++){for(int j=1;j<=n;j++)add(getid(i,j),getid(i+1,j),1),cin>>mpa[idcnt-1]>>mpb[idcnt-1];}
for(int i=1;i<=n;i++){for(int j=1;j<n;j++)add(getid(i,j),getid(i,j+1),1),cin>>mpa[idcnt-1]>>mpb[idcnt-1];}
init(0,1,0,0);pii A=calc();int xa=A.first,ya=A.second;init(0,0,1,0);pii B=calc();int xb=B.first,yb=B.second;
if(1ll*xa*ya<1ll*xb*yb)ansx=xa,ansy=ya;else ansx=xb,ansy=yb;
solve(xa,ya,xb,yb);cout<<1ll*ansx*ansy<<'\n';return 0;
}
P11531 [THUPC 2025 初赛] 检查站
还是经典的建模题。
相当于删掉一个检查站后这两个点之间相连的边就会被割掉。
于是考虑将检查站拆点,将边拆成连向检查站入点和从检查站出点连出的边。直接跑最小割即可。
code
点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int M=5e4+7;
int s,t,n,m,c,bel[M];
int gettra(int x){return x;}
int getcl(int x){return x+n;}
int getcr(int x){return x+n+c;}
namespace flow{
const int N=2e6+7,inf=1e9+7;
int hd[N],nxt[N],to[N],val[N],idcnt=1,dep[N],nw[N];
void add(int u,int v,int w){nxt[++idcnt]=hd[u],hd[u]=idcnt,to[idcnt]=v,val[idcnt]=w;swap(u,v),nxt[++idcnt]=hd[u],hd[u]=idcnt,to[idcnt]=v,val[idcnt]=0;}
bool bfs(){
for(int i=1;i<=t;i++)dep[i]=inf;
queue <int> q;dep[s]=1,nw[s]=hd[s];q.push(s);
while(!q.empty()){
int u=q.front();q.pop();
for(int i=hd[u];i;i=nxt[i]){
int v=to[i];if(dep[v]==inf&&val[i]){dep[v]=dep[u]+1,q.push(v);nw[v]=hd[v];if(v==t)return 1;}
}
}
return 0;
}
int dfs(int u,int sum){
if(u==t)return sum;
int k,res=0;
for(int i=nw[u];i;i=nxt[i]){
nw[u]=i;int v=to[i];if(!val[i]||dep[v]!=dep[u]+1)continue;
k=dfs(v,min(sum,val[i]));if(!k){dep[v]=inf;continue;}
sum-=k,val[i]-=k,val[i^1]+=k,res+=k;if(!sum)break;
}
return res;
}
}
using namespace flow;
signed main(){
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
cin>>n>>m>>c;s=n+2*c+1;t=s+1;add(s,1,inf),add(n,t,inf);
for(int u=1,v;u<=c;u++){add(getcl(u),getcr(u),1);cin>>v,bel[u]=v;}
for(int i=1,u,v,p;i<=m;i++){cin>>u>>v>>p;add(gettra(u),getcl(p),inf),add(getcr(p),gettra(v),1);}
int ans=0;while(bfs()){ans+=dfs(s,inf);}
cout<<ans;return 0;
}
这个时候比较基础的网络流题就做的差不多了。
P9879 [EC Final 2021] Check Pattern is Good
这种交叉的东西就可以先通过将某些格子纵坐标以及横坐标之和为奇数的格子的值取反,这样就变成了相邻四个格子全部是同一种颜色。最后输出方案的时候换回来即可。
考虑这个东西是经典的最小割问题,即捆绑二选一。因此直接建立虚点跑最小割即可。注意对于一些不确定的点我们可以直接不将其与源汇点连边,而确定的点就连相应的点,流量为 \(\infty\)。考虑这样确定的点的边一定不会被割掉。这样就可以满足性质了。
code
不知道为什么也写了相对久。
点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int M=105;
int s,t,st,n,m,mp[M][M],sign0[M][M],sign1[M][M];
int get(int x,int y){return (x-1)*m+y;}
namespace flow{
const int N=2e6+7,inf=1e9+7;
int hd[N],to[N],val[N],idcnt=1,dep[N],nxt[N],nw[N];
void add(int u,int v,int w){nxt[++idcnt]=hd[u];hd[u]=idcnt;to[idcnt]=v,val[idcnt]=w;swap(u,v);nxt[++idcnt]=hd[u];hd[u]=idcnt;to[idcnt]=v,val[idcnt]=0;}
void init(){for(int i=1;i<=st;i++)hd[i]=0;idcnt=1;}
bool bfs(){
for(int i=1;i<=st;i++)dep[i]=inf;
queue <int> q;nw[s]=hd[s],dep[s]=1;q.push(s);
while(!q.empty()){
int u=q.front();q.pop();
for(int i=hd[u];i;i=nxt[i]){
int v=to[i];if(dep[v]==inf&&val[i]){q.push(v),dep[v]=dep[u]+1,nw[v]=hd[v];if(v==t)return 1;}
}
}
return 0;
}
int dfs(int u,int sum){
if(u==t) return sum;
int k,res=0;
for(int i=nw[u];i;i=nxt[i]){
nw[u]=i;int v=to[i];if(dep[v]!=dep[u]+1||!val[i])continue;
k=dfs(v,min(sum,val[i]));if(!k){dep[v]=inf;continue;}
val[i]-=k,val[i^1]+=k,res+=k,sum-=k;if(!sum)break;
}
return res;
}
}
using namespace flow;
bool check(int i,int j,int x){return ((mp[i][j]==x||mp[i][j]==2)&&(mp[i+1][j]==x||mp[i+1][j]==2)&&(mp[i][j+1]==x||mp[i][j+1]==2)&&(mp[i+1][j+1]==x||mp[i+1][j+1]==2));}
void solve(){
cin>>n>>m;s=n*m+1,t=s+1;init();
for(int i=1;i<=n;i++)for(int j=1;j<=m;j++){char x;cin>>x;mp[i][j]=(x=='W');if((i+j)%2)mp[i][j]^=1;if(x=='?')mp[i][j]=2;if(mp[i][j]==0) add(s,get(i,j),inf);if(mp[i][j]==1) add(get(i,j),t,inf);}
int ans=0;st=t;
for(int i=1;i<n;i++){for(int j=1;j<m;j++){
sign0[i][j]=check(i,j,0);sign1[i][j]=check(i,j,1);
if(sign0[i][j])ans++,add(s,++st,1),add(st,get(i,j),inf),add(st,get(i+1,j),inf),add(st,get(i,j+1),inf),add(st,get(i+1,j+1),inf);
if(sign1[i][j])ans++,add(++st,t,1),add(get(i,j),st,inf),add(get(i+1,j),st,inf),add(get(i,j+1),st,inf),add(get(i+1,j+1),st,inf);
}}
while(bfs()){ans-=dfs(s,inf);}
cout<<ans<<'\n';
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
if(dep[get(i,j)]!=inf)mp[i][j]=0;else mp[i][j]=1;
if((i+j)%2)mp[i][j]^=1;
}
}
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){if(mp[i][j])cout<<'W';else cout<<'B';}
cout<<'\n';
}
}
signed main(){
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
int T;cin>>T;while(T--)solve();
return 0;
}
一些结论/trick/困难方法
最大流=最小割。
对于点的限制,将点拆开转化为对边的限制。
最小割神秘结论:平面图的最小割等于其对偶图的最短路
对于一些交叉的限制,那么可以将某些格子的限制全局异或 1。然后就变成了完整的限制。
网格图黑白染色!!!
想不出多项式算法可以想非多项式算法加上网络流优化+剪一下枝之类的。
可以以通过连多条重边来控制费用。
模拟最大流一般要用最大流与最小割的转化。
可以用原始对偶优化费用流。一般而言可以优化 \(\frac1 3\) 的常数。但是不是很熟练,个人就算了。(结果最后还是写了)
最大流上动态插入/删除边:
插入的话很好做。考虑插入后直接再跑就是对的。
删除的话,设删除边 \((u,v)\),其反边的流量是 \(k\)。可以考虑将 \(ans-=k\),删除边 \((u,v)\),即将正反边流量变成 0。然后新连边 \((s,u),(v,t)\),其中流量都为 \(k\)。然后再跑一遍。
这个的意思就是一个类似于反悔操作的东西。然后重新跑的原因是有可能构成新的增广路。
插入和删除的复杂度目测起来很小,因为是在一个很小的残量网络上跑的。总体来说是 \(O(很小)\)。
这个东西可以应用在一些题目的优化中,比如数据范围较大的费用流。由于一次 spfa 只能找到一条增广路,因此 dfs 聊胜于无。
于是我们可以动态加边来使得 spfa 的效率大大提高同时仍然可以保证正确性。但是这样貌似可能会出现负环。不知道。
由于是动态的,因此原始对偶优化直接爆了。不用就好了。
同时有可能可以用这种方法通过某种判断减少点与边的规模,也算是一种优化建图了。
对于所谓 “一面对多面” 的问题(一个限制会影响其他多个限制),可以通过建负权边的方式来控制流过的边。







浙公网安备 33010602011771号