网络流基础题
数字梯形问题
题目链接
这道题的三个操作,感觉比较全面的包括了建图的想法,也算比较基础的网络流入门题。对理解网络流应该比较有帮助吧
题目描述
给定一个由
n
n
n 行数字组成的数字梯形如下图所示。
梯形的第一行有
m
m
m 个数字。从梯形的顶部的
m
m
m 个数字开始,在每个数字处可以沿左下或右下方向移动,形成一条从梯形的顶至底的路径。
分别遵守以下规则:
1.从梯形的顶至底的 m m m 条路径互不相交;
2.从梯形的顶至底的 m m m 条路径仅在数字结点处相交;
3.从梯形的顶至底的 m m m 条路径允许在数字结点相交或边相交。
输入格式
第
1
1
1 行中有
1
1
1 个正整数
m
m
m 和
n
n
n,分别表示数字梯形的第一行有
m
m
m 个数字,共有
n
n
n 行。接下来的
n
n
n 行是数字梯形中各行的数字。
第 1 1 1 行有 m m m 个数字,第 2 2 2 行有 m + 1 m+1 m+1 个数字,以此类推。
输出格式
将按照规则
1
1
1,规则
2
2
2,和规则
3
3
3 计算出的最大数字总和并输出,每行一个最大总和。
输入输出样例
输入 #1
2 5
2 3
3 4 5
9 10 9 1
1 1 10 1 1
1 1 10 12 1 1
输出 #1
66
75
77
分析
操作1: m条路径互不相交,其实就是每个点只能走一次。(拆点)
操作2: 在操作1的基础上,取消了只能走一次的限制,那就不需要拆点正常连流量为1(边不可以相交)的边即可。但是要注意最后一排的点流向超级汇点(路径到达最后一排可以重复)。
操作3: 就是完全没有限制,点边都可以重复,把互相的流量改成INF即可。
关于拆点的问题:我们把一个顶点分成A,B两个点,并且将A点和B点之间连上一条流量为1的边,把A作为流入点,B作为流出点,那么如果有多条路径流向点A的话,最终也只有一条会流出点B,通过拆点就可以限制每个点只会被一条路径流过。
如上图所示,如果点1和点2同时流向点3,那么因为拆点限流的关系,最后结果只会计算一条路径。
然后就是具体的建图方法:
很简单 看代码就懂了
1.建立一个超级原点 s s s,连向每个第一排的点(流量为1,费用为0)
2.然后每一层都向下一层能到的地方连边(流量看操作,费用为点权)
3.操作一额外多了一个拆点,所以记录了一个 o o o表示所有点数,到时候拆点就加个 o o o就行
4.所有最后一层的点连向超级汇点 t t t(流量为INF,费用为点权)
三个操作分开写,每次要记得初始化。
AC代码
不会有人只看这个吧
#include<bits/stdc++.h>
#define pll pair<int,int>
#define pb push_back
#define FAST std::ios::sync_with_stdio(false),cin.tie(0),cout.tie(0)
#define ll long long
#define INF 1e18
#define inf 0x3f3f3f3f
#define pi acos(-1)
//#define ls now<<1
//#define rs now<<1|1
using namespace std;
const int mod = 1e9 + 7;
const int MAXN = 1e7 + 10;
const int N = 5e3 + 10;
int a[44][44],b[44][44];
int n,m,s,t;//从这里开始都是模板
struct node{
int t,nex;
ll w,c;
}e[MAXN];
ll ans,cost;
int head[N],cnt=1;
void add(int a,int b,ll c,ll d)
{
e[++cnt].t=b;e[cnt].nex=head[a];e[cnt].w=c; e[cnt].c=d;head[a]=cnt;
e[++cnt].t=a;e[cnt].nex=head[b];e[cnt].w=0;e[cnt].c=-d;head[b]=cnt;
}
ll st[N];
int pre[N];
bool vs[N];
bool spfa()
{
queue<int>q;
for(int i=0;i<=t;i++)
{
vs[i]=0;st[i]=-INF;pre[i]=-1;
}
q.push(s);st[s]=0;vs[s]=1;
while(!q.empty())
{
int u=q.front();q.pop();vs[u]=0;
for(int i=head[u];i!=-1;i=e[i].nex)
{
int v=e[i].t;
if(st[v]<st[u]+e[i].c&&e[i].w)
{
st[v]=st[u]+e[i].c;pre[v]=i;
if(!vs[v]){vs[v]=1;q.push(v);}
}
}
}
if(st[t]==-INF)return false;
ll d=INF;
for(int i=pre[t];i!=-1;i=pre[e[i^1].t]){
d=min(d,e[i].w);
}
for(int i=pre[t];i!=-1;i=pre[e[i^1].t]){
e[i].w-=d;
e[i^1].w+=d;
cost+=e[i].c*d;
}
ans+=d;
return true;
}//到这里开始建图
int o;
void op1()
{
memset(head,-1,sizeof(head));cnt=1;
s=0;t=o*2+1;
for(int i=1;i<=n;i++)add(s,b[1][i],1,0);
for(int i=1;i<=n+m-1;i++)add(b[m][i]+o,t,1,0);
for(int i=1;i<=m;i++)
{
for(int j=1;j<=n+i-1;j++)
{
add(b[i][j],b[i][j]+o,1,a[i][j]);
}
}
for(int i=1;i<m;i++)
{
for(int j=1;j<=n+i-1;j++)
{
add(b[i][j]+o,b[i+1][j],1,0);add(b[i][j]+o,b[i+1][j+1],1,0);
}
}
cost=0;ans=0;
while(spfa());
cout<<cost<<endl;
}
void op2()
{
memset(head,-1,sizeof(head));cnt=1;
s=0;t=o+1;
for(int i=1;i<=n;i++)add(s,b[1][i],1,0);
for(int i=1;i<=n+m-1;i++)add(b[m][i],t,INF,a[m][i]);//注意这里是INF
for(int i=1;i<m;i++)
{
for(int j=1;j<=n+i-1;j++)
{
add(b[i][j],b[i+1][j],1,a[i][j]);
add(b[i][j],b[i+1][j+1],1,a[i][j]);
}
}
cost=0;ans=0;
while(spfa());
cout<<cost<<endl;
}
void op3()
{
memset(head,-1,sizeof(head));cnt=1;
s=0;t=o+1;
for(int i=1;i<=n;i++)add(s,b[1][i],1,0);
for(int i=1;i<=n+m-1;i++)add(b[m][i],t,INF,a[m][i]);
for(int i=1;i<m;i++)
{
for(int j=1;j<=n+i-1;j++)
{
add(b[i][j],b[i+1][j],INF,a[i][j]);
add(b[i][j],b[i+1][j+1],INF,a[i][j]);
}
}
cost=0;ans=0;
while(spfa());
cout<<cost<<endl;
}
void work()
{
cin>>n>>m;
o=(n*2+m-1)*m/2;//计算总点数
int cntn=0;
for(int i=1;i<=m;i++)
{
for(int j=1;j<=n+i-1;j++)
{
cin>>a[i][j];
b[i][j]=++cntn;//给每个点加编号
}
}
op1();op2();op3();
}
int main() {//个人习惯分开写
FAST;
int t = 1;
//cin >> t;
while (t--)work();
return 0;
}
[CTSC1999]家园 / 星际转移问题
题目链接
题目描述
由于人类对自然资源的消耗,人们意识到大约在 2300 年之后,地球就不能再居住了。于是在月球上建立了新的绿地,以便在需要时移民。令人意想不到的是,2177 年冬由于未知的原因,地球环境发生了连锁崩溃,人类必须在最短的时间内迁往月球。
现有 n n n 个太空站位于地球与月球之间,且有 m m m 艘公共交通太空船在其间来回穿梭。每个太空站可容纳无限多的人,而太空船的容量是有限的,第 i i i 艘太空船只可容纳 h i h_i hi 个人。每艘太空船将周期性地停靠一系列的太空站,例如 ( 1 , 3 , 4 ) (1,3,4) (1,3,4)表示该太空船将周期性地停靠太空站 134134134 … 134134134… 134134134…。每一艘太空船从一个太空站驶往任一太空站耗时均为 1 1 1。人们只能在太空船停靠太空站(或月球、地球)时上、下船。
初始时所有人全在地球上,太空船全在初始站。试设计一个算法,找出让所有人尽快地全部转移到月球上的运输方案。
输入格式
输入的第一行是三个用空格隔开的整数,分别代表太空站个数
n
n
n,太空船个数
m
m
m 和地球上的人数
k
k
k。
第 2 2 2 到第 ( m + 1 ) (m+1) (m+1) 行,每行给出一艘太空船的信息,第 ( i + 1 ) (i+1) (i+1) 行的第一个整数 h i h_i hi 代表第 i i i 艘太空船可容纳的人数。随后有一个整数 rir_iri,代表第 iii 艘太空船停靠的站点数。之后有 r i r_i ri 个整数,依次代表该太空船停靠站点的编号 S i , j S_{i, j} Si,j,其中太空站自 1 1 1 至 n n n 编号,地球编号为 0 0 0,月球编号为 − 1 -1 −1。
输出格式
输出一行一个整数,代表将所有人转移到月球上的最短用时。若无解则输出
0
0
0。
输入输出样例
输入 #1
2 2 1
1 3 0 1 2
1 3 1 2 -1
输出 #1
5
数据范围
1 ≤ n ≤ 13 1 \leq n \leq13 1≤n≤13
1 ≤ m ≤ 20 1 \leq m \leq 20 1≤m≤20
1 ≤ k ≤ 50 1 \leq k \leq 50 1≤k≤50
1 ≤ r i ≤ n + 2 1 \leq r_i \leq n + 2 1≤ri≤n+2
− 1 ≤ S i , j ≤ n -1 \leq S_{i, j}\leq n −1≤Si,j≤n
分析
这道题数据范围很小,我们还是结合上面拆点的思想,将每个点编号,再按时间来拆点,造成一张分层图。
我们写一个结构体来存飞船信息(代码17到24行)
记录飞船容量以及流程,vector存每天所在位置,getd找两天的位置
我们考虑天数每增加一天该做的操作:
1.首先上一天肯定要和今天对应的点连边,表示可以停留。(第一个循环)
2.然后飞船会运人,那么就找到飞船上一天的位置和今天所在的位置连边,表示运输(第二个循环)
灵魂画师 呜呜 将就看吧
这里由于输入的时候地球为
0
0
0,月球为
−
1
-1
−1,所以我整体加了
2
2
2,保证正整数,所以每一层的节点数就是
n
+
2
n+2
n+2
1.S原点连向day0的地球,边权为K
2.所有天数的月球向T连边,边权为INF,因为到了月球就可以了
3.每新建一天就让上一天连向这一天,边权为INF,表示可以停留(船上不了了)
4.找到每个飞船这一天和上一天的位置,连边,边权为 h i h_i hi(一次只有那么多容量)
那么这题就做完了(doge)
好像还要判无解??
无解其实很简单,每个飞船能到的点用并查集维护,最后看看地球(2)和月球(1)是否在一个集合即可
AC代码
#include<bits/stdc++.h>
#define pll pair<int,int>
#define mp make_pair
#define pb push_back
#define FAST std::ios::sync_with_stdio(false),cin.tie(0),cout.tie(0)
#define ll long long
#define INF 1e18
#define inf 0x3f3f3f3f
#define pi acos(-1)
//#define ls now<<1
//#define rs now<<1|1
using namespace std;
const int mod = 1e9 + 7;
const int MAXN = 1e6 + 10;
const int N = 2e4 + 10;
int n,m,s,t,k;
struct ship{
int res,day;
vector<int>v;
pll getd(int res)
{
return mp(v[(res-1)%day],v[res%day]);
}
}sh[25];
struct node{
int t,nex;
ll w,c;
}e[MAXN];
ll ans,cost;
int head[N],cnt=1;
void add(int a,int b,ll c,ll d)
{
e[++cnt].t=b;e[cnt].nex=head[a];e[cnt].w=c; e[cnt].c=d;head[a]=cnt;
e[++cnt].t=a;e[cnt].nex=head[b];e[cnt].w=0;e[cnt].c=-d;head[b]=cnt;
}
ll st[N];
int pre[N];
bool vs[N];
bool spfa()
{
queue<int>q;
for(int i=0;i<=t;i++)
{
vs[i]=0;st[i]=INF;pre[i]=-1;
}
q.push(s);st[s]=0;vs[s]=1;
while(!q.empty())
{
int u=q.front();q.pop();vs[u]=0;
for(int i=head[u];i!=-1;i=e[i].nex)
{
int v=e[i].t;
if(st[v]>st[u]+e[i].c&&e[i].w)
{
st[v]=st[u]+e[i].c;pre[v]=i;
if(!vs[v]){vs[v]=1;q.push(v);}
}
}
}
if(st[t]==INF)return false;
ll d=INF;
for(int i=pre[t];i!=-1;i=pre[e[i^1].t]){
d=min(d,e[i].w);
}
for(int i=pre[t];i!=-1;i=pre[e[i^1].t]){
e[i].w-=d;
e[i^1].w+=d;
cost+=e[i].c*d;
}
ans+=d;
return true;
}
int fa[20];
int find(int x)
{
return fa[x]==x?x:fa[x]=find(fa[x]);
}
void work()
{
memset(head,-1,sizeof(head));
cin>>n>>m>>k;
for(int i=1;i<=n+2;i++)fa[i]=i;
for(int i=1;i<=m;i++)
{
cin>>sh[i].res>>sh[i].day;
for(int j=1;j<=sh[i].day;j++)
{
int x;
cin>>x;
x+=2;
sh[i].v.pb(x);
}
int a=find(sh[i].v[0]);
for(int j=1;j<sh[i].v.size();j++)
{
int b=find(sh[i].v[j]);
if(a!=b)fa[b]=a;
}
}
if(find(1)!=find(2))
{
cout<<0<<endl;
return;
}
s=0;t=10000;//t稍微赋大一些,不知道要几天才结束
add(s,2,INF,0);add(1,t,INF,0);
for(int day=1;;day++)
{
add(1+day*(n+2),t,INF,0);
for(int i=1;i<=n+2;i++)
{
add((day-1)*(n+2)+i,day*(n+2)+i,INF,0);
//cout<<(day-1)*(n+2)+i<<" "<<(day)*(n+2)+i<<endl;
}
for(int i=1;i<=m;i++)
{
pll a=sh[i].getd(day);
add((day-1)*(n+2)+a.first,day*(n+2)+a.second,sh[i].res,0);
//cout<<(day-1)*(n+2)+a.first<<" "<<day*(n+2)+a.second<<endl;
}
while(spfa())
//cout<<ans<<" "<<day<<endl;
if(ans>=k)
{
cout<<day<<endl;
return;
}
}
}
int main() {
FAST;
int t = 1;
//cin >> t;
while (t--)work();
return 0;
}
火星探险问题
题目链接
题目描述
火星探险队的登陆舱将在火星表面着陆,登陆舱内有多部障碍物探测车。登陆舱着陆后,探测车将离开登陆舱向先期到达的传送器方向移动。
探测车在移动中还必须采集岩石标本。每一块岩石标本由最先遇到它的探测车完成采集。每块岩石标本只能被采集一次。岩石标本被采集后,其他探测车可以从原来岩石标本所在处通过。探测车不能通过有障碍的地面。
本题限定探测车只能从登陆处沿着向南或向东的方向朝传送器移动,而且多个探测车可以在同一时间占据同一位置。如果某个探测车在到达传送器以前不能继续前进,则该车所采集的岩石标本将全部损失。
用一个
p
×
q
p \times q
p×q 网格表示登陆舱与传送器之间的位置。登陆舱的位置在
(
x
1
,
y
1
)
(x_1,y_1)
(x1,y1) 处,传送器的位置在
(
x
p
,
y
q
)
(x_p,y_q)
(xp,yq) 处。
给定每个位置的状态,计算探测车的最优移动方案,使到达传送器的探测车的数量最多,而且探测车采集到的岩石标本的数量最多。
输入格式
第一行为探测车数
n
n
n,接下来两行分别为
p
,
q
p,q
p,q。
接下来的
q
q
q 行是表示登陆舱与传送器之间的位置状态的
p
×
q
p \times q
p×q网格。
用三种数表示火星表面位置的状态:0 表示平坦无障碍,1 表示障碍,2 表示石块。
输出格式
每行包含探测车号和一个移动方向,0 表示向南移动,1 表示向东移动。
输入输出样例
输入 #1
2
10
8
0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 1 1 0 0 0
0 0 0 1 0 2 0 0 0 0
1 1 0 1 2 0 0 0 0 1
0 1 0 0 2 0 1 1 0 0
0 1 0 1 0 0 1 1 0 0
0 1 2 0 0 0 0 1 0 0
0 0 0 0 0 0 0 0 0 0
输出 #1
1 1
1 1
1 1
1 1
1 0
1 0
1 1
1 1
1 1
1 1
1 0
1 0
1 1
1 0
1 0
1 0
2 1
2 1
2 1
2 1
2 0
2 0
2 0
2 0
2 1
2 0
2 0
2 1
2 0
2 1
2 1
2 1
数据范围
1 ≤ n , p , q ≤ 35 1 \le n,p,q \le 35 1≤n,p,q≤35
分析
典型的拆点跑费用流
建图思路很简单
1.拆点,两个点连上流量为INF的边,当这个点有岩石时在连一条流量为1费用为1的边,表示岩石只能采集一次但可以多次通过这个点。
2.只能向下和向右走,所以如果能往两边走(另一边不为死路)就连流量为INF费用为0的边
3.(1,1)不是死路就由S流向,(n,n)不是死路就流向T。流量为INF费用为0
建图跑费用流
完结撒花 好像题目不是算最大费用emmmmmm
输出路径的话,首先我们想的话,路径的条数肯定就是最大流。
每个机器人走其中一条路径,那么我们就从1开始遍历到最大流
思考如果机器人走了这条路径,那么路径会有什么变化,首先正向边的流量减少1,反向边的流量就是加上1,然而反向边一开始的流量就是0。
所以如果最大流走的是这一条边的话,那么反向边就不为0,那我们就找反向边权不为0的边走,走过之后让反向边的边权减少1。
然后dfs遍历即可(代码70到91行)
注意点:
1.我们每次都是遍历拆点后的B点。注意点的编号pos
2.如果遍历到S,T和他的A点,边权也不为0,但我们不会在遍历下去,所以直接continue;
这下是真的AC了
AC代码
#include<bits/stdc++.h>
#define pll pair<int,int>
#define pb push_back
#define FAST std::ios::sync_with_stdio(false),cin.tie(0),cout.tie(0)
#define ll long long
#define INF 1e18
#define inf 0x3f3f3f3f
#define pi acos(-1)
#define debug(x) cout<<x<<endl
//#define ls now<<1
//#define rs now<<1|1
using namespace std;
const int mod = 1e9 + 7;
const int MAXN = 1e7 + 10;
const int N = 5e3 + 10;
int n,m,s,t;
struct node{
int t,nex;
ll w,c;
}e[MAXN];
ll ans,cost;
int head[N],cnt=1;
void add(int a,int b,ll c,ll d)
{
e[++cnt].t=b;e[cnt].nex=head[a];e[cnt].w=c; e[cnt].c=d;head[a]=cnt;
e[++cnt].t=a;e[cnt].nex=head[b];e[cnt].w=0;e[cnt].c=-d;head[b]=cnt;
}
ll st[N];
int pre[N];
bool vs[N];
bool spfa()
{
queue<int>q;
for(int i=0;i<=t;i++)
{
vs[i]=0;st[i]=-INF;pre[i]=-1;
}
q.push(s);st[s]=0;vs[s]=1;
while(!q.empty())
{
int u=q.front();q.pop();vs[u]=0;
for(int i=head[u];i!=-1;i=e[i].nex)
{
int v=e[i].t;
if(st[v]<st[u]+e[i].c&&e[i].w)
{
st[v]=st[u]+e[i].c;pre[v]=i;
if(!vs[v]){vs[v]=1;q.push(v);}
}
}
}
if(st[t]==-INF)return false;
ll d=INF;
for(int i=pre[t];i!=-1;i=pre[e[i^1].t]){
d=min(d,e[i].w);
}
for(int i=pre[t];i!=-1;i=pre[e[i^1].t]){
e[i].w-=d;
e[i^1].w+=d;
cost+=e[i].c*d;
}
ans+=d;
return true;
}
int get(int i,int j)
{
return (i-1)*m+j;
}
int ff[40][40];
void dfs(int x,int y,int pos,int k)
{
for(int i=head[pos];~i;i=e[i].nex)
{
int v=e[i].t;
if(v==s||v==t||v==pos-n*m)continue;
if(!e[i^1].w)continue;
e[i^1].w--;
if((v-1)%m+1==y+1)
{
cout<<k<<" "<<1<<endl;
dfs(x,y+1,v+n*m,k);
return;
}
else
{
cout<<k<<" "<<0<<endl;
dfs(x+1,y,v+n*m,k);
return;
}
}
}
void work()
{
memset(head,-1,sizeof(head));
int k;cin>>k;
cin>>m>>n;
int o=n*m;
s=0;t=n*m*2+1;
for(int i=1;i<=n;i++)
{
for(int j=1;j<=m;j++)
{
cin>>ff[i][j];
if(ff[i][j]==1)continue;
add(get(i,j),get(i,j)+o,INF,0);
if(ff[i][j]==2)add(get(i,j),get(i,j)+o,1,1);
}
}
if(ff[1][1]!=1);add(s,get(1,1),k,0);
if(ff[n][m]!=1)add(get(n,m)+o,t,INF,0);
for(int i=1;i<=n;i++)
{
for(int j=1;j<=m;j++)
{
if(i<n&&ff[i+1][j]!=1)
{
add(get(i,j)+o,get(i+1,j),INF,0);
}
if(j<m&&ff[i][j+1]!=1)
{
add(get(i,j)+o,get(i,j+1),INF,0);
}
}
}
while(spfa());
for(int i=1;i<=ans;i++)
{
dfs(1,1,1+n*m,i);
}
}
int main() {
FAST;
int t = 1;
//cin >> t;
while (t--)work();
return 0;
}