【洛谷4073】[WC2013] 平面图(对偶图)
- 给定一张 \(n\) 个点和 \(m\) 条边的平面图,每条边有一个边权。
- \(q\) 次询问,每次给定两个点,询问用一条曲线将它们相连所经最大边权的最小值,要求不能经过外平面。
- \(5\le n,m,q\le10^5\)
平面图求对偶图
这题其实是平面图对偶图相关的一些算法的模板题。(虽然感觉并不会考这种东西)
首先考虑如何求出平面图的对偶图。
先把一条无向边视作两条有向边,然后对每个点将它的所有出边极角排序,记下每条出边的下一条是谁。
任选一条边 \(x\rightarrow y\) 出发,然后选择 \(y\rightarrow x\) 的下一条边 \(y\rightarrow z\) 继续走,以此类推直至走回 \(x\),这样绕出的一个环围出的面就是对偶图中的一个点。
得出点之后,对每条无向边,在它拆成的两条有向边各自相邻的对偶图中的点之间连边,边权就是这条无向边的边权。这样便建出了对偶图。
显然求点的过程中每条边只需要访问一次,因此这里的复杂度瓶颈实际上在于极角排序。
注意到这题中还存在一个限制,就是不能经过外平面。
发现按照这样走出的环如果围出的是外平面,环上的点将是按照逆时针顺序访问的;如果是内平面,则将是按照顺时针顺序访问的。
可以利用叉积计算有向面积的方式来判断访问顺序,从而判断围出的面是否为外平面。
平面图点定位
为了实现询问,需要判断询问给出的所有点分别在哪个面中,也即属于对偶图中的哪个点。
可以扫描线,用 set 维护当前存在的所有边的相对顺序。
那么对于每个点,只要 lower_bound 求出它上方的第一条边,那么它就处于这条边下面的那个面中。
Kruskal 重构树
其实也可以 MST+树上倍增。不过我还是选择了直接 Kruskal 重构树。
询问就是求两点在 Kruskal 重构树中 LCA 的权值。
代码:\(O(n\log n)\)
#include<bits/stdc++.h>
#define Tp template<typename Ty>
#define Ts template<typename Ty,typename... Ar>
#define Rg register
#define RI Rg int
#define Cn const
#define CI Cn int&
#define I inline
#define W while
#define N 100000
#define LG 18
#define DB double
#define add(x,y,z) (e[++ee].nxt=lnk[x],e[lnk[x]=ee].to=y,e[ee].v=z)
using namespace std;
int n,m,Qt,ee=1,lnk[N+5];struct edge {int to,nxt,v,g;}e[2*N+5];
struct P
{
DB x,y;P(RI a=0,RI b=0):x(a),y(b){}
I P operator - (Cn P& o) Cn {return P(x-o.x,y-o.y);}
I DB operator ^ (Cn P& o) Cn {return x*o.y-y*o.x;}
I friend bool operator < (Cn P& A,Cn P& B) {RI a=A.x>0||!A.x&&A.y<0,b=B.x>0||!B.x&&B.y<0;return a^b?a>b:(A^B)>0;}
}p[N+5],q[2*N+5],tmp[N+5];
bool cmp(CI x,CI y) {return tmp[e[x].to]<tmp[e[y].to];}
int ct,Out,bl[2*N+5];DB s;void dfs(CI x,CI i)//x为起点,当前在第i条边
{
bl[i]=ct;RI j=e[i^1].g;if(bl[j]) return;s+=(p[e[j].to]-p[x])^(p[e[i].to]-p[x]),dfs(x,j);//走反向边的下一条边;s统计有向面积
}
int w[N+5];I void Init()//求出对偶图中的点
{
RI i,j,c;for(i=1;i<=n;++i)
{
for(c=0,j=lnk[i];j;j=e[j].nxt) w[++c]=j,tmp[e[j].to]=p[e[j].to]-p[i];
for(sort(w+1,w+c+1,cmp),j=1;j<=c;++j) e[w[j]].g=w[j%c+1];//极角排序,记录每条边的下一条边
}
for(i=2;i<=ee;++i) !bl[i]&&(++ct,s=0,dfs(e[i^1].to,i),s<=0&&(Out=ct));//每条边只需访问一次;Out记录外平面标号
}
namespace K//Kruskal重构树
{
int Nt,V[2*N+5],S[2*N+5][2],fa[2*N+5];I int Fa(CI x) {return fa[x]?fa[x]=Fa(fa[x]):x;}
struct edge {int x,y,v;bool operator < (Cn edge& o) Cn {return v<o.v;}}s[N+5];
int D[2*N+5],f[2*N+5][LG+1];void dfs(CI x)//倍增预处理
{
D[x]=D[f[x][0]]+1;for(RI i=1;f[x][i]=f[f[x][i-1]][i-1];++i);x>ct&&(dfs(S[x][0]),dfs(S[x][1]),0);
}
I void Build()//建树
{
RI et=0,i;for(i=2;i<=ee;i+=2) bl[i]^Out&&bl[i^1]^Out&&(s[++et]=(edge){bl[i],bl[i^1],e[i].v},0);//存下所有边
RI x,y;for(sort(s+1,s+et+1),Nt=ct,i=1;i<=et;++i)//Kruskal
(x=Fa(s[i].x))^(y=Fa(s[i].y))&&(V[f[x][0]=f[y][0]=fa[x]=fa[y]=++Nt]=s[i].v,S[Nt][0]=x,S[Nt][1]=y);
dfs(Nt);
}
I int LCA(RI x,RI y)//倍增LCA
{
RI i;for(D[x]<D[y]&&(swap(x,y),0),i=0;D[x]^D[y];++i) (D[x]^D[y])>>i&1&&(x=f[x][i]);if(x==y) return x;
for(i=0;f[x][i]^f[y][i];++i);for(--i;~i;--i) f[x][i]^f[y][i]&&(x=f[x][i],y=f[y][i]);return f[x][0];
}
I int Q(CI x,CI y) {if(x==Out||y==Out) return -1;RI z=LCA(x,y);return z?V[z]:-1;}//询问
}
namespace Scan//扫描线实现点定位
{
DB nw;struct line {int p;DB k,b;bool operator < (Cn line& o) Cn {return k*nw+b<o.k*nw+o.b;}};
int c;struct OP {int op;DB x;line s;bool operator < (Cn OP& o) Cn {return fabs(x-o.x)>0.1?x<o.x:op<o.op;}}g[2*N+5];
int id[2*N+5];bool cmp(CI x,CI y) {return q[x].x<q[y].x;}
int pos[2*N+5];set<line> G;I void Get()
{
RI i,j;line s;for(i=1;i<=n;++i) for(j=lnk[i];j;j=e[j].nxt) if(p[i].x<p[e[j].to].x)
{
s.p=bl[j],s.k=(p[e[j].to].y-p[i].y)/(p[e[j].to].x-p[i].x),s.b=p[i].y-s.k*p[i].x;//记录线段解析式
g[++c]=(OP){1,p[i].x,s},g[++c]=(OP){-1,p[e[j].to].x,s};//记录开始和结束位置
}
#define Work() (nw=q[id[j]].x,s=(line){0,0,q[id[j]].y},pos[id[j++]]=(G.empty()||*--G.end()<s)?Out:G.lower_bound(s)->p)//询问点,找到上方第一条线段
for(i=1;i<=2*Qt;++i) id[i]=i;for(sort(id+1,id+2*Qt+1,cmp),sort(g+1,g+c+1),i=j=1;i<=c;++i)
{
W(j<=2*Qt&&q[id[j]].x<g[i].x) Work();//当前操作前的点
nw=g[i].x+g[i].op*0.5,~g[i].op?(void)G.insert(g[i].s):(void)G.erase(g[i].s);//当前坐标±0.5防止值相同
}
W(j<=2*Qt) Work();//没有询问完的点
}
}
int main()
{
RI i;for(scanf("%d%d",&n,&m),i=1;i<=n;++i) scanf("%lf%lf",&p[i].x,&p[i].y);
RI x,y,z;for(i=1;i<=m;++i) scanf("%d%d%d",&x,&y,&z),add(x,y,z),add(y,x,z);
for(scanf("%d",&Qt),i=1;i<=Qt;++i) scanf("%lf%lf%lf%lf",&q[i].x,&q[i].y,&q[Qt+i].x,&q[Qt+i].y);
for(Init(),K::Build(),Scan::Get(),i=1;i<=Qt;++i) printf("%d\n",K::Q(Scan::pos[i],Scan::pos[Qt+i]));return 0;
}
待到再迷茫时回头望,所有脚印会发出光芒