网络流与二分图

· · 算法·理论

2025/4/6:添加了题目。重新提交。

如果你想查看卡常方法:请转到章节:几个玄学优化。

题单

网络流

EK

首先我们会想到一个暴力:暴力枚举每一条路径,然后流一遍,于是……

这样是错误的

我们很可能有沿着蓝色的路径流:

这样流了之后,就没法继续流了。因此我们需要反悔

建立原图 G 的残留网络 G',我们规定 G,G' 上的对应边的边权之和相加等于边的容量。

于是我们可以愉快地流绿色的边了 qwq。

这其实相当于流了 1\to2\to4,1\to3\to4 两条路径。

这就是 EK 的核心思想。O(VE^2)

Dinic

我们考虑多路增广。

我们先进行一次 bfs,将图分为很多层。

我们改用 dfs 进行增广。

第一条增广路:

第二条增广路(此时 1\to2 的流量只剩下 10-8=2 了)。完成后回溯,并更新残留网络。

更新残留网络。

第三条增广路:

再次分层。

如此往复。

当前弧优化 dinic

:::info[为什么能优化] 看下面的图:

上面的 3 条路径中,除了 \to 7 的路径之外,都跑满了(有一个流量为 1 的限制)

那我们再到这里的时候:

我们选择优化,不找上面两条了。 :::

代码就不贴了,改成 for(int i=cur[u];i&∩i=ed[i].nxt) 并更新 cur[u]=i; 就行了。

最大流最小割

不会证明,感性理解。 :::warning[理解]{open} 直观理解: 假设最大流为 f,找一个割集,把网络分割成了两个部分,那现在最大流就是 0 了,相当于找的割集得把原来的流阻断。 割要最小,就要恰好阻断所有流。如果割 <f,说明一定存在一条 S\to T 的路径有流,此时没有把网络分成两个部分。所以割一定 \ge f ,如果 >f 了,说明割集把最大流 f 阻断了,然而有多余流量的边,把多余流量的边去了,就是最小割 f 了。

:::

例题

P3931 SAC E#1 - 一道难题 Tree

我们显然可以看做最小割。

对于原来的树 T,新建超级汇点 t 连接所有叶子结点,对每一条父亲到儿子的边建立边,构成 G,跑最小割。 :::info[code]

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int maxn=1000002,inf=INT_MAX;
struct edge{
    int u,v,w,nxt;
}ed[maxn];
struct edge2{
    int v,w;
};
int n,s,t,lvl[maxn],head[maxn],cur[maxn],tot=2;
vector<edge2> G[maxn];
void adde(int u,int v,int w){
    ed[tot]={u,v,w,head[u]};
    head[u]=tot;
    tot++;
}
void Dfs(int u,int fa){
    bool leaf=1;
    for(auto [v,w]:G[u]){
        if(v!=fa){
            Dfs(v,u);
            adde(u,v,w);
            adde(v,u,0);
            leaf=0;
        }
    }
    if(leaf){
        adde(u,t,inf);
        adde(t,u,0);
    }
}
bool bfs(){
    queue<int> q;
    q.push(s);
    memset(lvl,-1,sizeof(lvl));
    lvl[s]=0;
    while(q.size()){
        int u=q.front();
        q.pop();
        for(int i=head[u];i;i=ed[i].nxt){
            int v=ed[i].v,w=ed[i].w;
            if(w&&lvl[v]==-1){
                lvl[v]=lvl[u]+1;
                q.push(v);
                if(v==t)return 1;
            }
        }
    }
    return 0;
}
int dfs(int u,int cap){
    int flow=0;
    if(u==t)return cap;
    for(int i=cur[u];i&&cap;i=ed[i].nxt){
        cur[u]=i;//u往下这一路已经处理完了,不用再次遍历
        int v=ed[i].v,w=ed[i].w;
        if(w&&lvl[v]==lvl[u]+1){
            int use=dfs(v,min(cap,w));
            if(use==0)lvl[v]=-1;
            if(use>0){
                ed[i].w-=use;
                ed[i^1].w+=use;
                cap-=use;
                flow+=use;
            }
        }
    }
    return flow;
}
void dinic(){
    int ans=0;
    while(bfs()){
        memcpy(cur,head,sizeof(head));
        ans+=dfs(s,inf);
    }
    cout<<ans;
}
signed main(){
    ios::sync_with_stdio(0);cin.tie(0);
    cin>>n>>s;
    t=n+1;//超汇
    for(int i=1;i<n;i++){
        int u,v,w;
        cin>>u>>v>>w;
        G[u].push_back({v,w});
        G[v].push_back({u,w});
    }
    Dfs(s,0);
    dinic();
    return 0;
}

:::

P3254 圆桌问题

考虑将造一个超级源点,连接每一个单位,容量为这个单位的人数,并造一个超级汇点,连接每一个餐桌,容量为餐桌能坐的人数。

对于单位和餐桌,连接一条容量为 1 的边,表示同一单位的代表中,只有一个能坐在这个餐桌。(两两餐桌不同)

最后跑网络流。对于方案,我们直接看单位到餐桌的边是否流满了(剩余流量为 0)就可以了。

给一个样例的图片:

:::info[code]

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=275,M=5005,inf=2147483647;
int n,m,s,t;
int lvl[N];//层数
struct edge{
    int u,v,w;
    int nxt;
}ed[M*2];
int head[N],cur[N],cnt=2;
void adde(int u,int v,int w){
    ed[cnt].u=u;
    ed[cnt].v=v;
    ed[cnt].w=w; 
    ed[cnt].nxt=head[u];
    head[u]=cnt;
    cnt++; 
}
bool bfs(){
    queue<int> q;
    for(int i=1;i<=n+m+2;i++)lvl[i]=-1;
    lvl[s]=0;
    q.push(s);
    while(!q.empty()){
        int u=q.front();
        q.pop();
        for(int i=head[u];i;i=ed[i].nxt){
            if(lvl[ed[i].v]==-1&&ed[i].w>0){
                lvl[ed[i].v]=lvl[u]+1;  
                q.push(ed[i].v);
                if(ed[i].v==t)return 1;
            }
        }
    }
    return(lvl[t]!=-1);
}
int dfs(int u,int cap){//cap:容量
    int flow=0;//跑过的流
    if(u==t)return cap;
    for(int i=cur[u];i&&cap;i=ed[i].nxt){
        cur[u]=i;
        int v=ed[i].v,w=ed[i].w;
        if(w&&lvl[v]==lvl[u]+1){
            int use=dfs(v,min(cap,w));
            if(use==0)lvl[v]=-1;
            if(use>0){
                ed[i].w-=use;
                ed[i^1].w+=use;
                cap-=use;
                flow+=use;
            }
        }
    }
    return flow;
}
int dinic(){
    int ans=0;
    while(bfs()){ 
        memcpy(cur,head,sizeof(head));
        ans+=dfs(s,inf);
    }
    return ans;
}
int ren=0;
vector<int> table[N];
signed main(){
    ios::sync_with_stdio(0);
    cin.tie(0); cout.tie(0);
    cin>>m>>n;
    s=1;t=n+m+2;
    for(int i=1;i<=m;i++){
        int ri;
        cin>>ri;
        ren+=ri;
        adde(s,i+1,ri);
        adde(i+1,s,0);
    }
    for(int i=1;i<=n;i++){
        int ci;
        cin>>ci;
        adde(i+m+1,t,ci);
        adde(t,i+m+1,0);
    }
    for(int i=1;i<=m;i++){
        for(int j=1;j<=n;j++){
            adde(i+1,j+m+1,1);
            adde(j+m+1,i+1,0);
        }
    }
    if(dinic()<ren){
        cout<<0;
        return 0;
    }
    cout<<1<<endl;
    for(int i=2;i<=m+1;i++){//man
        for(int j=head[i];j;j=ed[j].nxt){
            if(!ed[j].w)cout<<ed[j].v-m-1<<' ';
        }
        cout<<endl;
    }
    return 0;
}

:::

费用流

无负圈的费用流

我们在选择增广路的时候,尽量要选择距离小的路径进行增流,直到无法增加再切换到另一条。

具体的,我们需要先进行最短路,预处理 s 到每一个点 u 的距离 dis_u

bool spfa(){
    for(int i=0;i<maxn;i++)dis[i]=inf;
    dis[s]=0;
    inq[s]=1;
    q.push(s);
    while(q.size()){
        int u=q.front();
        q.pop();
        inq[u]=0;
        for(int i=head[u];i;i=ed[i].nxt){
            int v=ed[i].v,c=ed[i].c,flow=ed[i].w;
            if(dis[v]>dis[u]+c&&flow){//这里要判断能不能过
                dis[v]=dis[u]+c;
                if(!inq[v])q.push(v);
                inq[v]=1;
            }
        }
    }
    return dis[t]!=inf;
}

同时,我们要在 dfs 函数上进行改进:选择最短的那条路径。

int dfs(int u,int cap){
    int flow=0;
    if(u==t)return cap;
    vis[u]=1;
    for(int i=cur[u];i&&cap;i=ed[i].nxt){
        cur[u]=i;
        int v=ed[i].v,c=ed[i].c,fl=ed[i].w;
        if(dis[v]==dis[u]+c&&fl&&!vis[v]){//判断最短的条件就是 dis[v]=dis[u]+w(松弛到不能松弛)
            int use=dfs(v,min(cap,fl));
            if(use>0){
                ed[i].w-=use;
                ed[i^1].w+=use;
                flow+=use;
                cap-=use;
            }
        }
    }
    vis[u]=0;
    return flow;
}

有同学要问了:vis 数组是干嘛的?

原因:可能存在单位权值为 0 的环,所以在 dfs 找增广路时,需要对访问当前这条增广路的点进行标记,避免死循环。

最后是统计答案:

pair<int,int> dinic(){
    int flow=0,cost=0;
    while(spfa()){
        memcpy(cur,head,sizeof(head));
        int fl=dfs(s,inf);
        flow+=fl;
        cost+=fl*dis[t];
    }
    return {flow,cost};
}

有的同学又要问了:你这个 C(G)=\sum\limits_{<u,v>\in E}c_{u\to v}\cdot flow_{u\to v},但是你怎么能直接把这一轮的 fldis[t] 相乘呢。

原因:你刚刚不是走的是最短的路径吗。

那么对于两条路径 P_1,P_2

那么答案:\Delta cost=flow(P_1)\sum\limits_{e\in P_1} c_e+flow(P_2)\sum\limits_{e\in P_2}c_e=\sum c\cdot[flow(P_1)+flow(P_2)]

同理,对于多条最短路径,\Delta cost=\sum c\cdot\sum flow(Path)

完整代码:

#include<bits/stdc++.h>
using namespace std;
const int maxn=500100,inf=INT_MAX;
int n,m,s,t,head[maxn],tot=2,dis[maxn],cur[maxn];//inqueue
struct edge{
    int u,v,w,c,nxt;
}ed[maxn];
bool inq[maxn],vis[maxn];
queue<int> q;
void adde(int u,int v,int w,int c){
    ed[tot]={u,v,w,c,head[u]};
    head[u]=tot;
    tot++;
}
bool spfa(){
    for(int i=0;i<maxn;i++)dis[i]=inf;
    dis[s]=0;
    inq[s]=1;
    q.push(s);
    while(q.size()){
        int u=q.front();
        q.pop();
        inq[u]=0;
        for(int i=head[u];i;i=ed[i].nxt){
            int v=ed[i].v,c=ed[i].c,flow=ed[i].w;
            if(dis[v]>dis[u]+c&&flow){
                dis[v]=dis[u]+c;
                if(!inq[v])q.push(v);
                inq[v]=1;
            }
        }
    }
    return dis[t]!=inf;
}
int dfs(int u,int cap){
    int flow=0;
    if(u==t)return cap;
    vis[u]=1;
    for(int i=cur[u];i&&cap;i=ed[i].nxt){
        cur[u]=i;
        int v=ed[i].v,c=ed[i].c,fl=ed[i].w;
        if(dis[v]==dis[u]+c&&fl&&!vis[v]){
            int use=dfs(v,min(cap,fl));
            if(use>0){
                ed[i].w-=use;
                ed[i^1].w+=use;
                flow+=use;
                cap-=use;
            }
        }
    }
    vis[u]=0;
    return flow;
}
pair<int,int> dinic(){
    int flow=0,cost=0;
    while(spfa()){
        memcpy(cur,head,sizeof(head));
        int fl=dfs(s,inf);
        flow+=fl;
        cost+=fl*dis[t];
    }
    return {flow,cost};
}
signed main(){
    ios::sync_with_stdio(0);cin.tie(0);
    cin>>n>>m>>s>>t;
    for(int i=1;i<=m;i++){
        int u,v,w,c;
        cin>>u>>v>>w>>c;
        adde(u,v,w,c);
        adde(v,u,0,-c);
    }
    pair<int,int> ans=dinic();
    cout<<ans.first<<' '<<ans.second;
    return 0;
}

例题

P4014 分配问题

简洁明了,超源超汇。

s 与人建边 \{1,0\},将 t 与工作建边 \{1,0\},将人与工作建边 \{1,c_{i,j}\}

注意跑一次最小费用流,一次最大费用流。 :::info[code]

#include<bits/stdc++.h>
using namespace std;
const int maxn=5510,inf=INT_MAX;
int n,m,s,t,head[maxn],tot=2,dis[maxn],cur[maxn];//inqueue
struct edge{
    int u,v,w,c,nxt;
}ed[maxn],ed2[maxn];
bool inq[maxn],vis[maxn];
int c[51][51];
queue<int> q;
void adde(int u,int v,int w,int c){
    ed[tot]={u,v,w,c,head[u]};
    head[u]=tot;
    tot++;
}
bool spfa(){
    for(int i=0;i<maxn;i++)dis[i]=inf;
    dis[s]=0;
    inq[s]=1;
    q.push(s);
    while(q.size()){
        int u=q.front();
        q.pop();
        inq[u]=0;
        for(int i=head[u];i;i=ed[i].nxt){
            int v=ed[i].v,c=ed[i].c,flow=ed[i].w;
            if(dis[v]>dis[u]+c&&flow){
                dis[v]=dis[u]+c;
                if(!inq[v])q.push(v);
                inq[v]=1;
            }
        }
    }
    return dis[t]!=inf;
}
bool spfa2(){
    for(int i=0;i<maxn;i++)dis[i]=-inf;
    dis[s]=0;
    inq[s]=1;
    q.push(s);
    while(q.size()){
        int u=q.front();
        q.pop();
        inq[u]=0;
        for(int i=head[u];i;i=ed[i].nxt){
            int v=ed[i].v,c=ed[i].c,flow=ed[i].w;
            if(dis[v]<dis[u]+c&&flow){
                dis[v]=dis[u]+c;
                if(!inq[v])q.push(v);
                inq[v]=1;
            }
        }
    }
    return dis[t]!=-inf;
}
int dfs(int u,int cap){
    int flow=0;
    if(u==t)return cap;
    vis[u]=1;
    for(int i=cur[u];i&&cap;i=ed[i].nxt){
        cur[u]=i;
        int v=ed[i].v,c=ed[i].c,fl=ed[i].w;
        if(dis[v]==dis[u]+c&&fl&&!vis[v]){
            int use=dfs(v,min(cap,fl));
            if(use>0){
                ed[i].w-=use;
                ed[i^1].w+=use;
                flow+=use;
                cap-=use;
            }
        }
    }
    vis[u]=0;
    return flow;
}
pair<int,int> dinic(){
    int flow=0,cost=0;
    while(spfa()){
        memcpy(cur,head,sizeof(head));
        int fl=dfs(s,inf);
        flow+=fl;
        cost+=fl*dis[t];
    }
    return {flow,cost};
}
pair<int,int> dinic2(){
    int flow=0,cost=0;
    while(spfa2()){
        memcpy(cur,head,sizeof(head));
        int fl=dfs(s,inf);
        flow+=fl;
        cost+=fl*dis[t];
    }
    return {flow,cost};
}
signed main(){
    ios::sync_with_stdio(0);cin.tie(0);
    cin>>n;
    for(int i=1;i<=n;i++){
        for(int j=1;j<=n;j++){
            cin>>c[i][j];
        }
    }
    s=0,t=2*n+1;
    for(int i=1;i<=n;i++){
        adde(s,i,1,0);
        adde(i,s,0,0);
    }
    for(int i=1;i<=n;i++){
        adde(i+n,t,1,0);
        adde(t,i+n,0,0);
    }
    for(int i=1;i<=n;i++){
        for(int j=1;j<=n;j++){
            adde(i,j+n,1,c[i][j]);
            adde(j+n,i,0,-c[i][j]);
        }
    }
    for(int i=2;i<=tot;i++)ed2[i]=ed[i];
    cout<<dinic().second<<endl;
    for(int i=2;i<=tot;i++)ed[i]=ed2[i];
    cout<<dinic2().second;
    return 0;
}

:::

P4015 运输问题

和上面那道题一样,多了个数量而已 qwq,于是 s 到仓库,t 到商店的容量就不是 1,是 a_i,b_j 了喵。

:::info[code]

