图论中的二分
图论中最基础的算法是最短路,然而近些年在竞赛中已很少考最短路问题,许多图论题目往往是要求一种十分诡异的东西,这时候我们想直接求是不现实的,二分答案就应运而生了。
小奇回地球(earth)
从标号为 1 的星球到标号为 n 的星球,某一些星球之间有航线。由于超时空隧道的存在,从一个星球到另一个星球时间可能会倒流,而且,从星球 a 到 b 耗费的时间和星球 b 到 a 耗费的时间不一定相同。
有速度可以调节飞行的时间的调节装置,其功能可以使得整次航程中所有两星球间的飞行时间增加或减少相同的整数值。求最短时间,时间必须为非负数。
两点间最多只会有一条路。
1,2 号测试点,保证所有星球出度不超过 1
3,4 号测试点,n≤10
5,6 号测试点, −100≤t≤100 对于 100% 的数据 T≤10,n≤100,m≤n∗(n−1),−100000≤t≤100000
数据随机和构造结合生成
这道题的题意是关键,首先应该明确两个重要结论:
- 若存在与起点终点都联通的负环,则不存在最短路
- 若最短路权值为负数,则不符合宇宙法
易证:是否同时符合以上两个条件关于速度调节器的设定数值的函数具有单调性,故可以用二分法求出速度调节器的设定数值的最小值,且此时最短路即为所求
需要注意手写循环队列别开小了
#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
const int MAXN=100,MAXM=MAXN*(MAXN-1),INF=0x7fffffff;
int head[MAXN+1],ecnt;
struct node{int next,to,value;} e[MAXM+1];
void add(int x,int y,int z)
{
ecnt++;
e[ecnt].to=y;
e[ecnt].next=head[x];
head[x]=ecnt;
e[ecnt].value=z;
}
bool vis[MAXN+1],lt[MAXN+1];
void dfs(int x)
{
vis[x]=true;
for(int tmp=head[x];tmp;tmp=e[tmp].next)
if(!vis[e[tmp].to])
dfs(e[tmp].to);
}
bool in[MAXN+1];
int dis[MAXN+1];
bool dfs_spfa(int x,int ad)
{
in[x]=true;
for(int tmp=head[x];tmp;tmp=e[tmp].next)
{
int y=e[tmp].to;
if(dis[y]>dis[x]+e[tmp].value+ad && lt[y])
{
if(in[y]) return true;
dis[y]=dis[x]+e[tmp].value+ad;
if(dfs_spfa(y,ad)) return true;
}
}
in[x]=false;
return false;
}
int queue[10*MAXN+5];
int n,m;
void bfs_spfa(int ad)
{
memset(queue,0,sizeof(queue));
memset(in,false,sizeof(in));
memset(dis,63,sizeof(dis));
dis[1]=0;
in[1]=true;
int h=0,t=1;
queue[t]=1;
while(h<t)
{
h=(h+1)%(10*MAXN+5);
int x=queue[h];
in[x]=false;
for(int tmp=head[x];tmp;tmp=e[tmp].next)
{
int y=e[tmp].to;
if(dis[y]>dis[x]+e[tmp].value+ad&<[y])
{
dis[y]=dis[x]+e[tmp].value+ad;
if(!in[y])
{
in[y]=true;
t=(t+1)%(10*MAXN+5);
queue[t]=y;
}
}
}
}
}
int T;
bool check(int ad)
{
int i;
for(i=1;i<=n;i++)
if(lt[i])
{
memset(dis,0,sizeof(dis));
memset(in,0,sizeof(in));
if(dfs_spfa(i,ad)) return 0;
}
bfs_spfa(ad);
if(dis[n]>=0&&dis[n]<=(int)1e9) return 1;
return 0;
}
int main()
{
freopen("earth.in","r",stdin);
freopen("earth.out","w",stdout);
int i;
scanf("%d",&T);
while(T--)
{
ecnt=0;
memset(head,0,sizeof(head));
scanf("%d%d",&n,&m);
for(i=1;i<=m;i++)
{
int u,v,w;
scanf("%d%d%d",&u,&v,&w);
add(u,v,w);
}
//读边
bool selt;//起点终点是否连通
memset(vis,false,sizeof(vis));
dfs(1);
memset(lt,false,sizeof(lt));
for(i=1;i<=n;i++)
if(vis[i]) lt[i]=true;
if(lt[n]) selt=true;
for(i=1;i<=n;i++)
if(vis[i])
{
memset(vis,false,sizeof(vis));
dfs(i);
if(!vis[n]) lt[i]=false;
}
//前面两次循环遍历所有点,lt[i]表示点是否与起点终点都联通
int l=-100000,r=100000,ans=-1;
while(l<r)
{
int mid=(l+r)>>1;
if(check(mid)) ans=dis[n],r=mid;
else l=mid+1;
}
if(!selt) cout<<-1<<endl;
else cout<<ans<<endl;
}
return 0;
}
小澳的葫芦
给定一个有向无环图,
题目描述
小澳最喜欢的歌曲就是《葫芦娃》。一日表演唱歌,他尽了洪荒之力,唱响心中圣歌。随之,小澳进入了葫芦世界。葫芦世界有 n 个葫芦,标号为 1~ n。n 个葫芦由 m 条藤连接,每条藤连接了两个葫芦,这些藤构成了一张有向无环图。小澳爬过每条藤都会消耗一定的能量。小澳站在 1 号葫芦上(你可以认为葫芦非常大,可以承受小澳的体重),他想沿着藤爬到 n 号葫芦上,其中每个葫芦只经过一次。
小澳找到一条路径,使得消耗的能量与经过的葫芦数的比值最小。
输入格式
输入文件名为 calabash.in。
输入文件第一行两个正整数 n,m,分别表示葫芦的个数和藤数。
接下来 m 行,每行三个正整数 u,v,w,描述一条藤,表示这条藤由 u 连向 v,小澳爬过这条藤需要消耗 w 点能量。
输出格式
输出文件名为 calabash.out。
一行一个实数,表示答案(误差不超过 10^-3)。
输入输出样例
calabash.in
4 6
1 2 1
2 4 6
1 3 2
3 4 4
2 3 3
1 4 8
calabash.out
2.000
输入输出样例说明
有 4 种爬法:
1->4,消耗能量 8,经过 2 个葫芦,比值为 8/2=4。
1->2->4,消耗能量 1+6=7,经过 3 个葫芦,比值为 7/3≈2.33。
1->3->4,消耗能量 2+4=6,经过 3 个葫芦,比值为 6/3=2。
1->2->3->4,消耗能量 1+3+4=8,经过 4 个葫芦,比值为 8/4=2。
所以选第三种或第四种方案,答案为 2。
数据规模与约定
测试点编号 | n | m | 特殊说明 |
---|---|---|---|
1 | 2 | 1 | |
2 | 100 | 99 | 除 1 外,所有葫芦的入度均为 1 |
3 | 100 | 105 | 所有从 1 到 n 的路径经过的葫芦数相等 |
4 | 100 | 1000 | |
5 | 100 | 1000 | |
6 | 199 | 198 | 除 1 外,所有葫芦的入度均为 1 |
7 | 200 | 231 | 所有从 1 到 n 的路径经过的葫芦数相等 |
8 | 200 | 2000 | |
9 | 200 | 2000 | |
10 | 200 | 2000 |
对于所有数据,小澳爬过每条藤消耗的能量不会超过 10^3,且一定存在一条从 1到 n 的路径。
本题是最小比例生成树的经典例题,其实所谓的最小比例生成树与上一道题很像。
那道题是给一个图求给每一条边加上一个权值x使其最短路>=0且最小,方法就是二分x,每次求一次最短路,不过需要判负环和起点终点不连通的情况。
可是这道题如何用那道题的思路呢?
可以假设答案为x,则对于任意一条由k条边组成的路径:
(w1+w2+...+wk)/k>=x
化简:
(w1+w2+...+wk)>=kx
再化简:
(w1−x)+(w2−x)+...+(wk−x)>=0
这样就很明显了,只需要二分x,求出满足上式的最小x值即可
代码与 那道题相似,不贴了
W的火星工程 wproject
题目描述
大老板 W 的伟大工程扩大到了火星,他准备在火星建立一个自己的度假村。在他的度假村里,有两个大饭店 A,B。对于 W 来说,修建度假村必不可少的就是从 A 饭店向 B 饭店修路,以保证他可以短时间内享受各种美味。火星上有一些中转站,中转站之间以及它们与饭店之间有路径使得能从一个到达另一个(路径为单向)。对于每一条路径,工程师 ZQ 给出了它的两个消费参数 a, b。W 会从A 饭店出发,经过其中的一些路径,最终到达 B 饭店。W 希望他走过的所有道路的 sigma(a)/sigma(b)最小,也就是那些道路的 a 值之和除以他们的 b 值之和最小。可是路径实在太多了,W 不知道该如何选择。聪明的你需要帮助他计算出这个最小值。至于路径的选取方法你就不需要告诉他了。
输入格式
从文件 wproject.in 中输入数据。
输入两个整数 n,m,表示中转站的数量和边的数量。
随后 m 行,每行四个整数 x,y,a,b,分别表示路径的两端,路径的 a,b 消费参数。
其中 0 号点与 n+1 号点分别表示 W 的两个饭店 A,B。
注意你并不需要把所有中转站都连入路中,只要保证从 A 饭店可以到达 B饭店就行了。
输出格式
输出到文件 wproject.out 中。
输出一个小数,表示所求的最小值。输出保留小数点后 6 位(printf 保留6位即可)。
样例
样例输入
2 3
0 1 1 2
0 2 2 3
1 3 1 3
样例输出
0.400000
数据范围与提示
对于
20%
的数据,
n,m≤100
;
对于 50% 的数据, n≤1000 ;
对于 100% 的数据, 1≤n≤10000 , 1≤m≤100000 , 0≤x,y≤n+1,0<a,b≤100000 ;
数据保证 0 号饭店可以到达 n+1 号饭店,任意两个中转站或饭店间最多有一条边,且保证没有路可以构成环。
其实是比较简单的一道题,只是忘记了 Dijkstra 不能处理负边权图,暴力 60。
本题暴力搜索加上剪枝后还是比较有效的,若要写出正解的话,我们应该考虑到本题中要求的是所走路径中
∑a∑b
的值,对于这么诡异的一个东西,我们考虑二分答案。
二分答案的灵魂在于判断一个确定答案的可行性。
设要判断的答案为 k ,那么当满足
∑a∑b≤k
时, k 为可行答案。
我们将原式进行简单的变换,可得:
∑a−k∑b≤0
,这样,我们可以将每条边的边权赋为
a−kb
,之后再用 SPFA 求最短路,若最短路满足
dis[n+1]≤0
则说明答案可行。
由此我们可以求解出答案。
#include <cstdio>
#include <queue>
using namespace std;
const int MAXE=100005,MAXV=10005;
const double eps=1e-9,INF=1e7;
int n,m;
double dis[MAXV];
queue<int> que;
struct E{int next,to;double a,b;} e[MAXE];int ecnt,G[MAXV];
inline void add(int u,int v,double a,double b)
{e[++ecnt]=(E){G[u],v,a,b};G[u]=ecnt;}
bool spfa(double k)
{
for(int i=1;i<=n+1;++i)
dis[i]=INF;
que.push(0);
while(!que.empty())
{
int u=que.front();que.pop();
for(int i=G[u];i;i=e[i].next)
{
int v=e[i].to;
double w=e[i].a-k*e[i].b;
if(dis[v]>dis[u]+w)
{
dis[v]=dis[u]+w;
que.push(v);
}
}
}
return dis[n+1]<eps;
}
int main()
{
freopen("wproject.in","r",stdin);
freopen("wproject.out","w",stdout);
scanf("%d%d",&n,&m);
for(int i=1;i<=m;++i)
{
int x,y;double a,b;
scanf("%d%d%lf%lf",&x,&y,&a,&b);
add(x,y,a,b);
}
double l=0,r=INF;
while(r-l>eps)
{
double mid=(l+r)/2;
if(spfa(mid))
r=mid;
else l=mid;
}
printf("%.6lf",l);
return 0;
}