P5064 [Ynoi Easy Round 2014] 等这场战争结束之后 题解
题目描述
给定 \(n\) 个点的图,初始没有边,点有点权 \(a_i\) 。
接下来 \(m\) 次操作:
1 x y:在 \(x\) 和 \(y\) 之间添加一条双向边。2 x:回退到第 \(x\) 次操作后的图。3 x y:查询 \(x\) 所在连通块第 \(y\) 小权值,若不存在则输出-1。
数据范围
- \(1\le n,m\le 10^5,0\le a_i\le 10^9\) 。
- 对操作 \(1,3\) , \(1\le x,y\le n\) 。
- 对操作 \(2\) ,若当前为第 \(i\) 次操作,保证 \(0\le x\le i-1\) 。
时间限制 \(\texttt{500ms}\) ,空间限制 \(\texttt{20MB}\) 。
分析
双向边查询连通块,并查集肯定用得上。
对于回退操作,下面是固定的解决套路,建议积累下来:
- 如果只回退到上一个版本,使用可撤销并查集。
- 如果每次操作在之前任一版本基础上修改,使用可持久化并查集。
- 如果每次操作要么回退到之前任一版本,要么在最新版本上修改(即本题情形),建操作树后使用可撤销并查集。
第 \(2,3\) 种情形的区别是,前者单次操作是 "回退+修改" ,后者单次操作是 "回退 or 修改" 。
这里操作树的本质是改变了操作的生效顺序。按照输入顺序(即时间序),我们需要处理 "回退到任一版本" 操作;但是在操作树中,我们把当前操作 "嫁接" 到回退后的版本之下,相当于新开了一个分支。在遍历完当前分支后只需回退到操作树的父节点,使用可撤销并查集即可。
代码实现时,如果是第 \(2\) 类操作,则操作树中它的父节点为 \(x\) ,否则为 \(i-1\) 。
至此,我们通过操作树,将第 \(2\) 类操作转化为 "回退到上一个版本"。
查询集合第 \(k\) 小,常见做法要么二分,要么分块。
如果选择二分,将 \(\le mid\) 的权值赋成 \(1\) ,其余赋成 \(0\) ,我们需要判断 \(x\) 所在连通块权值和是否 \(\ge y\) 。
然后问题来了。虽然对单个 \(mid\) ,我们可以轻松维护每个连通块权值和,但是当 \(mid\) 变化的时候,我们不可能对每个 \(mid\) 都维护一遍,而且没有办法整体二分,因此这条路行不通。
接下来考虑分块。离散化后对值域分块,记块长为 \(B\) ,先判断答案落在哪个块内。
这一部分是容易的,对每个点用一个长为 \(\frac nB\) 的数组维护每个点所在连通块中有多少个数,加边删边动态更新,回答询问时扫一遍数组即可。
接下来只需逐一判断值域中的每个数(不超过 \(B\) 个)在连通块中出现了多少次,一直累加到刚好超过 \(y\) 即为答案。
不妨在离散化时不执行去重操作,这样值域中的每个数和图中每个点一一对应。我们只需判断这 \(\mathcal O(B)\) 个点是否和 \(x\) 在同一连通块中,刚好可以通过可撤销并查集解决。
时间复杂度 \(\mathcal O((n+m)\frac nB+mB\log n)\) ,取 \(B=\sqrt n\) ,但是很不幸, \(\mathcal O(n\sqrt n)\) 的空间根本吃不消。
发现问题出在遍历整块时开不下 \(\mathcal O(n\cdot\frac nB)\) 的数组,那就转换思路,对操作树跑 \(\frac nB\) 轮 dfs ,每次只统计一个块中的元素个数。
注意到 dfs 主体由于用到可撤销并查集所以自带一只 \(\log\) ,但是由于操作树是静态的,因此每轮加边和删边的顺序固定。对于操作 \(1,3\) ,预处理并查集中 \(x,y\) 的根节点,这样可以把 \(\mathcal O(m\cdot\frac nB\cdot\log n)\) 转化为 \(\mathcal O(m\cdot\frac nB+m\log n)\) ,降低时间复杂度。
时间复杂度 \(\mathcal O(m\cdot\frac nB+mB\log n)\) ,空间复杂度 \(\mathcal O(n)\) 。
取 \(B=\sqrt{\frac n{\log n}}\) 则时间复杂度为 \(\mathcal O(m\sqrt{n\log n})\) ,但实际上由于 dfs 主体常数较大,第 \(3\) 类操作卡不满 \(m\) 个而且可撤销并查集常数很小,需要适当调大块长,实测取 \(B=2000\) 可以无压力通过。
#include<bits/stdc++.h>
#define fi first
#define se second
#define mp make_pair
#define pii pair<int,int>
using namespace std;
const int B=2000,maxn=1e5+5;
int m,n,top;
pii p[maxn];
vector<int> g[maxn];
int f[maxn],st[maxn],sz[maxn];
struct node
{
int op,x,y,u,v,res;
}o[maxn];
int find(int x)
{
return f[x]==x?x:find(f[x]);
}
void roll(int lim)
{
while(top>lim)
{
int u=st[top--],v=f[u];
f[u]=u,sz[v]-=sz[u];
}
}
void dfs1(int k)
{
int now=top;
if(o[k].op==1)
{
int u=find(o[k].x),v=find(o[k].y);
if(u!=v)
{
if(sz[u]>sz[v]) swap(u,v);
f[u]=v,sz[v]+=sz[u],st[++top]=u;
o[k].u=u,o[k].v=v;
}
}
if(o[k].op==3) o[k].u=find(o[k].x),o[k].res=-1;
for(auto v:g[k]) dfs1(v);
roll(now);
}
void dfs2(int k,int id)
{
int now=top;
if(o[k].op==1&&o[k].u)
{
int u=o[k].u,v=o[k].v;
f[u]=v,sz[v]+=sz[u],st[++top]=u;
}
if(o[k].op==3)
{
if(id&&o[k].res==-1)
{///用 n/B 轮操作判断答案落在哪一块,如果 y 过大则 o[k].res=-1
if(sz[o[k].u]>=o[k].y) o[k].res=id;
else o[k].y-=sz[o[k].u];
}
if(!id&&o[k].res!=-1)
{///最后扫描块内元素确定答案
for(int i=(o[k].res-1)*B+1;;i++)
{
assert(i<=n);
if(!(o[k].y-=(find(p[i].se)==o[k].u)))
{
o[k].res=p[i].fi;
break;
}
}
}
}
for(auto v:g[k]) dfs2(v,id);
roll(now);
}
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++) scanf("%d",&p[i].fi),p[i].se=i;
sort(p+1,p+n+1);
for(int i=1;i<=m;i++)
{
scanf("%d%d",&o[i].op,&o[i].x);
if(o[i].op==2) g[o[i].x].push_back(i);
else scanf("%d",&o[i].y),g[i-1].push_back(i);
}
for(int j=1;j<=n;j++) f[j]=j,sz[j]=1;
dfs1(0);
for(int i=1;i<=(n-1)/B+1;i++)
{
for(int j=1;j<=n;j++) f[j]=j,sz[p[j].se]=(i-1)*B+1<=j&&j<=i*B;
dfs2(0,i);
}
dfs2(0,0);
for(int i=1;i<=m;i++) if(o[i].op==3) printf("%d\n",o[i].res);
return 0;
}
本文来自博客园,作者:peiwenjun,转载请注明原文链接:https://www.cnblogs.com/peiwenjun/p/19061602
浙公网安备 33010602011771号