负环和0/1分数规划学习笔记

负环

若图中存在一个负环,环上各边的权值之和是负数,则称这个环为负环。

 我们常用第二种判负环方法,即 cnt[v]>=n

 优化

  1. 若使用普通的判负环出现了超时,则使用写题经验, count 表示所有点一共被更新的次数,若 count>=2*N 或 count 大于一个比较大的数 ,则可以判断已经出现负环。(详见 单词环 代码)
  2. 若使用普通的判负环出现了超时,则使用写题经验,我们将存储数据的 队列 改成 ,可能会大大降低运行时间。只有出现普通判负环超时时再用,否则使用栈可能会超级慢。

总结

  1. 对于判负环问题,我们需要一个 超级源点,保证图中的每一条边都可以遍历到,因此我们建立一个超级源点,让超级源点与 n 个点分别连一条权值为 0 的边。实际上,我们在代码实现时可以省去超级源点与 n 个点连边的这一步,我们直接将这 n 个点加入到队列当中。
for(int i=1;i<=n;i++){
	q[tt++]=i;
	vis[i]=true;
}

例题:

1. AcWing 904. 虫洞

该题为模板题

若判断该图中是否有负环,则我们需要一个 超级源点,保证图中的每一条边都可以遍历到,因此我们建立一个超级源点,让超级源点与 n 个点分别连一条权值为 0 的边。实际上,我们在代码实现时可以省去超级源点与 n 个点连边的这一步,我们直接将这 n 个点加入到队列当中。

#include<bits/stdc++.h>
using namespace std;
const int N=510,M=5500;
struct node{
	int nex,to,w;
}e[M];
int head[N],cnt,n,m1,m2;
int vis[N],q[N],tot[N],dis[N];
void add(int u,int v,int w){
	e[++cnt].nex=head[u];
	e[cnt].to=v;
	e[cnt].w=w;
	head[u]=cnt;
}
bool spfa(){
	memset(vis,0,sizeof(vis));
	memset(dis,0,sizeof(dis));
	memset(tot,0,sizeof(tot));
	int hh=0,tt=0;
	for(int i=1;i<=n;i++){
		q[tt++]=i;
		vis[i]=1;
	}
	while(hh!=tt){
		int u=q[hh++];
		if(hh==N) hh=0;
		vis[u]=0;
		for(int i=head[u];i;i=e[i].nex){
			int v=e[i].to,w=e[i].w;
			if(dis[v]>dis[u]+w){
				dis[v]=dis[u]+w;
				tot[v]=tot[u]+1;
				if(tot[v]>=n) return true;
				if(!vis[v]){
					q[tt++]=v;
					if(tt==N) tt=0;
					vis[v]=1;
				}
			}
		}
	}
	return false;
}
int main(){
	int T;
	scanf("%d",&T);
	while(T--){
		memset(head,0,sizeof(head));
		cnt=0;
		scanf("%d%d%d",&n,&m1,&m2);
		for(int i=1;i<=m1;i++){
			int u,v,w;
			scanf("%d%d%d",&u,&v,&w);
			add(u,v,w); add(v,u,w);
		}
		for(int i=1;i<=m2;i++){
			int u,v,w;
			scanf("%d%d%d",&u,&v,&w);
			add(u,v,-w);
		}
		if(spfa()) printf("YES\n");
		else printf("NO\n");
	}
	return 0;
}

0/1 分数规划 

 0/1 分数规划 问题:

        给你一张图,图上每个点都有一个权值 f[i],每一条边都有一个权值 w[i],图保证有一个环。求出图中的一个环,使得“环上各点的权值之和”除以“环上各边的权值之和” 最大。

即使得环  \frac{\sum f[i]}{\sum w[i]} 的值最大。

对于此类问题,我们使用二分做法,使得  \frac{\sum f[i]}{\sum w[i]}=mid

我们将该式子化简 

\frac{\sum f[i]}{\sum w[i]}>mid

\Leftrightarrow\sum f[i]>mid\times \sum w[i]

\Leftrightarrow\sum f[i]-mid\times \sum w[i]>0

\Leftrightarrow\sum (f[i]-mid\times w[i])>0

\Leftrightarrow图中是否存在正环

我们发现将点上的权值 f[i] 放到边上并不会影响答案,因此将 f[i] 放到边上,计算 d[] 值时用到  f[i]

对于此题我们将 w=f[u]-mid\times w[i]

我们二分 mid 的答案,若当前的 mid 合法,即存在正环,则 l=mid,否则 r=mid,最终的 mid 即为答案

while(r-l>1e-4){
    double mid=(l+r)/2;
	if(check(mid)) l=mid;
	else r=mid;
}

例题:

