题意:
给你一棵树,再给你一个排列 a n a_n an,多次询问,每次询问 ( l , r , x ) (l,r,x) (l,r,x),问以x为根的子树上是否存在一个结点编号为排列里 a l a_l al到 a r a_r ar中的数。
分析:
由于题目是跟子树有关,每次询问都是问一个子树的信息,所以可以考虑一种常见的操作,就是利用dfs加上时间戳,给结点重新赋值,重新得到一个数组,记录每个结点开始的位置和子树结束的位置,就可以让分散的子树的节点序号,转而成为新的数组中连续的一段。(好像是什么dfn序来着)
然后这题我看了赛后的题解看半天没看懂,最后从排行榜里面发现一个大佬的代码简洁易懂,而且思想更加常见,下面我就来讲一下他的做法。
首先对所有的询问离线,在重新建立好这个排列后,考虑将问题拆分,询问
(
l
,
r
,
x
)
(l,r,x)
(l,r,x),可以拆分为
x
x
x的子树中具有
(
1
,
r
)
(1,r)
(1,r)中序号个数,减去
(
1
,
l
−
1
)
(1,l-1)
(1,l−1)中的序号个数,这个也是扫描线的常见操作。然后我们遍历题目中的排列
a
n
a_n
an,每次在树状数组上
a
[
i
]
a[i]
a[i]处插入1,然后再看看有没有以
i
i
i作为询问的子询问,如果有,则将
a
n
s
[
i
d
]
ans[id]
ans[id]加上或者减去(取决于查询是正查询还是负查询)树状数组中,结点
x
x
x对应的那一段dfn序连续的值。(这一段非常绕,建议自己画画图再多想想)
最后
a
n
s
[
i
]
ans[i]
ans[i]的值表示第
i
i
i个询问对应的结点中,在这次询问对应的
(
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;
}