HNOI 2012 永无乡
Posted 日拱一卒 功不唐捐
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了HNOI 2012 永无乡相关的知识,希望对你有一定的参考价值。
永无乡包含 n 座岛,编号从 1 到 n,每座岛都有自己的独一无二的重要度,按照重要度可
以将这 n 座岛排名,名次用 1 到 n 来表示。某些岛之间由巨大的桥连接,通过桥可以从一个岛
到达另一个岛。如果从岛 a 出发经过若干座(含 0 座)桥可以到达岛 b,则称岛 a 和岛 b 是连
通的。现在有两种操作:B x y 表示在岛 x 与岛 y 之间修建一座新桥。Q x k 表示询问当前与岛
x 连通的所有岛中第 k 重要的是哪座岛,即所有与岛 x 连通的岛中重要度排名第 k 小的岛是哪
座,请你输出那个岛的编号。
从文件 input.txt 中读入数据,输入文件第一行是用空格隔开的两个正整数 n 和 m,分别
表示岛的个数以及一开始存在的桥数。接下来的一行是用空格隔开的 n 个数,依次描述从岛 1
到岛 n 的重要度排名。随后的 m 行每行是用空格隔开的两个正整数 ai 和 bi,表示一开始就存
在一座连接岛 ai 和岛 bi 的桥。后面剩下的部分描述操作,该部分的第一行是一个正整数 q,
表示一共有 q 个操作,接下来的 q 行依次描述每个操作,操作的格式如上所述,以大写字母 Q
或 B 开始,后面跟两个不超过 n 的正整数,字母与数字以及两个数字之间用空格隔开。
对于 20%的数据 n≤1000,q≤1000
对于 100%的数据 n≤100000,m≤n,q≤300000
输出文件 output.txt 中,对于每个 Q x k 操作都要依次输出一行,其中包含一个整数,表
示所询问岛屿的编号。如果该岛屿不存在,则输出-1。
5 1
4 3 2 5 1
1 2
7
Q 3 2
Q 2 1
B 2 3
B 1 5
Q 2 1
Q 2 4
Q 2 3
-1
2
5
1
2
本题卡cin啊啊啊啊啊啊啊啊啊!!!!!!
法一:启发式合并+并查集+splay
#include<cstdio> #include<iostream> #include<algorithm> #define N 100100 using namespace std; int n,m,p; int key[N],tot,siz[N],ch[N][2],fa[N],F[N]; int find(int x) {return x==F[x] ? F[x] : F[x]=find(F[x]);} int read() { int x=0;char c=getchar(); while(c<\'0\'||c>\'9\') c=getchar(); while(c>=\'0\'&&c<=\'9\') {x=x*10+c-\'0\';c=getchar();} return x; } int getson(int x) { return ch[fa[x]][1]==x; } void update(int x) { if(!x) return; siz[x]=siz[ch[x][0]]+siz[ch[x][1]]+1; } void rotate(int x) { int y=fa[x],z=fa[y],kind=getson(x); ch[y][kind]=ch[x][kind^1];ch[x][kind^1]=y; fa[ch[y][kind]]=y;fa[y]=x; fa[x]=z; if(z) ch[z][ch[z][1]==y]=x; update(y); } void splay(int x) { for(int f;f=fa[x];rotate(x)) if(fa[f]) rotate(getson(x)==getson(f) ? f:x); update(x); } void link(int x,int y) { int now=x; while(ch[now][key[y]>key[now]]) now=ch[now][key[y]>key[now]]; fa[y]=now; ch[now][key[y]>key[now]]=y; siz[y]=1; update(now); splay(y); } void merge(int now,int y) { int tmp1=0,tmp2=0; if(ch[y][0]) tmp1=ch[y][0],ch[y][0]=0; if(ch[y][1]) tmp2=ch[y][1],ch[y][1]=0; link(now,y); if(tmp1) merge(y,tmp1); if(tmp2) merge(y,tmp2); } int query(int x,int k) { splay(x); if(k>siz[x]) return -1; int now=x; while(1) { int tmp=ch[now][0] ? siz[ch[now][0]] : 0; if(k<=tmp) now=ch[now][0]; else { if(k==tmp+1) return now; else { now=ch[now][1]; k-=tmp+1; } } } } int main() { n=read();m=read(); for(int i=1;i<=n;i++) { siz[i]++; key[i]=read(); F[i]=i; } int x,y,X,Y; for(int i=1;i<=m;i++) { x=read();y=read(); X=find(x);Y=find(y); if(X!=Y) { F[X]=Y; splay(X);splay(Y); if(siz[X]<siz[Y]) swap(X,Y); merge(X,Y); } } p=read(); char c[3]; for(int i=1;i<=p;i++) { scanf("%s",c); x=read();y=read(); if(c[0]==\'B\') { X=find(x);Y=find(y); if(X!=Y) { F[X]=Y; splay(X);splay(Y); if(siz[X]<siz[Y]) swap(X,Y); merge(X,Y); } } else { int t=query(x,y); printf("%d\\n",t); } } }
2个错误:
1、
void update(int x)
{
if(!x) return;
siz[x]=siz[ch[x][0]]+siz[ch[x][1]]+1;
}
没有判断x是否为0
2、
void link(int x,int y)
{
int now=x;
while(ch[now][key[y]>key[now]]) now=ch[now][key[y]>key[now]];
fa[y]=now;
ch[now][key[y]>key[now]]=y;
siz[y]=1;
update(now);
splay(y);
}
没有修改siz[y] 没有splay(y)
先修改siz[y]=1,防止y本身还带有左右孩子
同时 update(now) 更新now 的siz
splay(y)不是多余的操作,他的目的不是将y转到根节点
而是更改自y至根节点所在路径上的siz,splay操作恰好可以完成
同时为后续的merge 操作准备根节点
法二、启发式合并+并查集+线段树
#include<cstdio> #include<iostream> #define N 100001 using namespace std; int n,m,p,key[N],tot,sa[N],fa[N],root[N]; struct node { int l,r,siz; }tr[N*20]; int find(int x) {return x==fa[x] ? fa[x] :fa[x]=find(fa[x]);} void add(int & k,int l,int r,int x) { if(!k) k=++tot; if(l==r) {tr[k].siz=1;return;} int mid=l+r>>1; if(x<=mid) add(tr[k].l,l,mid,x); else add(tr[k].r,mid+1,r,x); tr[k].siz=tr[tr[k].l].siz+tr[tr[k].r].siz; } int merge(int x,int y) { if(!x) return y; if(!y) return x; tr[x].l=merge(tr[x].l,tr[y].l); tr[x].r=merge(tr[x].r,tr[y].r); tr[x].siz=tr[tr[x].l].siz+tr[tr[x].r].siz; return x; } int query(int x,int l,int r,int k) { if(l==r) return l; int mid=l+r>>1,tmp=tr[tr[x].l].siz; if(tmp>=k) return query(tr[x].l,l,mid,k); else return query(tr[x].r,mid+1,r,k-tmp); } int read() { int x=0,f=1;char ch=getchar(); while(ch<\'0\'||ch>\'9\'){if(ch==\'-\')f=-1;ch=getchar();} while(ch>=\'0\'&&ch<=\'9\') x=x*10+ch-48,ch=getchar(); return x*f; } int main() { n=read();m=read(); for(int i=1;i<=n;i++) key[i]=read(),fa[i]=i,sa[key[i]]=i; int x,y,r1,r2; for(int i=1;i<=m;i++) { x=read();y=read(); r1=find(x),r2=find(y); fa[r1]=r2; } for(int i=1;i<=n;i++) { int r=find(i); add(root[r],1,n,key[i]); } p=read(); char c[3];int s; for(int i=1;i<=p;i++) { scanf("%s",c); x=read();y=read(); if(c[0]==\'B\') { r1=find(x);r2=find(y); if(r1!=r2) { fa[r2]=r1; merge(root[r1],root[r2]); } } else { s=find(x); if(tr[root[s]].siz<y) printf("-1\\n"); else { s=query(root[s],1,n,y); printf("%d\\n",sa[s]); } } } }
联想 动态线段树 SDOI2014 旅行http://www.cnblogs.com/TheRoadToTheGold/p/6394842.html
2个错误:
1、合并两颗线段树
错误代码:
void merge(int x,int y)
{
if(!x&&!y) return;
tr[x].siz+=tr[y].siz;
merge(tr[x].l,tr[y].l);
merge(tr[x].r,tr[y].r);
}
原因:当x=0,y不为0时 应把y的信息合并到x所对应的位置上,但由于x为0,y的信息合并不上
所以正确合并:类似于主席树中的共用子节点
int merge(int x,int y)
{
if(!x) return y;
if(!y) return x;
tr[x].l=merge(tr[x].l,tr[y].l);
tr[x].r=merge(tr[x].r,tr[y].r);
tr[x].siz=tr[tr[x].l].siz+tr[tr[x].r].siz;
return x;
}
2、并查集与线段树合并的结合
if(r1!=r2)
{
fa[r2]=r1;
merge(root[r1],root[r2]);
}
并查集中i的父亲置为j,i的信息就要往j上合
r1、r2对应顺序不能乱
反思一:
两种方法相对比:
1、并查集的作用
①splay合并不需要考虑并查集内的父子关系,因为splay合并2棵树时,都将代表节点转至根节点
并查集只起指示哪两个集合的作用
②线段树中并查集内部i的父亲指向j,线段树合并时,必须i所在线段树合并到j所在线段树
因为合并哪两颗线段树需要root[i]指定 ,而i的确定是通过并查集找的
如果线段树合并j合向i,但i在并查集中的父亲指向j,
那么再来合并i和k时,合并的两棵树原来的是j和k,原来的j仍然只有j的信息
为什么splay不需要呢?
因为splay合并之前,先将2棵树的代表节点转至根节点
如果j合向i,那么k无论合向j还是i,都会将j或i转至根节点,而i或j是真的在同一棵树上
一句话解释就是 splay 中节点标号就是节点标号,线段树中对节点进行了重新标号,所以需要依靠root数组确定线段树中的节点对应题目中的哪个节点
2、合并方式
① splay 小的合并到大的上,因为合并是暴力把一颗splay上的点一个一个插到另一颗splay上,所以插得点越少越好
② 形态相同(权且这样认为)2颗线段树直接合并
3、合并之后,对被合并的树的处理
①splay 把被合并的那一颗树的所有信息都清零,因为节点编号不变,节点到新的树上信息仍存在这个编号下
② 线段树不用管,因为这棵树被合并后相当于消失了,并查集的父子关系使他不可能被找到
反思二:
为什么不是主席树
简单理解:主席树维护链上信息,线段树维护一坨点的信息
联想SDOI 2013 森林 http://www.cnblogs.com/TheRoadToTheGold/p/6524904.html
森林选用的是主席树,因为他要确定两点间路径,从这条链上找第k值
今天一天只做了1道题,2个原因
① 以前学过的数据结构没有理解透
② 本题卡cin!!,单个字符输入,为避空格可以直接按字符串输入啊
对拍数据生成代码:
#include<cstdio> #include<ctime> #include<cstdlib> using namespace std; bool v[100]; int main() { freopen("data","w",stdout); srand(time(0)); int n=rand()%10+1,m=rand()%n; n+=2; printf("%d %d\\n",n,m); for(int i=1;i<=n;i++) { int a=rand()%n+1; while(v[a]) a=rand()%n+1; v[a]=true; printf("%d ",a); } printf("\\n"); for(int i=1;i<=m;i++) { int a=rand()%n+1,b=rand()%n+1; while(a==b) b=rand()%n+1; printf("%d %d\\n",a,b); } int p=rand()%10+5; printf("%d\\n",p); for(int i=1;i<=p;i++) { if(i<=p/2) printf("B "); else printf("Q "); int a=rand()%n+1,b=rand()%n+1; while(a==b) b=rand()%n+1; printf("%d %d\\n",a,b); } }
以上是关于HNOI 2012 永无乡的主要内容,如果未能解决你的问题,请参考以下文章
bzoj2733: [HNOI2012]永无乡(splay)