【网络流-最小割】USA4.4——追查坏牛奶Pollutant Control

前言

你说你是个网络流的题,就算了嘛,还要输出方案,啧啧啧...

题目

题目描述

你第一天接手三鹿牛奶公司就发生了一件倒霉的事情:公司不小心发送了一批有三聚氰胺的牛奶。很不幸,你发现这件事的时候,有三聚氰胺的牛奶已经进入了送货网。这个送货网很大,而且关系复杂。你知道这批牛奶要发给哪个零售商,但是要把这批牛奶送到他手中有许多种途径。送货网由一些仓库和运输卡车组成,每辆卡车都在各自固定的两个仓库之间单向运输牛奶。在追查这些有三聚氰胺的牛奶的时候,有必要保证它不被送到零售商手里,所以必须使某些运输卡车停止运输,但是停止每辆卡车都会有一定的经济损失。你的任务是,在保证坏牛奶不送到零售商的前提下,制定出停止卡车运输的方案,使损失最小。

输入格式

第一行: 两个整数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;
}

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值