最小圈问题

做题学知识一

最小圈问题(01分数规划,加spfa判断负环)

这是一个新的模块,每一次找一道洛谷紫色以上的题去学习里面的思想和知识,这一次就是这道题。

01分数规划

这个思想其实是在去求一个二元组
emmm好像用电脑去写有一点难,手写一波在这里插入图片描述
这个就是01分数规划的计算公式,可以看出一般都是和图在一起考的对吧,下一次有机会找一道网络流加01的题(一看就是黑题了),回到正题,我们看这个式子是不是想到了爆搜,但是肯定不行,所以我们对这个进行一下小小的变形在这里插入图片描述
之后我们定义一个函数f(x)=这个式子,那么答案就是无数个这个函数与x轴的交点,我们可以画一画在这里插入图片描述这个图来自tianxiang971016

之后看着这个是不是就想到了二分答案,我们以max为例在这里插入图片描述
在这里插入图片描述
r就是ans,那我们就发现一个性质
在图中上任取一条垂直x xx轴的竖线,
如果***存在***直线与这条竖线的交点纵坐标为正,那么最优值一定在当前竖线的右侧;
如果***所有***直线与这条竖线交点纵坐标为负,那么最优值一定在当前竖线的左侧;
如果所有直线与这条竖线交点纵坐标非正且存在直线与这条竖线交点纵坐标为0,那么当前竖线横坐标即为最优值ans
利用这个我们就可以去二分答案,每一次只要max{f[x]}>0那么我们就往右边找,max{f[x]}<0我们就往左边找,max{f[x]}=0就是答案,这里f[x]是指和二分答案的交点,在这里插入图片描述
这样就可以具体问题具体解决了。
看上面的图,也很好理解,就是最左边的交点为ans

如果m i n { f ( x ) } > 0 ,那么ans在右边

如果m i n { f ( x ) } = 0,那么找到了

如果m i n { f ( x ) } < 0 ,那么ans就在左边

做题时认清哪个是价值,哪个是代价,再记住上面几张图,基本不会出错了。

spfa

这里先去讲他求单源最短路的算法,这个其实是贝尔曼福德的优化版本,我们知道,ford算法里每一次都要把所有的边都松弛一边,而其实假如说x上一次的距离没有变化,那么我们就没有必要去看x可以到达的点的距离会不会变化,因为一定不会变。所以我们用一个队列,最开始存的是源点,之后每一次我们发现有点的距离改变了,就把它入队(他的所有出边都要被松弛),每一次循环去拿出来一个点去进行松弛(另外我们可以用flag【x】,来记录是否进队列,如果现在在里面就没必要入队了,这是一个小优化)
代码如下

int flag[10000]=0;
int dis[10000];
void Floyd(int x){
	for(int i=1;i<=n;i++) dis[i]=oo;//初始赋值无穷大
	memset(flag,0,sizeof(flag)); 
	
	int que[10000];
	int headl=1,last=1;
	que[last]=s;
	flag[x]=1;//源点入队过了
	
	while(headl<=last){
		int u=que[headl];
		flag[u]=0;//这个点yijin出队了 
		headl++;
		for(int i=head[u];i>0;i=nex[i]){
			int ti=to[i];
			if(dis[ti]>dis[u]+val[i]){
				dis[ti]=dis[u]+val[i];
				if(!flag[ti]) {
					flag[ti]=1;
					que[++last]=ti;//松驰过如果没入队就入队 
				}
			}

		}
	} 
	
}

至于如何判断有没有负环,一个点不能入队n 次,否则有负环;一条最短路径长度不能到n(用cnt[x]表示从原点到这个点经过的点数,每一次松弛继承上一次状态就ok了),否则有负环。两个判断方法可以同时使用。

至于更快的就是dfs版本,我们每一次把一个点松弛过后就去直接递归他的点,之后如果又回到了他就有负环

本题

一看到分数求和就可以想到01在这里插入图片描述
分数规划,之后变形一下下
就发现是要去找一个ans使得图上有负环,那么不就是01
分数规划加spfa了吗,每一次如果有负环那么就往小了去找就OK了,最后01分数规划的初始l,r就看看要去随便取大数就欧克了
代码如下

#include<bits/stdc++.h>
using namespace std;
#define eps 1e-12
int head[20010],nex[20010],to[20010];
double val[20010];
int num=0;
void add(int x,int y,double z){
	to[++num]=y;
	nex[num]=head[x];
	head[x]=num;
	val[num]=z;
}


int n,m;

void get_input(){
	scanf("%d%d",&n,&m);
	int a,b;double c;
	for(int i=1;i<=m;i++){
		scanf("%d%d%lf",&a,&b,&c);
		add(a,b,c);
	//add(b,a,c);
	}
}

double dis[3005];
bool flag[3005];
bool jue=0;

void spfa(int x,double aa){
	flag[x]=1;
	for(int i=head[x];i>0;i=nex[i]){
		int ti=to[i];
		if(dis[ti]>dis[x]+val[i]-aa){//这时候边权要减去二分出来的mid 
			if(flag[ti]||jue) {
				jue=1;
				return;
			}
			dis[ti]=dis[x]+val[i]-aa;
			spfa(ti,aa);
		}
	}
	flag[x]=0;
}

void get_ans(){
	double l=-1e7,r=1e7;
	double mid;
	while(r-l>eps){
		mid=(l+r)/2.0;
		memset(flag,0,sizeof(flag));
		for(int i=1;i<=n;i++) dis[i]=0;
		jue=0;
		for(int i=1;i<=n;i++){
			spfa(i,mid);
			if(jue==1){
				break;
			}
		}
		if(jue==1) r=mid;
		else l=mid;
	}
 	printf("%.8lf",l);
}
int main(){
	get_input();
	get_ans();
	return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值