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的精度就可能不够了。

posted @ 2018-10-07 20:59  Dilthey  阅读(311)  评论(0编辑  收藏  举报