HDU5988/nowcoder 207G - Coding Contest - [最小费用最大流]
题目链接:https://www.nowcoder.com/acm/contest/207/G
题目链接:http://acm.hdu.edu.cn/showproblem.php?pid=5988
时间限制:C/C++ 2秒,其他语言4秒
空间限制:C/C++ 262144K,其他语言524288K
64bit IO Format: %lld
题目描述
A coding contest will be held in this university, in a huge playground. The whole playground would be divided into N blocks, and there would be M directed paths linking these blocks. The i-th path goes from the ui-th block to the vi-th block. Your task is to solve the lunch issue. According to the arrangement, there are si competitors in the i-th block. Limited to the size of table, bi bags of lunch including breads, sausages and milk would be put in the i-th block. As a result, some competitors need to move to another block to access lunch. However, the playground is temporary, as a result there would be so many wires on the path.
For the i-th path, the wires have been stabilized at first and the first competitor who walker through it would not break the wires. Since then, however, when a person go through the i−th path, there is a chance of pi to touch the wires and affect the whole networks. Moreover, to protect these wires, no more than ci competitors are allowed to walk through the i-th path.
Now you need to find a way for all competitors to get their lunch, and minimize the possibility of network crashing.
输入描述:
The first line of input contains an integer t which is the number of test cases. Then t test cases follow.
For each test case, the first line consists of two integers N (N ≤ 100) and M (M ≤ 5000). Each of the next N lines contains two integers si and bi (si,bi ≤ 200).
Each of the next M lines contains three integers ui,vi and ci(ci ≤ 100) and a float-point number pi(0 < pi < 1). It is guaranteed that there is at least one way to let every competitor has lunch.
输出描述:
For each turn of each case, output the minimum possibility that the networks would break down. Round it to 2 digits.
示例1
输入
1
4 4
2 0
0 3
3 0
0 3
1 2 5 0.5
3 2 5 0.5
1 4 5 0.5
3 4 5 0.5
输出
0.50
题意:
题目的灵感估计来源于现场赛发餐包的操作……
现在赛场划分成了 $N$ 个不相交区域,共有 $M$ 条有向路连接两个区域,
对于每个区域,给出 $s[i],b[i]$ 代表区域内有 $s[i]$ 个人,$b[i]$ 个餐包,一旦某人在本区域内拿不到餐包,就会前往其他区域获取餐包,
而众所周知,赛场上的路上是有很多电线的,一不小心就会踢到电线,所以现在每条路上都存在这一些电线,
现在已知,一旦某个选手走过第 $i$ 条有向边,就有 $p[i]$ 的概率踢到电线,进而影响整个电网,不过经过该路径的第一个人是必然不会踢到电线的,同时对于第 $i$ 条边,限制最多 $c[i]$ 个人走过。
现在,求整个电网被影响的最小概率。
题解:
首先,我们考虑既然将来还要深入学习数学相关知识,概率和期望这一块是怎么样都跑不掉的,所以还不如现在趁机好好巩固一下概率论的基础知识……
考虑每个人踢到电线的概率是相互独立的,我们将某次某个人经过某条边称作一次实验,
若每次实验踢到电线事件发生的概率相同,则 $n$ 个人踢到电线 $k$ 次的概率服从二项分布,众所周知二项分布的公式为
$P\left( {X = k} \right) = C_n^k p^k \left( {1 - p} \right)^{n - k}$
其中 $p$ 即为一次实验中发生踢到电线事件的概率;
而发生踢到电线这一事件发生 $1,2,3,\cdots$ 次均会影响电网,所以总共进行 $n$ 次独立实验后,电网被影响的概率为
$\sum\limits_{k = 1}^n {P\left( {X = k} \right)} = 1 - P\left( {X = 0} \right)$
易知
$P\left( {X = 0} \right) = \left( {1 - p} \right)^n$
但是我们知道,电网被影响的概率为
$P\left( {X = 1,2, \cdots ,n} \right) = 1 - \left( {1 - p} \right)^n$
当然,本题中,每次实验踢电线事件发生概率不一定相同,但是概率的计算方法依然符合上式:
$P = 1 - \prod\limits_{i = 1}^n {\left( {1 - p\left[ i \right]} \right)}$
然后需要考虑如何进行计算最小概率,显然本题为费用流题,那么不妨将选手的移动看做流的流动,故:
1、若第 $i$ 个区域内,选手比餐包多($s[i]>b[i]$),显然选手要流出,因此从源点向每个这样的区域连边,流量上限为 $s[i] - b[i]$(这部分人要流出),费用为 $0$;
2、若第 $i$ 个区域内,选手比餐包少($s[i]<b[i]$),显然选手要流入,因此从每个这样的区域向汇点连边,流量上限为 $b[i] - s[i]$(要流入这么多人),费用为 $0$;
3、若第 $i$ 个区域内,选手和餐包一样多($s[i]=b[i]$),这样的区域不用动了,选手老老实实待在自己区域内吃饭就行。
而题目中给出的 $M$ 条有向边,正好是可供选手流动的边,因此对于 $M$ 条有向边中任意一条 $edge(u,v,c,p)$:建立从节点 $u$ 连向节点 $v$ 的有向边,显然流量上限为 $c$,
而剩下来的费用是比较难搞的,首先,暂时不考虑第一个人经过某条路必然不会踢到电线这件事情,则因为每 $1$ 单位的流量流过该条边,正好就相当于一次实验,
那么就要贡献 $\times \left( {1 - p\left[ i \right]} \right)$,而 $k$ 个单位的流量流过该条边的贡献即为 $\times \left( {1 - p\left[ i \right]} \right)^k$,不难想到进行取对数操作,就有
$\log \left[ {\left( {1 - p\left[ 1 \right]} \right)^{k_1 } \times \left( {1 - p\left[ 2 \right]} \right)^{k_2 } \times \cdots \times \left( {1 - p\left[ n \right]} \right)^{k_n } } \right] = k_1 \log \left( {1 - p\left[ 1 \right]} \right) + k_2 \log \left( {1 - p\left[ 2 \right]} \right) + \cdots + k_n \log \left( {1 - p\left[ n \right]} \right)$
这样一来,就可以变成可以套用费用流的形式了,由于考虑到费用都是正数,将 $\log \left( {1 - p\left[ i \right]} \right)$ 取负,作为每条边的费用值,这样一来,求出来的费用值就是
$k_1 \left[ { - \log \left( {1 - p\left[ 1 \right]} \right)} \right] + k_2 \left[ { - \log \left( {1 - p\left[ 2 \right]} \right)} \right] + \cdots + k_n \left[ { - \log \left( {1 - p\left[ n \right]} \right)} \right]$
那么,要 $P = 1 - \prod\limits_{i = 1}^n {\left( {1 - p\left[ i \right]} \right)}$ 越小越好,即要 $\log \left[ {\left( {1 - p\left[ 1 \right]} \right)^{k_1 } \times \left( {1 - p\left[ 2 \right]} \right)^{k_2 } \times \cdots \times \left( {1 - p\left[ n \right]} \right)^{k_n } } \right]$ 越大越好(只要底数大于 $1$),也即 $k_1 \log \left( {1 - p\left[ 1 \right]} \right) + k_2 \log \left( {1 - p\left[ 2 \right]} \right) + \cdots + k_n \log \left( {1 - p\left[ n \right]} \right)$ 越大越好,也即 $k_1 \left[ { - \log \left( {1 - p\left[ 1 \right]} \right)} \right] + k_2 \left[ { - \log \left( {1 - p\left[ 2 \right]} \right)} \right] + \cdots + k_n \left[ { - \log \left( {1 - p\left[ n \right]} \right)} \right]$ 越小越好。
然后,在考虑第一个人经过某条路必然不会踢到电线这件事情,我刚开始考虑是对MCMF板子动点手脚,每次跑出来的 cost 减去那些第一次走人的边,事实和理论均证明这样是行不通的,
而费用流的题,一般都是在见图上搞文章,改板子改多了容易出事情……所以,实际上,我们把上面那些 $edge(u,v,c,p)$ 拆成两条边即可,
第一条 $edge(u,v,1,0)$,第二条 $edge(u,v,c-1,- \log(1-p))$,这样一来,最小费用的性质就可以保证第一条边空着必然先跑第一条,然后才考虑第二条。
所以,网络流,最精妙的,就是建图,少想点改板子的操作,多想点建图的操作,实在不行了再改板子(改最好也不要魔改……很容易就炸了……显然不影响板子正确性的前提下小改即可)。
所以,整道题就是建图后最小费用最大流,求出费用后,再取负再pow回去即可。
AC代码:
#include<bits/stdc++.h> using namespace std; const int maxn=110; const int INF=0x3f3f3f3f; const double eps=1e-8; struct Edge{ int u,v,cap,flow; double cost; }; struct MCMF { int s,t; //源点汇点 vector<Edge> E; vector<int> G[maxn]; void init(int l,int r) { E.clear(); for(int i=l;i<=r;i++) G[i].clear(); } void addedge(int from,int to,int cap,double cost) { E.push_back((Edge){from,to,cap,0,cost}); E.push_back((Edge){to,from,0,0,-cost}); G[from].push_back(E.size()-2); G[to].push_back(E.size()-1); } double d[maxn]; int vis[maxn]; int aug[maxn],pre[maxn]; bool spfa(int s,int t,int &flow,double &cost) { for(int i=s;i<=t;i++) d[i]=INF; memset(vis,0,sizeof(vis)); queue<int> q; q.push(s); d[s]=0, vis[s]=1, pre[s]=0, aug[s]=INF; while(!q.empty()) { int now=q.front(); q.pop(); vis[now]=0; for(int i=0;i<G[now].size();i++) { Edge& e=E[G[now][i]]; int nxt=e.v; if(e.cap>e.flow && d[nxt]>d[now]+e.cost+eps) { d[nxt]=d[now]+e.cost; pre[nxt]=G[now][i]; aug[nxt]=min(aug[now],e.cap-e.flow); if(!vis[nxt]) { q.push(nxt); vis[nxt]=1; } } } } if(d[t]==INF) return 0; flow+=aug[t]; cost+=d[t]*aug[t]; for(int i=t;i!=s;i=E[pre[i]].u) { E[pre[i]].flow+=aug[t]; E[pre[i]^1].flow-=aug[t]; } return 1; } double mincost() { int flow=0; double cost=0; while(spfa(s,t,flow,cost)); return cost; } }mcmf; int n,m; int s[maxn],b[maxn]; int main() { int T; scanf("%d",&T); while(T--) { scanf("%d%d",&n,&m); mcmf.init(0,n+1); mcmf.s=0; mcmf.t=n+1; for(int i=1;i<=n;i++) { scanf("%d%d",&s[i],&b[i]); if(s[i]>b[i]) mcmf.addedge(mcmf.s,i,s[i]-b[i],0.0); if(s[i]<b[i]) mcmf.addedge(i,mcmf.t,b[i]-s[i],0.0); } for(int i=1;i<=m;i++) { int u,v,c; double p; scanf("%d%d%d%lf",&u,&v,&c,&p); if(c<=0) continue; mcmf.addedge(u,v,c-1,-log2(1-p)); mcmf.addedge(u,v,1,0.0); } printf("%.2f\n",1-pow(2,-mcmf.mincost())); } }
补充一些注意的点:
首先,要考虑浮点误差,1.00000000000003 > 1.00000000000002 类似这种情况,很有可能是浮点误差造成的,而非真的前者大于后者,所以我们要设定一个精度,不影响到解题的正确性,又可以避免浮点误差。
其次,考虑取对数时的底数尽量小,这题之前有人说卡long double,其实大概是因为底数取大了,取成了 $10$,这样一来边的费用就会变小,double的精度就可能不够了。