#include<bits/stdc++.h>
using namespace std;
const int maxn=40101,inf=INT_MAX;
int n,m,s,t,head[maxn],tot=2,dis[maxn],cur[maxn];//inqueue
struct edge{
    int u,v,w,c,nxt;
}ed[maxn],ed2[maxn];
bool inq[maxn],vis[maxn];
int c[151][151],a[101],b[101];
queue<int> q;
void adde(int u,int v,int w,int c){
    ed[tot]={u,v,w,c,head[u]};
    head[u]=tot;
    tot++;
}
bool spfa(){
    for(int i=0;i<maxn;i++)dis[i]=inf;
    dis[s]=0;
    inq[s]=1;
    q.push(s);
    while(q.size()){
        int u=q.front();
        q.pop();
        inq[u]=0;
        for(int i=head[u];i;i=ed[i].nxt){
            int v=ed[i].v,c=ed[i].c,flow=ed[i].w;
            if(dis[v]>dis[u]+c&&flow){
                dis[v]=dis[u]+c;
                if(!inq[v])q.push(v);
                inq[v]=1;
            }
        }
    }
    return dis[t]!=inf;
}
bool spfa2(){
    for(int i=0;i<maxn;i++)dis[i]=-inf;
    dis[s]=0;
    inq[s]=1;
    q.push(s);
    while(q.size()){
        int u=q.front();
        q.pop();
        inq[u]=0;
        for(int i=head[u];i;i=ed[i].nxt){
            int v=ed[i].v,c=ed[i].c,flow=ed[i].w;
            if(dis[v]<dis[u]+c&&flow){
                dis[v]=dis[u]+c;
                if(!inq[v])q.push(v);
                inq[v]=1;
            }
        }
    }
    return dis[t]!=-inf;
}
int dfs(int u,int cap){
    int flow=0;
    if(u==t)return cap;
    vis[u]=1;
    for(int i=cur[u];i&&cap;i=ed[i].nxt){
        cur[u]=i;
        int v=ed[i].v,c=ed[i].c,fl=ed[i].w;
        if(dis[v]==dis[u]+c&&fl&&!vis[v]){
            int use=dfs(v,min(cap,fl));
            if(use>0){
                ed[i].w-=use;
                ed[i^1].w+=use;
                flow+=use;
                cap-=use;
            }
        }
    }
    vis[u]=0;
    return flow;
}
pair<int,int> dinic(){
    int flow=0,cost=0;
    while(spfa()){
        memcpy(cur,head,sizeof(head));
        int fl=dfs(s,inf);
        flow+=fl;
        cost+=fl*dis[t];
    }
    return {flow,cost};
}
pair<int,int> dinic2(){
    int flow=0,cost=0;
    while(spfa2()){
        memcpy(cur,head,sizeof(head));
        int fl=dfs(s,inf);
        flow+=fl;
        cost+=fl*dis[t];
    }
    return {flow,cost};
}
signed main(){
    ios::sync_with_stdio(0);cin.tie(0);
    cin>>m>>n;
    for(int i=1;i<=m;i++)cin>>a[i];
    for(int i=1;i<=n;i++)cin>>b[i];
    for(int i=1;i<=m;i++){
        for(int j=1;j<=n;j++){
            cin>>c[i][j];
        }
    }
    s=0,t=m+n+1;
    for(int i=1;i<=m;i++){
        adde(s,i,a[i],0);
        adde(i,s,0,0);
    }
    for(int i=1;i<=n;i++){
        adde(i+m,t,b[i],0);
        adde(t,i+m,0,0);
    }
    for(int i=1;i<=m;i++){
        for(int j=1;j<=n;j++){
            adde(i,j+m,inf,c[i][j]);
            adde(j+m,i,0,-c[i][j]);
        }
    }
    for(int i=2;i<=tot;i++)ed2[i]=ed[i];
    cout<<dinic().second<<endl;
    for(int i=2;i<=tot;i++)ed[i]=ed2[i];
    cout<<dinic2().second;
    return 0;
}

:::

农场之旅—POJ2135

:::info[题目描述] 题目描述: 约翰的农场有 N(1\le N\le 1000) 块田地,编号为1到N。第1块田地上有他的房子,第N块田地上有一个大谷仓。有 M(1\le M\le10000) 条道路连接田地,每条道路都连接了两个不同的田地,并且长度大于 0 小于 35000。他从家开始,可能穿过一些田地,最后到谷仓。后来,他又回到家里。 他希望自己的旅程尽可能短,但不想经过任何一条道路超过一次。

:::

我们考虑利用网络流的性质。两点之间建立边,流量为 1,费用为 w,这样就可以来做到每条边经过一次的性质了。

建立超级源点 S,超级汇点 TS1 连边 \{2,0\}nT 连边 \{2,0\},这样能保证来往共两次。

注意到 1\to n 的路径和 n\to 1 的路径互为反向,且 \sum\limits_{e\in Path(1\to n)} w_e=\sum\limits_{e\in Path(n\to 1)} w_e,边权和相等。所以我们其实只需要跑一遍费用流,就可以计算了。

P2770 航空路线问题

和上面的那道题很像啊。

我们考虑拆点,把 u 拆成入点 u 和出点 u',建立边 e_{u\to u'},费用为 1,容量为 1

最后跑一遍最大费用最大流即可。

特殊处理 u\to v 的边,这条边的容量为 2

判断无解嘛,就是判断 t' 的流量是否为 2。注:t'=2n。 :::info[code]

#include<bits/stdc++.h>
using namespace std;
const int maxn=10001,inf=INT_MAX;
string name[maxn];
struct edge{
    int u,v,w,c,nxt;
}ed[maxn];
int tot=2,s,t,dis[maxn],head[maxn],cur[maxn],n;
bool inq[maxn],vis[maxn];
void adde(int u,int v,int w,int c){
    ed[tot]={u,v,w,c,head[u]};
    head[u]=tot;
    tot++;
}
bool spfa(){
    queue<int> q;
    for(int i=0;i<maxn;i++)dis[i]=inf;
    dis[s]=0;
    q.push(s);
    inq[s]=1;
    while(q.size()){
        int u=q.front();
        q.pop();
        inq[u]=0;
        for(int i=head[u];i;i=ed[i].nxt){
            int v=ed[i].v,flow=ed[i].w,c=ed[i].c;
            if(dis[v]>dis[u]+c&&flow){
                dis[v]=dis[u]+c;
                if(!inq[v])q.push(v);
                inq[v]=1;
            }
        }
    }
    return dis[t]!=inf;
}
int dfs(int u,int cap){
    int flow=0;
    vis[u]=1;
    if(u==t)return cap;
    for(int i=cur[u];i&&cap;i=ed[i].nxt){
        cur[u]=i;
        int v=ed[i].v,fl=ed[i].w,c=ed[i].c;
        if(dis[v]==dis[u]+c&&fl&&!vis[v]){
            int use=dfs(v,min(cap,fl));
            if(use>0){
                cap-=use;
                flow+=use;
                ed[i].w-=use;
                ed[i^1].w+=use;
            }
        }

    }
    vis[u]=0;
    return flow;
}
pair<int,int> dinic(){
    int flow=0,cost=0;
    while(spfa()){
        memset(vis,0,sizeof(vis));
        memcpy(cur,head,sizeof(head));
        int fl=dfs(s,inf);
        flow+=fl;
        cost+=fl*dis[t];
    }
    return {flow,cost};
}
map<string,int> re;
vector<int> rt1,rt2;
bool route[maxn];
void dfs1(int u){
    if(u!=n+1)route[u]=1;
    if(u==t)return;
    if(u<=n)rt1.push_back(u);
    for(int i=head[u];i;i=ed[i].nxt){
        int v=ed[i].v,fl=ed[i].w,c=ed[i].c;
        if(c<=0&&fl==0){
            dfs1(v);
            break;
        }
    }
}
void dfs2(int u){
    if(u<=n)rt2.push_back(u);
    for(int i=head[u];i;i=ed[i].nxt){
        int v=ed[i].v,fl=ed[i].w,c=ed[i].c;
        if(c<=0&&fl==0&&!route[v]){
            dfs2(v);
            break;
        }
    }
}
signed main(){
    ios::sync_with_stdio(0);cin.tie(0);
    int m;
    cin>>n>>m;
    s=1,t=n*2;
    for(int i=1;i<=n;i++){
        cin>>name[i];
        re[name[i]]=i;
    }
    adde(1,1+n,2,0);
    adde(1+n,1,0,0);
    adde(n,n*2,2,0);
    adde(n*2,n,0,0);
    for(int i=2;i<n;i++){
        adde(i,i+n,1,0);
        adde(i+n,i,0,0);
    }
    for(int i=1;i<=m;i++){
        string su,sv;
        cin>>su>>sv;
        int u=re[su],v=re[sv];
        if(u>v)swap(u,v);
        adde(u+n,v,1+(u==1&&v==n),-1);
        adde(v,u+n,0,1);
    }
    pair<int,int> ans=dinic();
    if(ans.first!=2){
        cout<<"No Solution!";
        return 0;
    }
    cout<<-ans.second<<endl;
    dfs1(s);
    for(auto i:rt1){
        cout<<name[i]<<endl;
    }
    dfs2(1+n);
    reverse(rt2.begin(),rt2.end());
    for(auto i:rt2){
        cout<<name[i]<<endl;
    }
    cout<<name[1];
    return 0;
}

:::

P2045 方格取数加强版

还是拆点喵。

将每一个格子拆成入点和出点喵,两个点之间连边喵。

  1. (i,j)(i,j)' 连边 \{1,c_{i,j}\} 喵,这代表了首次经过这个格子有 c 的收益喵。
  2. (i,j)(i,j)' 连边 \{k-1,c_{i,j}\} 喵,这代表了经过一次之后,再经过这个格子没有收益了喵。k-1 是因为第一次已经过了一次了喵。
  3. (i,j)'(i,j+1),(i+1,j) 连边 \{k,0\} 喵。这代表了进行移动,且这个路径最多经过 k 次喵。

最后求出最大费用最大流喵。 :::info[code]

#include<bits/stdc++.h>
using namespace std;
const int maxn=10001,inf=INT_MAX;
int n,m,k,s,t,head[maxn],tot=2,dis[maxn],cur[maxn],val[maxn][maxn];//inqueue
struct edge{
    int u,v,w,c,nxt;
}ed[maxn];
bool inq[maxn],vis[maxn];
queue<int> q;
void adde(int u,int v,int w,int c){
    ed[tot]={u,v,w,c,head[u]};
    head[u]=tot;
    tot++;
}
bool spfa(){
    for(int i=0;i<maxn;i++)dis[i]=-inf;
    dis[s]=0;
    inq[s]=1;
    q.push(s);
    while(q.size()){
        int u=q.front();
        q.pop();
        inq[u]=0;
        for(int i=head[u];i;i=ed[i].nxt){
            int v=ed[i].v,c=ed[i].c,flow=ed[i].w;
            if(dis[v]<dis[u]+c&&flow){
                dis[v]=dis[u]+c;
                if(!inq[v])q.push(v);
                inq[v]=1;
            }
        }
    }
    return dis[t]!=-inf;
}
int dfs(int u,int cap){
    int flow=0;
    if(u==t)return cap;
    vis[u]=1;
    for(int i=cur[u];i&&cap;i=ed[i].nxt){
        cur[u]=i;
        int v=ed[i].v,c=ed[i].c,fl=ed[i].w;
        if(dis[v]==dis[u]+c&&fl&&!vis[v]){
            int use=dfs(v,min(cap,fl));
            if(use>0){
                ed[i].w-=use;
                ed[i^1].w+=use;
                flow+=use;
                cap-=use;
            }
        }
    }
    vis[u]=0;
    return flow;
}
pair<int,int> dinic(){
    int flow=0,cost=0;
    while(spfa()){
        memcpy(cur,head,sizeof(head));
        int fl=dfs(s,inf);
        flow+=fl;
        cost+=fl*dis[t];
    }
    return {flow,cost};
}
int id(int x,int y){
    return (x-1)*n+y;
}
int id2(int x,int y){
    return n*n+id(x,y);
}
signed main(){
    ios::sync_with_stdio(0);cin.tie(0);
    cin>>n>>k;
    s=1;t=2*n*n;
    for(int i=1;i<=n;i++){
        for(int j=1;j<=n;j++){
            cin>>val[i][j];
        }
    }
    for(int i=1;i<=n;i++){
        for(int j=1;j<=n;j++){
            adde(id(i,j),id2(i,j),1,val[i][j]);
            adde(id2(i,j),id(i,j),0,-val[i][j]);
            adde(id(i,j),id2(i,j),k-1,0);
            adde(id2(i,j),id(i,j),0,0);
            if(j<n)adde(id2(i,j),id(i,j+1),k,0);
            if(j<n)adde(id(i,j+1),id2(i,j),0,0);
            if(i<n)adde(id2(i,j),id(i+1,j),k,0);
            if(i<n)adde(id(i+1,j),id2(i,j),0,0);
        }
    }
    cout<<dinic().second;
    return 0;
}

:::

网络流综合练习

P2774 方格取数问题

先染一下色。

\fcolorbox{black}{black}{\color{white}{1}} \fcolorbox{black}{white}{2} \fcolorbox{black}{black}{\color{white}{3}}\\ \fcolorbox{black}{white}{3} \fcolorbox{black}{black}{\color{white}{2}} \fcolorbox{black}{white}{3}\\ \fcolorbox{black}{black}{\color{white}{2}} \fcolorbox{black}{white}{3} \fcolorbox{black}{black}{\color{white}{1}}

可以发现,选择了一个黑色格子,就不能选择相邻的白色格子。

我们的思路大概出来了。

由于最大权独立集 = 总价值 - 最小点权覆盖,又因为最小点权覆盖 = 最小割 = 最大流,所以我们只需跑一次最大流就行了。

:::info[code]

#include<bits/stdc++.h>
using namespace std;
const int maxn=10101,maxc=101,inf=INT_MAX;
struct edge{
    int u,v,w,nxt;
}ed[maxn];
int tot=2,a[maxc][maxc],head[maxn],cur[maxn],lvl[maxn],s,t,n,m;
bool color[maxc][maxc];
void adde(int u,int v,int w){
    ed[tot]={u,v,w,head[u]};
    head[u]=tot;
    tot++;
}
bool bfs(){
    queue<int> q;
    q.push(s);
    for(int i=0;i<maxn;i++)lvl[i]=-1;
    lvl[s]=0;
    while(q.size()){
        int u=q.front();
        q.pop();
        for(int i=head[u];i;i=ed[i].nxt){
            int v=ed[i].v,flow=ed[i].w;
            if(flow&&lvl[v]==-1){
                lvl[v]=lvl[u]+1;
                q.push(v);
            }
        }
    }
    return lvl[t]!=-1;
}
int dfs(int u,int cap){
    if(u==t)return cap;
    int flow=0;
    for(int i=cur[u];i&&cap;i=ed[i].nxt){
        int v=ed[i].v,fl=ed[i].w;
        cur[u]=i;
        if(lvl[v]==lvl[u]+1){
            int use=dfs(v,min(cap,fl));
            if(use>0){
                flow+=use;
                cap-=use;
                ed[i].w-=use;
                ed[i^1].w+=use;
            }
        }
    }
    return flow;
}
int dinic(){
    int flow=0;
    while(bfs()){
        memcpy(cur,head,sizeof(head));
        flow+=dfs(s,inf);
    }
    return flow;
}
int id(int x,int y){
    return (x-1)*n+y;
}
signed main(){
    ios::sync_with_stdio(0);cin.tie(0);
    cin>>m>>n;
    s=0,t=m*n+1;
    int sum=0;
    for(int i=1;i<=m;i++){
        for(int j=1;j<=n;j++){
            cin>>a[i][j];
            sum+=a[i][j];
            color[i][j]=(i+j)%2;
        }
    }
    for(int i=1;i<=m;i++){
        for(int j=1;j<=n;j++){
            if(color[i][j]){
                adde(s,id(i,j),a[i][j]);
                adde(id(i,j),s,0);
                if(j<n)adde(id(i,j),id(i,j+1),inf);
                if(j<n)adde(id(i,j+1),id(i,j),0);
                if(j>1)adde(id(i,j),id(i,j-1),inf);
                if(j>1)adde(id(i,j-1),id(i,j),0);
                if(i<m)adde(id(i,j),id(i+1,j),inf);
                if(i<m)adde(id(i+1,j),id(i,j),0);
                if(i>1)adde(id(i,j),id(i-1,j),inf);
                if(i>1)adde(id(i-1,j),id(i,j),0);
            }
            else{
                adde(id(i,j),t,a[i][j]);
                adde(t,id(i,j),0);
            }
        }
    }
    cout<<sum-dinic();
    return 0;
}

::: 还有双倍经验 P4474 王者之剑。

P4012 深海机器人问题

考虑对于每一条边,建立一条边 \{1,w_{(i,j)\to(i,j+1)}\},表示只能获得一次这个价值。同时建立 \{\infty,0\},表示可以无限经过,但不能获得价值。

对于 (i,j)\to(i+1,j) 一样处理。

怎么处理出发点和结束点呢?

建立超级源点 s,既然有 k_i 个机器人从 (x_i,y_i) 出发,那就把 s\to (x_i,y_i) 建立 \{k_i,0\} 的边。

对于结束点,将结束点和 t 连起来就可以了。 :::info[code]

#include<bits/stdc++.h>
using namespace std;
const int maxn=5000,inf=INT_MAX;
int n,m,s,t,head[maxn],tot=2,dis[maxn],cur[maxn],a,b,p;//inqueue
struct edge{
    int u,v,w,c,nxt;
}ed[maxn];
bool inq[maxn],vis[maxn];
void adde(int u,int v,int w,int c){
    ed[tot]={u,v,w,c,head[u]};
    head[u]=tot;
    tot++;
}
bool spfa(){
    queue<int> q;
    for(int i=0;i<maxn;i++)dis[i]=-inf;
    dis[s]=0;
    inq[s]=1;
    q.push(s);
    while(q.size()){
        int u=q.front();
        q.pop();
        inq[u]=0;
        for(int i=head[u];i;i=ed[i].nxt){
            int v=ed[i].v,c=ed[i].c,flow=ed[i].w;
            if(dis[v]<dis[u]+c&&flow){
                dis[v]=dis[u]+c;
                if(!inq[v])q.push(v);
                inq[v]=1;
            }
        }
    }
    return dis[t]!=-inf;
}
int dfs(int u,int cap){
    int flow=0;
    if(u==t)return cap;
    vis[u]=1;
    for(int i=cur[u];i&&cap;i=ed[i].nxt){
        cur[u]=i;
        int v=ed[i].v,c=ed[i].c,fl=ed[i].w;
        if(dis[v]==dis[u]+c&&fl&&!vis[v]){
            int use=dfs(v,min(cap,fl));
            if(use>0){
                ed[i].w-=use;
                ed[i^1].w+=use;
                flow+=use;
                cap-=use;
            }
        }
    }
    vis[u]=0;
    return flow;
}
pair<int,int> dinic(){
    int flow=0,cost=0;
    while(spfa()){
        memcpy(cur,head,sizeof(head));
        int fl=dfs(s,inf);
        flow+=fl;
        cost+=fl*dis[t];
    }
    return {flow,cost};
}
int id[40][40],idx=0;
signed main(){
    int q;
    ios::sync_with_stdio(0);cin.tie(0);
    cin>>a>>b>>p>>q;
    s=idx,t=++idx;
    for(int i=0;i<=p;i++){
        for(int j=0;j<=q;j++){
            id[i][j]=++idx;
        }
    }
    for(int i=0;i<=p;i++){
        for(int j=0;j<q;j++){
            int w;
            cin>>w;
            adde(id[i][j],id[i][j+1],1,w);
            adde(id[i][j+1],id[i][j],0,-w);
            adde(id[i][j],id[i][j+1],inf,0);
            adde(id[i][j+1],id[i][j],0,0);
        }
    }
    for(int i=0;i<=q;i++){
        for(int j=0;j<p;j++){
            int w;
            cin>>w;
            adde(id[j][i],id[j+1][i],1,w);
            adde(id[j+1][i],id[j][i],0,-w);
            adde(id[j][i],id[j+1][i],inf,0);
            adde(id[j+1][i],id[j][i],0,0);
        }
    }
    for(int i=1;i<=a;i++){
        int k,x,y;
        cin>>k>>x>>y;
        adde(s,id[x][y],k,0);
        adde(id[x][y],s,0,0);
    }
    for(int i=1;i<=b;i++){
        int r,x,y;
        cin>>r>>x>>y;
        adde(id[x][y],t,r,0);
        adde(t,id[x][y],0,0);
    }
    cout<<dinic().second;
    return 0;
}

