G.Unusual Entertainment题解(codeforces 909 div.3)【dsu on tree/树上启发式合并】

题意:

给你一棵树,再给你一个排列 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,l1)中的序号个数,这个也是扫描线的常见操作。然后我们遍历题目中的排列 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;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
.equals(Obje​​ct)方法在Java中用于比较两个对象是否相等。它检查两个对象的内容是否相同而不是引用是否相同。通常,我们可以使用"=="运算符来比较基本数据类型的值或判断两个对象引用是否相等。但是,对于比较复杂的对象,比如自定义的类对象,它们可能具有相同的属性值,但却不被认为是相等的,因为它们不是同一个对象的实例。 .equals(Obje​​ct)方法为我们提供了一种自定义比较两个对象内容的方式。这个方法是从Object类继承而来的,因此在所有的类中都可用。某些类,比如String类、Integer类等,已经重写了.equals(Obje​​ct)方法,以便实现比较它们的内容。但是,对于自定义的类,如果不重写.equals(Obje​​ct)方法的话,将继承Object类的默认实现,即比较两个对象的引用是否相等。 与"=="运算符相比,.equals(Obje​​ct)方法的作用更灵活、更具体。它可以根据具体的比较规则来决定两个对象是否相等。但是,由于.equals(Obje​​ct)方法的默认实现比较对象的引用,所以在自定义类中使用.equals(Obje​​ct)方法时,需要注意重写该方法以实现我们自己的比较逻辑。 综上所述,.equals(Obje​​ct)方法在Java中是相对不寻常的,因为大部分时候我们可以使用"=="运算符来比较对象引用是否相等。然而,在比较复杂对象内容时,.equals(Obje​​ct)方法提供了一种更具体、更灵活的比较方式。这是Java中为了满足不同需求而提供的一种对象比较工具。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值