=== ===
这里放传送门
=== ===
题解
因为题目中说每次长跑跑道方向是固定的,也就是一条边不能来回经过。但是答案肯定不是从A沿着一条最近的路跑到B就一定最优了,可能出去绕一圈还能绕回来。那这个出去绕了一圈的过程肯定沿着两条不同的路径从A到了某个点B又从B回到了某个点A,那这可以联想到无向图的双连通分量。因为双连通分量缩点以后可以形成一棵树,那么实际上就是要用LCT维护缩点以后的图,每次加入一条边如果成环了,就把环上的所有点合并起来。一开始想要打个什么标记什么的,但里面那一坨Splay一转就转的乱七八糟。。但是实际上每个点只会被合并一次,被合并过的点再跳的时候直接跳到它的代表元素就可以了。这样的话这个题就很好写了。。但是写完以后在CodeVS上T了4个点,在Tsinsen上T了5个点。。
然后就是蛋疼的卡常过程。。尽量减少无用的找代表元素的Find过程就可以大大提高速度。然而ATP的代码虽然在BZOJ上能过但是在CodeVS上会T一个点,在Tsinsen上竟然T了4个点。。
代码
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
int n,m,v[150010],lkd[150010],top;
struct Node{
Node *ch[2],*fa;
int sum,num,val;
bool rev;
Node();
inline bool pl();
inline bool is_root();
void Del(Node *F);
inline void Rev(){swap(ch[1],ch[0]);rev^=1;}
inline void count();
inline void push();
}*null,P[150010],*ptr[150010],*father[150010],*st[150010];
inline Node* find(Node *x){
if (x==father[x->num]) return x;
father[x->num]=find(father[x->num]);
return father[x->num];
}
Node::Node(){ch[1]=ch[0]=fa=null;sum=val=num=0;rev=false;}
inline bool Node::pl(){return this==fa->ch[1];}
inline bool Node::is_root(){return this!=fa->ch[1]&&this!=fa->ch[0];}
inline void Node::count(){
ch[0]=find(ch[0]);ch[1]=find(ch[1]);
sum=ch[0]->sum+ch[1]->sum+val;
}
inline void Node::push(){
if (rev==true){
if (ch[0]!=null) ch[0]->Rev();
if (ch[1]!=null) ch[1]->Rev();
rev=false;
}
}
void Node::Del(Node *F){
if (this==null) return;
ch[0]=find(ch[0]);ch[1]=find(ch[1]);
ch[0]->Del(F);ch[1]->Del(F);
father[num]=F;
ch[1]=ch[0]=fa=null;
}
inline void Rotate(Node *k){
Node *r=k->fa;
if (k==null||r==null) return;
int x=k->pl()^1;
r->ch[x^1]=k->ch[x];
if (r->ch[x^1]!=null)
r->ch[x^1]->fa=r;
if (!r->is_root())
r->fa->ch[r->pl()]=k;
k->fa=r->fa;
r->fa=k;
k->ch[x]=r;
r->count();k->count();
}
inline void Splay(Node *r){
top=0;Node *tmp=r;
st[++top]=tmp;
tmp->fa=find(tmp->fa);
while (!tmp->is_root()){
st[++top]=tmp->fa;
tmp=tmp->fa;
tmp->fa=find(tmp->fa);
}
for (int i=top;i>=1;i--) st[i]->push();
for (;!r->is_root();Rotate(r))
if (!r->fa->is_root())
Rotate(r->pl()==r->fa->pl()?r->fa:r);
}
inline void Access(Node *k){
Node *r=null;
while (k!=null){
Splay(k);
k->ch[1]=r;
k->count();
r=k;k=k->fa;
}
}
void Change_root(Node *x){Access(x);Splay(x);x->Rev();}
void Link(Node *x,Node *y){Change_root(x);x->fa=y;}
void Get_path(Node *x,Node *y){Change_root(y);Access(x);Splay(x);}
inline int check(int x){
if (lkd[x]==x) return x;
lkd[x]=check(lkd[x]);
return lkd[x];
}
inline int get(){
int x=0;
char c=getchar();
while (c<'0'||c>'9') c=getchar();
while (c<='9'&&c>='0'){
x=(x<<3)+(x<<1)+c-'0';c=getchar();
}
return x;
}
int main()
{
null=new Node;*null=Node();
n=get();m=get();
for (int i=0;i<=n;i++){
Node *w=&P[i];
*w=Node();ptr[i]=w;
father[i]=w;
w->num=lkd[i]=i;
}
father[0]=ptr[0]=null;
for (int i=1;i<=n;i++){
v[i]=get();
P[i].val=P[i].sum=v[i];
}
for (int i=1;i<=m;i++){
int type,a,b;
type=get();a=get();b=get();
switch (type){
case 1:{
int r1,r2;
r1=check(a);r2=check(b);
if (r1!=r2){
Link(find(ptr[a]),find(ptr[b]));
lkd[r2]=r1;
}else{
Node *tmp,*to;
tmp=find(ptr[a]);to=find(ptr[b]);
if (tmp==to) break;
Get_path(to,tmp);
Splay(to);to->val=to->sum;
to->Del(to);
}
break;
}
case 2:{
Node *tmp=find(ptr[a]);
Access(tmp);Splay(tmp);
tmp->val=tmp->val-v[a]+b;
v[a]=b;tmp->count();
break;
}
case 3:{
Node *tmp1,*tmp2;
int r1,r2;
r1=check(a);r2=check(b);
if (r1!=r2){printf("-1\n");break;}
tmp1=find(ptr[a]);tmp2=find(ptr[b]);
Get_path(tmp1,tmp2);
printf("%d\n",tmp1->sum);
break;
}
}
}
return 0;
}
偏偏在最后出现的补充说明
这种“每个点被合并一次”的均摊分析思路还是很常见的