题意:
给你一棵树,再给你一个排列ana_nan,多次询问,每次询问(l,r,x)(l,r,x)(l,r,x),问以x为根的子树上是否存在一个结点编号为排列里ala_lal到ara_rar中的数。
分析:
由于题目是跟子树有关,每次询问都是问一个子树的信息,所以可以考虑一种常见的操作,就是利用dfs加上时间戳,给结点重新赋值,重新得到一个数组,记录每个结点开始的位置和子树结束的位置,就可以让分散的子树的节点序号,转而成为新的数组中连续的一段。(好像是什么dfn序来着)
然后这题我看了赛后的题解看半天没看懂,最后从排行榜里面发现一个大佬的代码简洁易懂,而且思想更加常见,下面我就来讲一下他的做法。
首先对所有的询问离线,在重新建立好这个排列后,考虑将问题拆分,询问(l,r,x)(l,r,x)(l,r,x),可以拆分为xxx的子树中具有(1,r)(1,r)(1,r)中序号个数,减去(1,l−1)(1,l-1)(1,l−1)中的序号个数,这个也是扫描线的常见操作。然后我们遍历题目中的排列ana_nan,每次在树状数组上a[i]a[i]a[i]处插入1,然后再看看有没有以iii作为询问的子询问,如果有,则将ans[id]ans[id]ans[id]加上或者减去(取决于查询是正查询还是负查询)树状数组中,结点xxx对应的那一段dfn序连续的值。(这一段非常绕,建议自己画画图再多想想)
最后ans[i]ans[i]ans[i]的值表示第iii个询问对应的结点中,在这次询问对应的(l,r)(l,r)(l,r)范围的数的个数,如果为0,代表没有输出NO,否则输出YES。
下面是代码环节
#include<bits/stdc++.h>
//#define int long long
//#pragma GCC optimize(3,"Ofast","inline")//bitset配合用
#define inf 0x3f3f3f3f
#define ll long long
#define pii pair<int,int>
#define db double
using namespace std;
const int maxn=1e5+10;
const int mod=998244353;
vector<int>G[maxn];
vector<pii>q1[maxn];
vector<pii>q2[maxn];
int st[maxn],ed[maxn],cnt,mp[maxn],tree[maxn],ans[maxn];
int lowbit(int x){return x&(-x);}
void update(int i,int x){
for(int pos=i;pos<maxn;pos+=lowbit(pos))
tree[pos]+=x;
}
int ask(int i){
int res=0;
for(int pos=i;pos;pos-=lowbit(pos))
res+=tree[pos];
return res;
}
void dfs(int u,int f){
st[u]=++cnt;
for(auto v:G[u]){
if(v!=f) dfs(v,u);
}
ed[u]=cnt;
}
void solve(){
int n,q;cin>>n>>q;
for(int i=1;i<=n;i++) G[i].clear(),q1[i].clear(),q2[i].clear();
for(int i=1;i<=q;i++) ans[i]=0;
for(int i=1;i<n;i++){
int u,v;cin>>u>>v;
G[u].push_back(v);
G[v].push_back(u);
}
dfs(1,1);
for(int i=1;i<=n;i++) cin>>mp[i];
for(int i=1;i<=q;i++){
int l,r,x;cin>>l>>r>>x;
q2[r].push_back(pii(i,x));
q1[l-1].push_back(pii(i,x));
}
for(int i=1;i<=n;i++){
update(st[mp[i]],1);
for(auto now:q1[i]){
ans[now.first]-=ask(ed[now.second])-ask(st[now.second]-1);
}
for(auto now:q2[i]){
ans[now.first]+=ask(ed[now.second])-ask(st[now.second]-1);
}
}
for(int i=1;i<=q;i++)
if(ans[i]) cout<<"YES"<<endl;
else cout<<"NO"<<endl;
cout<<endl;
}
signed main(){
ios::sync_with_stdio(false);
cin.tie(0);cout.tie(0);
int t;cin>>t;
while(t--) solve();
//system("pause");
return 0;
}
补充
今天在学树上启发式合并(dsu on tree)的时候想到这题也可以用书上启发式合并来做,而且用两种做法。
第一种,按大小合并
这种其实是一开始的暴力想法,直接每个结点维护一个数组存储子树所有的序号,然后dfs一下,儿子给父节点时候判断一下谁的数组size大,把小的复制给大的,空间居然能过,还是挺不可思议的。具体的话,由于查询是区间,所以搞一个映射,将结点值跟序号映射一下,每次加入的不是编号,而是题目给的排列顺序下标,就可以在查询时候二分查找,总代码如下:
#include<bits/stdc++.h>
//#define int long long
//#pragma GCC optimize(3,"Ofast","inline")//bitset配合用
#define inf 0x3f3f3f3f
#define ll long long
#define pii pair<int,int>
#define db double
using namespace std;
const int maxn=1e5+10;
const int mod=998244353;
vector<int>G[maxn];
int id[maxn],ans[maxn];
set<int>s[maxn];
struct Node{
int l,r,id;
};
vector<Node>query[maxn];
void dfs(int u,int f){
s[u].insert(id[u]);
if(u==10){
int tem=0;
tem++;
}
for(auto v:G[u]){
if(v==f) continue;
dfs(v,u);
if(s[v].size()<s[u].size()){
s[u].insert(s[v].begin(),s[v].end());
}
else{
s[v].insert(s[u].begin(),s[u].end());
swap(s[u],s[v]);
}
}
for(auto it:query[u]){
int l=it.l,r=it.r;
if(s[u].lower_bound(l)==s[u].end()||*s[u].lower_bound(l)>r)
ans[it.id]=0;
else
ans[it.id]=1;
}
}
void solve(){
int n,q;cin>>n>>q;
for(int i=1;i<=n;i++) G[i].clear(),s[i].clear(),query[i].clear();
for(int i=1;i<n;i++){
int u,v;cin>>u>>v;
G[u].push_back(v);
G[v].push_back(u);
}
for(int i=1;i<=n;i++){
int tem;cin>>tem;
id[tem]=i;
}
for(int i=1;i<=q;i++){
int l,r,x;cin>>l>>r>>x;
query[x].push_back(Node{l,r,i});
}
dfs(1,1);
for(int i=1;i<=q;i++){
if(ans[i]) cout<<"YES"<<endl;
else cout<<"NO"<<endl;
}
cout<<endl;
}
signed main(){
ios::sync_with_stdio(false);
cin.tie(0);cout.tie(0);
int t;cin>>t;
while(t--) solve();
//system("pause");
return 0;
}
第二种
也就是今天刚学的标准dsu on tree的模板改编,总的只维护一个set表示当前查询到的结点的子树所有编号。然后add操作就是将编号映射的下标存入set,删点时候,直接清空set即可,比模板题还要简单,代码见下:
#include<bits/stdc++.h>
#define int long long
//#pragma GCC optimize(3,"Ofast","inline")//bitset配合用
#define inf 0x3f3f3f3f
#define ll long long
#define pii pair<int,int>
#define db double
using namespace std;
const int maxn=1e5+10;
const int mod=998244353;
set<int>s;
int dfn,siz[maxn],son[maxn],L[maxn],R[maxn],mp[maxn],ans[maxn],id[maxn];
vector<int>G[maxn];
struct query{
int l,r,id;
};
vector<query>q[maxn];
void dfs1(int u,int f){
siz[u]=1;son[u]=0;
dfn++;L[u]=dfn;mp[dfn]=u;
for(auto v:G[u]){
if(v==f) continue;
dfs1(v,u);
siz[u]+=siz[v];
if(siz[v]>siz[son[u]]) son[u]=v;
}
R[u]=dfn;
}
void dfs2(int u,int f,bool keep){
for(auto v:G[u])
if(v!=f&&v!=son[u])
dfs2(v,u,0);//先遍历轻儿子,计算答案,不保留贡献
if(son[u]) dfs2(son[u],u,1);//再遍历重儿子,保留贡献
//然后加入u单点的贡献
s.insert(id[u]);
for(auto v:G[u])//第二次遍历轻儿子,保留贡献
if(v!=f&&v!=son[u])
for(int i=L[v];i<=R[v];i++)
s.insert(id[mp[i]]);
int tem=0;
for(auto it:q[u]){
int l=it.l,r=it.r;
if(s.lower_bound(l)==s.end()||*s.lower_bound(l)>r)
ans[it.id]=0;
else
ans[it.id]=1;
}
if(!keep){
s.clear();
}
}
void solve(){
s.clear();
int n,m;cin>>n>>m;
for(int i=1;i<=n;i++) G[i].clear(),q[i].clear();
for(int i=1;i<n;i++){
int u,v;cin>>u>>v;
G[u].push_back(v);
G[v].push_back(u);
}
for(int i=1;i<=n;i++){
int tem;cin>>tem;
id[tem]=i;
}
for(int i=1;i<=m;i++){
int l,r,x;cin>>l>>r>>x;
q[x].push_back(query{l,r,i});
}
dfs1(1,1);
dfs2(1,1,1);
for(int i=1;i<=m;i++)
if(ans[i]) cout<<"YES"<<endl;
else cout<<"NO"<<endl;
cout<<endl;
}
signed main(){
ios::sync_with_stdio(false);
cin.tie(0);cout.tie(0);
int t;cin>>t;
while(t--) solve();
//system("pause");
return 0;
}
文章讲述了如何使用深度优先搜索(DFS)和树状数组解决一个关于在子树范围内统计特定排列元素的问题,通过离线处理询问、拆分问题并借助启发式合并的方法,简化了复杂度并实现高效求解。
278





