图论(01分数规划+负环spfa)P2868 [USACO07DEC]
题目描述
给你一张 \(n\) 点 \(m\) 边的有向图,第 \(i\) 个点点权为 \(F_{i}\),第 \(i\) 条边边权为 \(T_{i}\)。
找一个环,设环上的点组成的集合为 \(S\),环的边组成的集合为 \(E\),最大化\( \frac{\sum_{u \in S}F_{u}} {{\sum_{e \in E} T_{e}}}\)的值
思路分析
先给一个显而易见的小结论:任意一个环中的点数和边数一定相等 我们假设有\(m\)个
接着假设最大答案为\(ans\)
有
移项,
运用乘法分配律,
移项,使用加(减)法交换结合律,
得到结论:答案 \(ans\) 在\({\sum_{i}^m (T_{i}\times ans-F_{i}) }\geq 0\)条件下合法 求解最大的\(ans\)
我们把\({\sum_{i}^m (T_{i}\times ans-F_{i}) }\)设为\(f(ans)\) 得到\(f(ans)\)为增函数
尝试二分\(ans\) \((ans \in [10^{-3},10^3])\)
- case 1:\(f(ans)<0\) 不满足\(ans\)的条件 应增大\(ans\)的值 (代码
l=mid
) - case2:\(f(ans)\geq 0\)满足\(ans\)的条件 应减小\(ans\)的值(代码
r=mid
)
由此可以打出\(01\)分数规划二分的板子:
double l=0.001,r=1000;
double mid;
while(r-l>0.0001)
{
mid=(l+r)/2.0;
if(check(mid)<0) l=mid;
else r=mid;
}
想一想怎么打\(check()\)函数
考虑\(f(ans)<0\) 即\({\sum_{i}^m (T_{i}\times ans-F_{i}) }< 0\)
因为要求选的点形成一个环 而\({\sum_{i}^m (T_{i}\times ans-F_{i}) }\)值又为负数 可以很显然想到负环算法
我们给每条边重新赋值边权 对于任意边\([u,v]\)边权赋值为\(w[u][v]\times ans-F_u\)
那么判断负环的公式就是\({\sum_{i}^m (T_{i}\times ans-F_{i}) }< 0\) 即\(f(ans)<0\)
这样就实现了用负环算法完成\(check\)函数
代码实现
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=5e3+10;
int f[N];
struct edge
{
int v,w;
};
vector<edge> e[N];
int num[N];
double dis[N];
bool vis[N];
int n,m;
bool spfa(double ans)
{
queue<int> q;
memset(num,0,sizeof(num));
memset(vis,0,sizeof(vis));
memset(dis,0,sizeof(dis));
for(int i=1;i<=n;++i)
{
q.push(i);
vis[i]=1;
}
while(!q.empty())
{
int u=q.front();
q.pop();
vis[u]=0;
for(int i=0;i<e[u].size();++i)
{
int v=e[u][i].v;
double w=e[u][i].w;
w=w*ans-f[u];//重新赋值边权
if(dis[v]>dis[u]+w)
{
dis[v]=dis[u]+w;
num[v]=num[u]+1;
if(num[v]>=n) return 1;
if(!vis[v])
{
q.push(v);
vis[v]=1;
}
}
}
}
return 0;
}
signed main()
{
ios::sync_with_stdio(0);
cin.tie(0),cout.tie(0);
cin>>n>>m;
for(int i=1;i<=n;++i)
cin>>f[i];
for(int i=1;i<=m;++i)
{
int u,v,w;
cin>>u>>v>>w;
e[u].push_back({v,w});
}
double l=0.001,r=1000;
double mid;
while(r-l>0.0001)
{
mid=(l+r)/2.0;
if(spfa(mid)) l=mid;
else r=mid;
}
printf("%.2lf",mid);
return 0;
}