前言
你说你是个网络流的题,就算了嘛,还要输出方案,啧啧啧...
题目
题目描述
你第一天接手三鹿牛奶公司就发生了一件倒霉的事情:公司不小心发送了一批有三聚氰胺的牛奶。很不幸,你发现这件事的时候,有三聚氰胺的牛奶已经进入了送货网。这个送货网很大,而且关系复杂。你知道这批牛奶要发给哪个零售商,但是要把这批牛奶送到他手中有许多种途径。送货网由一些仓库和运输卡车组成,每辆卡车都在各自固定的两个仓库之间单向运输牛奶。在追查这些有三聚氰胺的牛奶的时候,有必要保证它不被送到零售商手里,所以必须使某些运输卡车停止运输,但是停止每辆卡车都会有一定的经济损失。你的任务是,在保证坏牛奶不送到零售商的前提下,制定出停止卡车运输的方案,使损失最小。
输入格式
第一行: 两个整数N(2<=N<=32)、M(0<=M<=1000), N表示仓库的数目,M表示运输卡车的数量。仓库1代 表发货工厂,仓库N代表有三聚氰胺的牛奶要发往的零售商。 第2..M+1行: 每行3个整数Si,Ei,Ci。其中Si,Ei表示这 辆卡车的出发仓库,目的仓库。Ci(0 <= C i <= 2,000,000) 表示让这辆卡车停止运输的损失。
输出格式
两个整数C、T:C表示最小的损失,T表示在损失最小的前提下,最少要停止的卡车数。
输入输出样例
输入
4 5
1 3 100
3 2 50
2 4 60
1 2 40
2 3 80
输出
60 1
说明/提示
题目翻译来自NOCOW。
USACO Training Section 4.4
其他
USACO网站上还要求输出方案,即停止了哪些卡车,字典序输出
(哇,好恶心qwq!)
分析
哎,这道题+博客工程量巨大,本来想咕掉来着...
既然下定决心要写,那我们就开始吧...
【洛谷版】洛谷的这道题简化了,没有让你输出方案,于是会好处理很多
1.本题第一问求“使起点与终点变成两个独立的连通块的最小费用”,最小割板题,可以直接套用【最小割】即【最大流】的板子,我用的Dinic
2.关于求“割边的数量”,有个十分巧妙的方法 / 重点!敲黑板!:
【法一】
建两次图,一次按原边权建图跑最大流求得最小割,再按边权为1建图跑最大流求割的边数
【法二】更优秀的做法qwq
建图时将边权w=w*a+1(w为本来的边权,a为大于1000的数),这样我们能求得最大流ans,
则最小割的值为ans/a,割的边数为ans%a
这很容易理解,但是还是解释一下:
因为最小割的边集中有w1+w2+w3…+wn=ans(这个ans为本来的最小割),
所以必然有w1*a+w2*a+w3*a…+wn*a=ans*a,
于是必然有w1*a+1+w2*a+1+w3*a+1…+wn*a+1=ans*a+k(k为最小割的边数,k<=m<=1000),
这样就很明显了,因为边数m不大于1000,所以k的最大值为1000,
我们只要使设定的a的值大于1000,那么按上述方法建图,跑出的最大流除以a就是最小割的值ans,最大流模a就是最小割的边数k
【USACO版】要求按字典序输出割边的编号
参考某大佬博客:https://blog.csdn.net/csyzcyj/article/details/11951595
本题思路为网络流求最大流和最小割边集。
首先加流量。为了构造解,我记录了输入的顺序并对边的大小进行了排序
<排序按边权从大到小排序,这一步主要是为了后面求最小割>,
然后先求一遍最大流S,然后枚举去掉每一条边<设边权值为T>,
然后求当前最大流X,若S=T+X,则该边在最小割边集中,将该边从图中去掉,S=X。
注意,每次DINIC后若用同一个数组记流量,需在DINIC后还原。
但是为什么【排序按边权从大到小排序】呢?这里我不是很懂
这里有个例子,但是本蒟蒻解释不清楚为什么qwq...
8 9
1 2 2
1 3 1
3 4 1
2 4 2
4 5 3
5 6 1
5 7 2
7 8 2
6 8 1
如果不排序,会输出2,要割1、2这两条边,但是正确答案是割5这条边
后来看了另某大佬的博客才恍然大悟——
因为要找最少的边(有的oj还要找出边的标号),所以要把边从大到小排一次序,就能更好的找出“最少的边”
【某大佬的讲解完整版】我觉得讲得也很好,贴一下 :https://www.luogu.org/blog/kkksc03666/solution-p1344
这一道题是由最小割转最大流
我就这样解释吧:
最大流是从点1能流到n的最大流量,流量的大小主要是由每条路的最小边决定的(大概是这样的)
最小割为了消耗费用最小,就肯定要割去最小消耗的边。
也可以这样说,先找出1到n的最大流,把这些流量全部切掉,就是最小割(很多条边都是多余的),感觉和网络流一模一样
关于第二问:
因为要找最少的边(有的oj还要找出边的标号),所以要把边从大到小排一次序,就能更好的找出“最少的边”,为了不出
现低级错误,我就用了一个用时多一些但是不容易出错的方法来做
如果要找出这些边,就可以这样想一想,有一些边是“重要的”,就是说这一条边是满流量的,而且这种边是直接关系到
这条路(不是边)到终点的流量(就是刚刚说的最小边,流量是由最小边决定的)
其实就是把这一条边去掉后的最大流+这条边的流量=最大流,只要是找出这样的边就可以了
注意:这里有一个细节,就是可能会出现多条这样的边,这些边之和大于最大流,还有就是同一条路可能存在两条同流
量的边,这样的边就只要找一条
【其他要点】可能这个知识点很基础,但是我基础不好...qwq...现在才知道...
要用"^1"即异或1等的操作话(例如跑网络流会写到E[i^1]、表示前后对应关系等),
有时候必须从二开始,在此题中也就是int cnt=1,因为E[ ++cnt ]...所以边的计数从2开始(我寻思从0开始好像也可以)
e.g.
亦或:两个相等就为1,否则为0
0^1=0
1^1=1
2^1=10^01=11=3
3^1=11^01=10=2
4^1=100^001=101=5
5^1=101^001=100=4
AC代码——无输出方案
/*
ID:lunasmi2
TASK:ditch
LANG:C++
*/
#include<cstdio>
#include<cmath>
#include<queue>
#include<cstring>
#include<iostream>
#include<algorithm>
using namespace std;
typedef long long ll;
const int MAXN=2000,MAXM=2*MAXN,INF=0x3f3f3f3f,del=1005;
int head[MAXN+5],nxt[MAXM+5],cnt=1;
int dep[MAXN+5];
int n,m;
ll maxf;
struct Edge
{
int u,v,f;
Edge(int _u=0,int _v=0,int _f=0){u=_u,v=_v,f=_f;}
Edge(Edge &e){u=e.u,v=e.v,f=e.f;}
}E[MAXM+5];
void Addedge(int u,int v,int f)
{
E[++cnt]=Edge(u,v,f);
nxt[cnt]=head[u];
head[u]=cnt;
}
bool bfs()
{
memset(dep,0,sizeof(dep));
dep[1]=1;
queue<int> que;
que.push(1);
while(!que.empty())
{
int u=que.front();que.pop();
for(int i=head[u];i;i=nxt[i])
if(E[i].f&&!dep[E[i].v])
{
dep[E[i].v]=dep[u]+1;
que.push(E[i].v);
}
}
if(!dep[n])
return 0;
return 1;
}
int dfs(int u,int f)
{
if(u==n)
return f;
int w,used=0;
for(int i=head[u];i;i=nxt[i])
if(E[i].f&&dep[E[i].v]==dep[u]+1)
{
w=dfs(E[i].v,min(f-used,E[i].f));
used+=w;E[i].f-=w;E[i^1].f+=w;
if(used==f)
return f;
}
return used;
}
void dinic()
{
while(bfs())
maxf+=dfs(1,INF);
}
int main()
{
//freopen("ditch.in","r",stdin);
//freopen("ditch.out","w",stdout);
scanf("%d%d",&n,&m);
for(int i=1;i<=m;i++)
{
int u,v,f;
scanf("%d%d%d",&u,&v,&f);
Addedge(u,v,f*del+1);
Addedge(v,u,0);
}
dinic();
printf("%lld %lld\n",maxf/del,maxf%del);
return 0;
}
AC代码——有输出方案
/*
ID:lunasmi2
TASK:milk6
LANG:C++
*/
#include<cstdio>
#include<cmath>
#include<vector>
#include<queue>
#include<cstring>
#include<iostream>
#include<algorithm>
using namespace std;
typedef long long ll;
const int MAXN=100,MAXM=3000,INF=0x3f3f3f3f;
int head[MAXN+5],nxt[MAXM+5],cnt=1;//cnt必须从1开始!我也不知道为什么,从0开始时调了很久
int dep[MAXN+5],w[MAXM+5],ans[MAXM+5];
bool vis[MAXM+5];
int n,m,maxf,cnt_w,cnt_ans;
bool cmp(int a,int b)
{
return a>b;
}
struct Edge
{
int u,v,f,g;//g用来储存边权原值,全程不发生改变
Edge(int _u=0,int _v=0,int _f=0,int _g=0){u=_u,v=_v,f=_f,g=_g;}
Edge(Edge &e){u=e.u,v=e.v,f=e.f,g=e.g;}
}E[MAXM+5];
void Addedge(int u,int v,int f,int g)
{
E[++cnt]=Edge(u,v,f,g);
nxt[cnt]=head[u];
head[u]=cnt;
}
void restore()//边权恢复为原值
{
for(int i=1;i<=2*m;i++)
E[i].f=E[i].g;
}
bool bfs()
{
memset(dep,0,sizeof(dep));
dep[1]=1;
queue<int> que;
que.push(1);
while(!que.empty())
{
int u=que.front();que.pop();
for(int i=head[u];i;i=nxt[i])
if(E[i].f&&!dep[E[i].v])//是正向边且没被访问过
{
dep[E[i].v]=dep[u]+1;
que.push(E[i].v);
}
}
if(!dep[n])
return 0;
return 1;
}
int dfs(int u,int f)
{
if(u==n)
return f;
int w,used=0;
for(int i=head[u];i;i=nxt[i])
if(E[i].f&&dep[E[i].v]==dep[u]+1)
{
w=dfs(E[i].v,min(f-used,E[i].f));
used+=w;E[i].f-=w;E[i^1].f+=w;
if(used==f)
return f;
}
return used;
}
void dinic()
{
maxf=0;
while(bfs())
maxf+=dfs(1,INF);
restore();//因为要多次跑最大流,所以跑完一遍就恢复成原图(除删掉的割边值一直为0)
}
int main()
{
//freopen("milk6.in","r",stdin);
//freopen("milk6.out","w",stdout);
scanf("%d%d",&n,&m);
for(int i=1;i<=m;i++)
{
int u,v,f;
scanf("%d%d%d",&u,&v,&f);
w[++cnt_w]=f;
Addedge(u,v,f,f);
Addedge(v,u,0,0);
}
sort(w+1,w+m+1,cmp);//边必须按边权从大到小枚举
dinic();
restore();//因为已经跑了一遍dinic,边权有变化,所以要恢复原值
printf("%d ",maxf);
int Maxf=maxf;
for(int i=1;i<=m;i++)
{
for(int j=2;j<=2*m;j+=2)//按边权从大到小枚举被删去检查是否是割边的边
{
if(!vis[j]&&E[j].f==w[i])//找到枚举到的边
{
vis[j]=1;
int tmp=E[j].f;
E[j].f=0;//假若删去这条边
dinic();
if(Maxf-tmp==maxf)//新最大流+该边权=原始最大流则该边为割边
{
ans[++cnt_ans]=(j+1)/2;//因为加了反向边,所以边的编号有变化
E[j].g=E[j].f=0;//删掉该割边
Maxf=maxf;//最大流替换为新的最大流,继续找割边
}
break;
}
}
}
sort(ans+1,ans+cnt_ans+1);
printf("%d\n",cnt_ans);
for(int i=1;i<=cnt_ans;i++)//USA上要求输出删去的割边编号
printf("%d\n",ans[i]);
return 0;
}