网络流与线性规划24题详解(上)
前言
题单
刷24题刷魔怔了,写个详解。
难度不断递增,T1-T9为蓝题,T10-T23为紫题。(什么?你问我为什么没有T24?)
好了,让我们开始吧!
T1 孤岛营救问题
思路:这题数据小,所以用BFS
\(key[x][y][k]\)记录\((x,y)\)的第k把钥匙
\(wall[x1][y1][x2][y2]\)记录墙和门
\(vis[x1][y1][k]\)记录是否走过\(x\)和\(y\),这里再多加一维存钥匙状态
关于钥匙,由于这题数据小\((\leq10)\),所以采用状压的思想存钥匙
注意:
1.同一格子可能有多把钥匙
2.一开始的\((1,1)\)也可能有钥匙,别忘了算进去
3.一把钥匙能用无数次
代码如下:
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<queue>
using namespace std;
int wall[15][15][15][15];
int key[15][15][15],n,m,p,r,s,cnt[15][15];
int dir[4][2]={{0,1},{1,0},{-1,0},{0,-1}};
bool vis[15][15][1<<15];
struct node{
int x,y,step,k;
};
int main()
{
ios::sync_with_stdio(0);
cin.tie(0),cout.tie(0);
cin>>n>>m>>p>>r;
for(int i=1;i<=r;i++)
{
int x1,y1,x2,y2,g;
cin>>x1>>y1>>x2>>y2>>g;
if(!g)wall[x1][y1][x2][y2]=-1,wall[x2][y2][x1][y1]=-1;
else wall[x1][y1][x2][y2]=g,wall[x2][y2][x1][y1]=g;
}
cin>>s;
for(int i=1;i<=s;i++)
{
int x1,y1,q;
cin>>x1>>y1>>q;
cnt[x1][y1]++;
key[x1][y1][cnt[x1][y1]]=q;
}
///////////////////////////bfs///////////////////////////
queue<node> q;
int sk=0;
for(int i=1;i<=cnt[1][1];i++)
sk|=1<<key[1][1][i];//状压
vis[1][1][sk]=1;
q.push((node){1,1,0,sk});
while(!q.empty())
{
node u=q.front();
q.pop();
if(u.x==n&&u.y==m)
{
cout<<u.step;
return 0;
}
for(int i=0;i<4;i++)
{
int vx=u.x+dir[i][0],vy=u.y+dir[i][1];
int w=wall[u.x][u.y][vx][vy];
if(vx<1||vy<1||vx>n||vy>m||w<0)continue;
if(w&&((1<<w)&u.k)==0)continue;
int vk=0;
for(int i=1;i<=cnt[vx][vy];i++)
vk|=1<<key[vx][vy][i];
vk=u.k|vk;
if(vis[vx][vy][vk])continue;
q.push((node){vx,vy,u.step+1,vk});
vis[vx][vy][vk]=1;
}
}
cout<<-1;
return 0;
}
T2 飞行员配对方案问题
思路:二分图网络流
但这题确实是二分图裸模板,无脑套就行了。
代码如下:
#include<bits/stdc++.h>
using namespace std;
int n,m,e,ans,match[510],match2[510];
bool vis[510],vis2[510];
vector<int> t[510];
bool dfs(int u){
for(int i=0;i<t[u].size();i++){
int v=t[u][i];
if(!vis[v]){
vis[v]=1;
if(!match[v]||dfs(match[v])){
match2[u]=v;
match[v]=u;
return 1;
}
}
}
return 0;
}
int main(){
cin>>n>>m;
while(1){
int u,v;
cin>>u>>v;
if(u==-1&&v==-1)break;
t[u].push_back(v);
}
for(int i=1;i<=n;i++){
if(dfs(i))ans++,vis2[i]=1;
memset(vis,0,sizeof(vis));
}
cout<<ans<<endl;
for(int i=1;i<=n;i++)
if(vis2[i])
cout<<i<<' '<<match2[i]<<endl;
return 0;
}
第二种做法是网络流,新加一个点把所有左边的点连起来,作为源点;新加一个点把所有右边的点连起来,作为汇点;每条边权值赋为\(1\)然后跑一遍最大流就行了。
注意输出匹配的时候判断汇点与右边点的反向边以及原点与左边点的反向边是否存在。
代码如下:
#include<bits/stdc++.h>
#define ll long long
using namespace std;
ll n,m,t[110][110],dep[110],ans,st,ed;
ll pre[110];
void bfs(int s)
{
dep[s]=1;
queue<ll> q;
q.push(s);
while(!q.empty())
{
ll u=q.front();
q.pop();
for(int i=1;i<=n+1;i++)
{
if(dep[i]==0&&t[u][i]>0)
{
dep[i]=dep[u]+1;
pre[i]=u;
q.push(i);
}
}
}
return;
}
ll dfs(ll u,ll fl)
{
if(u==ed)
return fl;
for(int i=1;i<=n+1;i++)
{
if(t[u][i]&&dep[i]==dep[u]+1)
{
ll d=dfs(i,min(fl,t[u][i]));
if(d>0)
{
t[u][i]-=d;
t[i][u]+=d;
return d;
}
}
}
return 0;
}
ll dinic()
{
while(1)
{
memset(dep,0,sizeof(dep));
bfs(st);
if(!dep[ed])break;
ll flg=1;
while(flg)
{
flg=dfs(st,1e18);
if(flg)ans+=flg;
}
}
return ans;
}
int main()
{
ios::sync_with_stdio(0);
cin.tie(0),cout.tie(0);
cin>>m>>n;
st=0,ed=n+1;
int u,v;
while(cin>>u>>v)
{
if(u==-1&&v==-1)break;
t[u][v]=1;
}
for(int i=1;i<=m;i++)t[0][i]=1;
for(int i=m+1;i<=n;i++)t[i][n+1]=1;
cout<<dinic()<<'\n';
for(int i=m+1;i<=n;i++)
for(int j=1;j<=m;j++)
if(t[n+1][i]&&t[j][0]&&t[i][j])
cout<<j<<' '<<i<<'\n';
return 0;
}
T3 软件补丁问题
思路:跑最短路。
怎么跑?既然是最短路,就得有点(起点和终点)和边。本题\(n\)(错误总数)范围很小\((≤20)\),考虑状压,一开始全是错误,所以起点编号设为\(2^n-1\)(表示所有错误全都存在),最后错误全部消除,所以终点编号设为\(0\)。
判断当前状态\((u)\)是否包含\(b_1\),不包含\(b_2\),考虑用二进制的\(\&\)(与)符号,若\(u\&b_1==b_1\)则包含\(b_1\),若\(u\&b_2==0\)则不包含\(b_2\)。
要消除\(u\)中的\(f_1\),加上\(f_2\),考虑使用\(|\)(或)和^(异或)符号,先让\(v=u|(f_1|f_2)\),这时\(v\)肯定包含\(f_1\)和\(f_2\),再异或\(f_1\)(把\(f_1\)消掉)。
把状态\(u\)时使用补丁\(m_i\)后的结果设为\(v\),则\(u-v\)为一条边,\(t_i\)为边权。之后就可以愉快地跑最短路了。
代码如下:
#include<bits/stdc++.h>
#define pii pair<int,int>
#define mp make_pair
using namespace std;
struct bag{
int t,b1,b2,f1,f2;
}b[110];
int n,m,dis[1<<20+1];
bool vis[1<<20+1];
void dij()//最短路
{
memset(dis,0x7f,sizeof(dis));
priority_queue<pii,vector<pii>,greater<pii>> q;
q.push(mp(0,(1<<n)-1));
dis[(1<<n)-1]=0;
while(!q.empty())
{
int d=q.top().first,u=q.top().second;
q.pop();
if(vis[u])continue;
vis[u]=1;
for(int i=1;i<=m;i++)
{
if((u&b[i].b1)!=b[i].b1||(u&b[i].b2)!=0)
continue;
int v=(u|(b[i].f1|b[i].f2))^b[i].f1;
dis[v]=min(dis[v],d+b[i].t);
q.push(mp(dis[v],v));
}
}
return;
}
int main()
{
ios::sync_with_stdio(0);
cin.tie(0),cout.tie(0);
cin>>n>>m;
for(int i=1;i<=m;i++)
{
cin>>b[i].t;
string s;
cin>>s;
for(int j=0;j<s.length();j++)
{
if(s[j]=='+')b[i].b1|=(1<<j);
if(s[j]=='-')b[i].b2|=(1<<j);
}
cin>>s;
for(int j=0;j<s.length();j++)
{
if(s[j]=='-')b[i].f1|=(1<<j);
if(s[j]=='+')b[i].f2|=(1<<j);
}
}
dij();
if(dis[0]==0x7f7f7f7f)cout<<0;
else cout<<dis[0];
return 0;
}
T4 负载平衡问题
思路:数学+贪心网络流
虽然是网络流题单里面的题但还是要提及一下贪心做法。
这题怎么看上去这么像均分纸牌?只不过加了个环形的条件。
我们设\(\bar x_a\)为\(a1,a2,\cdots,a_n\)的平均数,\(x_i\)表示为第\(i\)个人给左边的人的纸牌数,则\(ans=|x_1|+|x_2|+\cdots+|x_n|=\sum\limits_{i=1}^{n}x_i\)。
考虑用\(x_1\)来表示\(x_i\)。我们列出如下方程:
代入消元,可得:\(x_i=x_1-(a_1+\cdots+a_{i-1}-(i-1)\bar x_a)\)
令\(c_i=\sum\limits_{j=1}^{i-1}-(i-1)\bar x_a\)
则有\(x_i=x_1-c_i\)
\(ans=|x_1-c_1|\)(\(i=1\)时,\(c_i=0\))\(+|x_1-c_2|+\cdots+|x_1-c_n|\)。
明显这就是一个普通的绝对值方程,取\(x_1\)为\(c_1\sim c_n\)中位数时\(ans\)最小
代码如下:
#include<bits/stdc++.h>
using namespace std;
int n,x[110],a[110],xn,ans,x_1;
int main()
{
ios::sync_with_stdio(0);
cin.tie(0),cout.tie(0);
cin>>n;
for(int i=1;i<=n;i++)
{
cin>>a[i];
a[i]+=a[i-1];
}
xn=a[n]/n;
for(int i=1;i<=n;i++)x[i]=a[i-1]-(i-1)*xn;
sort(x+1,x+n+1);
x_1=x[(n+1)/2];
for(int i=1;i<=n;i++)
ans+=abs(x_1-x[i]);
cout<<ans;
return 0;
}
接下来我们再讲讲网络流做法。
将每个仓库库存量减去平均数。建一个超级源点连向库存量为正数的仓库,而库存量为负数的仓库连向超级汇点,
单位费用为\(0\),容量为库存量的绝对值。
当然,每个点也要向旁边连边,单位费用为\(1\)(即搬运量),容量设为无限大。然后跑一遍\(dinic\)就行了,最后答案为最小花费。
代码如下:
#include<bits/stdc++.h>
#define ll long long
#define mxn 1010
using namespace std;
ll n,m,ans,st,ed,vis[110],ansc;
ll pre[110],dis[110],cnt;
ll head[mxn],to[mxn],val[mxn],t[mxn],nxt[mxn],w;
ll x[110],sum,flow[mxn],use[mxn];
ll ansmx,ansmn;
void add_edge(ll u,ll v,ll w,ll c)
{
to[++cnt]=v;
nxt[cnt]=head[u];
t[cnt]=w,val[cnt]=c;
head[u]=cnt;
return;
}
bool bfs(int s)
{
for(int i=0;i<=n+1;i++)
dis[i]=1e15,pre[i]=0,vis[i]=0;
vis[s]=1,dis[s]=0,flow[s]=1e15;
queue<ll> q;
q.push(s);
while(!q.empty())
{
ll u=q.front();
q.pop();
vis[u]=0;
for(int i=head[u];i;i=nxt[i])
{
ll v=to[i];
if(dis[v]>dis[u]+val[i]&&t[i])
{
dis[v]=dis[u]+val[i];
flow[v]=min(t[i],flow[u]);
pre[v]=u,use[v]=i;
if(!vis[v])
{
vis[v]=1;
q.push(v);
}
}
}
}
return pre[n+1]!=0;
}
pair<ll,ll> dinic()
{
while(bfs(0))
{
ansmx+=flow[n+1];
int u=n+1;
ansmn+=flow[n+1]*dis[n+1];
while(u)
{
t[use[u]]-=flow[n+1];
t[use[u]^1]+=flow[n+1];
u=pre[u];
}
}
return make_pair(ansmx,ansmn);
}
int main()
{
ios::sync_with_stdio(0);
cin.tie(0),cout.tie(0);
cin>>n;
for(int i=1;i<=n;i++)
{
cin>>x[i];
sum+=x[i];
}
cnt=1;
sum/=n;
for(int i=1;i<=n;i++)
x[i]-=sum;
for(int i=1;i<=n;i++)
{
if(x[i]>0)
{
add_edge(0,i,x[i],0);
add_edge(i,0,0,0);
}
if(x[i]<0)
{
add_edge(i,n+1,-x[i],0);
add_edge(n+1,i,0,0);
}
}
for(int i=1;i<=n;i++)
{
if(i!=1)
{
add_edge(i,i-1,1e15,1);
add_edge(i-1,i,0,-1);
}
if(i!=n)
{
add_edge(i,i+1,1e15,1);
add_edge(i+1,i,0,-1);
}
}
add_edge(1,n,1e15,1);
add_edge(n,1,0,-1);
add_edge(n,1,1e15,1);
add_edge(1,n,0,-1);
pair<ll,ll> ansp=dinic();
cout<<ansp.second;
return 0;
}
T5 分配问题
思路:KM最小费用流
其实可以一眼看出是二分图最大权完美匹配,但是我不会这是网络流题单,我们用\(dinic\)做。
还是建一个超级源点,一个超级汇点,将每个人和每份工作连一条边,因为不能选多份工作,所以容量设为\(1\)就行了,而把单位费用设为效益。源点向每个人连一条边,容量,每份工作向汇点连一条边,容量为\(1\),费用为\(0\)。
要求最大费用,就把费用取负求最小费用流,最后再把答案取负就行了。
代码如下:
#include<bits/stdc++.h>
#define pil pair<long long,long long>
#define ll long long
#define mp make_pair
#define mxn 100010
#define mxm 1010
using namespace std;
ll n,m,ans,st,ed,vis[mxm],ansc;
ll pre[mxm],dis[mxm],cnt;
ll head[mxn],to[mxn],val[mxn],t[mxn],nxt[mxn],w;
ll sum,flow[mxn],use[mxn],c[mxm][mxm];
ll ansmx,ansmn;
void add_edge(ll u,ll v,ll w,ll c)
{
to[++cnt]=v;
nxt[cnt]=head[u];
t[cnt]=w,val[cnt]=c;
head[u]=cnt;
}
bool bfs(int s)
{
for(int i=0;i<=n*2+1;i++)
dis[i]=1e15,pre[i]=-1,vis[i]=0,flow[i]=1e15;
vis[s]=1,dis[s]=0;
queue<ll> q;
q.push(s);
while(!q.empty())
{
ll u=q.front();
q.pop();
vis[u]=0;
for(int i=head[u];i;i=nxt[i])
{
ll v=to[i];
if(dis[v]>dis[u]+val[i]&&t[i])
{
dis[v]=dis[u]+val[i];
flow[v]=min(t[i],flow[u]);
pre[v]=u,use[v]=i;
if(!vis[v])
{
vis[v]=1;
q.push(v);
}
}
}
}
return pre[ed]!=-1;
}
pil dinic()
{
while(bfs(st))
{
int u=ed;
ansmx+=flow[ed];
ansmn+=flow[ed]*dis[ed];
while(u)
{
t[use[u]]-=flow[ed];
t[use[u]^1]+=flow[ed];
u=pre[u];
}
}
return make_pair(ansmx,ansmn);
}
void init()
{
for(int i=0;i<=cnt;i++)
{
head[i]=to[i]=t[i]=nxt[i]=val[i]=0;
flow[i]=use[i]=0;
}
cnt=1;
ansmx=ansmn=0;
return;
}
int main()
{
ios::sync_with_stdio(0);
cin.tie(0),cout.tie(0);
cin>>n;
st=0,ed=n*2+1;
cnt=1;
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
{
cin>>w;
c[i][j]=w;
add_edge(i,j+n,1,w);
add_edge(j+n,i,0,-w);
}
for(int i=1;i<=n;i++)
{
add_edge(st,i,1,0);
add_edge(i,st,0,0);
add_edge(i+n,ed,1,0);
add_edge(ed,i+n,0,0);
}
pil ansp=dinic();
cout<<ansp.second<<'\n';
init();
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
{
w=c[i][j];
add_edge(i,j+n,1,-w);
add_edge(j+n,i,0,w);
}
for(int i=1;i<=n;i++)
{
add_edge(st,i,1,0);
add_edge(i,st,0,0);
add_edge(i+n,ed,1,0);
add_edge(ed,i+n,0,0);
}
ansp=dinic();
cout<<-ansp.second;
return 0;
}
T6 运输问题
思路:费用流
与上题差不多,就是输入输出变了一下。
源点连向仓库,商店连向货物,容量为货物量/需要货物量,费用\(0\)。仓库与商店连边,容量无限大,费用题干已给出。然后跑\(dinic\)。
代码如下:
#include<bits/stdc++.h>
#define ll long long
#define mp make_pair
#define mxn 100010
#define mxm 2010
#define pil pair<long long,long long>
using namespace std;
ll n,m,st,ed,vis[mxm];
ll pre[mxm],dis[mxm],cnt;
ll head[mxn],to[mxn],val[mxn],t[mxn],nxt[mxn],w;
ll flow[mxn],use[mxn],a[mxm],b[mxm],c[mxm][mxm];
ll ansmx,ansmn,ans[mxm];
void add_edge(ll u,ll v,ll w,ll c)
{
to[++cnt]=v;
nxt[cnt]=head[u];
t[cnt]=w,val[cnt]=c;
head[u]=cnt;
}
bool bfs(int s)
{
for(int i=0;i<=n+m+1;i++)
dis[i]=1e15,pre[i]=-1,vis[i]=0,flow[i]=1e15;
vis[s]=1,dis[s]=0;
queue<ll> q;
q.push(s);
while(!q.empty())
{
ll u=q.front();
q.pop();
vis[u]=0;
for(int i=head[u];i;i=nxt[i])
{
ll v=to[i];
if(dis[v]>dis[u]+val[i]&&t[i])
{
dis[v]=dis[u]+val[i];
flow[v]=min(t[i],flow[u]);
pre[v]=u,use[v]=i;
if(!vis[v])
{
vis[v]=1;
q.push(v);
}
}
}
}
return pre[ed]!=-1;
}
pil dinic()
{
while(bfs(st))
{
int u=ed;
ansmx+=flow[ed];
ansmn+=flow[ed]*dis[ed];
while(u)
{
t[use[u]]-=flow[ed];
t[use[u]^1]+=flow[ed];
ans[u]=pre[u];
u=pre[u];
}
}
return mp(ansmx,ansmn);
}
void init()
{
for(int i=0;i<=cnt;i++)
{
head[i]=to[i]=t[i]=nxt[i]=val[i]=0;
flow[i]=use[i]=0;
}
cnt=1;
ansmx=ansmn=0;
return;
}
int main()
{
ios::sync_with_stdio(0);
cin.tie(0),cout.tie(0);
cin>>n>>m;
st=0,ed=n+m+1,cnt=1;
for(int i=1;i<=n;i++)
{
ll k;
cin>>k;
a[i]=k;
add_edge(0,i,k,0);
add_edge(i,0,0,0);
}
for(int i=1;i<=m;i++)
{
ll k;
cin>>k;
b[i]=k;
add_edge(n+i,ed,k,0);
add_edge(ed,n+i,0,0);
}
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
{
ll k;
cin>>k;
c[i][j]=k;
add_edge(i,j+n,1e15,k);
add_edge(j+n,i,0,-k);
}
pil ansp=dinic();
cout<<ansp.second<<'\n';
init();
for(int i=1;i<=n;i++)
{
ll k;
k=a[i];
add_edge(0,i,k,0);
add_edge(i,0,0,0);
}
for(int i=1;i<=m;i++)
{
ll k;
k=b[i];
add_edge(n+i,ed,k,0);
add_edge(ed,n+i,0,0);
}
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
{
ll k;
k=c[i][j];
add_edge(i,j+n,1e15,-k);
add_edge(j+n,i,0,k);
}
ansp=dinic();
cout<<-ansp.second;
return 0;
}
T7 圆桌问题
思路:最大流
这题也很简单,建超级源汇点,源点连向单位,餐桌流向汇点,容量为人数/餐桌可容纳人数。注意是最大流,不需要费用。每个单位向每个餐桌连边,由于同一个单位的人不在同一餐桌就餐,所以容量设为\(1\)就行了。
最后判断最大流与总人数是否相同,不相同则无解。输出方案时枚举一下就行了。
另外注意这不是费用流,不用跑\(spfa\),所以到了汇点直接退出,不然会超时。
代码如下:
#include<bits/stdc++.h>
#define mxn 100010
#define mxm 1010
using namespace std;
int m,n,cnt=1,st,ed;
int to[mxn],nxt[mxn],head[mxn],t[mxn];
int r[mxm],c[mxm],sum,ansmx,vis[mxn];
int flow[mxn],dep[mxn],use[mxn],pre[mxn];
void add_edge(int u,int v,int w)
{
to[++cnt]=v;
nxt[cnt]=head[u];
t[cnt]=w;
head[u]=cnt;
return;
}
bool bfs(int s)
{
for(int i=0;i<=ed;i++)
dep[i]=0,pre[i]=-1,vis[i]=0,flow[i]=1e15;
dep[s]=1;
queue<int> q;
q.push(s);
while(!q.empty())
{
int u=q.front();
q.pop();
for(int i=head[u];i;i=nxt[i])
{
int v=to[i];
if(vis[v]||t[i]==0)continue;
q.push(v);
vis[v]=1;
flow[v]=min(t[i],flow[u]);
pre[v]=u;
use[v]=i;
if(v==ed)return 1;
}
}
return pre[ed]!=-1;
}
int dinic()
{
while(bfs(st))
{
int u=ed;
ansmx+=flow[ed];
while(u)
{
t[use[u]]-=flow[ed];
t[use[u]^1]+=flow[ed];
u=pre[u];
}
}
return ansmx;
}
int main()
{
ios::sync_with_stdio(0);
cin.tie(0),cout.tie(0);
cin>>m>>n;
st=0,ed=n+m+1;
for(int i=1;i<=m;i++)
{
int k;
cin>>k;
sum+=k;
add_edge(0,i,k);
add_edge(i,0,0);
}
for(int i=1;i<=n;i++)
{
int k;
cin>>k;
add_edge(m+i,ed,k);
add_edge(ed,m+i,0);
}
for(int i=1;i<=m;i++)
for(int j=1;j<=n;j++)
{
add_edge(i,m+j,1);
add_edge(m+j,i,0);
}
int ret=dinic();
if(ret!=sum)
{
cout<<0;
return 0;
}
cout<<1<<'\n';
for(int i=1;i<=m;i++,cout<<'\n')
for(int j=m+1;j<ed;j++)
for(int k=head[j];k;k=nxt[k])
if(to[k]==i&&t[k])
cout<<j-m<<' ';
return 0;
}
T8 试题库问题
思路:最大流
这道题太典了,还是与上题一样,不过是建图方式变了。
源点连向试题,容量\(1\),每类题连向汇点,容量为规定数量。每道试题连向所属那一类,容量\(1\)。判无解和输出方案与T7类似。
代码如下:
#include<bits/stdc++.h>
#define ll long long
#define mxn 100010
#define mxm 1010
using namespace std;
int p,m,n,cnt=1,st,ed;
int to[mxn],nxt[mxn],head[mxn],t[mxn];
int r[mxm],c[mxm],sum,ansmx,vis[mxn];
int flow[mxn],dep[mxn],use[mxn],pre[mxn],pro[mxm];
void add_edge(int u,int v,int w)
{
to[++cnt]=v;
nxt[cnt]=head[u];
t[cnt]=w;
head[u]=cnt;
return;
}
bool bfs(int s)
{
for(int i=0;i<=ed;i++)
dep[i]=0,pre[i]=-1,vis[i]=0,flow[i]=1e15;
dep[s]=1;
queue<int> q;
q.push(s);
while(!q.empty())
{
int u=q.front();
q.pop();
for(int i=head[u];i;i=nxt[i])
{
int v=to[i];
if(vis[v]||t[i]==0)continue;
q.push(v);
vis[v]=1;
flow[v]=min(t[i],flow[u]);
pre[v]=u;
use[v]=i;
if(v==ed)return 1;
}
}
return pre[ed]!=-1;
}
int dinic()
{
while(bfs(st))
{
int u=ed;
ansmx+=flow[ed];
while(u)
{
t[use[u]]-=flow[ed];
t[use[u]^1]+=flow[ed];
u=pre[u];
}
}
return ansmx;
}
int main()
{
ios::sync_with_stdio(0);
cin.tie(0),cout.tie(0);
cin>>p>>n;
for(int i=1;i<=p;i++)
{
cin>>pro[i];
m+=pro[i];
}
st=0,ed=n+p+1,cnt=1;
for(int i=1;i<=p;i++)
{
add_edge(n+i,ed,pro[i]);
add_edge(ed,n+i,0);
}
for(int i=1;i<=n;i++)
{
ll a;
cin>>a;
for(int j=1;j<=a;j++)
{
ll opt;
cin>>opt;
add_edge(i,n+opt,1);
add_edge(n+opt,i,0);
}
}
for(int i=1;i<=n;i++)
{
add_edge(0,i,1);
add_edge(i,0,0);
}
ll ans=dinic();
if(ans!=m)
{
cout<<"No Solution!";
return 0;
}
for(int i=1;i<=p;i++,cout<<'\n')
{
cout<<i<<':';
for(int j=head[n+i];j;j=nxt[j])
if(to[j]<=n&&t[j])
cout<<to[j]<<' ';
}
return 0;
}
T9 骑士共存问题
思路:还是最大流
但是这题挺有趣,还带点思维。
我们观察到题干给出的图:
发现我们可以对棋盘交替染色,同色格子上的马互相攻击不到对方。所以我们可以建一个源点连向所有红色(或黄色,这里假设为红色)格子。然后将每个红色格连向它们能攻击到的黄色(只能是黄色)格子,再将黄色格子连向汇点。容量都设为\(1\),然后跑最大流。
注意这里的最大流是指红色格子能攻击到黄色格子的最大数量。所以最后统计是要减去它。
代码如下:
#include<bits/stdc++.h>
#define ll long long
#define mxn 6000010
#define mxm 2010
using namespace std;
int p,m,n,cnt=1,st,ed,cnt2;
ll to[mxn],nxt[mxn],head[mxn],t[mxn],cur[mxn];
ll mp[mxm][mxm],ansmx,vis[mxn];
ll dep[mxn],use[mxn],pre[mxn],pro[mxm];
ll dir[8][2]={{-2,-1},{-2,1},{2,-1},{2,1},{1,2},{-1,-2},{1,-2},{-1,2}};
inline ll getid(ll x,ll y)
{
return (x-1)*n+y;
}
inline bool is_odd(ll x,ll y)
{
if((x+y)%2==1) return 1;
return 0;
}
void add_edge(ll u,ll v,ll w)
{
to[++cnt]=v;
nxt[cnt]=head[u];
t[cnt]=w;
head[u]=cnt;
return;
}
bool bfs(ll s)
{
for(ll i=0;i<=ed;i++)
dep[i]=0;
dep[s]=1;
queue<ll> q;
q.push(s);
while(!q.empty())
{
ll u=q.front();
q.pop();
for(ll i=head[u];i;i=nxt[i])
{
ll v=to[i];
if(dep[v]||t[i]==0)continue;
dep[v]=dep[u]+1;
q.push(v);
}
}
return dep[ed];
}
ll dfs(ll u,ll fl)
{
if(u==ed)
return fl;
ll flow=0;
for(ll i=head[u];i;i=nxt[i])
{
if(flow==fl)return flow;
ll v=to[i];
if(t[i]>0&&dep[v]==dep[u]+1)
{
ll d=dfs(v,min(t[i],fl-flow));
if(d>0)
{
t[i]-=d;
t[i^1]+=d;
flow+=d;
}
}
}
if(!flow)dep[u]=0;//一个小优化
//如果分不出流就不用走了
return flow;
}
ll dinic()
{
while(bfs(st))
{
ll flg=1;
while(flg)
{
flg=dfs(st,1e15);
ansmx+=flg;
}
}
return ansmx;
}
int main()
{
ios::sync_with_stdio(0);
cin.tie(0),cout.tie(0);
cin>>n>>m;
st=0,ed=n*n+1;
for(ll i=1;i<=m;i++)
{
ll u,v;
cin>>u>>v;
mp[u][v]=1;
}
for(ll i=1;i<=n;i++)
for(ll j=1;j<=n;j++)
{
if(is_odd(i,j))
{
if(mp[i][j])continue;
add_edge(0,getid(i,j),1);
add_edge(getid(i,j),0,0);
for(ll k=0;k<8;k++)
{
ll fi=i+dir[k][0],fj=j+dir[k][1];
if(fi<1||fj<1||fi>n||fj>n||mp[fi][fj])continue;
add_edge(getid(i,j),getid(fi,fj),1);
add_edge(getid(fi,fj),getid(i,j),0);
}
}
else
{
if(mp[i][j])continue;
add_edge(getid(i,j),ed,1);
add_edge(ed,getid(i,j),0);
}
}
ll ans=dinic();
cout<<n*n-m-ans;
return 0;
}
T10 最长k可重区间集问题
思路:费用流
从这题开始,题单的难度再上一个层次,思维强度提升很大,不像之前几题一样套板子了。
这题第一眼看上去像一个匹配问题,把区间拆成点之后将每个区间匹配到对应的几个点上跑最大费用流。但是这里每个区间是强制匹配好几个点的,有点难搞。
这里的做法是把区间串起来,这样就可以强制匹配了。比如样例,我们可以这样建图:
其中,直线边容量为\(k\),费用为\(0\);曲线边容量为\(1\),费用为\(r-l\)。这样能保证当一段区间有\(k\)个重叠时,它后面的区间就没有流量了,可以试着手推一下。
另外,可以将拆出来的点离散化一下,最后跑一遍最大费用流就行了。
代码如下:
#include<bits/stdc++.h>
#define pil pair<long long,long long>
#define ll long long
#define mp make_pair
#define mxn 100010
#define mxm 1010
using namespace std;
ll n,k,ans,st,ed,vis[mxm],cnt2;
ll pre[mxm],dis[mxm],cnt;
ll head[mxn],to[mxn],val[mxn],t[mxn],nxt[mxn],w;
ll flow[mxn],use[mxn],b[mxm],x[mxm],y[mxm];
ll ansmx,ansmn,ls[mxm];
void lisan()
{
sort(b+1,b+cnt2+1);
int a=cnt2;
cnt2=0;
for(int i=1;i<=a;i++)
{
if(b[i]==b[i+1])continue;
ls[++cnt2]=b[i];
}
for(int i=1;i<=n;i++)
{
x[i]=lower_bound(ls+1,ls+cnt2+1,x[i])-ls;
y[i]=lower_bound(ls+1,ls+cnt2+1,y[i])-ls;
}
return;
}
void add_edge(ll u,ll v,ll w,ll c)
{
to[++cnt]=v;
nxt[cnt]=head[u];
t[cnt]=w,val[cnt]=c;
head[u]=cnt;
}
bool bfs(int s)
{
for(int i=0;i<=n*2+1;i++)
dis[i]=1e15,pre[i]=-1,vis[i]=0,flow[i]=1e15;
vis[s]=1,dis[s]=0;
queue<ll> q;
q.push(s);
while(!q.empty())
{
ll u=q.front();
q.pop();
vis[u]=0;
for(int i=head[u];i;i=nxt[i])
{
ll v=to[i];
if(dis[v]>dis[u]+val[i]&&t[i])
{
dis[v]=dis[u]+val[i];
flow[v]=min(t[i],flow[u]);
pre[v]=u,use[v]=i;
if(!vis[v])
{
vis[v]=1;
q.push(v);
}
}
}
}
return pre[ed]!=-1;
}
pil dinic()
{
while(bfs(st))
{
int u=ed;
ansmx+=flow[ed];
ansmn+=flow[ed]*dis[ed];
while(u)
{
t[use[u]]-=flow[ed];
t[use[u]^1]+=flow[ed];
u=pre[u];
}
}
return make_pair(ansmx,ansmn);
}
int main()
{
ios::sync_with_stdio(0);
cin.tie(0),cout.tie(0);
cin>>n>>k;
for(int i=1;i<=n;i++)
{
cin>>x[i]>>y[i];
if(x[i]>y[i])swap(x[i],y[i]);
b[++cnt2]=x[i],b[++cnt2]=y[i];
}
st=0,ed=n*2+1;
cnt=1;
lisan();
st=0,ed=cnt2+1;
for(int i=1;i<=cnt2;i++)
{
if(i==1)
{
add_edge(st,i,k,0);
add_edge(i,st,0,0);
continue;
}
add_edge(i-1,i,k,0);
add_edge(i,i-1,0,0);
if(i==cnt2)
{
add_edge(i,ed,k,0);
add_edge(ed,i,0,0);
}
}
for(int i=1;i<=n;i++)
{
add_edge(x[i],y[i],1,ls[x[i]]-ls[y[i]]);
add_edge(y[i],x[i],0,ls[y[i]]-ls[x[i]]);
}
pil ansp=dinic();
cout<<-ansp.second;
return 0;
}
T11 方格取数问题
思路:最大流
先把题意翻译一下:“没有公共边”指的是选的任意两个格子不相邻。
咦?这题看起来怎么这么像骑士共存问题?只不过是将同色格与相邻格连起来而已。建图时,源点连向黑色点(假定为黑色)时,容量为黑色点的点权;白色点连向汇点时,容量为白色点的点权;黑色点连向白色点时,容量无限大;然后我们就可以开心跑最大流了。
还有一点要注意的地方:此题\(m\)可能小于\(n\),所以取编号时注意一下。
代码如下:
#include<bits/stdc++.h>
#define ll long long
#define mxn 6000010
#define mxm 2010
using namespace std;
int p,m,n,cnt=1,st,ed,cnt2;
ll to[mxn],nxt[mxn],head[mxn],t[mxn],cur[mxn];
ll mp[mxm][mxm],ansmx,vis[mxn],sum;
ll dep[mxn],use[mxn],pre[mxn],pro[mxm];
ll dir[4][2]={{-1,0},{1,0},{0,1},{0,-1}};
inline ll getid(ll x,ll y)
{
if(n<m)return (y-1)*m+x;
return (x-1)*n+y;
}
inline bool is_odd(ll x,ll y)
{
if((x+y)%2==1) return 1;
return 0;
}
void add_edge(ll u,ll v,ll w)
{
to[++cnt]=v;
nxt[cnt]=head[u];
t[cnt]=w;
head[u]=cnt;
return;
}
bool bfs(ll s)
{
for(ll i=0;i<=ed;i++)
dep[i]=0;
dep[s]=1;
queue<ll> q;
q.push(s);
while(!q.empty())
{
ll u=q.front();
q.pop();
for(ll i=head[u];i;i=nxt[i])
{
ll v=to[i];
if(dep[v]||t[i]==0)continue;
dep[v]=dep[u]+1;
q.push(v);
}
}
return dep[ed];
}
ll dfs(ll u,ll fl)
{
if(u==ed)
return fl;
ll flow=0;
for(ll i=head[u];i;i=nxt[i])
{
if(flow==fl)return flow;
ll v=to[i];
if(t[i]>0&&dep[v]==dep[u]+1)
{
ll d=dfs(v,min(t[i],fl-flow));
if(d>0)
{
t[i]-=d;
t[i^1]+=d;
flow+=d;
}
}
}
if(!flow)dep[u]=0;
return flow;
}
ll dinic()
{
while(bfs(st))
{
ll flg=1;
while(flg)
{
flg=dfs(st,1e15);
ansmx+=flg;
}
}
return ansmx;
}
int main()
{
ios::sync_with_stdio(0);
cin.tie(0),cout.tie(0);
cin>>m>>n;
st=0,ed=m*n+1;
for(ll i=1;i<=m;i++)
for(ll j=1;j<=n;j++)
{
cin>>mp[i][j];
sum+=mp[i][j];
}
for(ll i=1;i<=m;i++)
for(ll j=1;j<=n;j++)
{
if(is_odd(i,j))
{
add_edge(0,getid(i,j),mp[i][j]);
add_edge(getid(i,j),0,0);
for(ll k=0;k<4;k++)
{
ll fi=i+dir[k][0],fj=j+dir[k][1];
if(fi<1||fj<1||fi>m||fj>n)continue;
add_edge(getid(i,j),getid(fi,fj),1e15);
add_edge(getid(fi,fj),getid(i,j),0);
}
}
else
{
add_edge(getid(i,j),ed,mp[i][j]);
add_edge(ed,getid(i,j),0);
}
}
ll ans=dinic();
cout<<sum-ans;
return 0;
}
T12 汽车加油行驶问题
思路:记忆化搜索费用流
好吧我们还是讲一讲记忆化搜索的解法
一、记忆化搜索(来自mzy4003大佬的题解)
其实我一开始也只想到了搜索
我们设\(f[x][y][z]\)为到了\((x,y)\)且剩余步数为\(z\)时的最小费用。初始化为无限大,若当前费用大于最小费用(以及剩余步数大于\(z\)时的状态的最小费用),则进行剪枝。
代码如下:
#include<bits/stdc++.h>
#define ll long long
using namespace std;
int n,k,a,b,c,mp[110][110],f[110][110][11],ans=1e9;
void dfs(int x,int y,int m,int step)
{
if(x<1||y<1||x>n||y>n)return;
if(m>=ans)return;
step--;
for(int i=step;i<=k;i++)
if(m>=f[x][y][i])return;
f[x][y][step]=m;
if(x==n&&y==n)
{
ans=min(ans,m);
return;
}
if(mp[x][y])m+=a,step=k;
if(step==0)m+=a+c,step=k;
dfs(x+1,y,m,step);
dfs(x,y+1,m,step);
dfs(x-1,y,m+b,step);
dfs(x,y-1,m+b,step);
return;
}
int main()
{
ios::sync_with_stdio(0);
cin.tie(0),cout.tie(0);
cin>>n>>k>>a>>b>>c;
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
cin>>mp[i][j];
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
for(int l=0;l<=k;l++)
f[i][j][l]=1e9;
dfs(1,1,0,k+1);
cout<<ans;
return 0;
}
这上面的剪枝,少一个都会\(TLE\)!
二、网络流
若无流量限制,很容易想到源点连向\((1,1)\),\((n,n)\)连向汇点,费用为\(0\);向右下走的费用为\(0\),向左上走的费用为\(b\)。所有边容量为\(1\),跑最小费用流就行了,但这里有油量限制,有点难搞。
这时我们就要推出一个全新的东西:分层图
把图分成\(k+1\)层,第一层为满油,第二层为\(k-1\)格油,依此类推,第\(k+1\)层为\(0\)格油。
连边时这样连:
若\((i,j)\)处无加油站,就在往旁边连的时候往上连一层,费用\(0\)。若到了第\(k+1\)层就往第一层连一条费用为\(a+c\)的边。
若\((i,j)\)处有加油站,就往第一层连,费用为\(a\);
若是往左上连,费用还要多加一个\(b\);
源点连向第一层的\((1,1)\);每一层的\((n,n)\)都连向源点。最后跑一遍最小费用流就行了。
代码如下:
#include<bits/stdc++.h>
#define ll long long
#define pii pair<int,int>
#define mxn 1000010
#define mxm 110010
using namespace std;
int n,k,a,b,c,ans,st,ed,vis[mxm],ansc;
int pre[mxm],dis[mxm],cnt;
int head[mxn],to[mxn],val[mxn],t[mxn],nxt[mxn],w;
int sum,flow[mxn],use[mxn];
int ansmx,ansmn,mp[110][110],dir[4][2]={{0,1},{1,0},{-1,0},{0,-1}};
inline void add_edge(int u,int v,int w,int c)
{
to[++cnt]=v;
nxt[cnt]=head[u];
t[cnt]=w,val[cnt]=c;
head[u]=cnt;
}
int getid(int x,int y,int k)
{
return (x-1)*n+y+(k-1)*n*n;
}
bool bfs(int s)
{
for(int i=0;i<=ed;i++)
dis[i]=1e15,pre[i]=0,vis[i]=0;
vis[s]=1,dis[s]=0,flow[s]=1e15;
queue<int> q;
q.push(s);
while(!q.empty())
{
int u=q.front();
q.pop();
vis[u]=0;
for(int i=head[u];i;i=nxt[i])
{
int v=to[i];
if(dis[v]>dis[u]+val[i]&&t[i])
{
dis[v]=dis[u]+val[i];
flow[v]=min(t[i],flow[u]);
pre[v]=u,use[v]=i;
if(!vis[v])
{
vis[v]=1;
q.push(v);
}
}
}
}
return pre[ed]!=0;
}
pair<int,int> dinic()
{
while(bfs(st))
{
int u=ed;
ansmx+=flow[ed];
ansmn+=flow[ed]*dis[ed];
while(u)
{
t[use[u]]-=flow[ed];
t[use[u]^1]+=flow[ed];
u=pre[u];
}
}
return make_pair(ansmx,ansmn);
}
int main()
{
ios::sync_with_stdio(0);
cin.tie(0),cout.tie(0);
cin>>n>>k>>a>>b>>c;
st=0,ed=n*n*(k+1)+1;
add_edge(0,1,1,0);
add_edge(1,0,0,0);
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
{
cin>>mp[i][j];
if(!mp[i][j])
{
for(int p=0;p<4;p++)
{
int fi=i+dir[p][0],fj=j+dir[p][1];
if(fi<1||fj<1||fi>n||fj>n)continue;
for(int l=1;l<=k;l++)
{
if(p<2)add_edge(getid(i,j,l),getid(fi,fj,l+1),1,0),add_edge(getid(fi,fj,l+1),getid(i,j,l),0,0);
else add_edge(getid(i,j,l),getid(fi,fj,l+1),1,b),add_edge(getid(fi,fj,l+1),getid(i,j,l),0,-b);
}
if(p<2)add_edge(getid(i,j,k+1),getid(fi,fj,2),1,a+c),add_edge(getid(fi,fj,2),getid(i,j,k+1),0,-a-c);
else add_edge(getid(i,j,k+1),getid(fi,fj,2),1,a+b+c),add_edge(getid(fi,fj,2),getid(i,j,k+1),0,-a-b-c);
}
}
else
{
for(int p=0;p<4;p++)
{
int fi=i+dir[p][0],fj=j+dir[p][1];
if(fi<1||fj<1||fi>n||fj>n)continue;
for(int l=1;l<=k+1;l++)
{
if(p<2)add_edge(getid(i,j,l),getid(fi,fj,2),1,a),add_edge(getid(fi,fj,2),getid(i,j,l),0,-a);
else add_edge(getid(i,j,l),getid(fi,fj,2),1,a+b),add_edge(getid(fi,fj,2),getid(i,j,l),0,-a-b);
}
}
}
if(i==n&&j==n)
for(int l=1;l<=k+1;l++)
add_edge(getid(i,j,l),ed,1,0),add_edge(ed,getid(i,j,l),0,0);
}
pii ansp=dinic();
cout<<ansp.second;
return 0;
}
观察代码,容易发现每条边的容量都是\(1\),于是完全可以把容量去掉,就变成了求最短路,跑一遍\(SPFA/dijkstra\)就行了。
古语有云:行百里者半五十,我们已经完成一半了,继续前进吧!
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】