1. AcWing 361. 观光奶牛

此题就是刚刚上述讲的如何解决 0/1 分数规划问题的题,我们直接按照上述讲的做即可,不再赘述。

#include<bits/stdc++.h>
using namespace std;
const int N=1010,M=5010;
struct node{
	int nex,to,w;
}e[M];
int head[N],cnt,tot[N],q[N];
double dis[N];
int n,m,f[N];
bool vis[N];
void add(int u,int v,int w){
	e[++cnt].nex=head[u];
	e[cnt].to=v;
	e[cnt].w=w;
	head[u]=cnt;
}
int check(double mid){
	int hh=0,tt=0;
	memset(vis,0,sizeof(vis));
	memset(tot,0,sizeof(tot));
	for(int i=1;i<=n;i++){
		q[tt++]=i;
		vis[i]=true;
	}
	while(hh!=tt){
		int u=q[hh++];
		if(hh==N) hh=0;
		vis[u]=false;
		for(int i=head[u];i;i=e[i].nex){
			int v=e[i].to;
			double w=f[u]-mid*e[i].w;
			if(dis[v]<dis[u]+w){
				dis[v]=dis[u]+w;
				tot[v]=tot[u]+1;
				if(tot[v]>=n) return true;
				if(!vis[v]){
					q[tt++]=v;
					if(tt==N) tt=0;
					vis[v]=true;
				}
			}
		}
	}
	return false;
}
int main(){
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;i++) scanf("%d",&f[i]);
	for(int i=1;i<=m;i++){
		int u,v,w;
		scanf("%d%d%d",&u,&v,&w);
		add(u,v,w);
	}
	double l=0,r=1000;
	while(r-l>1e-4){
		double mid=(l+r)/2;
		if(check(mid)) l=mid;
		else r=mid;
	}
	printf("%.2lf\n",l);
	return 0;
}
2. AcWing 1165. 单词环

要求的答案为 ans=\frac{\sum w[i]}{\sum 1},求环的最大的 ans,其中 \sum 1=n 为这个环由几个单词构成。

我们发现这道题只用到了每一个单词的首2个字母、 尾2个字母该单词的长度

因此两个字母的情况只有 26*26=676 种。

因此对于每一个单词,其首2个字母为 u,尾2个字母尾 v,该单词的长度为 len,则 add(u,v,len)

此题的平均长度不能为 0,因此特判一下 check(0),如果非真,则输出 No solution

此题用了一个小优化,若使用普通的判负环出现了超时,则使用写题经验, count 表示所有点一共被更新的次数,若 count>=2*N 或 count 大于一个比较大的数 ,则可以判断已经出现负环。由于此题 N  太小了,所有写了个较大的数。

#include<bits/stdc++.h>
using namespace std;
const int N=700,M=100010;
struct node{
	int nex,to,w;
}e[M];
int head[N],cnt,vis[N],tot[N],q[N];
double dis[N];
int n;
char s[1010];
void add(int u,int v,int w){
	e[++cnt].nex=head[u];
	e[cnt].to=v;
	e[cnt].w=w;
	head[u]=cnt;
}
bool check(double mid){
	memset(tot,0,sizeof(tot));
	memset(vis,0,sizeof(vis));
	int hh=0,tt=0;
	for(int i=0;i<676;i++){
		q[tt++]=i;
		vis[i]=1;
	}
	int count=0;
	while(hh!=tt){
		int u=q[hh++];
		if(hh==N) hh=0;
		vis[u]=0;
		for(int i=head[u];i;i=e[i].nex){
			int v=e[i].to,w=e[i].w;
			if(dis[v]<dis[u]+w-mid){
				dis[v]=dis[u]+w-mid;
				tot[v]=tot[u]+1;
				if(++count>10000) return true;
				if(tot[v]>=N) return true;
				if(!vis[v]){
					q[tt++]=v;
					if(tt==N) tt=0;
					vis[v]=1;
				}
			}
		}
	}
	return false;
}

int main(){
	while(scanf("%d",&n)&&n){
		memset(head,0,sizeof(head));
		cnt=0;
		for(int i=1;i<=n;i++){
			getchar();
			scanf("%s",s);
			int len=strlen(s);
			if(len>=2){
				int u=(s[0]-'a')*26+s[1]-'a';
				int v=(s[len-2]-'a')*26+s[len-1]-'a';
				add(u,v,len);
			}
		}
		if(!check(0)) puts("No solution");
		else{
			double l=0,r=1000;
			while(r-l>1e-4){
				double mid=(l+r)/2;
				if(check(mid)) l=mid;
				else r=mid;
			}
			printf("%lf\n",r);
		}
	}
	
	return 0;
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值