:::

P2762 太空飞行计划问题

挺有意思。其实和方格取数那道题有点相同,都是使用最小割来进行选择决策

还是超源超汇,将 s 与实验连接,容量为 E_i,将仪器与 t 连接,容量为 I_i。中间再进行连接,容量为 \infty

这里其实是把容量看成权值进行决策。

考虑证明答案最优。

设选出来的实验集合为 P,选出来的仪器集合为 Q

![](https://cdn.luogu.com.cn/upload/image_hosting/gcku28ni.png) 可以参考上面的图片,红色是割。 可以看到 $\sum\limits_{i\notin P}E_i-\sum\limits_{j\in Q}I_j$ 就是最小割容量 $Cut(G)$。($\text{\color{green}{绿色}}$、$\text{\color{orange}{橙色}}$ 部分) 所以 $ans=\sum E-Cut(G)$。 :::info[code] ```cpp #include<bits/stdc++.h> using namespace std; int m,n; const int maxn=10001,inf=INT_MAX; int lvl[maxn],tot=2,head[maxn],cur[maxn],s,t; struct edge{ int u,v,w,nxt; }ed[maxn]; void adde(int u,int v,int w){ ed[tot]={u,v,w,head[u]}; head[u]=tot; tot++; } bool bfs(){ queue<int> q; for(int i=0;i<maxn;i++)lvl[i]=-1; lvl[s]=0; q.push(s); while(q.size()){ int u=q.front(); q.pop(); for(int i=head[u];i;i=ed[i].nxt){ int v=ed[i].v,flow=ed[i].w; if(lvl[v]==-1&&flow){ lvl[v]=lvl[u]+1; q.push(v); } } } return lvl[t]!=-1; } int dfs(int u,int cap){ if(u==t)return cap; int flow=0; for(int i=cur[u];i&&cap;i=ed[i].nxt){ cur[u]=i; int v=ed[i].v,fl=ed[i].w; if(lvl[v]==lvl[u]+1){ int use=dfs(v,min(cap,fl)); if(use>0){ cap-=use; flow+=use; ed[i].w-=use; ed[i^1].w+=use; } } } return flow; } void dinic(){ while(bfs()){ memcpy(cur,head,sizeof(head)); int tmp=dfs(s,inf); } } int E[maxn],I[maxn]; signed main(){ //有m个实验,每个实验只可以进行一次,但会获得相应的奖金。 //有n个仪器,每个实验都需要一定的仪器,每个仪器可以运用于多个实验,但需要一定的价值。 //问奖金与代价的差的最大值是多少? ios::sync_with_stdio(0);cin.tie(0); cin>>m>>n; string str; s=0,t=n+m+1; for(int i=1,p,id;i<=m;i++){ cin>>p;//p 为赞助商同意支付该实验的费用 E[i]=p; getline(cin,str); adde(s,i,p); adde(i,s,0); stringstream inpt(str); while(inpt>>id){ adde(i,id+m,inf); adde(id+m,i,0); } } for(int i=1;i<=n;i++){ int p; cin>>p; adde(i+m,t,p); adde(t,i+m,0); I[i+m]=p; } //最小割选择点 dinic(); int g=0,k=0; for(int i=head[s];i;i=ed[i].nxt){ if(lvl[ed[i].v]!=-1){ cout<<ed[i].v<<' '; g+=E[ed[i].v]; } } cout<<endl; for(int i=head[t];i;i=ed[i].nxt){ if(lvl[ed[i].v]!=-1){ cout<<ed[i].v-m<<' '; k+=I[ed[i].v]; } } cout<<endl<<g-k; return 0; } ``` ::: ### [P1251 餐巾计划问题](https://www.luogu.com.cn/problem/P1251) 将一天拆成两个点,$i+n$ 是早上,$i$ 是晚上。早上收到的都是**干净的餐巾**。 **为了方便表示,下面的 $n,m,s,t$ 不与题目中的相同。** 建立超级源点汇点 $s,t$。 对于一天: - 晚上会产生 $need_i$ 的脏餐巾,于是将 $s$ 与 $i$ 建边 $\{need_i,0\}$。 - 早上需要 $need_i$ 的餐巾,于是将 $i+n$ 与 $t$ 建边 $\{need_i,0\}$,如果流满了,表示**足够用**。 - 快洗店:$i$ 天晚上送出餐巾,$i+day_f$ 天的早上($i+n+day_f$)收到餐巾,于是将 $i$ 与 $i+n+day_f$ 连 $\{\infty,cost_f\}$ 的边。毕竟要花钱嘛。 - 慢洗店:$i$ 天晚上送出餐巾,$i+day_s$ 天的早上($i+n+day_s$)收到餐巾,于是将 $i$ 与 $i+n+day_s$ 连 $\{\infty,cost_s\}$ 的边。 - 脏餐巾可以留到明天晚上,因为明天可能本来就能收到足够的干净餐巾。于是把 $i$ 与 $i+1$ 连一条 $\{\infty,0\}$ 的边。 - 最后,还可以早上直接买来干净毛巾,所以将 $s$ 与 $i+n$ 连接 $\{\infty,p\}$。 :::info[为什么要最小费用最大流] 必须要最大流量,才能把 $i+n\to t$ 的边流满,表示够用。 ::: :::info[code] ```cpp #include<bits/stdc++.h> using namespace std; #define int long long const int maxn=200001,inf=INT_MAX; int n,m,s,t,head[maxn],tot=2,dis[maxn],cur[maxn],a,b,p;//inqueue struct edge{ int u,v,w,c,nxt; }ed[maxn]; bool inq[maxn],vis[maxn]; void adde(int u,int v,int w,int c){ ed[tot]={u,v,w,c,head[u]}; head[u]=tot; tot++; } bool spfa(){ queue<int> q; for(int i=0;i<maxn;i++)dis[i]=inf; dis[s]=0; inq[s]=1; q.push(s); while(q.size()){ int u=q.front(); q.pop(); inq[u]=0; for(int i=head[u];i;i=ed[i].nxt){ int v=ed[i].v,c=ed[i].c,flow=ed[i].w; if(dis[v]>dis[u]+c&&flow){ dis[v]=dis[u]+c; if(!inq[v])q.push(v); inq[v]=1; } } } return dis[t]!=inf; } int dfs(int u,int cap){ int flow=0; if(u==t)return cap; vis[u]=1; for(int i=cur[u];i&&cap;i=ed[i].nxt){ cur[u]=i; int v=ed[i].v,c=ed[i].c,fl=ed[i].w; if(dis[v]==dis[u]+c&&fl&&!vis[v]){ int use=dfs(v,min(cap,fl)); if(use>0){ ed[i].w-=use; ed[i^1].w+=use; flow+=use; cap-=use; } } } vis[u]=0; return flow; } pair<int,int> dinic(){ int flow=0,cost=0; while(spfa()){ memcpy(cur,head,sizeof(head)); int fl=dfs(s,inf); flow+=fl; cost+=fl*dis[t]; } return {flow,cost}; } int nd[maxn],idx=0; signed main(){ int q,k,d,f; ios::sync_with_stdio(0);cin.tie(0); cin>>n; for(int i=1;i<=n;i++)cin>>nd[i]; cin>>p>>m>>f>>d>>k; s=0;t=n*2+1; for(int i=1;i<=n;i++){//i+n是早上,i是晚上 adde(s,i,nd[i],0); adde(i,s,0,0); adde(i+n,t,nd[i],0); adde(t,i+n,0,0); if(i+m<=n)adde(i,i+n+m,inf,f); if(i+m<=n)adde(i+n+m,i,0,-f); if(i+d<=n)adde(i,i+n+d,inf,k); if(i+d<=n)adde(i+n+d,i,0,-k); adde(s,i+n,inf,p); adde(i+n,s,0,-p); if(i+1<=n)adde(i,i+1,inf,0); if(i+1<=n)adde(i+1,i,0,0); } cout<<dinic().second; return 0; } ``` ::: ### [P4013 数字梯形问题](https://www.luogu.com.cn/problem/P4013) 这题太水了,降降降,蓝吧。 还是先建立超源超汇。 规则 1:将点拆成入点和出点,连边 $\{1,c_{i,j}\}$,表示只能经过一次。 规则 2:不用拆点了,边的容量设为 $1$,点权转边权即可。 规则 3:……太水了吧,容量 $\infty$。 就是注意清空。 :::info[code] ```cpp #include<bits/stdc++.h> using namespace std; const int maxn=10010,inf=INT_MAX; int s,t,head[maxn],tot=2,dis[maxn],cur[maxn]; struct edge{ int u,v,w,c,nxt; }ed[maxn]; bool vis[maxn],inq[maxn]; void adde(int u,int v,int w,int c){ ed[tot]={u,v,w,c,head[u]}; head[u]=tot; tot++; } bool spfa(){ for(int i=0;i<maxn;i++)dis[i]=-inf; queue<int> q; q.push(s); inq[s]=1; dis[s]=0; while(q.size()){ int u=q.front(); q.pop(); inq[u]=0; for(int i=head[u];i;i=ed[i].nxt){ int v=ed[i].v,flow=ed[i].w,c=ed[i].c; if(dis[v]<dis[u]+c&&flow){ dis[v]=dis[u]+c; if(!inq[v])q.push(v); } } } return dis[t]!=-inf; } int dfs(int u,int cap){ if(u==t)return cap; int flow=0; vis[u]=1; for(int i=cur[u];i&&cap;i=ed[i].nxt){ cur[u]=i; int v=ed[i].v,fl=ed[i].w,c=ed[i].c; if(dis[v]==dis[u]+c&&fl&&!vis[v]){ int use=dfs(v,min(cap,fl)); if(use>0){ ed[i].w-=use; ed[i^1].w+=use; cap-=use; flow+=use; } } } vis[u]=0; return flow; } pair<int,int> dinic(){ int flow=0,cost=0; while(spfa()){ memcpy(cur,head,sizeof(head)); int fl=dfs(s,inf); flow+=fl; cost+=fl*dis[t]; } return {flow,cost}; } void reset(){ tot=2; memset(head,0,sizeof(head)); memset(cur,0,sizeof(cur)); for(int i=0;i<maxn;i++)ed[i]={0,0,0,0,0}; } int num[40][40],n,m,idx=0,id[40][40]; signed main(){ ios::sync_with_stdio(0);cin.tie(0); cin>>m>>n; for(int i=1;i<=n;i++){ for(int j=1;j<=m+i-1;j++){ cin>>num[i][j]; id[i][j]=++idx; } } s=0; for(int i=1;i<=m;i++){ adde(s,id[1][i],1,0); adde(id[1][i],s,0,0); } for(int i=1;i<=n;i++){ for(int j=1;j<=m+i-1;j++){ adde(id[i][j],id[i][j]+idx,1,num[i][j]); adde(id[i][j]+idx,id[i][j],0,-num[i][j]); if(i<n)adde(id[i][j]+idx,id[i+1][j],1,0); if(i<n)adde(id[i+1][j],id[i][j]+idx,0,0); if(i<n)adde(id[i][j]+idx,id[i+1][j+1],1,0); if(i<n)adde(id[i+1][j+1],id[i][j]+idx,0,0); } } t=idx*2+1; for(int i=1;i<=m+n-1;i++){ adde(id[n][i]+idx,t,inf,0); adde(t,id[n][i]+idx,0,0); } cout<<dinic().second<<endl; //____________________________________________ reset(); for(int i=1;i<=m;i++){ adde(s,id[1][i],1,0); adde(id[1][i],s,0,0); } for(int i=1;i<=n;i++){ for(int j=1;j<=m+i-1;j++){ if(i<n)adde(id[i][j],id[i+1][j],1,num[i][j]); if(i<n)adde(id[i+1][j],id[i][j],0,-num[i][j]); if(i<n)adde(id[i][j],id[i+1][j+1],1,num[i][j]); if(i<n)adde(id[i+1][j+1],id[i][j],0,-num[i][j]); } } t=idx+1; for(int i=1;i<=m+n-1;i++){ adde(id[n][i],t,inf,num[n][i]); adde(t,id[n][i],0,-num[n][i]); } cout<<dinic().second<<endl; //____________________________________________ reset(); for(int i=1;i<=m;i++){ adde(s,id[1][i],1,0); adde(id[1][i],s,0,0); } for(int i=1;i<=n;i++){ for(int j=1;j<=m+i-1;j++){ if(i<n)adde(id[i][j],id[i+1][j],inf,num[i][j]); if(i<n)adde(id[i+1][j],id[i][j],0,-num[i][j]); if(i<n)adde(id[i][j],id[i+1][j+1],inf,num[i][j]); if(i<n)adde(id[i+1][j+1],id[i][j],0,-num[i][j]); } } t=idx+1; for(int i=1;i<=m+n-1;i++){ adde(id[n][i],t,inf,num[n][i]); adde(t,id[n][i],0,-num[n][i]); } cout<<dinic().second; return 0; } ``` ::: ### [P2765 魔术球问题](https://www.luogu.com.cn/problem/P2765) 双倍经验:[UVA10276 Hanoi Tower Troubles Again!](https://www.luogu.com.cn/problem/UVA10276) ::::info[题解]{open} ## [UVA10276 Hanoi Tower Troubles Again!](https://www.luogu.com.cn/problem/UVA10276) 前置知识:[P2764 最小路径覆盖问题](https://www.luogu.com.cn/problem/P2764)。 双倍经验:[P2765 魔术球问题](https://www.luogu.com.cn/problem/P2765)。~~这俩编号怎么只相差一。~~ ## 分析 ### Part 1:如何建模 我们考虑对于数 $i,j(i<j)$,如果 $i+j$ 是完全平方数,那么就连边。 可以发现这就是构成了一个有向无环图。 ### Part 2:如何根据球的数量计算柱子 其实,这就是一个最小路径覆盖。 让我们看样例的**一个**构造方案: ```cpp 11 1 8 2 7 9 3 6 10 4 5 11 ``` ![](https://cdn.luogu.com.cn/upload/image_hosting/1nay8l66.png) ![](https://cdn.luogu.com.cn/upload/image_hosting/ekejgs0o.png) 看出来了吗,这些颜色就是柱子上放的数! 当然,还有另一种构造方案: ![](https://cdn.luogu.com.cn/upload/image_hosting/954n14m4.png) 其中 1 号点颜色不同。 ### Part 3:如何计算路径覆盖 ~~由于我们懒得写网络流~~,于是我们翻出了匈牙利。 参见我写的:[【最小路径覆盖问题】](https://www.cnblogs.com/luogu-article/articles/19234660)。 我们先进行拆点,将 $u$ 拆成 $u_{in},u_{out}$。 根据 Dilworth 推广定理:最小路径覆盖数 = 顶点数 − 最大匹配数。 所以我们只需要跑二分图最大匹配了。 ```cpp for(int i=0;i<m;i++){ int u,v; cin>>u>>v; G[u].push_back(v);//这里实际上是拆点了 } ``` 为什么这里拆点了呢?其实左边的 $u$ 是 $u_{out}$,$v$ 就是 $v_{in}$。 ### Part 4:如何根据柱子的数量计算球 考虑二分球的数量。与 $n$ 比较大小即可。 ```cpp bool check(int mid){ if(solve(mid)<=n)return 1; return 0; } ``` ```cpp int l=1,r=M,mid; while(l<=r){ mid=(l+r)>>1; if(check(mid))l=mid+1,now=mid; else r=mid-1; // cout<<l<<' '<<r<<endl; } if(check(now-1))now++; if(!check(now-1))now--; cout<<now-1<<endl; ``` 我们发现边数大约为 $O(n^{3/2})$,所以时间复杂度大约 $O(n^{5/2}\log M)$,$V$ 可以取 $2000$。 当然你也可以不二分,直接枚举到 $V$,然后每次只加上新增的边跑二分图,这样是 $O(n^{3/2}V)$ 的。 :::info[code] ```cpp #include<bits/stdc++.h> using namespace std; const int maxn=10000; bool matched[maxn]; vector<int> G[maxn]; int match[maxn]; bool find(int u){ for(auto v:G[u]){ if(!matched[v]){ matched[v]=1; if(!match[v]||find(match[v])){ match[v]=u; return 1; } } } return 0; } const int M=10000; bool pd[M]; int solve(int ma){//根据球数算柱子 for(int i=0;i<maxn;i++)G[i].clear(); for(int i=1;i<=ma;i++){ for(int j=i+1;j<=ma;j++){ if((int)sqrt(i+j)*(int)sqrt(i+j)==i+j){ G[i].push_back(j);//这里实际上是拆点了 } } } int ans=0; memset(match,0,sizeof(match)); for(int i=1;i<=ma;i++){ memset(matched,0,sizeof(matched)); if(find(i))ans++; } return ma-ans; } int pre[maxn],nxt[maxn],n; bool vis[maxn]; bool check(int mid){ if(solve(mid)<=n)return 1; return 0; } signed main(){ int now=1; cin>>n; int l=1,r=M,mid; while(l<=r){ mid=(l+r)>>1; if(check(mid))l=mid+1,now=mid; else r=mid-1; // cout<<l<<' '<<r<<endl; } if(check(now-1))now++; if(!check(now-1))now--; cout<<now-1<<endl; // for(int v=1;v<=now-1;v++){ // int u=match[v]; // if(u){ // nxt[u]=v; // pre[v]=u; // } // } // memset(vis,0,sizeof(vis));//标记顶点是否已被包含在路径中 // vector<vector<int>>paths;//存储最终的所有路径 // for(int u=1;u<=now-1;u++){ // if(pre[u]==0){ // vector<int> path; // int v=u; // while(v){ // vis[v]=1; // path.push_back(v); // v=nxt[v]; // } // paths.push_back(path); // } // } // for(auto &path:paths){ // for(int i=0;i<path.size();i++){ // cout<<path[i]<<' '; // } // cout<<endl; // } return 0; } ``` ::: ### Part 5:优化 还是上网络流。这样单轮时间复杂度是 $O(m\sqrt n)=O(n^{3/2}\cdot n^{1/2})=O(n^2)$,总计 $O(n^2\log V)$,完全可过。 :::info[code] ```cpp #include<bits/stdc++.h> using namespace std; const int N=5055,M=125005,inf=2147483647; int m,s,t; int lvl[N];//层数 struct edge{ int u,v,w; int nxt; }ed[M*2]; int head[N],cur[N],cnt=2; void adde(int u,int v,int w){ ed[cnt].u=u; ed[cnt].v=v; ed[cnt].w=w; ed[cnt].nxt=head[u]; head[u]=cnt; cnt++; } bool bfs(){ queue<int> q; for(int i=0;i<N;i++)lvl[i]=-1; lvl[s]=0; q.push(s); while(!q.empty()){ int u=q.front(); q.pop(); for(int i=head[u];i;i=ed[i].nxt){ if(lvl[ed[i].v]==-1&&ed[i].w>0){ lvl[ed[i].v]=lvl[u]+1; q.push(ed[i].v); if(ed[i].v==t)return 1; } } } return(lvl[t]!=-1); } int dfs(int u,int cap){//cap:容量 int flow=0;//跑过的流 if(u==t)return cap; for(int i=cur[u];i&&cap;i=ed[i].nxt){ cur[u]=i; int v=ed[i].v,w=ed[i].w; if(w&&lvl[v]==lvl[u]+1){ int use=dfs(v,min(cap,w)); if(use==0)lvl[v]=-1; if(use>0){ ed[i].w-=use; ed[i^1].w+=use; cap-=use; flow+=use; } } } return flow; } int dinic(){ int ans=0; while(bfs()){ memcpy(cur,head,sizeof(head)); ans+=dfs(s,inf); } return ans; } const int MM=2000; bool pd[M]; int solve(int ma){//根据球数算柱子 s=0,t=ma*2+1; cnt=2; memset(head,0,sizeof(head)); memset(cur,0,sizeof(cur)); for(int i=1;i<=ma;i++){ adde(s,i,1); adde(i,s,0); adde(i+ma,t,1); adde(t,i+ma,0); for(int j=i+1;j<=ma;j++){ if((int)sqrt(i+j)*(int)sqrt(i+j)==i+j){ adde(i,j+ma,1); adde(j+ma,i,0); } } } return ma-dinic(); } int pre[M],nxt[M],n; bool vis[M]; bool check(int mid){ if(solve(mid)<=n)return 1; return 0; } signed main(){ int now=1; cin>>n; int l=1,r=MM,mid; while(l<=r){ mid=(l+r)>>1; if(check(mid))l=mid+1,now=mid; else r=mid-1; // cout<<l<<' '<<r<<endl; } if(check(now-1))now++; if(!check(now-1))now--; cout<<now-1<<endl; return 0; } ``` ::: :::: ### [P1361 小M的作物](https://www.luogu.com.cn/problem/P1361) 首先,我们先不考虑特殊组合,如果仅仅是作物 $i$ 放在 $A$ 上获得 $a_i$ 的收益,放在 $B$上获得 $b_i$ 的收益,如何建图? 这时,可以从 $A$ 到 $i$ 点连一条边,权值为 $a_i$,然后 $i$ 向 $B$ 连一条边,权值为 $b_i$,这时利用最小割切割一下,就表示 $i$ 要么放在 $A$ 上,要么放在 $B$ 上,与题意相符,针对样例,如下图: ![](https://cdn.luogu.com.cn/upload/image_hosting/yt9rr9da.png) 我们有了上面的方法之后,想到,可以为每一个组合建立两个虚拟节点 $X,Y$,分别与所包含的作物连边 $\{\infty\}$。(这是为了**保证无法被切割**,组合被切开你还想要拿 $c_{i,1},c_{i,2}$ 简直是痴心妄想)其中,一个组合节点 $X_i$ 与 $s$ 连边表示选了组合方法 $1$,$Y_i$ 与 $t$ 连边表示选了组合方法 $2$。 ![](https://cdn.luogu.com.cn/upload/image_hosting/2djakth7.png) **下面解释为什么不能只建一个节点:** 考虑只建一个节点。 ![](https://cdn.luogu.com.cn/upload/image_hosting/31yyym50.png) ![](https://cdn.luogu.com.cn/upload/image_hosting/awmehtb9.png) 显然是不选择组合的。 但是你又无法同时割掉 $0\to4,4\to5$。这样不满足最小割的定义。 ![](https://cdn.luogu.com.cn/upload/image_hosting/0jkh4ax6.png) 这根本不能割啊。 :::info[code] ```cpp #include<bits/stdc++.h> using namespace std; #define int long long const int maxn=2e5,inf=1e18; struct edge{ int u,v,w,nxt; }ed[maxn*10]; int head[maxn],cur[maxn],tot=2,lvl[maxn],s,t,a[maxn],b[maxn];//head:上一个 void adde(int u,int v,int w){ ed[tot]={u,v,w,head[u]}; head[u]=tot; tot++; } bool bfs(){ queue<int> q; for(int i=0;i<maxn;i++)lvl[i]=-1; lvl[s]=0; q.push(s); while(q.size()){ int u=q.front(); q.pop(); for(int i=head[u];i;i=ed[i].nxt){ int v=ed[i].v,flow=ed[i].w; if(lvl[v]==-1&&flow){ lvl[v]=lvl[u]+1; q.push(v); } } } return lvl[t]!=-1; } int dfs(int u,int cap){ if(u==t)return cap; int flow=0; for(int i=cur[u];i&&cap;i=ed[i].nxt){ cur[u]=i; int v=ed[i].v,fl=ed[i].w; if(lvl[v]==lvl[u]+1&&fl){ int use=dfs(v,min(cap,fl)); if(use>0){ flow+=use; cap-=use; ed[i].w-=use; ed[i^1].w+=use; } } } return flow; } int dinic(){ int ans=0; while(bfs()){ memcpy(cur,head,sizeof(head)); ans+=dfs(s,inf); } return ans; } signed main(){ ios::sync_with_stdio(0);cin.tie(0); int nn,mm,sum=0; cin>>nn; s=0; for(int i=1;i<=nn;i++){ cin>>a[i]; adde(s,i,a[i]); adde(i,s,0); sum+=a[i]; } for(int i=1;i<=nn;i++){ cin>>b[i]; sum+=b[i]; } cin>>mm; t=nn+mm*2+1; for(int i=1;i<=nn;i++){ adde(i,t,b[i]); adde(t,i,0); } int idx=nn+1; for(int i=1;i<=mm;i++){ int k,c1,c2; cin>>k>>c1>>c2; adde(s,idx,c1); adde(idx,s,0); for(int j=1;j<=k;j++){ int id; cin>>id; adde(idx,id,inf); adde(id,idx,0); adde(id,idx+1,inf); adde(idx+1,id,0); } adde(idx+1,t,c2); adde(t,idx+1,0); idx+=2; sum+=c1+c2; } cout<<sum-dinic(); return 0; } ``` ::: ### [P4313 文理分科](https://www.luogu.com.cn/problem/P4313) 这个和小 M 的作物很像。就是相当于把自己和周围同学捆绑成一个组合,建图还是和小 M 的作物一样。 :::info[code] ```cpp #include<bits/stdc++.h> using namespace std; #define int long long const int maxn=2e5,inf=1e18; struct edge{ int u,v,w,nxt; }ed[maxn*10]; int head[maxn],cur[maxn],tot=2,lvl[maxn],s,t,a[maxn],b[maxn],n,m;//head:上一个 void adde(int u,int v,int w){ ed[tot]={u,v,w,head[u]}; head[u]=tot; tot++; } bool bfs(){ queue<int> q; for(int i=0;i<maxn;i++)lvl[i]=-1; lvl[s]=0; q.push(s); while(q.size()){ int u=q.front(); q.pop(); for(int i=head[u];i;i=ed[i].nxt){ int v=ed[i].v,flow=ed[i].w; if(lvl[v]==-1&&flow){ lvl[v]=lvl[u]+1; q.push(v); } } } return lvl[t]!=-1; } int dfs(int u,int cap){ if(u==t)return cap; int flow=0; for(int i=cur[u];i&&cap;i=ed[i].nxt){ cur[u]=i; int v=ed[i].v,fl=ed[i].w; if(lvl[v]==lvl[u]+1&&fl){ int use=dfs(v,min(cap,fl)); if(use>0){ flow+=use; cap-=use; ed[i].w-=use; ed[i^1].w+=use; } } } return flow; } int dinic(){ int ans=0; while(bfs()){ memcpy(cur,head,sizeof(head)); ans+=dfs(s,inf); } return ans; } int id[110][110],idx=0,sum=0; signed main(){ ios::sync_with_stdio(0);cin.tie(0); cin>>n>>m; s=0; t=n*m*4+1; for(int i=1;i<=n;i++){ for(int j=1;j<=m;j++){ id[i][j]=++idx; } } for(int i=1;i<=n;i++){ for(int j=1;j<=m;j++){ int art; cin>>art; sum+=art; adde(s,id[i][j],art); adde(id[i][j],s,0); } } for(int i=1;i<=n;i++){ for(int j=1;j<=m;j++){ int sci; cin>>sci; sum+=sci; adde(id[i][j],t,sci); adde(t,id[i][j],0); } } for(int i=1;i<=n;i++){ for(int j=1;j<=m;j++){ int sma; cin>>sma; sum+=sma; int nw=++idx; adde(s,nw,sma); adde(nw,s,0); adde(nw,id[i][j],inf); adde(id[i][j],nw,0); if(j<m)adde(nw,id[i][j+1],inf); if(j<m)adde(id[i][j+1],nw,0); if(j>1)adde(nw,id[i][j-1],inf); if(j>1)adde(id[i][j-1],nw,0); if(i<n)adde(nw,id[i+1][j],inf); if(i<n)adde(id[i+1][j],nw,0); if(i>1)adde(nw,id[i-1][j],inf); if(i>1)adde(id[i-1][j],nw,0); } } for(int i=1;i<=n;i++){ for(int j=1;j<=m;j++){ int smc; cin>>smc; sum+=smc; int nw=++idx; adde(nw,t,smc); adde(t,nw,0); adde(id[i][j],nw,smc); adde(nw,id[i][j],0); if(j<m)adde(id[i][j+1],nw,inf); if(j<m)adde(nw,id[i][j+1],0); if(j>1)adde(id[i][j-1],nw,inf); if(j>1)adde(nw,id[i][j-1],0); if(i<n)adde(id[i+1][j],nw,inf); if(i<n)adde(nw,id[i+1][j],0); if(i>1)adde(id[i-1][j],nw,inf); if(i>1)adde(nw,id[i-1][j],0); } } cout<<sum-dinic(); return 0; } ``` ::: ### [P1646 [国家集训队] happiness](https://www.luogu.com.cn/problem/P1646) 相当于双倍经验吧,只是把组合拆开了。 :::info[code] ```cpp #include<bits/stdc++.h> using namespace std; #define int long long const int maxn=2e5,inf=1e18; struct edge{ int u,v,w,nxt; }ed[maxn*10]; int head[maxn],cur[maxn],tot=2,lvl[maxn],s,t,a[maxn],b[maxn],n,m;//head:上一个 void adde(int u,int v,int w){ ed[tot]={u,v,w,head[u]}; head[u]=tot; tot++; } bool bfs(){ queue<int> q; for(int i=0;i<maxn;i++)lvl[i]=-1; lvl[s]=0; q.push(s); while(q.size()){ int u=q.front(); q.pop(); for(int i=head[u];i;i=ed[i].nxt){ int v=ed[i].v,flow=ed[i].w; if(lvl[v]==-1&&flow){ lvl[v]=lvl[u]+1; q.push(v); } } } return lvl[t]!=-1; } int dfs(int u,int cap){ if(u==t)return cap; int flow=0; for(int i=cur[u];i&&cap;i=ed[i].nxt){ cur[u]=i; int v=ed[i].v,fl=ed[i].w; if(lvl[v]==lvl[u]+1&&fl){ int use=dfs(v,min(cap,fl)); if(use>0){ flow+=use; cap-=use; ed[i].w-=use; ed[i^1].w+=use; } } } return flow; } int dinic(){ int ans=0; while(bfs()){ memcpy(cur,head,sizeof(head)); ans+=dfs(s,inf); } return ans; } int id[110][110],idx=0,sum=0; signed main(){ ios::sync_with_stdio(0);cin.tie(0); cin>>n>>m; s=0; t=n*m*6+1; for(int i=1;i<=n;i++){ for(int j=1;j<=m;j++){ id[i][j]=++idx; } } for(int i=1;i<=n;i++){ for(int j=1;j<=m;j++){ int art; cin>>art; sum+=art; adde(s,id[i][j],art); adde(id[i][j],s,0); } } for(int i=1;i<=n;i++){ for(int j=1;j<=m;j++){ int sci; cin>>sci; sum+=sci; adde(id[i][j],t,sci); adde(t,id[i][j],0); } } for(int i=1;i<n;i++){ for(int j=1;j<=m;j++){ int val,nw=++idx; cin>>val; sum+=val; adde(nw,id[i][j],inf); adde(id[i][j],nw,0); adde(nw,id[i+1][j],inf); adde(id[i+1][j],nw,0); adde(s,nw,val); adde(nw,s,0); } } for(int i=1;i<n;i++){ for(int j=1;j<=m;j++){ int val,nw=++idx; cin>>val; sum+=val; adde(id[i][j],nw,inf); adde(nw,id[i][j],0); adde(id[i+1][j],nw,inf); adde(nw,id[i+1][j],0); adde(nw,t,val); adde(t,nw,0); } } for(int i=1;i<=n;i++){ for(int j=1;j<m;j++){ int val,nw=++idx; cin>>val; sum+=val; adde(nw,id[i][j],inf); adde(id[i][j],nw,0); adde(nw,id[i][j+1],inf); adde(id[i][j+1],nw,0); adde(s,nw,val); adde(nw,s,0); } } for(int i=1;i<=n;i++){ for(int j=1;j<m;j++){ int val,nw=++idx; cin>>val; sum+=val; adde(id[i][j],nw,inf); adde(nw,id[i][j],0); adde(id[i][j+1],nw,inf); adde(nw,id[i][j+1],0); adde(nw,t,val); adde(t,nw,0); } } cout<<sum-dinic(); return 0; } ``` ::: ### [P2057 [SHOI2007] 善意的投票 / [JLOI2010] 冠军调查](https://www.luogu.com.cn/problem/P2057) 将同意的与 $s$ 连容量为 $1$ 的边,不同意的就和 $t$ 连接。 为什么要双向边?因为 $u,v$ 互斥的话,可以是 $v$ 违反自己意愿,也可以 $u$ 违反自己意愿。 :::info[code] ```cpp #include<bits/stdc++.h> using namespace std; const int maxn=109310,inf=INT_MAX; int lvl[maxn],head[maxn],cur[maxn],tot=2,s,t; struct edge{ int u,v,w,nxt; }ed[maxn]; void adde(int u,int v,int w){ ed[tot]={u,v,w,head[u]}; head[u]=tot; tot++; } bool bfs(){ queue<int> q; for(int i=0;i<maxn;i++)lvl[i]=-1; lvl[s]=0; q.push(s); while(q.size()){ int u=q.front(); q.pop(); for(int i=head[u];i;i=ed[i].nxt){ int v=ed[i].v,flow=ed[i].w; if(lvl[v]==-1&&flow){ lvl[v]=lvl[u]+1; q.push(v); } } } return lvl[t]!=-1; } int dfs(int u,int cap){ int flow=0; if(u==t)return cap; for(int i=cur[u];i&&cap;i=ed[i].nxt){ cur[u]=i; int v=ed[i].v,fl=ed[i].w; if(lvl[v]==lvl[u]+1&&fl){ int use=dfs(v,min(cap,fl)); if(use>0){ flow+=use; cap-=use; ed[i].w-=use; ed[i^1].w+=use; } } } return flow; } int dinic(){ int flow=0; while(bfs()){ memcpy(cur,head,sizeof(head)); flow+=dfs(s,inf); } return flow; } bool agr[maxn]; signed main(){ ios::sync_with_stdio(0);cin.tie(0); int n,m; cin>>n>>m; s=0;t=n+1; for(int i=1;i<=n;i++){ cin>>agr[i]; if(agr[i]){ adde(s,i,1); adde(i,s,0); } else{ adde(i,t,1); adde(t,i,0); } } for(int i=1;i<=m;i++){ int u,v; cin>>u>>v; adde(u,v,1); adde(v,u,0); adde(v,u,1); adde(u,v,0); } cout<<dinic(); return 0; } ``` ::: :::info[为什么错误] 不建双向边,只能统计到 $v$ 违反自己意愿和 $u$ 违反自己意愿这两个中的一个,所以答案错误。 ::: :::error[错误代码!虽然目前数据能 AC]{open} upd:我自己交了 hack。 ```cpp #include<bits/stdc++.h> using namespace std; const int maxn=59310,inf=INT_MAX; int lvl[maxn],head[maxn],cur[maxn],tot=2,s,t; struct edge{ int u,v,w,nxt; }ed[maxn]; void adde(int u,int v,int w){ ed[tot]={u,v,w,head[u]}; head[u]=tot; tot++; } bool bfs(){ queue<int> q; for(int i=0;i<maxn;i++)lvl[i]=-1; lvl[s]=0; q.push(s); while(q.size()){ int u=q.front(); q.pop(); for(int i=head[u];i;i=ed[i].nxt){ int v=ed[i].v,flow=ed[i].w; if(lvl[v]==-1&&flow){ lvl[v]=lvl[u]+1; q.push(v); } } } return lvl[t]!=-1; } int dfs(int u,int cap){ int flow=0; if(u==t)return cap; for(int i=cur[u];i&&cap;i=ed[i].nxt){ cur[u]=i; int v=ed[i].v,fl=ed[i].w; if(lvl[v]==lvl[u]+1&&fl){ int use=dfs(v,min(cap,fl)); if(use>0){ flow+=use; cap-=use; ed[i].w-=use; ed[i^1].w+=use; } } } return flow; } int dinic(){ int flow=0; while(bfs()){ memcpy(cur,head,sizeof(head)); flow+=dfs(s,inf); } return flow; } bool agr[maxn]; signed main(){ ios::sync_with_stdio(0);cin.tie(0); int n,m; cin>>n>>m; s=0;t=n+1; for(int i=1;i<=n;i++){ cin>>agr[i]; if(agr[i]){ adde(s,i,1); adde(i,s,0); } else{ adde(i,t,1); adde(t,i,0); } } for(int i=1;i<=m;i++){ int u,v; cin>>u>>v; if(agr[v]&&!agr[u])swap(u,v); adde(u,v,1); adde(v,u,0); // adde(v,u,1); // adde(u,v,0); } cout<<dinic(); return 0; } ``` ::: ### [P2598 [ZJOI2009] 狼和羊的故事](https://www.luogu.com.cn/problem/P2598) 将 $s$ 与狼连接,将羊与 $t$ 连接,连接容量都为 $\infty$,表示这个格子就是狼/羊的,不能分开,最小割不可能割掉 $\infty$。 对于一个格子,和四周连边,如果割开了,就表示修栅栏。 最后最小割会把图分为两部分,一部分属于狼,一部分属于羊,之间没有连接,符合要求。 :::info[code] ```cpp #include<bits/stdc++.h> using namespace std; #define int long long const int maxn=109310,inf=INT_MAX; int lvl[maxn],head[maxn],cur[maxn],tot=2,s,t; struct edge{ int u,v,w,nxt; }ed[maxn]; void adde(int u,int v,int w){ ed[tot]={u,v,w,head[u]}; head[u]=tot; tot++; } bool bfs(){ queue<int> q; for(int i=0;i<maxn;i++)lvl[i]=-1; lvl[s]=0; q.push(s); while(q.size()){ int u=q.front(); q.pop(); for(int i=head[u];i;i=ed[i].nxt){ int v=ed[i].v,flow=ed[i].w; if(lvl[v]==-1&&flow){ lvl[v]=lvl[u]+1; q.push(v); } } } return lvl[t]!=-1; } int dfs(int u,int cap){ int flow=0; if(u==t)return cap; for(int i=cur[u];i&&cap;i=ed[i].nxt){ cur[u]=i; int v=ed[i].v,fl=ed[i].w; if(lvl[v]==lvl[u]+1&&fl){ int use=dfs(v,min(cap,fl)); if(use>0){ flow+=use; cap-=use; ed[i].w-=use; ed[i^1].w+=use; } } } return flow; } int dinic(){ int flow=0; while(bfs()){ memcpy(cur,head,sizeof(head)); flow+=dfs(s,inf); } return flow; } int n,m; int ld[110][110]; int id(int x,int y){ return (x-1)*m+y; } void add(int u,int v,int w){ adde(u,v,w); adde(v,u,0); } signed main(){ ios::sync_with_stdio(0);cin.tie(0); cin>>n>>m; s=0;t=n*m+1; for(int i=1;i<=n;i++){ for(int j=1;j<=m;j++){ cin>>ld[i][j]; if(ld[i][j]==1)add(s,id(i,j),inf); else if(ld[i][j]==2)add(id(i,j),t,inf); } } for(int i=1;i<=n;i++){ for(int j=1;j<=m;j++){ if(i<n)add(id(i,j),id(i+1,j),1); if(i>1)add(id(i,j),id(i-1,j),1); if(j<m)add(id(i,j),id(i,j+1),1); if(j>1)add(id(i,j),id(i,j-1),1); } } cout<<dinic(); return 0; } ``` ::: ### [P2472 [SCOI2007] 蜥蜴](https://www.luogu.com.cn/problem/P2472) 我们考虑一个格子,每个格子只能经过 $h_{i,j}$ 次,因此拆点。 将 $s$ 与蜥蜴的格子连边 $1$,表示有一只蜥蜴。 对于能够跳到的格子($dis\le d$)我们直接连边,边权为 $\infty$。 :::info[code] ```cpp #include<bits/stdc++.h> #define int long long using namespace std; const int N=1205,M=15005,inf=2147483647; int n,m,s,t,r,c; int lvl[N];//层数 struct edge{ int u,v,w; int nxt; }ed[M*2]; int head[N],cur[N],cnt=2; void adde(int u,int v,int w){ ed[cnt].u=u; ed[cnt].v=v; ed[cnt].w=w; ed[cnt].nxt=head[u]; head[u]=cnt; cnt++; } bool bfs(){ queue<int> q; for(int i=0;i<N;i++)lvl[i]=-1; lvl[s]=0; q.push(s); while(!q.empty()){ int u=q.front(); q.pop(); for(int i=head[u];i;i=ed[i].nxt){ if(lvl[ed[i].v]==-1&&ed[i].w>0){ lvl[ed[i].v]=lvl[u]+1; q.push(ed[i].v); if(ed[i].v==t)return 1; } } } return(lvl[t]!=-1); } int dfs(int u,int cap){//cap:容量 int flow=0;//跑过的流 if(u==t)return cap; for(int i=cur[u];i&&cap;i=ed[i].nxt){ cur[u]=i; int v=ed[i].v,w=ed[i].w; if(w&&lvl[v]==lvl[u]+1){ int use=dfs(v,min(cap,w)); if(use==0)lvl[v]=-1; if(use>0){ ed[i].w-=use; ed[i^1].w+=use; cap-=use; flow+=use; } } } return flow; } int dinic(){ int ans=0; while(bfs()){ memcpy(cur,head,sizeof(head)); ans+=dfs(s,inf); } return ans; } int h[21][21],d,zz[21][21]; int id(int x,int y){ if(x<1||y<1||x>r||y>c)return t; return (x-1)*c+y; } void add(int u,int v,int w){ adde(u,v,w); adde(v,u,0); } void connect(int x,int y,int X,int Y){ int now=id(x,y)+r*c,nx=id(X,Y); if(nx!=t){ if(!zz[X][Y])return; } add(now,nx,inf); } #define con(dx,dy) connect(i,j,i+dx,j+dy) signed main(){ ios::sync_with_stdio(0); cin.tie(0); cout.tie(0); cin>>r>>c>>d; s=0,t=r*c*2+1; for(int i=1;i<=r;i++){ string ss; cin>>ss; for(int j=1;j<=c;j++){ char he=ss[j-1]; if(he!='0'){ zz[i][j]=he-'0'; } } } for(int i=1;i<=r;i++){ for(int j=1;j<=c;j++){ if(zz[i][j]){ add(id(i,j),id(i,j)+r*c,zz[i][j]); if(i==1||j==1||i==r||j==c)add(id(i,j)+r*c,t,inf); if(d>=1){ con(1,0); con(-1,0); con(0,1); con(0,-1); } if(d>=2){ con(2,0); con(1,1); con(0,2); con(-1,1); con(-2,0); con(-1,-1); con(0,-2); con(1,-1); } if(d>=3){ con(3,0); con(2,1); con(2,2); con(1,2); con(0,3); con(-1,2); con(-2,2); con(-2,1); con(-3,0); con(-2,-1); con(-2,-2); con(-1,-2); con(0,-3); con(1,-2); con(2,-2); } if(d>=4){ con(4,0); con(3,1); con(3,2); con(2,3); con(1,3); con(0,4); con(-1,3); con(-2,3); con(-3,2); con(-3,1); con(-4,0); con(-3,-1); con(-3,-2); con(-2,-3); con(-1,-3); con(0,-4); con(1,-3); con(2,-3); con(3,-2); con(3,-1); } } } } int lz=0; for(int i=1;i<=r;i++){ for(int j=1;j<=c;j++){ char ch; cin>>ch; if(ch=='L'){ lz++; add(s,id(i,j),1); } } } cout<<lz-dinic(); return 0; } ``` ::: ### [P4174 [NOI2006] 最大获利](https://www.luogu.com.cn/problem/P4174) 与 [SP1476 PROFIT](https://www.luogu.com.cn/problem/SP1476) 类似 [P2762 太空飞行计划问题](https://www.luogu.com.cn/problem/P2762),但是放一个题解吧。 首先我们来看题目。每个用户依赖 $A_i,B_i$,然后可以获得 $C_i$ 的价格。 还是超源超汇,将 $s$ 与服务站连接,容量为 $p_i$,将顾客与 $t$ 连接,将顾客和需要的服务站连接,容量为 $c_i$。中间再进行连接,将 容量为 $\infty$。 这里其实是把容量看成权值进行决策。 考虑证明答案最优。 设选出来的人集合为 $P$,选出来的服务站集合为 $Q ![](https://cdn.luogu.com.cn/upload/image_hosting/dbxsecn9.png) 可以参考上面的图片,红色是割。 可以看到 $\sum\limits_{i\notin P}p_i+\sum\limits_{j\in Q}c_j$ 就是最小割容量 $Cut(G)$。(绿色,橙色部分) 所以 $ans=\sum p-Cut(G)$,最小化割即可。 :::info[code] ```cpp #include<bits/stdc++.h> using namespace std; int m,n; const int maxn=800001,inf=INT_MAX; int lvl[maxn],tot=2,head[maxn],cur[maxn],s,t; struct edge{ int u,v,w,nxt; }ed[maxn]; void adde(int u,int v,int w){ ed[tot]={u,v,w,head[u]}; head[u]=tot; tot++; } bool bfs(){ queue<int> q; for(int i=0;i<maxn;i++)lvl[i]=-1; lvl[s]=0; q.push(s); while(q.size()){ int u=q.front(); q.pop(); for(int i=head[u];i;i=ed[i].nxt){ int v=ed[i].v,flow=ed[i].w; if(lvl[v]==-1&&flow){ lvl[v]=lvl[u]+1; q.push(v); } } } return lvl[t]!=-1; } int dfs(int u,int cap){ if(u==t)return cap; int flow=0; for(int i=cur[u];i&&cap;i=ed[i].nxt){ cur[u]=i; int v=ed[i].v,fl=ed[i].w; if(lvl[v]==lvl[u]+1){ int use=dfs(v,min(cap,fl)); if(use>0){ cap-=use; flow+=use; ed[i].w-=use; ed[i^1].w+=use; } } } return flow; } void dinic(){ while(bfs()){ memcpy(cur,head,sizeof(head)); int tmp=dfs(s,inf); } } int E[maxn],I[maxn]; signed main(){ //有m个实验,每个实验只可以进行一次,但会获得相应的奖金。 //有n个仪器,每个实验都需要一定的仪器,每个仪器可以运用于多个实验,但需要一定的价值。 //问奖金与代价的差的最大值是多少? ios::sync_with_stdio(0);cin.tie(0); int T; cin>>T; while(T--){ cin>>n>>m; string str; s=0,t=n+m+1; tot=2; memset(head,0,sizeof(head)); for(int i=0;i<maxn;i++)ed[i]={0,0,0,0}; memset(E,0,sizeof(E)); memset(I,0,sizeof(I)); for(int i=1;i<=n;i++){ int p; cin>>p; adde(i+m,t,p); adde(t,i+m,0); I[i+m]=p; } for(int i=1;i<=m;i++){ int idA,idB,p; cin>>idA>>idB; adde(i,idA+m,inf); adde(idA+m,i,0); adde(i,idB+m,inf); adde(idB+m,i,0); cin>>p;//p 为赞助商同意支付该实验的费用 E[i]=p; adde(s,i,p); adde(i,s,0); } //最小割选择点 dinic(); int g=0,k=0; for(int i=head[s];i;i=ed[i].nxt){ if(lvl[ed[i].v]!=-1){ g+=E[ed[i].v]; } } for(int i=head[t];i;i=ed[i].nxt){ if(lvl[ed[i].v]!=-1){ k+=I[ed[i].v]; } } cout<<g-k<<endl; } return 0; } ``` ::: ### [P4177 [CEOI 2008] order](https://www.luogu.com.cn/problem/P4177) 这道题新增了租借功能,可以对于单个工作,租借机器。 怎么处理呢? 考虑在之前的建模上进行修改,将工作与机器之间的连边的边权从 $\infty$ 改为 $b_{ij}$。 为什么这样是对的? 想一下之前为什么要设为 $\infty$:是为了让最小割无法选择这条边割掉,来保证**选了某一个工作就必须选择所有对应的机器**。 但现在不是了。选择了某一个工作,不一定要选择所有对应的机器,还可以花钱 $b_{ij}$ 租机器。 ~~不要在意注释。~~ :::info[code] ```cpp #include<bits/stdc++.h> using namespace std; int m,n; const int maxn=3000001,inf=INT_MAX; int lvl[maxn],tot=2,head[maxn],cur[maxn],s,t; struct edge{ int u,v,w,nxt; }ed[maxn]; void adde(int u,int v,int w){ ed[tot]={u,v,w,head[u]}; head[u]=tot; tot++; } bool bfs(){ queue<int> q; for(int i=0;i<maxn;i++)lvl[i]=-1; lvl[s]=0; q.push(s); while(q.size()){ int u=q.front(); q.pop(); for(int i=head[u];i;i=ed[i].nxt){ int v=ed[i].v,flow=ed[i].w; if(lvl[v]==-1&&flow){ lvl[v]=lvl[u]+1; q.push(v); } } } return lvl[t]!=-1; } int dfs(int u,int cap){ if(u==t)return cap; int flow=0; for(int i=cur[u];i&&cap;i=ed[i].nxt){ cur[u]=i; int v=ed[i].v,fl=ed[i].w; if(lvl[v]==lvl[u]+1){ int use=dfs(v,min(cap,fl)); if(use>0){ cap-=use; flow+=use; ed[i].w-=use; ed[i^1].w+=use; } } } return flow; } int dinic(){ int ans=0; while(bfs()){ memcpy(cur,head,sizeof(head)); ans+=dfs(s,inf); } return ans; } int E[maxn],I[maxn]; signed main(){ //有m个实验,每个实验只可以进行一次,但会获得相应的奖金。 //有n个仪器,每个实验都需要一定的仪器,每个仪器可以运用于多个实验,但需要一定的价值。 //问奖金与代价的差的最大值是多少? ios::sync_with_stdio(0);cin.tie(0); cin>>n>>m; long long sum=0; s=0,t=n+m+1; for(int i=1;i<=n;i++){ int x,p; cin>>x>>p; sum+=x; adde(s,i,x); adde(i,s,0); for(int j=1;j<=p;j++){ int a,b; cin>>a>>b; adde(i,a+n,b); adde(a+n,i,0); } } for(int i=1;i<=m;i++){ int y; cin>>y; adde(i+n,t,y); adde(t,i+n,0); } cout<<sum-dinic(); return 0; } // 喵喵喵! ``` ::: ### [P3980 [NOI2008] 志愿者招募](https://www.luogu.com.cn/problem/P3980) 首先建立超级源点和汇点,提供流量。 注:本题中使用 $\inf$ 而非 $\infty$,因为这两者不同。 对于每一天,将 $i,i+1$ 连接容量 $\inf-a_i$,表示需求,每一种志愿者连接 $\{s_i,t_i+1,\inf,c_i\}$。 为什么要这样做? 我们发现,志愿者连的边其实就是不够的,还需要补充的人数。 那为什么是 $s_i\to t_i+1$ 呢? 这表示每一种志愿者管辖的区间。在 $s_i$ 天,一些志愿者沿着边流走,表示使用了这些志愿者,到了第 $t_i+1$ 天,他们流回来了,表示这些志愿者的服务结束了。 **事实上,如果主轴流量为 $f$,表示当前的需求为 $\inf -f$。志愿者流走,表示志愿者进行了工作,需求减少了。** :::info[code] ```cpp #include<bits/stdc++.h> using namespace std; const int maxn=500100,inf=INT_MAX; int n,m,s,t,head[maxn],tot=2,dis[maxn],cur[maxn];//inqueue struct edge{ int u,v,w,c,nxt; }ed[maxn]; bool inq[maxn],vis[maxn]; queue<int> q; void adde(int u,int v,int w,int c){ ed[tot]={u,v,w,c,head[u]}; head[u]=tot; tot++; } bool spfa(){ for(int i=0;i<maxn;i++)dis[i]=inf; dis[s]=0; inq[s]=1; q.push(s); while(q.size()){ int u=q.front(); q.pop(); inq[u]=0; for(int i=head[u];i;i=ed[i].nxt){ int v=ed[i].v,c=ed[i].c,flow=ed[i].w; if(dis[v]>dis[u]+c&&flow){ dis[v]=dis[u]+c; if(!inq[v])q.push(v); inq[v]=1; } } } return dis[t]!=inf; } int dfs(int u,int cap){ int flow=0; if(u==t)return cap; vis[u]=1; for(int i=cur[u];i&&cap;i=ed[i].nxt){ cur[u]=i; int v=ed[i].v,c=ed[i].c,fl=ed[i].w; if(dis[v]==dis[u]+c&&fl&&!vis[v]){ int use=dfs(v,min(cap,fl)); if(use>0){ ed[i].w-=use; ed[i^1].w+=use; flow+=use; cap-=use; } } } vis[u]=0; return flow; } pair<int,int> dinic(){ int flow=0,cost=0; while(spfa()){ memcpy(cur,head,sizeof(head)); int fl=dfs(s,inf); flow+=fl; cost+=fl*dis[t]; } return {flow,cost}; } signed main(){ ios::sync_with_stdio(0);cin.tie(0); cin>>n>>m; s=0,t=n+2; adde(s,1,inf,0); adde(1,s,0,0); adde(n+1,t,inf,0); adde(t,n+1,0,0); for(int i=1;i<=n;i++){ int ai; cin>>ai; adde(i,i+1,inf-ai,0); adde(i+1,i,0,0); } for(int i=1;i<=m;i++){ int l,r,c; cin>>l>>r>>c; adde(l,r+1,inf,c); adde(r+1,l,0,-c); } cout<<dinic().second; return 0; } ``` ::: ### [P2457 [SDOI2006] 仓库管理员的烦恼](https://www.luogu.com.cn/problem/P2457) 考虑如果这个仓库 $i$ 含有 $a_{i,j}$ 个 $j$ 物品,那么这个仓库全放 $j$ 的代价就是 $sum_j-a_{i,j}$。 考虑建立二分图,左边为物品,右边为仓库,物品 $j$ 与仓库 $i$ 之间连边,容量为 $1$,费用为 $sum_j-a_{i,j}$。(实际上,只要容量 $>1$ 就行了) 然后就是二分图最小权最大匹配了。 :::info[为什么是最大匹配] 因为要将每一个仓库装满,并且: - 每一种物品必须在同一个仓库中。 - 每一个仓库中只有一种物品。 ::: :::info[code] ```cpp #include<bits/stdc++.h> using namespace std; const int maxn=500100,inf=INT_MAX; int n,m,s,t,head[maxn],tot=2,dis[maxn],cur[maxn];//inqueue struct edge{ int u,v,w,c,nxt; }ed[maxn]; bool inq[maxn],vis[maxn]; queue<int> q; void adde(int u,int v,int w,int c){ ed[tot]={u,v,w,c,head[u]}; head[u]=tot; tot++; } bool spfa(){ for(int i=0;i<maxn;i++)dis[i]=inf; dis[s]=0; inq[s]=1; q.push(s); while(q.size()){ int u=q.front(); q.pop(); inq[u]=0; for(int i=head[u];i;i=ed[i].nxt){ int v=ed[i].v,c=ed[i].c,flow=ed[i].w; if(dis[v]>dis[u]+c&&flow){ dis[v]=dis[u]+c; if(!inq[v])q.push(v); inq[v]=1; } } } return dis[t]!=inf; } int dfs(int u,int cap){ int flow=0; if(u==t)return cap; vis[u]=1; for(int i=cur[u];i&&cap;i=ed[i].nxt){ cur[u]=i; int v=ed[i].v,c=ed[i].c,fl=ed[i].w; if(dis[v]==dis[u]+c&&fl&&!vis[v]){ int use=dfs(v,min(cap,fl)); if(use>0){ ed[i].w-=use; ed[i^1].w+=use; flow+=use; cap-=use; } } } vis[u]=0; return flow; } pair<int,int> dinic(){ int flow=0,cost=0; while(spfa()){ memcpy(cur,head,sizeof(head)); int fl=dfs(s,inf); flow+=fl; cost+=fl*dis[t]; } return {flow,cost}; } int pt[155][155],sum[155]; signed main(){ ios::sync_with_stdio(0);cin.tie(0); int n; cin>>n; s=0,t=n*2+2; for(int i=1;i<=n;i++){//仓库 for(int j=1;j<=n;j++){//货物 cin>>pt[i][j]; sum[j]+=pt[i][j]; } } for(int i=1;i<=n;i++){ adde(s,i,1,0); adde(i,s,0,0); adde(i+n,t,1,0); adde(t,i+n,0,0); } for(int i=1;i<=n;i++){//货物 for(int j=1;j<=n;j++){//仓库 int val=pt[j][i]; adde(i,j+n,1,sum[i]-pt[j][i]); adde(j+n,i,0,-sum[i]+pt[j][i]); } } cout<<dinic().second; return 0; } ``` ::: ## 最小割树 最小割树是一种**求解两点之间最小割的算法**。 具体地,我们使用分治法。 我们先在原图 $G$ 中**任意选择两点** $u,v$,得到他们的最小割。 那么我们就得到了两个**点集** $U,V$。 我们再在 $U$ 中选择 $u',v'$,继续跑最小割,再继续分。 这样为什么是对的呢? 设 $C(u,v)$ 表示 $u,v$ 之间的最小割,$F(u,v)$ 表示 $u,v$ 之间的最大流。 我们知道,$\forall u,v\in G,F(u,v)=C(u,v)$。 :::info[引理 1:$\forall x\in U,y\in V,C(x,y)\le C(u,v)$] 反证法,设 $C(x,y)>C(u,v)$,那么 $F(x,y)>F(u,v)$,显然不成立。 ::: 我们构造一棵树 $T$,将每次选择的 $u,v$ 连接,边权为 $C(u,v)$。 :::info[为什么能构成树] 首先考虑边数。递归树是一棵二叉树,叶节点($|S_{now}|=1$)的个数为 $n$,那么非叶节点个数为 $n-1$。 由于只有在 $|S_{now}|>1$ 时分割,所以分割次数为 $n-1$,对应边数为 $n-1$。 连通性显然。 ::: 那么 $C(u,v)$ 就转化为 $u,v$ 在 $T$ 上的路径边权最小值。 :::info[证明] 割掉 $w_{\min}$ 显然 $u,v$ 分开,所以这个一个割,由于最小割是割中最小的,所以 $c\le w_{\min}$。 考虑存在一条树边 $(a,b)$,且这条边被割开,即 $a\in U,b\in V$。 由引理 1,如果树边 $(a,b)$ 边权为 $w$,那么 $w\le c$。 所以 $w_{\min}\le w\le c$。 $w_{\min}\le c\le w_{\min},w_{\min}=c$。 ::: 使用树剖解决。 ~~怎么会有【数据删除】写树剖啊!~~ $n$ 这么小,其实暴力可行。 :::warning[树剖的注意]{open} 我们需要使用边权转点权。 ![](https://cdn.luogu.com.cn/upload/image_hosting/ven7ak4a.png) ![](https://cdn.luogu.com.cn/upload/image_hosting/ju2u1kvu.png) 但是,记得在查询 $u,v$ 时,不要计算 $\operatorname{lca}(u,v)$ 的边权! ![](https://cdn.luogu.com.cn/upload/image_hosting/cue6wi0d.png) ::: ### [P4897 【模板】最小割树(Gomory-Hu Tree)](https://www.luogu.com.cn/problem/P4897) :::info[code] ```cpp #include<bits/stdc++.h> using namespace std; #define endl '\n' const int N=510,M=6514,inf=INT_MAX; int head[N],cur[N],lvl[N],tot=2; struct edge{ int u,v,w,nxt; }ed[M],ed2[M]; void adde(int u,int v,int w){ ed[tot]={u,v,w,head[u]}; head[u]=tot; tot++; } bool bfs(int s,int t){ for(int i=0;i<N;i++)lvl[i]=-1; lvl[s]=0; queue<int> q; q.push(s); while(q.size()){ int u=q.front(); q.pop(); for(int i=head[u];i;i=ed[i].nxt){ int v=ed[i].v,flow=ed[i].w; if(flow&&lvl[v]==-1){ lvl[v]=lvl[u]+1; q.push(v); } } } return lvl[t]!=-1; } int dfs(int t,int u,int cap){ if(u==t)return cap; int flow=0; for(int i=cur[u];i&&cap;i=ed[i].nxt){ cur[u]=i; int v=ed[i].v,fl=ed[i].w; if(lvl[v]==lvl[u]+1&&fl){ int use=dfs(t,v,min(cap,fl)); if(use>0){ ed[i].w-=use; ed[i^1].w+=use; flow+=use; cap-=use; } } } return flow; } int dinic(int s,int t){ int flow=0; while(bfs(s,t)){ memcpy(cur,head,sizeof(head)); flow+=dfs(t,s,inf); } return flow; } int n,m; struct TreeE{ int v,w; }; vector<TreeE> T[N]; bitset<N> ins; vector<int> pihua; int dep[N],top[N],fa[N],siz[N],heavy[N],dfn[N],ord=0,td[N],val[N]; void div(vector<int> S){//分治 if(S.size()<=1)return; int s=S[0],t=S[1]; for(int i=0;i<M;i++)ed[i]=ed2[i]; int cut=dinic(s,t); T[s].push_back({t,cut}); T[t].push_back({s,cut}); vector<int> lf,rg; for(auto i:S){ if(lvl[i]!=-1)lf.push_back(i); else rg.push_back(i); } div(lf); div(rg); } struct segment{ int l,r,min; }t[N*4]; #define ls id<<1 #define rs id<<1|1 void up(int id){ t[id].min=min(t[ls].min,t[rs].min); } void build(int id,int l,int r){ t[id]={l,r,inf}; if(l==r){ t[id].min=val[td[l]]; return; } int mid=(l+r)>>1; build(ls,l,mid); build(rs,mid+1,r); up(id); } int query(int id,int l,int r){ if(t[id].l>r||l>t[id].r)return inf; if(l<=t[id].l&&t[id].r<=r)return t[id].min; return min(query(ls,l,r),query(rs,l,r)); } void dfs1(int u,int f){ dep[u]=dep[f]+1; fa[u]=f; siz[u]=1; int miz=0; for(auto [v,tmp]:T[u]){ if(v!=f){ dfs1(v,u); siz[u]+=siz[v]; if(siz[v]>miz)heavy[u]=v,miz=siz[v]; } } } void dfs2(int u,int tp){ top[u]=tp; dfn[u]=++ord; td[ord]=u; if(!heavy[u])return; dfs2(heavy[u],tp); for(auto [v,tmp]:T[u]){ if(v==fa[u])continue; if(v!=heavy[u])dfs2(v,v); } } void tov(int u){ for(auto [v,vv]:T[u]){ if(v!=fa[u]){ val[v]=vv; tov(v); } } } int get(int u,int v){ int ans=inf; while(top[u]!=top[v]){ if(dep[top[u]]<dep[top[v]])swap(u,v); ans=min(ans,query(1,dfn[top[u]],dfn[u])); u=fa[top[u]]; } if(dep[u]>dep[v])swap(u,v); if(u!=v)ans=min(ans,query(1,dfn[u]+1,dfn[v])); return ans; } int root=1; signed main(){ ios::sync_with_stdio(0);cin.tie(0); top[root]=root; cin>>n>>m; n++; while(m--){ int u,v,w; cin>>u>>v>>w; u++,v++; adde(u,v,w); adde(v,u,0); adde(v,u,w); adde(u,v,0); } for(int i=0;i<M;i++)ed2[i]=ed[i]; for(int i=1;i<=n;i++)pihua.push_back(i);//点集 div(pihua); for(int i=0;i<N;i++)val[i]=inf; dfs1(root,root); dfs2(root,1); tov(root); build(1,1,n); int q; cin>>q; while(q--){ int u,v; cin>>u>>v; u++,v++; cout<<get(u,v)<<endl; } return 0; } ``` ::: ## 上下界网络流 这种网络流不仅有最高流量,还有最低流量限制,也就是边的流量满足 $l(u,v)\le flow(u,v)\le r(u,v)$。 ## 无源汇上下界可行流 设 $H(u\to v)$ 表示有 $u\to v$ 这条边。 其实就是:请设计边权,满足下列条件: 1. $l(u,v)\le w(u,v)\le r(u,v)$。 2. $\forall u\in V,\sum\limits_{\{x\in V|H(x\to u)\}}w(x,u)=\sum\limits_{\{x\in V|H(u\to x)\}}w(u,x)$。 我们建立下界网络 $G_{\text{low}}$。 我们从[阴阳](https://cn.puzzle-yin-yang.com/)中联想到可以用**调整大法**。直接假设现在网络已经流了下界的流量 $l(u,v)$。 现在,这个流函数 $f(u)=\sum\limits_{\{x\in V|H(x\to u)\}}w(x,u)-\sum\limits_{\{x\in V|H(u\to x)\}}w(u,x)$ 不一定为 $0$,开始在 $G_{\text{low}}$ 调整。 - 如果 $f(u)=0$,不用调整。 - 如果 $f(u)<0$,将超级源点 $s$ 和 $u$ 连接边 $|f(u)|$,表示提供流。 - 如果 $f(u)>0$,将 $u$ 和超级汇点 $t$ 连接边 $|f(u)|$,表示提供流。 - 跑最大流,如果 $s$ 连接的所有边全部流满了,那么说明有解,每一条边的流量是 $l_i+use_i$,也就是下界与 $G_{\text{low}}$ 最大流流过的流量之和。 ```cpp for(int i=1;i<=m;i++){ cout<<R[i]-ed[i<<1].w<<endl;//就是L[i]+ed[i<<1|1].w } ``` 上面的代码中,`ed[i<<1].w` 就是剩下的流量,`ed[i<<1|1].w` 就是流过的流量,反正怎么写都是一样的。 :::info[为什么不用判断和 $t$ 连接的边是否流满]{open} 流量守恒,这样多余了,没有必要。 ::: :::info[为什么要跑最大流]{open} 我们的目的是尽可能让网络平衡,所以要让最大流来流满新建的差值边。 ::: :::info[code] ```cpp #include<bits/stdc++.h> #define int long long using namespace std; const int N=1205,M=25005,inf=2147483647; int n,m,s,t; int lvl[N];//层数 struct edge{ int u,v,w; int nxt; }ed[M*2]; int head[N],cur[N],cnt=2,infl[N],outfl[N],R[M]; void adde(int u,int v,int w){ ed[cnt].u=u; ed[cnt].v=v; ed[cnt].w=w; ed[cnt].nxt=head[u]; head[u]=cnt; cnt++; } bool bfs(){ queue<int> q; for(int i=0;i<=n+1;i++)lvl[i]=-1; lvl[s]=0; q.push(s); while(!q.empty()){ int u=q.front(); q.pop(); for(int i=head[u];i;i=ed[i].nxt){ if(lvl[ed[i].v]==-1&&ed[i].w>0){ lvl[ed[i].v]=lvl[u]+1; q.push(ed[i].v); if(ed[i].v==t)return 1; } } } return(lvl[t]!=-1); } int dfs(int u,int cap){//cap:容量 int flow=0;//跑过的流 if(u==t)return cap; for(int i=cur[u];i&&cap;i=ed[i].nxt){ cur[u]=i; int v=ed[i].v,w=ed[i].w; if(w&&lvl[v]==lvl[u]+1){ int use=dfs(v,min(cap,w)); if(use==0)lvl[v]=-1; if(use>0){ ed[i].w-=use; ed[i^1].w+=use; cap-=use; flow+=use; } } } return flow; } int dinic(){ int ans=0; while(bfs()){ cerr<<"ok"; memcpy(cur,head,sizeof(head)); ans+=dfs(s,inf); } return ans; } signed main(){ cin>>n>>m; for(int i=1;i<=m;i++){ int u,v,l,r; cin>>u>>v>>l>>r; adde(u,v,r-l); adde(v,u,0); outfl[u]+=l; infl[v]+=l; R[i]=r; } s=0,t=n+1; bool pd=1; for(int i=1;i<=n;i++){ if(infl[i]>outfl[i]){//连接s->i adde(s,i,infl[i]-outfl[i]); adde(i,s,0); } else if(infl[i]<outfl[i]){ adde(i,t,outfl[i]-infl[i]); adde(t,i,0); } } int ans=dinic(); for(int i=head[s];i&&pd;i=ed[i].nxt){ if(ed[i].w)pd=0; } if(pd){ cout<<"Yes\n"; for(int i=1;i<=m;i++){ cout<<R[i]-ed[i<<1].w<<endl; } } else cout<<"No"; return 0; } ``` ::: ## 有源汇上下界可行流 我们仍然遵循无源汇上下界可行流的做法,将**真实汇点 $T$ 与真实源点 $S$ 连接一条上界为 $\infty$,下界为 $0$ 的引流边**,将 $T$ 的流量引到 $S$。 引流边的流量就是可行流大小,因为这对应着 $T$ 的流出。 :::info[为什么要引流]{open} 比较无源汇,这里要求变了。 设 $V'=V\setminus\{S,T\}$。 1. $l(u,v)\le w(u,v)\le r(u,v)$。 2. $\forall u\in V',\sum\limits_{\{x\in V|H(x\to u)\}}w(x,u)=\sum\limits_{\{x\in V|H(u\to x)\}}w(u,x)$。 3. $\sum\limits_{\{x\in V|H(x\to T)\}}w(x,T)=\sum\limits_{\{x\in V|H(S\to x)\}}w(S,x)

说人话就是:S 点有净流出,T 点有净流入,且这两个相等。

我们要把这个网络变得循环,连一条边即可。 :::

有源汇上下界最大流

跑完可行流之后,删掉新建的 T\to S 的边,跑最大流。 :::info[为什么并不是直接在增量网络上跑最大流,而是在跑完可行流后的残量网络上再去跑最大流。]{open} 我们还是使用调整的方式思考,对于这个可行流,我们直接在这个网络上增广,才能保证至少可行。 ::: :::info[code]

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=1205,M=25005,inf=2147483647;
int n,m,s,t,S,T;
int lvl[N];//层数
struct edge{
    int u,v,w;
    int nxt;
}ed[M*2];
int head[N],cur[N],cnt=2,infl[N],outfl[N],R[M];
void adde(int u,int v,int w){
    ed[cnt].u=u;
    ed[cnt].v=v;
    ed[cnt].w=w; 
    ed[cnt].nxt=head[u];
    head[u]=cnt;
    cnt++; 
}
bool bfs(){
    queue<int> q;
    for(int i=0;i<=n+1;i++)lvl[i]=-1;
    lvl[s]=0;
    q.push(s);
    while(!q.empty()){
        int u=q.front();
        q.pop();
        for(int i=head[u];i;i=ed[i].nxt){
            if(lvl[ed[i].v]==-1&&ed[i].w>0){
                lvl[ed[i].v]=lvl[u]+1;  
                q.push(ed[i].v);
                if(ed[i].v==t)return 1;
            }
        }
    }
    return(lvl[t]!=-1);
}
int dfs(int u,int cap){//cap:容量
    int flow=0;//跑过的流
    if(u==t)return cap;
    for(int i=cur[u];i&&cap;i=ed[i].nxt){
        cur[u]=i;
        int v=ed[i].v,w=ed[i].w;
        if(w&&lvl[v]==lvl[u]+1){
            int use=dfs(v,min(cap,w));
            if(use==0)lvl[v]=-1;
            if(use>0){
                ed[i].w-=use;
                ed[i^1].w+=use;
                cap-=use;
                flow+=use;
            }
        }
    }
    return flow;
}
int dinic(){
    int ans=0;
    while(bfs()){
        cerr<<"ok";
        memcpy(cur,head,sizeof(head));
        ans+=dfs(s,inf);
    }
    return ans;
}
signed main(){
    cin>>n>>m>>S>>T;
    for(int i=1;i<=m;i++){
        int u,v,l,r;
        cin>>u>>v>>l>>r;
        adde(u,v,r-l);
        adde(v,u,0);
        outfl[u]+=l;
        infl[v]+=l;
        R[i]=r;
    }
    s=0,t=n+1;
    bool pd=1;
    for(int i=1;i<=n;i++){
        if(infl[i]>outfl[i]){//连接s->i
            adde(s,i,infl[i]-outfl[i]);
            adde(i,s,0);
        }
        else if(infl[i]<outfl[i]){
            adde(i,t,outfl[i]-infl[i]);
            adde(t,i,0);
        }
    }
    adde(T,S,inf);
    int sp=cnt;
    adde(S,T,0);
    dinic();
    for(int i=head[s];i&&pd;i=ed[i].nxt){
        if(ed[i].w)pd=0;
    }
    int able=ed[sp].w;//可行流
    if(!pd){
        cout<<"N";
        return 0;
    }
    ed[sp].w=ed[sp^1].w=0;//删边
    s=S,t=T;
    cout<<able+dinic();
    return 0;
}

:::

有源汇上下界最小流

我们还是使用调整的方式思考,对于这个可行流,我们直接在这个网络上退流,才能保证至少可行。

因为是退流,所以要反着跑,从 TS

最后答案就是可行流减去退的流(反着的最大流)。 :::info[code]

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=1205,M=25005,inf=2147483647;
int n,m,s,t,S,T;
int lvl[N];//层数
struct edge{
    int u,v,w;
    int nxt;
}ed[M*2];
int head[N],cur[N],cnt=2,infl[N],outfl[N],R[M];
void adde(int u,int v,int w){
    ed[cnt].u=u;
    ed[cnt].v=v;
    ed[cnt].w=w; 
    ed[cnt].nxt=head[u];
    head[u]=cnt;
    cnt++; 
}
bool bfs(){
    queue<int> q;
    for(int i=0;i<=n+1;i++)lvl[i]=-1;
    lvl[s]=0;
    q.push(s);
    while(!q.empty()){
        int u=q.front();
        q.pop();
        for(int i=head[u];i;i=ed[i].nxt){
            if(lvl[ed[i].v]==-1&&ed[i].w>0){
                lvl[ed[i].v]=lvl[u]+1;  
                q.push(ed[i].v);
                if(ed[i].v==t)return 1;
            }
        }
    }
    return(lvl[t]!=-1);
}
int dfs(int u,int cap){//cap:容量
    int flow=0;//跑过的流
    if(u==t)return cap;
    for(int i=cur[u];i&&cap;i=ed[i].nxt){
        cur[u]=i;
        int v=ed[i].v,w=ed[i].w;
        if(w&&lvl[v]==lvl[u]+1){
            int use=dfs(v,min(cap,w));
            if(use==0)lvl[v]=-1;
            if(use>0){
                ed[i].w-=use;
                ed[i^1].w+=use;
                cap-=use;
                flow+=use;
            }
        }
    }
    return flow;
}
int dinic(){
    int ans=0;
    while(bfs()){
        cerr<<"ok";
        memcpy(cur,head,sizeof(head));
        ans+=dfs(s,inf);
    }
    return ans;
}
signed main(){
    cin>>n>>m>>S>>T;
    for(int i=1;i<=m;i++){
        int u,v,l,r;
        cin>>u>>v>>l>>r;
        adde(u,v,r-l);
        adde(v,u,0);
        outfl[u]+=l;
        infl[v]+=l;
        R[i]=r;
    }
    s=0,t=n+1;
    bool pd=1;
    for(int i=1;i<=n;i++){
        if(infl[i]>outfl[i]){//连接s->i
            adde(s,i,infl[i]-outfl[i]);
            adde(i,s,0);
        }
        else if(infl[i]<outfl[i]){
            adde(i,t,outfl[i]-infl[i]);
            adde(t,i,0);
        }
    }
    adde(T,S,inf);
    int sp=cnt;
    adde(S,T,0);
    dinic();
    for(int i=head[s];i&&pd;i=ed[i].nxt){
        if(ed[i].w)pd=0;
    }
    int able=ed[sp].w;//可行流
    if(!pd){
        cout<<"N";
        return 0;
    }
    ed[sp].w=ed[sp^1].w=0;//删边
    s=T,t=S;
    cout<<able-dinic();
    return 0;
}

:::

最小割树(Gomory-Hu Tree)

最小割树是一种求解两点之间最小割的算法

具体地,我们使用分治法。

我们先在原图 G任意选择两点 u,v,得到他们的最小割。

那么我们就得到了两个点集 U,V

我们再在 U 中选择 u',v',继续跑最小割,再继续分……

这样为什么是对的呢?

C(u,v) 表示 u,v 之间的最小割,F(u,v) 表示 u,v 之间的最大流。

我们知道,\forall u,v\in G,F(u,v)=C(u,v)。 :::info[引理 1:\forall x\in U,y\in V,C(x,y)\le C(u,v)]{open} 反证法,设 C(x,y)>C(u,v),那么 F(x,y)>F(u,v),显然不成立。 :::

我们构造一棵树 T,将每次选择的 u,v 连接,边权为 C(u,v)。 :::info[为什么能构成树]{open}

首先考虑边数。递归树是一棵二叉树,叶节点(|S_{now}|=1)的个数为 n,那么非叶节点个数为 n-1

由于只有在 |S_{now}|>1 时分割,所以分割次数为 n-1,对应边数为 n-1

连通性显然。 :::

那么 C(u,v) 就转化为 u,vT 上的路径边权最小值。

:::info[证明]{open}

割掉 w_{\min} 显然 u,v 分开,所以这个一个割,由于最小割是割中最小的,所以 c\le w_{\min}

考虑存在一条树边 (a,b),且这条边被割开,即 a\in U,b\in V

由引理 1,如果树边 (a,b) 边权为 w,那么 w\le c

所以 w_{\min}\le w\le c

::: 使用树剖解决。 :::warning[树剖的注意] 我们需要使用边权转点权。 ![](https://cdn.luogu.com.cn/upload/image_hosting/ven7ak4a.png) ![](https://cdn.luogu.com.cn/upload/image_hosting/ju2u1kvu.png) 但是,记得在查询 $u,v$ 时,不要计算 $\operatorname{lca}(u,v)$ 的边权! ![](https://cdn.luogu.com.cn/upload/image_hosting/cue6wi0d.png) ::: ## 玄学时间复杂度 ### [P4480 [BJWC2018] 餐巾计划问题](https://www.luogu.com.cn/problem/P4480) 本题和网络流24题中的餐巾计划**为**重题。 考虑**分类**加边,将容量为 $\infty$ 的边放到最后加即可。 $O(n^3)$ 过 $n=200000$,~~暴力踩标算~~这个还是算了。 那个 spfa 的优化,没啥用。 :::info[code] ```cpp #include<bits/stdc++.h> using namespace std; const int maxn=12200001,inf=INT_MAX; int n,m,s,t,head[maxn],tot=2,dis[maxn],cur[maxn],a,b,p;//inqueue struct edge{ int u,v,w,c,nxt; }ed[maxn]; bool inq[maxn],vis[maxn]; void adde(int u,int v,int w,int c){ ed[tot]={u,v,w,c,head[u]}; head[u]=tot; tot++; } deque<int> q; int sum=0; bool spfa(){ for(int i=1;i<min(n*3+3,maxn);i++)dis[i]=inf; dis[s]=0; sum+=dis[s]; int num=1; q.push_back(s); while(!q.empty()){ int u=q.front(); while(dis[u]*num>sum){ q.pop_front(); q.push_back(u); u=q.front(); } q.pop_front(); sum-=dis[u]; num--; inq[u]=0; for(int i=head[u];i;i=ed[i].nxt){ int v=ed[i].v,c=ed[i].c; if(dis[v]>dis[u]+c&&ed[i].w){ dis[v]=dis[u]+c; if(!q.empty()&&dis[v]>dis[q.front()])q.push_back(v); else q.push_front(v); sum+=dis[v]; num++; inq[v]=1; } } } return dis[t]!=inf; } int dfs(int u,int cap){ int flow=0; if(u==t)return cap; vis[u]=1; for(int i=cur[u];i&&cap;i=ed[i].nxt){ cur[u]=i; int v=ed[i].v,c=ed[i].c,fl=ed[i].w; if(dis[v]==dis[u]+c&&fl&&!vis[v]){ int use=dfs(v,min(cap,fl)); if(use>0){ ed[i].w-=use; ed[i^1].w+=use; flow+=use; cap-=use; } } } vis[u]=0; return flow; } pair<int,int> dinic(){ int flow=0,cost=0; while(spfa()){ for(int i=0;i<min(n*3+3,maxn);i++)cur[i]=head[i]; int fl=dfs(s,inf); flow+=fl; cost+=fl*dis[t]; } return {flow,cost}; } int nd[maxn],idx=0; signed main(){ int q,k,d,f; ios::sync_with_stdio(0);cin.tie(0); cin>>n>>m>>d>>f>>k>>p; for(int i=1;i<=n;++i)cin>>nd[i]; s=0;t=n*2+1; for(int i=1;i<=n;i++){//i+n是早上,i是晚上 adde(s,i,nd[i],0); adde(i,s,0,0); adde(i+n,t,nd[i],0); adde(t,i+n,0,0); } for(int i=1;i<=n;i++){ if(i+m<=n)adde(i,i+n+m,inf,f); if(i+m<=n)adde(i+n+m,i,0,-f); if(i+d<=n)adde(i,i+n+d,inf,k); if(i+d<=n)adde(i+n+d,i,0,-k); if(i+1<=n)adde(i,i+1,inf,0); if(i+1<=n)adde(i+1,i,0,0); adde(s,i+n,inf,p); adde(i+n,s,0,-p); } cout<<dinic().second; return 0; } ``` ::: ### 几个玄学优化 ### Part A-1:用费用流跑最大流 费用设为 $\inf/w$,算是估价? 得分:$\small\textbf{\textsf{\color{#F39C11}{52}}}$。 :::info[code] ```cpp #include<bits/stdc++.h> using namespace std; #define int long long constexpr int maxn=240001,inf=1e18; int n,m,s,t,head[maxn],tot=2,dis[maxn],cur[maxn];//inqueue struct edge{ int u,v,w,c,nxt; }ed[maxn]; bool inq[maxn],vis[maxn]; __inline void adde(int u,int v,int w,int c){ ed[tot]={u,v,w,c,head[u]}; head[u]=tot; tot++; } int sum=0; deque<int> q; __inline bool spfa(){ for(int i=1;i<=n;i++)dis[i]=inf; dis[s]=0; sum+=dis[s]; int num=1; q.push_back(s); while(!q.empty()){ int u=q.front(); while(dis[u]*num>sum){ q.pop_front(); q.push_back(u); u=q.front(); } q.pop_front(); sum-=dis[u]; num--; inq[u]=0; for(int i=head[u];i;i=ed[i].nxt){ int v=ed[i].v,c=ed[i].c; if(dis[v]>dis[u]+c&&ed[i].w){ dis[v]=dis[u]+c; if(!q.empty()&&dis[v]>dis[q.front()])q.push_back(v); else q.push_front(v); sum+=dis[v]; num++; inq[v]=1; } } } return dis[t]!=inf; } int dfs(int u,int cap){ int flow=0; if(u==t)return cap; vis[u]=1; for(int i=cur[u];i&&cap;i=ed[i].nxt){ cur[u]=i; int v=ed[i].v,c=ed[i].c,fl=ed[i].w; if(dis[v]==dis[u]+c&&fl&&!vis[v]){ int use=dfs(v,min(cap,fl)); if(use==0)dis[v]=inf; if(use>0){ ed[i].w-=use; ed[i^1].w+=use; flow+=use; cap-=use; } } } vis[u]=0; return flow; } pair<int,int> dinic(){ int flow=0,cost=0; while(spfa()){ for(int i=1;i<=n;++i)cur[i]=head[i]; int fl=dfs(s,inf); flow+=fl; cost+=fl*dis[t]; } return {flow,cost}; } #ifdef LINUX #define putchar putchar_unlocked #define getchar getchar_unlocked #endif inline int read(){ int x=0,f=1;char ch=getchar(); while (ch<'0'||ch>'9'){if (ch=='-') f=-1;ch=getchar();} while (ch>='0'&&ch<='9'){x=(x << 1) + (x << 3)+ch-48;ch=getchar();} return x*f; } inline void write(int x){ if(x<0){putchar('-');x=-x;} if(x>9)write(x/10); putchar(x%10+'0'); } signed main(){ ios::sync_with_stdio(0);cin.tie(0); n=read(),m=read(),s=read(),t=read(); for(int i=1;i<=m;++i){ int u,v,w; u=read(),v=read(),w=read(); adde(u,v,w,200000000000000/w); adde(v,u,0,-200000000000000/w); } pair<int,int> ans=dinic(); write(ans.first); return 0; } ``` ::: ### Part B-1:分段加边 跑 $\log w_{\max}$ 次最大流,每次只加入 $2^i\le w<2^{i+1}$ 的边。 记得去掉重边。 得分:$\small\textbf{\textsf{\color{#FADB14}{68}}}$。 :::info[code] ```cpp #include<bits/stdc++.h> using namespace std; #define int long long const int N=1205,M=121005,inf=1e18; int n,m,s,t; int lvl[N],wt[N][N];//层数 struct edge{ int u,v,w; int nxt; }ed[M*2]; int head[N],cnt=2; inline void adde(int u,int v,int w){ ed[cnt]={u,v,w,head[u]}; head[u]=cnt; cnt++; } inline bool bfs(){ queue<int> q; for(int i=1;i<=n;i++)lvl[i]=-1; lvl[s]=0; q.push(s); while(!q.empty()){ int u=q.front(); q.pop(); for(int i=head[u];i;i=ed[i].nxt){ if(lvl[ed[i].v]==-1&&ed[i].w>0){ lvl[ed[i].v]=lvl[u]+1; if(ed[i].v==t)return 1; q.push(ed[i].v); } } } return(lvl[t]!=-1); } int cur[N]; int dfs(int u,int cap){//cap:容量 int flow=0;//跑过的流 if(u==t)return cap; for(int i=cur[u];i&&cap;i=ed[i].nxt){ cur[u]=i; int v=ed[i].v,w=ed[i].w; if(w&&lvl[v]==lvl[u]+1){ int use=dfs(v,min(cap,w)); if(use==0)lvl[v]=-1; if(use>0){ ed[i].w-=use; ed[i^1].w+=use; cap-=use; flow+=use; } } } return flow; } inline int dinic(){ int ans=0; while(bfs()){ for(int i=1;i<=n;i++)cur[i]=head[i]; ans+=dfs(s,inf); } return ans; } struct inp{ int u,v,w; }; vector<inp> ve; #ifdef __linux__ #define getchar getchar_unlocked #define putchar putchar_unlocked #else #define getchar _getchar_nolock #define putchar _putchar_nolock #endif inline int read(){ int x=0,f=1;char ch=getchar(); while (ch<'0'||ch>'9'){if (ch=='-') f=-1;ch=getchar();} while (ch>='0'&&ch<='9'){x=(x << 1) + (x << 3)+ch-48;ch=getchar();} return x*f; } inline void write(int x){ if(x<0){putchar('-');x=-x;} if(x>9)write(x/10); putchar(x%10+'0'); } signed main(){ ios::sync_with_stdio(0); cin.tie(0);cout.tie(0); n=read(),m=read(),s=read(),t=read(); for(int i=1;i<=m;++i){ int u,v,w; u=read(),v=read(),w=read(); ve.push_back({u,v,w}); wt[u][v]+=w; } int fl=0; for(int bt=32;bt>=0;bt-=4){ int l=1ll<<bt,r=1ll<<(bt+4); bool W=0; for(int u=1;u<=n;u++){ for(int v=1;v<=n;v++){ if(l<=wt[u][v]&&wt[u][v]<r&&wt[u][v]){ adde(u,v,wt[u][v]); adde(v,u,0); wt[u][v]=0; W=1; } } } if(W)fl+=dinic(); } write(fl); dinic(); return 0; } ``` ::: ### Part B-2:贪心初始流 先不加残余边跑一边(只修改残余边,不遍历),再加上残余边跑。 得分:$\small\textbf{\textsf{\color{#FADB14}{68}}}$。 :::info[code] ```cpp #include<bits/stdc++.h> using namespace std; #define int long long const int N=1205,M=121005,inf=1e18; int n,m,s,t; int lvl[N],wt[N][N];//层数 struct edge{ int u,v,w; int nxt; }ed[M*2]; int head[N],cnt=2,cur[N]; inline void adde(int u,int v,int w){ ed[cnt]={u,v,w,head[u]}; head[u]=cnt; cnt++; } inline bool bfs(){ queue<int> q; for(int i=1;i<=n;i++)lvl[i]=-1; lvl[s]=0; q.push(s); while(!q.empty()){ int u=q.front(); q.pop(); for(int i=head[u];i;i=ed[i].nxt){ if(lvl[ed[i].v]==-1&&ed[i].w>0){ lvl[ed[i].v]=lvl[u]+1; if(ed[i].v==t)return 1; q.push(ed[i].v); } } } return(lvl[t]!=-1); } inline bool gbfs(){ queue<int> q; for(int i=1;i<=n;i++)lvl[i]=-1; lvl[s]=0; q.push(s); while(!q.empty()){ int u=q.front(); q.pop(); for(int i=head[u];i;i=ed[i].nxt){ if(i%2)continue; if(lvl[ed[i].v]==-1&&ed[i].w>0){ lvl[ed[i].v]=lvl[u]+1; if(ed[i].v==t)return 1; q.push(ed[i].v); } } } return(lvl[t]!=-1); } int dfs(int u,int cap){//cap:容量 int flow=0;//跑过的流 if(u==t)return cap; for(int i=cur[u];i&&cap;i=ed[i].nxt){ cur[u]=i; int v=ed[i].v,w=ed[i].w; if(w&&lvl[v]==lvl[u]+1){ int use=dfs(v,min(cap,w)); if(use==0)lvl[v]=-1; if(use>0){ ed[i].w-=use; ed[i^1].w+=use; cap-=use; flow+=use; } } } return flow; } int greed(int u,int cap){//贪心初始流 int flow=0;//跑过的流 if(u==t)return cap; for(int i=cur[u];i&&cap;i=ed[i].nxt){ cur[u]=i; if((i%2))continue; int v=ed[i].v,w=ed[i].w; if(w&&lvl[v]==lvl[u]+1){ int use=greed(v,min(cap,w)); if(use==0)lvl[v]=-1; if(use>0){ ed[i].w-=use; ed[i^1].w+=use; cap-=use; flow+=use; } } } return flow; } inline int dinic(){ int ans=0; while(bfs()){ for(int i=1;i<=n;i++)cur[i]=head[i]; ans+=dfs(s,inf); } return ans; } inline int gflow(){ int ans=0; while(gbfs()){ for(int i=1;i<=n;i++)cur[i]=head[i]; ans+=greed(s,inf); } return ans; } #ifdef __linux__ #define getchar getchar_unlocked #define putchar putchar_unlocked #else #define getchar _getchar_nolock #define putchar _putchar_nolock #endif inline int read(){ int x=0,f=1;char ch=getchar(); while (ch<'0'||ch>'9'){if (ch=='-') f=-1;ch=getchar();} while (ch>='0'&&ch<='9'){x=(x << 1) + (x << 3)+ch-48;ch=getchar();} return x*f; } inline void write(int x){ if(x<0){putchar('-');x=-x;} if(x>9)write(x/10); putchar(x%10+'0'); } signed main(){ ios::sync_with_stdio(0); cin.tie(0);cout.tie(0); n=read(),m=read(),s=read(),t=read(); for(int i=1;i<=m;++i){ int u,v,w; u=read(),v=read(),w=read(); wt[u][v]+=w; } int fl=0; for(int bt=28;bt>=0;bt-=4){ int l=1ll<<bt,r=1ll<<(bt+4); bool W=0; for(int u=1;u<=n;++u){ for(int v=1;v<=n;++v){ if(v==u)continue; if(l<=wt[u][v]&&wt[u][v]<r&&wt[u][v]){ adde(u,v,wt[u][v]); adde(v,u,0); wt[u][v]=0; W=1; } } } if(W)fl+=gflow(),fl+=dinic(); } write(fl); return 0; } ``` ::: ### Part B-3:分类加边 分类加边,将 $u\to v(u<v)$ 和 $u\to v(u>v)$ 分开加。 得分:$\small\textbf{\textsf{\color{#52C41A}{84}}}$。 :::info[code] ```cpp #include<bits/stdc++.h> using namespace std; #define int long long const int N=1205,M=121005,inf=1e18; int n,m,s,t; int lvl[N],wt[N][N];//层数 struct edge{ int u,v,w; int nxt; }ed[M*2]; int head[N],cnt=2,cur[N]; inline void adde(int u,int v,int w){ ed[cnt]={u,v,w,head[u]}; head[u]=cnt; cnt++; } inline bool bfs(){ queue<int> q; for(int i=1;i<=n;i++)lvl[i]=-1; lvl[s]=0; q.push(s); while(!q.empty()){ int u=q.front(); q.pop(); for(int i=head[u];i;i=ed[i].nxt){ if(lvl[ed[i].v]==-1&&ed[i].w>0){ lvl[ed[i].v]=lvl[u]+1; if(ed[i].v==t)return 1; q.push(ed[i].v); } } } return(lvl[t]!=-1); } inline bool gbfs(){ queue<int> q; for(int i=1;i<=n;i++)lvl[i]=-1; lvl[s]=0; q.push(s); while(!q.empty()){ int u=q.front(); q.pop(); for(int i=head[u];i;i=ed[i].nxt){ if(i%2)continue; if(lvl[ed[i].v]==-1&&ed[i].w>0){ lvl[ed[i].v]=lvl[u]+1; if(ed[i].v==t)return 1; q.push(ed[i].v); } } } return(lvl[t]!=-1); } int dfs(int u,int cap){//cap:容量 int flow=0;//跑过的流 if(u==t)return cap; for(int i=cur[u];i&&cap;i=ed[i].nxt){ cur[u]=i; int v=ed[i].v,w=ed[i].w; if(w&&lvl[v]==lvl[u]+1){ int use=dfs(v,min(cap,w)); if(use==0)lvl[v]=-1; if(use>0){ ed[i].w-=use; ed[i^1].w+=use; cap-=use; flow+=use; } } } return flow; } int greed(int u,int cap){//贪心初始流 int flow=0;//跑过的流 if(u==t)return cap; for(int i=cur[u];i&&cap;i=ed[i].nxt){ cur[u]=i; if((i%2))continue; int v=ed[i].v,w=ed[i].w; if(w&&lvl[v]==lvl[u]+1){ int use=greed(v,min(cap,w)); if(use==0)lvl[v]=-1; if(use>0){ ed[i].w-=use; ed[i^1].w+=use; cap-=use; flow+=use; } } } return flow; } inline int dinic(){ int ans=0; while(bfs()){ for(int i=1;i<=n;i++)cur[i]=head[i]; ans+=dfs(s,inf); } return ans; } inline int gflow(){ int ans=0; while(gbfs()){ for(int i=1;i<=n;i++)cur[i]=head[i]; ans+=greed(s,inf); } return ans; } #ifdef __linux__ #define getchar getchar_unlocked #define putchar putchar_unlocked #else #define getchar _getchar_nolock #define putchar _putchar_nolock #endif inline int read(){ int x=0,f=1;char ch=getchar(); while (ch<'0'||ch>'9'){if (ch=='-') f=-1;ch=getchar();} while (ch>='0'&&ch<='9'){x=(x << 1) + (x << 3)+ch-48;ch=getchar();} return x*f; } inline void write(int x){ if(x<0){putchar('-');x=-x;} if(x>9)write(x/10); putchar(x%10+'0'); } signed main(){ ios::sync_with_stdio(0); cin.tie(0);cout.tie(0); n=read(),m=read(),s=read(),t=read(); for(int i=1;i<=m;++i){ int u,v,w; u=read(),v=read(),w=read(); wt[u][v]+=w; } int fl=0; for(int bt=28;bt>=0;bt-=4){ int l=1ll<<bt,r=1ll<<(bt+4); bool W=0; for(int u=1;u<=n;++u){ for(int v=1;v<u;++v){ if(l<=wt[u][v]&&wt[u][v]<r&&wt[u][v]){ adde(u,v,wt[u][v]); adde(v,u,0); wt[u][v]=0; W=1; } } } // if(W)fl+=gflow(),fl+=dinic(); // W=0; for(int u=1;u<=n;++u){ for(int v=u+1;v<=n;++v){ if(l<=wt[u][v]&&wt[u][v]<r&&wt[u][v]){ adde(u,v,wt[u][v]); adde(v,u,0); wt[u][v]=0; W=1; } } } if(W)fl+=gflow(),fl+=dinic(); } write(fl); return 0; } ``` ::: ### Part B-4:双向边优化 在无向图中,对于每一个双向边,其实只需要建一正一反两条边就行了,残余网络与反向边合二为一。 成功 AC。 :::info[code] ```cpp #include<bits/stdc++.h> using namespace std; const int N=1205,M=121005; const long long inf=1e18; int n,m,s,t; int lvl[N],wt[N][N];//层数 struct edge{ int v,w; int nxt; }ed[M*2]; int head[N],cnt=2,cur[N]; inline void adde(int u,int v,int w){ ed[cnt]={v,w,head[u]}; head[u]=cnt; cnt++; } queue<int> q; inline bool bfs(){ for(int i=1;i<=n;i++)lvl[i]=-1; while(!q.empty())q.pop(); lvl[s]=0; q.push(s); while(!q.empty()){ int u=q.front(); q.pop(); for(int i=head[u];i;i=ed[i].nxt){ if(lvl[ed[i].v]==-1&&ed[i].w>0){ lvl[ed[i].v]=lvl[u]+1; if(ed[i].v==t)return 1; q.push(ed[i].v); } } } return(lvl[t]!=-1); } inline bool gbfs(){ for(int i=1;i<=n;i++)lvl[i]=-1; while(!q.empty())q.pop(); lvl[s]=0; q.push(s); while(!q.empty()){ int u=q.front(); q.pop(); for(int i=head[u];i;i=ed[i].nxt){ if(i%2)continue; if(lvl[ed[i].v]==-1&&ed[i].w>0){ lvl[ed[i].v]=lvl[u]+1; if(ed[i].v==t)return 1; q.push(ed[i].v); } } } return(lvl[t]!=-1); } long long dfs(int u,long long cap){//cap:容量 int flow=0;//跑过的流 if(u==t)return cap; for(int i=cur[u];i&&cap;i=ed[i].nxt){ cur[u]=i; int v=ed[i].v,w=ed[i].w; if(w&&lvl[v]==lvl[u]+1){ int use=dfs(v,min(cap,w*1ll)); if(use==0)lvl[v]=-1; if(use>0){ ed[i].w-=use; ed[i^1].w+=use; cap-=use; flow+=use; } } } return flow; } long long greed(int u,long long cap){//贪心初始流 int flow=0;//跑过的流 if(u==t)return cap; for(int i=cur[u];i&&cap;i=ed[i].nxt){ cur[u]=i; if((i%2))continue; int v=ed[i].v,w=ed[i].w; if(w&&lvl[v]==lvl[u]+1){ int use=greed(v,min(cap,w*1ll)); if(use==0)lvl[v]=-1; if(use>0){ ed[i].w-=use; ed[i^1].w+=use; cap-=use; flow+=use; } } } return flow; } inline long long dinic(){ long long ans=0; while(bfs()){ for(int i=1;i<=n;i++)cur[i]=head[i]; ans+=dfs(s,inf); } return ans; } inline long long gflow(){ long long ans=0; while(gbfs()){ for(int i=1;i<=n;i++)cur[i]=head[i]; ans+=greed(s,inf); } return ans; } #ifdef __linux__ #define getchar getchar_unlocked #define putchar putchar_unlocked #else #define getchar _getchar_nolock #define putchar _putchar_nolock #endif inline int read(){ int x=0,f=1;char ch=getchar(); while (ch<'0'||ch>'9'){if (ch=='-') f=-1;ch=getchar();} while (ch>='0'&&ch<='9'){x=(x << 1) + (x << 3)+ch-48;ch=getchar();} return x*f; } inline void write(int x){ if(x<0){putchar('-');x=-x;} if(x>9)write(x/10); putchar(x%10+'0'); } signed main(){ ios::sync_with_stdio(0); cin.tie(0);cout.tie(0); n=read(),m=read(),s=read(),t=read(); for(int i=1;i<=m;++i){ int u,v,w; u=read(),v=read(),w=read(); wt[u][v]+=w; } int fl=0; for(int bt=28;bt>=0;bt-=4){ long long l=1ll<<bt,r=1ll<<(bt+4); int wc=0; for(int u=1;u<=n;u++){ for(int v=u+1;v<=n;v++){ if((wt[u][v]||wt[v][u])&&((l<=wt[u][v]&&wt[u][v]<r)||(l<=wt[v][u]&&wt[v][u]<r))){ adde(u,v,wt[u][v]); adde(v,u,wt[v][u]); wt[u][v]=wt[v][u]=0; wc++; } } } if(!wc)continue; if(wc<=n/10)fl+=gflow(); fl+=dinic(); } write(fl); return 0; } ``` ::: ### [P5331 [SNOI2019] 通信](https://www.luogu.com.cn/problem/P5331) 这道题看似很强,实际上只需要使用 Primal-Dual 加上 vector 存图即可通过。 学习链接:[《费用流的 Primal-Dual 算法》](https://www.cnblogs.com/tkandi/p/10532774.html),[《【学习笔记】Primal-Dual 原始对偶算法 》](https://www.cnblogs.com/SoyTony/p/Learning_Notes_about_Primal-Dual_Algorithm.html)。 另外为了增加 cache 命中率,我们希望把数组放进缓存,所以对于数组,可以开小,把 `int` 换为 `short`。 :::info[code] ```cpp #include<bits/stdc++.h> using namespace std; #define int long long #define sg signed const int N=2002,M=930001,inf=1e18; short tot=2,s,t,cur[N],n; int dis[N]; bitset<N> vis; struct edge{ short v; sg w,c; }; vector<edge> G[N]; int h[N],a[N]; sg fb[N][N]; inline void One(sg u,sg v,int w,int c){ G[u].push_back({v,w,c}); fb[v][u]=G[u].size()-1; } inline void adde(sg u,sg v,int w,int c){ One(u,v,w,c); One(v,u,0,-c); } struct qn{ short v; sg w; inline friend bool operator<(qn aa,qn bb){ return aa.w>bb.w; } }; priority_queue<qn> q; inline bool dijkstra(){ for(int i=s;i<=t;i++)dis[i]=inf; q.push({s,0}); dis[s]=0; while(q.size()){ int u=q.top().v; q.pop(); if(vis[u])continue; vis[u]=1; for(auto [v,flow,c]:G[u]){ c+=h[u]-h[v]; if(dis[v]>dis[u]+c&&flow){ dis[v]=dis[u]+c; q.push({v,dis[v]}); } } } vis.reset(); return dis[t]!=inf; } int dfs(int u,int cap){ if(u==t)return cap; vis[u]=1; int flow=0; for(int i=cur[u];i<G[u].size();i++){ cur[u]=i; int v=G[u][i].v,fl=G[u][i].w,c=G[u][i].c+h[u]-h[v]; if(dis[v]==dis[u]+c&&fl&&!vis[v]){ int use=dfs(v,min(cap,fl)); if(use){ cap-=use; flow+=use; G[u][i].w-=use; G[v][fb[u][v]].w+=use; } } } vis[u]=0; return flow; } inline int dinic(){ int cost=0; while(dijkstra()){ memset(cur,0,sizeof(cur)); int fl=dfs(s,inf); for(int i=s;i<=t;i++)if(dis[i]<inf)h[i]+=dis[i]; cost+=fl*h[t]; } return cost; } inline int read(){ int x=0,f=1;char ch=getchar(); while (ch<'0'||ch>'9'){if (ch=='-') f=-1;ch=getchar();} while (ch>='0'&&ch<='9'){x=(x << 1) + (x << 3)+ch-48;ch=getchar();} return x*f; } #define ind(i) (i) #define outd(i) (i+n) signed main(){ int w; n=read(),w=read(); s=0,t=n*2+1; for(int i=1;i<=n;i++){ a[i]=read(); } for(int i=1;i<=n;i++){ adde(s,ind(i),1,0); adde(outd(i),t,1,0); adde(ind(i),t,1,w); } for(int i=1;i<=n;i++){ for(int j=1;j<i;j++){ int wg=abs(a[i]-a[j]); if(wg>=w)continue;//剪枝 adde(ind(i),outd(j),1,wg); } } cout<<dinic(); return 0; } ``` ::: ## 几个别的题 ### [P4298 [CTSC2008] 祭祀](https://www.luogu.com.cn/problem/P4298) #### Q1 最小链覆盖的板子,跑一个传递闭包,把间接相连改成直接相连再用 [P2764 最小路径覆盖问题](https://www.luogu.com.cn/problem/P2764)。 #### Q2 和 [P2762 太空飞行计划问题](https://www.luogu.com.cn/problem/P2762) 那一部分有点像,这里是求**反**最大权闭合子图。 [最大权闭合子图](https://blog.csdn.net/qq_45735851/article/details/113811882)。 跑完之后,左边没有匹配的,和右边匹配的,就是最小点覆盖。 #### Q3 对于每一个点,删掉与其**在传递闭包上有边(包括自己)的点**,再跑一边 Q1。如果这个点在最长反链中,那么最长反链长度要 $-1$。据此判断即可。 :::warning[斤氏后人] 重新跑 Q1 时,答案的总点数不是 $n$ 而是 $n-\text{del}$。 ::: 不过懒得写网络流了。所以是 $O(n^4)$。 :::info[code] ```cpp #include<bits/stdc++.h> using namespace std; #define int long long const int maxn=155; vector<int> G[maxn],G0[maxn]; bool can[maxn][maxn],matched[maxn],le[maxn],ri[maxn],q2[maxn],del[maxn],latch[maxn]; int match[maxn],ans=0,n,m; bool find(int u){//匈牙利 for(int v:G[u]){ if(!matched[v]){ matched[v]=1; if(!match[v]||find(match[v])){ match[v]=u; return 1; } } } return 0; } bool find0(int u){//匈牙利 for(int v:G0[u]){ if(!matched[v]){ matched[v]=1; if(!match[v]||find0(match[v])){ match[v]=u; return 1; } } } return 0; } void find2(int u){//网络流就不需要这一步了…… le[u]=1; for(int v:G[u]){ if(!ri[v]){ ri[v]=1; if(match[v]&&!le[match[v]]){ find2(match[v]); } } } } void floyd(){ for(int k=1;k<=n;k++){ for(int i=1;i<=n;i++){ for(int j=1;j<=n;j++){ can[i][j]|=can[i][k]&can[k][j]; } } } } void build(){ floyd(); for(int i=1;i<=n;i++){ for(int j=1;j<=n;j++){ if(can[i][j])G[i].push_back(j); } } } void clear(){ for(int i=1;i<=n;i++)G0[i].clear(); } signed main(){ ios::sync_with_stdio(0); cin.tie(0); cin>>n>>m; for(int i=0;i<m;i++){ int u,v; cin>>u>>v; can[u][v]=1; } build(); for(int i=1;i<=n;i++){ memset(matched,0,sizeof(matched)); if(find(i))ans++; } for(int i=1;i<=n;i++){ if(match[i]){ latch[match[i]]=1;//有匹配 } } cout<<n-ans<<endl;//Q1 //最大反链=最小链覆盖的补集 for(int i=1;i<=n;i++){ if(!latch[i])find2(i);//需要没有匹配 } for(int i=1;i<=n;i++){ q2[i]=!((!le[i])||(ri[i]));//不取反就是最大权闭合子图 cout<<q2[i]; } cout<<endl; for(int x=1;x<=n;x++){ clear(); memset(del,0,sizeof(del)); memset(match,0,sizeof(match)); int rm=n; for(int i=1;i<=n;i++){ if(can[x][i]||can[i][x]||i==x){//删除 del[i]=1; rm--; } } for(int i=1;i<=n;i++){ for(int j=1;j<=n;j++){ if(can[i][j]&&!del[i]&&!del[j]){ G0[i].push_back(j); } } } int tans=0,trans=0; for(int i=1;i<=n;i++){ memset(matched,0,sizeof(matched)); if(find0(i))tans++; } trans=rm-tans; if(trans+1==n-ans)cout<<1; else cout<<0; } return 0; } ``` ::: ### [P3974 [TJOI2015] 组合数学](https://www.luogu.com.cn/problem/P3974) 这题显然是一个二分套费用流,显然过不了。$V=nm,E=nm$。 $O(Tn^3m^3\log Ans)$ 能过就怪了。不过能得 $30$ 分是好的。 :::info[code] ```cpp /* This code is from wangxx2012. This code is from R272171678. */ #include<bits/stdc++.h> #define int long long #define endl '\n' using namespace std; const int maxn=1e6+10; const int inf=1e18; int n,m,s,t,sum; int a[1010][1010]; struct node{ int to,next,cap,cost; }e[2*maxn]; int head[maxn],tot=1; void add(int u,int v,int cap,int cost){ e[++tot].to=v; e[tot].cap=cap; e[tot].cost=cost; e[tot].next=head[u]; head[u]=tot; } int dis[maxn],cur[maxn]; bool st[maxn],vis[maxn]; bool spfa(){ queue<int> q; fill(st+1,st+2*n*m+2,0); fill(dis+1,dis+2*n*m+2,inf); q.push(s); dis[s]=0; st[s]=1; while(!q.empty()){ int u=q.front(); q.pop(); st[u]=0; for(int i=head[u];i;i=e[i].next){ int v=e[i].to; if(dis[v]>dis[u]+e[i].cost&&e[i].cap){ dis[v]=dis[u]+e[i].cost; if(!st[v]){ q.push(v); st[v]=1; } } } } return dis[t]!=inf; } int dfs(int u,int flow,int &mincost){ if(u==t) return flow; int used=flow; vis[u]=1; for(int i=cur[u];i;i=e[i].next){ cur[u]=i; int v=e[i].to; if(dis[v]==dis[u]+e[i].cost&&e[i].cap&&!vis[v]){ int rlow=dfs(v,min(used,e[i].cap),mincost); e[i].cap-=rlow; e[i^1].cap+=rlow; used-=rlow; mincost+=rlow*e[i].cost; if(used==0) break; } } vis[u]=0; return flow-used; } int dinic(int &mincost){ int ans=0; while(spfa()){ memcpy(cur,head,sizeof(head)); ans+=dfs(s,inf,mincost); } return ans; } int get_id(int x,int y){ return (x-1)*m+y; } int get_out(int x,int y){ return get_id(x,y)+n*m; } void reset(int k){ s=0,t=2*n*m+1; memset(head,0,sizeof(head)); tot=1; for(int i=1;i<=n;i++){ for(int j=1;j<=m;j++){ int u_in=get_id(i,j),u_out=get_out(i,j); if(a[i][j]>0){ add(u_in,u_out,a[i][j],-1); add(u_out,u_in,0,1); } if(k-a[i][j]>0){ add(u_in,u_out,k-a[i][j],0); add(u_out,u_in,0,0); } } } int dx[]={0,1},dy[]={1,0}; for(int i=1;i<=n;i++){ for(int j=1;j<=m;j++){ int u_out=get_out(i,j); for(int type=0;type<2;type++){ int ddx=i+dx[type],ddy=j+dy[type]; if(ddx>=1&&ddx<=n&&ddy>=1&&ddy<=m){ int v_in=get_id(ddx,ddy); add(u_out,v_in,k,0); add(v_in,u_out,0,0); } } } } add(s,1,k,0); add(1,s,0,0); add(2*n*m,t,k,0); add(t,2*n*m,0,0); } signed main(){ ios::sync_with_stdio(0); cin.tie(0); cout.tie(0); int T; cin>>T; while(T--){ cin>>n>>m; sum=0; for(int i=1;i<=n;i++){ for(int j=1;j<=m;j++) cin>>a[i][j],sum+=a[i][j]; } int l=0,r=sum,ans=sum; while(l<=r){ int k=(l+r)/2; reset(k); int mincost=0; dinic(mincost); if(-mincost==sum) ans=k,r=k-1; else l=k+1; } cout<<ans<<endl; } return 0; } ``` ::: ### [AT_abc163_e [ABC163E] Active Infants](https://www.luogu.com.cn/problem/AT_ABC163_E) 正解是区间 DP,但是费用流卡卡就能过。