观光奶牛-

个人博客:我的博客地址

观光奶牛


题目描述

image-20210712160410274


核心思路

一般来说,求 点权之和/边权之和 的最值问题,都属于01分数规划问题。

图论的01分数规划问题的步骤:

  • 确认答案区间,然后用二分算法,判断性质
  • 借助二分出来的中点,推导出性质的公式
  • 套用图论模板

本题要求我们求一个环内 ∑ f i ∑ t i \dfrac {\sum f_i}{\sum t_i} tifi的最大值,而这个答案本身就是具有单调性,因此可以用二分算法,来二分出最终的答案。

首先来确定答案的区间:

我们设 c = ∑ f i ∑ t i c =\dfrac {\sum f_i}{\sum t_i} c=tifi

  • 要想求出区间的左范围,那么分子应该最小,分母应该最大,那么分子应该取一个点,该点的点权为1;分母应该取5000条边,边权为1000,那么此时 c = 1 × 1 5000 × 1000 c=\dfrac {1\times1}{5000\times1000} c=5000×10001×1,很难明显,这个 c c c不为0,肯定是大于0的,注意这里不能理解为C++中的整除会向下取整,这里应该理解为浮点数的除法,因为题目说了要保留两位小数。因此,我们确定了左范围是大于0的
  • 要想求出区间的右范围,那么分子应该最大,分母应该最小,那么分子应该取1000个点,每个点的点权都为1000;由于有了1000个点,那么对于环来说,至少得是1000条边(注意不可能说有1000个点,然后取1条边,这样不能形成环),这1000条边的权值都为1,那么此时 c = 1000 × 1000 1000 × 1 = 1000 c=\dfrac {1000\times1000}{1000\times1}=1000 c=1000×11000×1000=1000。因此,我们确定了右范围是1000
  • 所以,答案区间就是(0,1000]

由于我们发现答案区间是单调递增的,也就是说具有单调性,那么就可以用二分算法,来快速地求出 c c c。我们设 L = 0 , R = 1000 L=0,R=1000 L=0,R=1000,假设某个时刻,我们设中点为 m i d mid mid,那么:

∑ f i ∑ t i > m i d \dfrac {\sum f_i}{\sum t_i}>mid tifi>mid

   ⟺    \iff ∑ f i > m i d ∗ ∑ t i \sum f_i>mid*\sum t_i fi>midti

   ⟺    \iff ∑ f i − m i d ∗ ∑ t i > 0 \sum f_i-mid*\sum t_i>0 fimidti>0

   ⟺    \iff ∑ ( f i − m i d ∗ t i ) > 0 \sum (f_i-mid*t_i)>0 (fimidti)>0

根据上述推导的公式可知,对于满足要求的 m i d mid mid,就是要满足图中存在一个,它的 ∑ ( f i − m i d ∗ t i ) > 0 \sum (f_i-mid*t_i)>0 (fimidti)>0 ,要求一个环,它的权值之和大于0,这不就是想让我们求正环嘛?

因此,原问题就转换为 求图中是否存在一个正环 的问题了

我们每次二分出一个 m i d mid mid,然后 c h e c k ( m i d ) check(mid) check(mid),如果它满足上面的这个式子,那么由于答案是单调递增的,我们想要求出最大的 m i d mid mid,因此此时左范围 L L L应该往右侧收缩,即 L = m i d L=mid L=mid,不可能让右范围往左收缩吧,即不可能是 R = m i d R=mid R=mid(如果这样的话,那么更新过后的区间的最大值不就小于 m i d mid mid了嘛,这就不可能找到最大值了)。因此一旦二分出的 m i d mid mid满足上述式子,那么就往右侧收缩 L = m i d L=mid L=mid,这样会更快地逼近最大值;如果二分出的这个 m i d mid mid不满足上述式子,则说明答案肯定比当前二分的 m i d mid mid还小,那么就要往左侧收缩 R = m i d R=mid R=mid,因为右边已经不可能了。

