【CF464E】The Classic Problem(主席树+最短路)
大致题意: 给你一张无向图,每条边的边权为\(2^{x_i}\),求\(s\)到\(t\)的最短路。
最短路
最短路,首先考虑\(Dijkstra\)。这里用\(SPFA\)似乎不太好,因为此题中计算边权是比较费时间的。
说句实话,这里的最短路和普通的最短路是一样的,唯一区别就是边权很大。
则我们需要支持的操作就应该是大二进制数的加法和比大小。
线段树?——加法
先考虑最暴力的,我们对于每个点,开一个线段树,每一位维护二进制下这一位的值,表示其距离。
然后由于边权是\(2\)的幂,所以也就相当于在二进制下某一位上加上\(1\)。
这看似简单,但要进位啊!而且加法结束后马上就是比大小,因此这是刻不容缓的。
不过没关系,我们可以研究进位的性质,例如下面这个例子:
1011101
+ 1000
--------
1100101
这么一看,性质好像还是挺显然的,就是找出高于或等于当前加\(1\)位(设其为\(x\))的最低的为\(0\)的位(设其为\(t\)),然后把\(x\sim t-1\)这些位上改为\(0\),把第\(t\)位改为\(1\)即可。
简单地总结一下,就是要实现区间赋值为\(0\)和单点赋值为\(1\)两种操作,这似乎是线段树的基本操作?
但新的问题来了:如何求出高于或等于当前加\(1\)位的最低为\(0\)的位?
二分!
考虑当前二分到的位为\(mid\),那么若\(x\sim mid\)间的\(1\)的个数小于等于\(t-x\),就说明\(x\sim mid\)之间存在至少一个\(0\),\(Check()\)返回\(true\),否则返回\(false\)。
而要求\(1\)的个数,也是线段树的基本操作吧,记一下每个点子树内的\(Size\)然后区间查询即可。
线段树?——比大小
现在考虑如何比较两个大二进制数的大小。
我们可以从两棵线段树的根节点出发,由于比较的是最高位,所以若某一节点右儿子内有\(1\)(\(Size>0\)),另一节点没有,就可以直接比较出大小。
若两个节点右儿子内都没有\(1\),就去比较左儿子。
但如果两个节点右儿子内都有\(1\),就比较棘手了,因为如果我们直接去比较右儿子,如果比完之后发现右儿子完全一样,我们又得去比较左儿子,复杂度就退化成了\(O(n)\)。
仔细思考,便可以发现这个做法错误的关键就在于两个右儿子可能完全一样无法比较大小。
那么如果我们能快速判断两个右儿子是否一样,不就可以直接去比较左儿子,而避免这个问题了吗?
于是就可以想到哈希,这样就轻松避免了复杂度的退化。
主席树
通过上面的总结,我们可以发现,用线段树可以轻松维护这些信息。
但是,之前说的对于每个点开一棵线段树显然不现实,因此就要主席树。
这样一来,这道题就彻底做完了。
莫名其妙
呃,这里提一下,我在具体实现时碰到一个诡异的问题。
不知道为什么,我写\(Dijkstra\)时调用\(STL\)的优先队列莫名挂了,调到心态爆炸后手写了一个线段树,结果就过了?!
莫名其妙。
代码
#include<bits/stdc++.h>
#define Tp template<typename Ty>
#define Ts template<typename Ty,typename... Ar>
#define Reg register
#define RI Reg int
#define Con const
#define CI Con int&
#define I inline
#define W while
#define N 110000
#define LN 20
#define X 1000000007
#define add(x,y,v) (e[++ee].nxt=lnk[x],e[lnk[x]=ee].to=y,e[ee].val=v)
using namespace std;
int n,m,s,t,ee,lnk[N+5];struct edge {int to,nxt,val;}e[(N<<1)+5];
class FastIO
{
private:
#define FS 100000
#define tc() (A==B&&(B=(A=FI)+fread(FI,1,FS,stdin),A==B)?EOF:*A++)
#define pc(c) (C==E&&(clear(),0),*C++=c)
#define tn (x<<3)+(x<<1)
#define D isdigit(c=tc())
int T;char c,*A,*B,*C,*E,FI[FS],FO[FS],S[FS];
public:
I FastIO() {A=B=FI,C=FO,E=FO+FS;}
Tp I void read(Ty& x) {x=0;W(!D);W(x=tn+(c&15),D);}
Ts I void read(Ty& x,Ar&... y) {read(x),read(y...);}
Tp I void write(Ty x) {W(S[++T]=x%10+48,x/=10);W(T) pc(S[T--]);}
Tp I void write(Con Ty& x,Con char& y) {write(x),pc(y);}
I void clear() {fwrite(FO,1,C-FO,stdout),C=FO;}
#undef D
}F;
class ChairmanTree//主席树
{
private:
#define L l,mid,O[rt].S[0]
#define R mid+1,r,O[rt].S[1]
#define PU(x)\
(\
O[x].Sz=O[O[x].S[0]].Sz+O[O[x].S[1]].Sz,\
O[x].H=O[O[x].S[0]].H+O[O[x].S[1]].H\
)//上传信息,维护子树内1的个数和哈希值
int tot,p[N+5];
class Hash//哈希
{
private:
#define ull unsigned long long
#define RU Reg ull
#define CU Con ull
ull x,y;
public:
I Hash() {x=y=0;}I Hash(CU a):x(a),y(a){}I Hash(CU a,CU b):x(a),y(b){}
I Hash operator + (Con Hash& o) Con {return Hash(x+o.x,y+o.y);}
I Hash operator - (Con Hash& o) Con {return Hash(x-o.x,y-o.y);}
I Hash operator * (Con Hash& o) Con {return Hash(x*o.x,y*o.y);}
I bool operator == (Con Hash& o) Con {return x==o.x&&y==o.y;}
I bool operator != (Con Hash& o) Con {return x^o.x||y^o.y;}
}seed,pw[N+5];
struct node {int Sz,S[2];Hash H;}O[N*LN*10];
I int Chk(CI tl,CI tr,CI l,CI r,CI rt)//求出区间内1的个数,用于检验二分
{
if(!rt||tl<=l&&r<=tr) return O[rt].Sz;RI mid=l+r>>1;
return (tl<=mid?Chk(tl,tr,L):0)+(tr>mid?Chk(tl,tr,R):0);
}
I int Find(CI rt,CI x)//二分,找出高于或等于当前位的最低的为0的位
{
RI l=x,r=N,t;W(l<r) Chk(x,t=l+r-1>>1,0,N,rt)<=t-x?r=t:l=t+1;//二分
return r;
}
I void Upt0(CI tl,CI tr,CI l,CI r,int& rt)//区间赋值为0
{
if(O[++tot]=O[rt],rt=tot,tl<=l&&r<=tr) return (void)(rt=0);RI mid=l+r>>1;
tl<=mid&&(Upt0(tl,tr,L),0),tr>mid&&(Upt0(tl,tr,R),0),PU(rt);
}
I void Upt1(CI t,CI l,CI r,int& rt)//单点赋值为1
{
if(O[++tot]=O[rt],rt=tot,l==r) return (void)(O[rt].Sz=1,O[rt].H=pw[l]);
RI mid=l+r>>1;t<=mid?Upt1(t,L):Upt1(t,R),PU(rt);
}
I bool le(CI l,CI r,CI rt1,CI rt2)//比大小
{
if(l==r) return O[rt1].Sz<O[rt2].Sz;RI mid=l+r>>1;//如果为叶节点,直接比较
if(!O[O[rt1].S[1]].Sz&&O[O[rt2].S[1]].Sz) return true;//如果rt1右节点内无1,而rt2内有,说明rt2大
if(O[O[rt1].S[1]].Sz&&!O[O[rt2].S[1]].Sz) return false;//如果rt2右节点内无1,而rt1内有,说明rt1大
if(O[O[rt1].S[1]].H!=O[O[rt2].S[1]].H) return le(mid+1,r,O[rt1].S[1],O[rt2].S[1]);//如果哈希值不同,比较右子树
if(!O[O[rt1].S[0]].Sz) return true;if(!O[O[rt2].S[0]].Sz) return false;
return le(l,mid,O[rt1].S[0],O[rt2].S[0]);//比较左子树
}
public:
int Rt[N+5];
I ChairmanTree()//初始化
{
p[0]=1,pw[0]=Hash(1,1),seed=Hash(233333,456789);
for(RI i=1;i<=N;++i) p[i]=(p[i-1]<<1)%X,pw[i]=pw[i-1]*seed;
}
I int Add(CI k,CI x)//求加上2^x后的和
{
RI t=Find(k,x),w=k;x^t&&(Upt0(x,t-1,0,N,w),0);//找到高于或等于当前位的最低的为0的位后,区间赋值为0
return Upt1(t,0,N,w),w;//单点赋值为1
}
I bool Less(CI k1,CI k2) {return le(0,N,k1,k2);}//比大小
I int GV(CI l,CI r,CI rt)//求出这个二进制数转化为十进制后的值
{
if(!rt||l==r) return O[rt].Sz?p[l]:0;RI mid=l+r>>1;
return (GV(L)+GV(R))%X;
}
#undef L
#undef R
#undef PU
}C;
int did[N+5];
class Dijkstra//最短路
{
private:
int vis[N+5],lst[N+5],cnt[N+5],St[N+5];
class SegmentTree//线段树优化
{
private:
#define P CI l=1,CI r=n,CI rt=1
#define L l,mid,rt<<1
#define R mid+1,r,rt<<1|1
#define mp make_pair
#define fir first
#define sec second
typedef pair<int,int> Pr;Pr V[N<<2];
I void PU(CI x)//上传信息
{
if(!~V[x<<1].fir) return (void)(V[x]=V[x<<1|1]);
if(!~V[x<<1|1].fir) return (void)(V[x]=V[x<<1]);
V[x]=V[C.Less(V[x<<1].fir,V[x<<1|1].fir)?x<<1:x<<1|1];
}
public:
I void Build(P)//建树,初始化全为-1
{
if(l==r) return (void)(V[rt]=mp(-1,l));RI mid=l+r>>1;
Build(L),Build(R),PU(rt);
}
I void Upt(CI x,CI v,P)//修改
{
if(l==r) return (void)(V[rt].fir=v);RI mid=l+r>>1;
x<=mid?Upt(x,v,L):Upt(x,v,R),PU(rt);
}
I int Query() {return ~V[1].fir?V[1].sec:-1;}//询问最小值
}S;
public:
I void Solve()//求解最短路
{
RI i,k,f,T=0;S.Build(),did[s]=1,S.Upt(s,0);
W(~(k=S.Query()))
{
for(S.Upt(k,-1),i=lnk[k];i;i=e[i].nxt) !vis[e[i].to]&&
(
f=C.Add(C.Rt[k],e[i].val),(!did[e[i].to]||C.Less(f,C.Rt[e[i].to]))&&
(did[e[i].to]=1,lst[e[i].to]=k,C.Rt[e[i].to]=f,S.Upt(e[i].to,C.Rt[e[i].to]),0)
);
}
if(!did[t]) puts("-1"),exit(0);k=t;W(St[++T]=k,k^s) k=lst[k];//判无解,求路径
F.write(C.GV(0,N,C.Rt[t]),'\n'),F.write(T,'\n');W(T) F.write(St[T--],' ');//输出最终答案
}
}D;
int main()
{
RI i,x,y,z;for(F.read(n,m),i=1;i<=m;++i) F.read(x,y,z),add(x,y,z),add(y,x,z);F.read(s,t);//读入,建边
return D.Solve(),F.clear(),0;
}