个人博客:我的博客地址
观光奶牛
题目描述
核心思路
一般来说,求 点权之和/边权之和 的最值问题,都属于01分数规划问题。
图论的01分数规划问题的步骤:
- 确认答案区间,然后用二分算法,判断性质
- 借助二分出来的中点,推导出性质的公式
- 套用图论模板
本题要求我们求一个环内 ∑ f i ∑ t i \dfrac {\sum f_i}{\sum t_i} ∑ti∑fi的最大值,而这个答案本身就是具有单调性,因此可以用二分算法,来二分出最终的答案。
首先来确定答案的区间:
我们设 c = ∑ f i ∑ t i c =\dfrac {\sum f_i}{\sum t_i} c=∑ti∑fi
- 要想求出区间的左范围,那么分子应该最小,分母应该最大,那么分子应该取一个点,该点的点权为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 ∑ti∑fi>mid
⟺ \iff ⟺ ∑ f i > m i d ∗ ∑ t i \sum f_i>mid*\sum t_i ∑fi>mid∗∑ti
⟺ \iff ⟺ ∑ f i − m i d ∗ ∑ t i > 0 \sum f_i-mid*\sum t_i>0 ∑fi−mid∗∑ti>0
⟺ \iff ⟺ ∑ ( f i − m i d ∗ t i ) > 0 \sum (f_i-mid*t_i)>0 ∑(fi−mid∗ti)>0
根据上述推导的公式可知,对于满足要求的
m
i
d
mid
mid,就是要满足图中存在一个环
,它的
∑
(
f
i
−
m
i
d
∗
t
i
)
>
0
\sum (f_i-mid*t_i)>0
∑(fi−mid∗ti)>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) ∑(fi−mid×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 fi−mid×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;
}