浮点数的二分比较简单,就是执行 L = m i d L=mid L=mid或者 R = m i d R=mid R=mid

这里还有个问题,就是我们该怎么处理点权和边权呢?我们以前都只是见过有边权的情况。其实,我们可以把节点的点权放到它的出边上,那么此时就只有边权的情况了,不存在点权。为什么可以这么做呢?

  • 假设同时存在点权和边权,那么所有点权之和为 ∑ f i \sum f_i fi,所有边权之和为 ∑ t i \sum t_i ti,因此,总的权值之和为 ∑ f i + ∑ t i \sum f_i+\sum t_i fi+ti
  • 假设把点权放到出边上,此时只有边权,那么某个出边的权值为 f i + t i f_i+t_i fi+ti,因此,总的权值之和为 ∑ ( f i + t i ) \sum (f_i+t_i) (fi+ti)
  • 由于 ∑ \sum 是可以分开的,因此 ∑ ( f i + t i ) = ∑ f i + ∑ t i \sum(f_i+t_i)=\sum f_i+\sum t_i (fi+ti)=fi+ti

有了上面的处理之和,我们建图就会更加方便了,只需要处理边权就好了。

我们来看这个式子 ∑ ( f i − m i d × t i ) \sum(f_i-mid\times t_i) (fimid×ti),那么其实就是 ∑ ( f i + ( − m i d × t i ) ) > 0 = ∑ f i + ∑ − m i d × t i \sum (f_i+(-mid\times t_i))>0=\sum f_i+\sum-mid\times t_i (fi+(mid×ti))>0=fi+mid×ti,也就是说把原来的边权 t i t_i ti换成了 f i − m i d × t i f_i-mid\times t_i fimid×ti来存储了,把每个点的权值都放入它的出边中。


代码

#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int N=1010,M=5010;
int n,m;
//点权
int wf[N];
int h[N],e[M],ne[M],wt[M],idx;  //wt是边权
int q[N],cnt[N];
double dist[N];
bool st[N];
void add(int a,int b,int c)
{
    e[idx]=b;
    wt[idx]=c;
    ne[idx]=h[a];
    h[a]=idx++;
}
//spfa判断正环
bool check(double mid)
{
    memset(dist,-0x3f,sizeof dist);
    memset(st,0,sizeof st);
    memset(cnt,0,sizeof cnt);
    int hh=0,tt=1;
    //一开始将原图中所有点加入队列q中就  等效于建立了一个带有虚拟源点的新图 
    for(int i=1;i<=n;i++)
    {
        q[tt++]=i;
        dist[i]=0;
        st[i]=true;
    }
    while(hh!=tt)
    {
        int t=q[hh++];
        if(hh==N)
        hh=0;
        st[t]=false;
        for(int i=h[t];~i;i=ne[i])
        {
            int j=e[i];
            //wf[t]-mid*wt[i]是将点权放到了边上
            if(dist[j]<dist[t]+wf[t]-mid*wt[i])
            {
                dist[j]=dist[t]+wf[t]-mid*wt[i];
                cnt[j]=cnt[t]+1;
                //说明存在正环
                if(cnt[j]>=n)
                return true;
                if(!st[j])
                {
                    q[tt++]=j;
                    if(tt==N)
                    tt=0;
                    st[j]=true;
                }
            }
        }
    }
    //说明不存在正环
    return false;
}
int main()
{
    memset(h,-1,sizeof h);
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++)
    scanf("%d",&wf[i]);
    while(m--)
    {
        int a,b,c;
        scanf("%d%d%d",&a,&b,&c);
        add(a,b,c);
    }
    double l=0,r=1e4;
    //二分找到答案
    while(l+1e-4<r)
    {
        double mid=(l+r)/2;
        //满足条件 则向右侧收缩
        if(check(mid))
        l=mid;
        else
        r=mid;
    }
    printf("%.2lf\n",l);
    return 0;
}

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

卷心菜不卷Iris

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值