前言
原本以为这个也就在重链剖分里用一下,没想到这么厉害!
一、原理
具体原理详见重链剖分的复杂度分析()
当没有修改操作,且可以通过遍历子树建立的信息得到答案时,就可以考虑树上启发式合并。
首先,在处理树上启发式合并问题中,通常考虑设置全局的信息,然后在递归过程中实时修改。之后,考虑定义递归函数 dfs(u,keep) 为当前来到 u 节点,keep 表示是否保留子树 u 对信息的贡献。之后先遍历所有轻儿子的子树,结束时消除对信息的贡献,即 dfs(v,0)。之后再遍历重儿子的子树,结束时保留对信息的贡献,即 dfs(v,1)。
之后考察单个节点 u,对信息进行贡献,再遍历所有轻儿子的子树,重新对信息进行贡献。这里是因为在遍历每个轻儿子时,需要让各个儿子之间互不干扰。在最后遍历完重儿子后,把 u 的信息合并进去,再贡献轻儿子得到 u 的答案。
复杂度分析方面,前面的递归计算贡献没啥好说的,就是 O(n),重点是后面重新贡献和清除部分。由于只有当一个子树是轻儿子时,才会重新遍历消除这棵子树的贡献。那么此时必然就有一个重儿子,即大小大于等于当前子树的子树。也就是说,只有当存在一个比当前集合更大的集合时,才会重新遍历贡献一遍。所以每次重新贡献时,当前集合的大小都会至少翻一倍,所以对于每个节点,其重新贡献被遍历的次数就是 logn 的。取消操作类似。
太抽象了,还是去题目里看吧()
二、题目
1.树上数颜色
cpp
#include <bits/stdc++.h>
using namespace std;
/* /\_/\
* (= ._.)
* / > \>
*/
/*
*想好再写
*注意审题 注意特判
*不要红温 不要急躁 耐心一点
*WA了不要立马觉得是思路不对 先耐心找反例
*/
#define endl '\n'
#define dbg(x) cout<<#x<<endl;cout<<x<<endl;
#define vdbg(a) cout<<#a<<endl;for(auto x:a)cout<<x<<" ";cout<<endl;
#define YES cout<<"YES"<<endl;return ;
#define Yes cout<<"Yes"<<endl;return ;
#define NO cout<<"NO"<<endl;return ;
#define No cout<<"No"<<endl;return ;
#define popcount __builtin_popcount
using ll=long long;
using i128=__int128;
using ld=long double;
using pii=pair<int,int>;
using pll=pair<ll,ll>;
const int INF=1e9;
const ll INFLL=1e18;
const int dx[]={-1,1,0,0};
const int dy[]={0,0,-1,1};
const int ddx[]={-2,-1,1,2,2,1,-1,-2};
const int ddy[]={1,2,2,1,-1,-2,-2,-1};
template<typename T>
struct Heavy_Light_Decomposition{
int n;
vector<int>fat;
vector<int>dep;
vector<int>siz;
vector<int>son;
vector<int>top;
vector<int>dfn;
vector<int>seg;
int cnt;
Heavy_Light_Decomposition(){}
//_n: number of node, not size!!
Heavy_Light_Decomposition(int _n,const vector<vector<int>>&g,int root=1){
n=_n;
fat.assign(n+1,0);
dep.assign(n+1,0);
siz.assign(n+1,0);
son.assign(n+1,0);
top.assign(n+1,0);
dfn.assign(n+1,0);
seg.assign(n+1,0);
cnt=0;
dfs1(root,0,g);
}
void dfs1(int u,int fa,const vector<vector<int>>&g){
fat[u]=fa;
dep[u]=dep[fa]+1;
siz[u]=1;
for(auto v:g[u]){
if(v!=fa){
dfs1(v,u,g);
siz[u]+=siz[v];
if(siz[son[u]]<siz[v]){
son[u]=v;
}
}
}
}
};
void solve()
{
int n;
cin>>n;
vector<vector<int>>g(n+1);
for(int i=1,u,v;i<=n-1;i++)
{
cin>>u>>v;
g[u].push_back(v);
g[v].push_back(u);
}
vector<int>a(n+1);
for(int i=1;i<=n;i++)
{
cin>>a[i];
}
Heavy_Light_Decomposition<int>hld(n,g);
vector<int>cnts(n+1);
int kind=0;
vector<int>ans(n+1);
auto add=[&](auto &&self,int u)->void
{
if(++cnts[a[u]]==1)
{
kind++;
}
for(auto v:g[u])
{
if(v!=hld.fat[u])
{
self(self,v);
}
}
};
auto erase=[&](auto &&self,int u)->void
{
if(--cnts[a[u]]==0)
{
kind--;
}
for(auto v:g[u])
{
if(v!=hld.fat[u])
{
self(self,v);
}
}
};
auto dfs=[&](auto &&self,int u,int keep)->void
{
for(auto v:g[u])
{
if(v!=hld.fat[u]&&v!=hld.son[u])
{
self(self,v,0);
}
}
if(hld.son[u]!=0)
{
self(self,hld.son[u],1);
}
if(++cnts[a[u]]==1)
{
kind++;
}
for(auto v:g[u])
{
if(v!=hld.fat[u]&&v!=hld.son[u])
{
add(add,v);
}
}
ans[u]=kind;
if(keep==0)
{
erase(erase,u);
}
};
dfs(dfs,1,0);
int q;
cin>>q;
int x;
while(q--)
{
cin>>x;
cout<<ans[x]<<endl;
}
}
void init()
{
}
signed main()
{
ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
int t=1;
//cin>>t;
init();
while(t--)
{
solve();
}
return 0;
}
在这个题里,考虑维护一个全局词频数组 cnts 和种类数 kind,这两个信息在来到每个节点时是可以 O(1) 更新好的。之后,当来到节点 u 时,先去所有轻儿子收集信息。由于每次遍历后都取消贡献,所以在遍历完后,此时的两个信息就都是空的状态。此时再去重儿子收集信息,回来的时候直接把当前节点 u 的贡献计算上,然后再遍历所有轻儿子重新计算贡献,记录此时 u 的答案。在返回时,若此时 u 是轻儿子,那么就再回去遍历一遍 u 整棵子树,把信息清空即可。
若将重新贡献和清空信息看作集合的合并,由于这个操作只会对轻儿子做,所以每做一次就相当于将当前集合合并到更大的集合中,所以合并的次数就是 logn 的。
2.颜色平衡树
cpp
#include <bits/stdc++.h>
using namespace std;
/* /\_/\
* (= ._.)
* / > \>
*/
/*
*想好再写
*注意审题 注意特判
*不要红温 不要急躁 耐心一点
*WA了不要立马觉得是思路不对 先耐心找反例
*/
#define endl '\n'
#define dbg(x) cout<<#x<<endl;cout<<x<<endl;
#define vdbg(a) cout<<#a<<endl;for(auto x:a)cout<<x<<" ";cout<<endl;
#define YES cout<<"YES"<<endl;return ;
#define Yes cout<<"Yes"<<endl;return ;
#define NO cout<<"NO"<<endl;return ;
#define No cout<<"No"<<endl;return ;
#define popcount __builtin_popcount
using ll=long long;
using i128=__int128;
using ld=long double;
using pii=pair<int,int>;
using pll=pair<ll,ll>;
const int INF=1e9;
const ll INFLL=1e18;
const int dx[]={-1,1,0,0};
const int dy[]={0,0,-1,1};
const int ddx[]={-2,-1,1,2,2,1,-1,-2};
const int ddy[]={1,2,2,1,-1,-2,-2,-1};
template<typename T>
struct Heavy_Light_Decomposition{
int n;
vector<int>fat;
vector<int>dep;
vector<int>siz;
vector<int>son;
vector<int>top;
vector<int>dfn;
vector<int>seg;
int cnt;
Heavy_Light_Decomposition(){}
//_n: number of node, not size!!
Heavy_Light_Decomposition(int _n,const vector<vector<int>>&g,int root=1){
n=_n;
fat.assign(n+1,0);
dep.assign(n+1,0);
siz.assign(n+1,0);
son.assign(n+1,0);
top.assign(n+1,0);
dfn.assign(n+1,0);
seg.assign(n+1,0);
cnt=0;
dfs1(root,0,g);
}
void dfs1(int u,int fa,const vector<vector<int>>&g){
fat[u]=fa;
dep[u]=dep[fa]+1;
siz[u]=1;
for(auto v:g[u]){
if(v!=fa){
dfs1(v,u,g);
siz[u]+=siz[v];
if(siz[son[u]]<siz[v]){
son[u]=v;
}
}
}
}
};
void solve()
{
int n;
cin>>n;
vector<vector<int>>g(n+1);
vector<int>a(n+1);
for(int i=1,fa;i<=n;i++)
{
cin>>a[i]>>fa;
g[i].push_back(fa);
g[fa].push_back(i);
}
Heavy_Light_Decomposition<int>hld(n,g);
vector<int>cnts(n+1);
vector<int>kinds(n+1);
vector<int>ans(n+1);
auto add=[&](auto &&self,int u)->void
{
kinds[cnts[a[u]]]--;
cnts[a[u]]++;
kinds[cnts[a[u]]]++;
for(auto v:g[u])
{
if(v!=hld.fat[u])
{
self(self,v);
}
}
};
auto erase=[&](auto &&self,int u)->void
{
kinds[cnts[a[u]]]--;
cnts[a[u]]--;
kinds[cnts[a[u]]]++;
for(auto v:g[u])
{
if(v!=hld.fat[u])
{
self(self,v);
}
}
};
auto dfs=[&](auto &&self,int u,int keep)->void
{
for(auto v:g[u])
{
if(v!=hld.fat[u]&&v!=hld.son[u])
{
self(self,v,0);
}
}
if(hld.son[u]!=0)
{
self(self,hld.son[u],1);
}
kinds[cnts[a[u]]]--;
cnts[a[u]]++;
kinds[cnts[a[u]]]++;
for(auto v:g[u])
{
if(v!=hld.fat[u]&&v!=hld.son[u])
{
add(add,v);
}
}
ans[u]=(hld.siz[u]==kinds[cnts[a[u]]]*cnts[a[u]]);
if(keep==0)
{
erase(erase,u);
}
};
dfs(dfs,1,0);
int sum=0;
for(int i=1;i<=n;i++)
{
sum+=ans[i];
}
cout<<sum<<endl;
}
void init()
{
}
signed main()
{
ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
int t=1;
//cin>>t;
init();
while(t--)
{
solve();
}
return 0;
}
考虑在上一个题的基础上,多维护一个 kinds 数组表示出现次数为 i 的颜色有几种,这个同样是可以在 O(1) 时间内完成增加和删除的。之后还是去整合答案,对于当前子树的根节点 u,先把 u 的颜色的词频取出 x,再去查词频为 x 的颜色种类 y。若 x 乘以 y 恰好等于当前子树的大小,就说明当前子树内出现的颜色的种类数都是相等的。
3.E. Lomsat gelral
怎么感觉这个信息的维护方式和莫队很像(?
cpp
#include <bits/stdc++.h>
using namespace std;
/* /\_/\
* (= ._.)
* / > \>
*/
/*
*想好再写
*注意审题 注意特判
*不要红温 不要急躁 耐心一点
*WA了不要立马觉得是思路不对 先耐心找反例
*/
#define endl '\n'
#define dbg(x) cout<<#x<<endl;cout<<x<<endl;
#define vdbg(a) cout<<#a<<endl;for(auto x:a)cout<<x<<" ";cout<<endl;
#define YES cout<<"YES"<<endl;return ;
#define Yes cout<<"Yes"<<endl;return ;
#define NO cout<<"NO"<<endl;return ;
#define No cout<<"No"<<endl;return ;
#define popcount __builtin_popcount
using ll=long long;
using i128=__int128;
using ld=long double;
using pii=pair<int,int>;
using pll=pair<ll,ll>;
const int INF=1e9;
const ll INFLL=1e18;
const int dx[]={-1,1,0,0};
const int dy[]={0,0,-1,1};
const int ddx[]={-2,-1,1,2,2,1,-1,-2};
const int ddy[]={1,2,2,1,-1,-2,-2,-1};
template<typename T>
struct Heavy_Light_Decomposition{
int n;
vector<int>fat;
vector<int>dep;
vector<int>siz;
vector<int>son;
vector<int>top;
vector<int>dfn;
vector<int>seg;
int cnt;
Heavy_Light_Decomposition(){}
//_n: number of node, not size!!
Heavy_Light_Decomposition(int _n,const vector<vector<int>>&g,int root=1){
n=_n;
fat.assign(n+1,0);
dep.assign(n+1,0);
siz.assign(n+1,0);
son.assign(n+1,0);
top.assign(n+1,0);
dfn.assign(n+1,0);
seg.assign(n+1,0);
cnt=0;
dfs1(root,0,g);
}
void dfs1(int u,int fa,const vector<vector<int>>&g){
fat[u]=fa;
dep[u]=dep[fa]+1;
siz[u]=1;
for(auto v:g[u]){
if(v!=fa){
dfs1(v,u,g);
siz[u]+=siz[v];
if(siz[son[u]]<siz[v]){
son[u]=v;
}
}
}
}
};
void solve()
{
int n;
cin>>n;
vector<int>a(n+1);
for(int i=1;i<=n;i++)
{
cin>>a[i];
}
vector<vector<int>>g(n+1);
for(int i=1,u,v;i<=n-1;i++)
{
cin>>u>>v;
g[u].push_back(v);
g[v].push_back(u);
}
Heavy_Light_Decomposition<int>hld(n,g);
vector<int>cnts(n+1);
int maxcnt=0;
ll sum=0;
auto add=[&](auto &&self,int u)->void
{
cnts[a[u]]++;
if(cnts[a[u]]==maxcnt)
{
sum+=a[u];
}
else if(cnts[a[u]]>maxcnt)
{
maxcnt=cnts[a[u]];
sum=a[u];
}
for(auto v:g[u])
{
if(v!=hld.fat[u])
{
self(self,v);
}
}
};
auto erase=[&](auto &&self,int u)->void
{
cnts[a[u]]=0;
maxcnt=0;
for(auto v:g[u])
{
if(v!=hld.fat[u])
{
self(self,v);
}
}
};
vector<ll>ans(n+1);
auto dfs=[&](auto &&self,int u,int keep)->void
{
for(auto v:g[u])
{
if(v!=hld.fat[u]&&v!=hld.son[u])
{
self(self,v,0);
}
}
if(hld.son[u]!=0)
{
self(self,hld.son[u],1);
}
cnts[a[u]]++;
if(cnts[a[u]]==maxcnt)
{
sum+=a[u];
}
else if(cnts[a[u]]>maxcnt)
{
maxcnt=cnts[a[u]];
sum=a[u];
}
for(auto v:g[u])
{
if(v!=hld.fat[u]&&v!=hld.son[u])
{
add(add,v);
}
}
ans[u]=sum;
if(keep==0)
{
erase(erase,u);
}
};
dfs(dfs,1,0);
for(int i=1;i<=n;i++)
{
cout<<ans[i]<<" ";
}
cout<<endl;
}
void init()
{
}
signed main()
{
ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
int t=1;
//cin>>t;
init();
while(t--)
{
solve();
}
return 0;
}
对于出现次数最多的颜色累加和,考虑在如何 O(1) 更新这个信息。那么就是在词频数组 cnts 的基础上,再维护一个当前出现的最大词频 maxcnt。那么在加入当前节点 u 的颜色时,若增加词频后小于最大词频,那么累加和信息肯定不变。若恰好等于最大词频,那么就可以直接往累加和里增加了。而若超过了最大词频,就直接重置两个信息即可。
重点是删除方法。由于 cnts 和 maxcnt 维护的是当前子树的信息,那么在遍历这个子树清除后,这两个信息必然会全被清成 0。那么若此时发现每次删除的信息不好维护,就可以直接将碰到节点的颜色的词频重设成 0,反正最后都会被清成 0。
4.E. Blood Cousins Return
cpp
#include <bits/stdc++.h>
using namespace std;
/* /\_/\
* (= ._.)
* / > \>
*/
/*
*想好再写
*注意审题 注意特判
*不要红温 不要急躁 耐心一点
*WA了不要立马觉得是思路不对 先耐心找反例
*/
#define endl '\n'
#define dbg(x) cout<<#x<<endl;cout<<x<<endl;
#define vdbg(a) cout<<#a<<endl;for(auto x:a)cout<<x<<" ";cout<<endl;
#define YES cout<<"YES"<<endl;return ;
#define Yes cout<<"Yes"<<endl;return ;
#define NO cout<<"NO"<<endl;return ;
#define No cout<<"No"<<endl;return ;
#define popcount __builtin_popcount
using ll=long long;
using i128=__int128;
using ld=long double;
using pii=pair<int,int>;
using pll=pair<ll,ll>;
const int INF=1e9;
const ll INFLL=1e18;
const int dx[]={-1,1,0,0};
const int dy[]={0,0,-1,1};
const int ddx[]={-2,-1,1,2,2,1,-1,-2};
const int ddy[]={1,2,2,1,-1,-2,-2,-1};
template<typename T>
struct Heavy_Light_Decomposition{
int n;
vector<int>fat;
vector<int>dep;
vector<int>siz;
vector<int>son;
vector<int>top;
vector<int>dfn;
vector<int>seg;
int cnt;
Heavy_Light_Decomposition(){}
//_n: number of node, not size!!
Heavy_Light_Decomposition(int _n,const vector<vector<int>>&g,int root=1){
n=_n;
fat.assign(n+1,0);
dep.assign(n+1,0);
siz.assign(n+1,0);
son.assign(n+1,0);
top.assign(n+1,0);
dfn.assign(n+1,0);
seg.assign(n+1,0);
cnt=0;
dfs1(root,0,g);
}
void dfs1(int u,int fa,const vector<vector<int>>&g){
fat[u]=fa;
dep[u]=dep[fa]+1;
siz[u]=1;
for(auto v:g[u]){
if(v!=fa){
dfs1(v,u,g);
siz[u]+=siz[v];
if(siz[son[u]]<siz[v]){
son[u]=v;
}
}
}
}
};
void solve()
{
int n;
cin>>n;
vector<string>s(n+1);
vector<vector<int>>g(n+1);
vector<int>head(n+1);
for(int i=1,fa;i<=n;i++)
{
cin>>s[i]>>fa;
if(fa==0)
{
head[i]=1;
}
else
{
g[i].push_back(fa);
g[fa].push_back(i);
}
}
map<string,int>mp;
for(int i=1,cnt=0;i<=n;i++)
{
if(mp.find(s[i])==mp.end())
{
mp[s[i]]=++cnt;
}
}
vector<int>a(n+1);
for(int i=1;i<=n;i++)
{
a[i]=mp[s[i]];
}
vector<int>fat(n+1);
vector<int>dep(n+1);
vector<int>siz(n+1);
vector<int>son(n+1);
auto dfs1=[&](auto &&self,int u,int fa)->void
{
fat[u]=fa;
dep[u]=dep[fa]+1;
siz[u]=1;
for(auto v:g[u]){
if(v!=fa){
self(self,v,u);
siz[u]+=siz[v];
if(siz[son[u]]<siz[v]){
son[u]=v;
}
}
}
};
for(int i=1;i<=n;i++)
{
if(head[i])
{
dfs1(dfs1,i,0);
}
}
int m;
cin>>m;
vector<vector<pii>>q(n+1);
for(int i=1,u,k;i<=m;i++)
{
cin>>u>>k;
q[u].push_back({k,i});
}
vector<set<int>>lev(n+1);
auto add=[&](auto &&self,int u)->void
{
lev[dep[u]].insert(a[u]);
for(auto v:g[u])
{
if(v!=fat[u])
{
self(self,v);
}
}
};
auto erase=[&](auto &&self,int u)->void
{
lev[dep[u]].erase(a[u]);
for(auto v:g[u])
{
if(v!=fat[u])
{
self(self,v);
}
}
};
vector<int>ans(m+1);
auto dfs2=[&](auto &&self,int u,int keep)->void
{
for(auto v:g[u])
{
if(v!=fat[u]&&v!=son[u])
{
self(self,v,0);
}
}
if(son[u]!=0)
{
self(self,son[u],1);
}
lev[dep[u]].insert(a[u]);
for(auto v:g[u])
{
if(v!=fat[u]&&v!=son[u])
{
add(add,v);
}
}
for(auto [k,id]:q[u])
{
if(dep[u]+k<=n)
{
ans[id]=lev[dep[u]+k].size();
}
}
if(keep==0)
{
erase(erase,u);
}
};
for(int i=1;i<=n;i++)
{
if(head[i])
{
dfs2(dfs2,i,0);
}
}
for(int i=1;i<=m;i++)
{
cout<<ans[i]<<endl;
}
}
void init()
{
}
signed main()
{
ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
int t=1;
//cin>>t;
init();
while(t--)
{
solve();
}
return 0;
}
首先,考虑对每个字符串离散化转成数字,那么问题就转化为和前几个题一样了。
之后,由于对于每个节点,要查子树内距离不同高度的信息,所以考虑先把查询离线下来,然后对每个节点设置查询列表。那么在到达当前节点时,就可以根据信息直接回答这些查询了。
在信息的设置上,考虑维护每个深度有的颜色个数,这个可以通过对每个深度都开一个 set 实现去重。又因为在删除时可以直接清空,所以这个也是好操作的。
5.E. Blood Cousins
cpp
#include <bits/stdc++.h>
using namespace std;
/* /\_/\
* (= ._.)
* / > \>
*/
/*
*想好再写
*注意审题 注意特判
*不要红温 不要急躁 耐心一点
*WA了不要立马觉得是思路不对 先耐心找反例
*/
#define endl '\n'
#define dbg(x) cout<<#x<<endl;cout<<x<<endl;
#define vdbg(a) cout<<#a<<endl;for(auto x:a)cout<<x<<" ";cout<<endl;
#define YES cout<<"YES"<<endl;return ;
#define Yes cout<<"Yes"<<endl;return ;
#define NO cout<<"NO"<<endl;return ;
#define No cout<<"No"<<endl;return ;
#define popcount __builtin_popcount
using ll=long long;
using i128=__int128;
using ld=long double;
using pii=pair<int,int>;
using pll=pair<ll,ll>;
const int INF=1e9;
const ll INFLL=1e18;
const int dx[]={-1,1,0,0};
const int dy[]={0,0,-1,1};
const int ddx[]={-2,-1,1,2,2,1,-1,-2};
const int ddy[]={1,2,2,1,-1,-2,-2,-1};
void solve()
{
int n;
cin>>n;
vector<vector<int>>g(n+1);
vector<int>head(n+1);
for(int i=1,fa;i<=n;i++)
{
cin>>fa;
if(fa==0)
{
head[i]=1;
}
else
{
g[fa].push_back(i);
g[i].push_back(fa);
}
}
auto log2=[&](int n)->int
{
int ans=0;
while((1<<ans)<=(n>>1)){
ans++;
}
return ans;
};
vector<int>fat(n+1);
vector<int>dep(n+1);
vector<int>siz(n+1);
vector<int>son(n+1);
int MAXP=log2(n);
vector<vector<int>>stjump(n+1,vector<int>(MAXP+1));
auto dfs1=[&](auto &&self,int u,int fa)->void
{
fat[u]=fa;
dep[u]=dep[fa]+1;
siz[u]=1;
stjump[u][0]=fa;
for(int p=1;(1<<p)<=dep[u];p++){
stjump[u][p]=stjump[stjump[u][p-1]][p-1];
}
for(auto v:g[u]){
if(v!=fa){
self(self,v,u);
siz[u]+=siz[v];
if(siz[son[u]]<siz[v]){
son[u]=v;
}
}
}
};
for(int i=1;i<=n;i++)
{
if(head[i])
{
dfs1(dfs1,i,0);
}
}
auto query=[&](int x,int k)->int
{
int res=x;
for(int p=MAXP;p>=0;p--)
{
if(k>>p&1)
{
res=stjump[res][p];
}
}
return res;
};
int m;
cin>>m;
vector<vector<pii>>q(n+1);
for(int i=1,x,k;i<=m;i++)
{
cin>>x>>k;
int res=query(x,k);
if(res)
{
q[res].push_back({k,i});
}
}
vector<int>cnts(n+1);
auto add=[&](auto &&self,int u)->void
{
cnts[dep[u]]++;
for(auto v:g[u])
{
if(v!=fat[u])
{
self(self,v);
}
}
};
auto erase=[&](auto &&self,int u)->void
{
cnts[dep[u]]=0;
for(auto v:g[u])
{
if(v!=fat[u])
{
self(self,v);
}
}
};
vector<int>ans(m+1);
auto dfs2=[&](auto &&self,int u,int keep)->void
{
for(auto v:g[u])
{
if(v!=fat[u]&&v!=son[u])
{
self(self,v,0);
}
}
if(son[u])
{
self(self,son[u],1);
}
cnts[dep[u]]++;
for(auto v:g[u])
{
if(v!=fat[u]&&v!=son[u])
{
add(add,v);
}
}
for(auto [k,id]:q[u])
{
if(dep[u]+k<=n)
{
ans[id]=cnts[dep[u]+k]-1;
}
}
if(keep==0)
{
erase(erase,u);
}
};
for(int i=1;i<=n;i++)
{
if(head[i])
{
dfs2(dfs2,i,0);
}
}
for(int i=1;i<=m;i++)
{
cout<<ans[i]<<" ";
}
cout<<endl;
}
void init()
{
}
signed main()
{
ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
int t=1;
//cin>>t;
init();
while(t--)
{
solve();
}
return 0;
}
乍一看上去可能会觉得过程很复杂,但其实转化一下就行。对于当前节点 x 的查询,利用 st 表往上跳 k 步来到其要查的祖先节点,然后将这条查询任务发给这个祖先节点即可。那么这个问题就转化为之前的几个问题了,直接套模板即可。注意要特判没有祖先的情况,不加入查询即可。
6.D. Arpa's letter-marked tree and Mehrdad's Dokhtar-kosh paths
cpp
#include <bits/stdc++.h>
using namespace std;
/* /\_/\
* (= ._.)
* / > \>
*/
/*
*想好再写
*注意审题 注意特判
*不要红温 不要急躁 耐心一点
*WA了不要立马觉得是思路不对 先耐心找反例
*/
#define endl '\n'
#define dbg(x) cout<<#x<<endl;cout<<x<<endl;
#define vdbg(a) cout<<#a<<endl;for(auto x:a)cout<<x<<" ";cout<<endl;
#define YES cout<<"YES"<<endl;return ;
#define Yes cout<<"Yes"<<endl;return ;
#define NO cout<<"NO"<<endl;return ;
#define No cout<<"No"<<endl;return ;
#define popcount __builtin_popcount
using ll=long long;
using i128=__int128;
using ld=long double;
using pii=pair<int,int>;
using pll=pair<ll,ll>;
const int INF=1e9;
const ll INFLL=1e18;
const int dx[]={-1,1,0,0};
const int dy[]={0,0,-1,1};
const int ddx[]={-2,-1,1,2,2,1,-1,-2};
const int ddy[]={1,2,2,1,-1,-2,-2,-1};
template<typename T>
struct Heavy_Light_Decomposition{
int n;
vector<int>fat;
vector<int>dep;
vector<int>siz;
vector<int>son;
vector<int>top;
vector<int>dfn;
vector<int>seg;
int cnt;
Heavy_Light_Decomposition(){}
//_n: number of node, not size!!
Heavy_Light_Decomposition(int _n,const vector<vector<int>>&g,int root=1){
n=_n;
fat.assign(n+1,0);
dep.assign(n+1,0);
siz.assign(n+1,0);
son.assign(n+1,0);
top.assign(n+1,0);
dfn.assign(n+1,0);
seg.assign(n+1,0);
cnt=0;
dfs1(root,0,g);
dfs2(root,root,g);
}
void dfs1(int u,int fa,const vector<vector<int>>&g){
fat[u]=fa;
dep[u]=dep[fa]+1;
siz[u]=1;
for(auto v:g[u]){
if(v!=fa){
dfs1(v,u,g);
siz[u]+=siz[v];
if(siz[son[u]]<siz[v]){
son[u]=v;
}
}
}
}
void dfs2(int u,int up,const vector<vector<int>>&g){
top[u]=up;
dfn[u]=++cnt;
seg[cnt]=u;
if(son[u]==0){
return ;
}
dfs2(son[u],up,g);
for(auto v:g[u]){
if(v!=fat[u]&&v!=son[u]){
dfs2(v,v,g);
}
}
}
};
void solve()
{
int n;
cin>>n;
vector<vector<pii>>g(n+1);
int fa;char ch;
for(int i=2;i<=n;i++)
{
cin>>fa>>ch;
g[i].push_back({fa,ch-'a'});
g[fa].push_back({i,ch-'a'});
}
vector<int>fat(n+1);
vector<int>dep(n+1);
vector<int>siz(n+1);
vector<int>son(n+1);
vector<int>eor(n+1);
auto dfs1=[&](auto &&self,int u,int fa)->void
{
fat[u]=fa;
dep[u]=dep[fa]+1;
siz[u]=1;
for(auto [v,w]:g[u]){
if(v!=fa){
eor[v]=eor[u]^(1<<w);
self(self,v,u);
siz[u]+=siz[v];
if(siz[son[u]]<siz[v]){
son[u]=v;
}
}
}
};
dfs1(dfs1,1,0);
vector<int>maxdep(1<<22);
auto add=[&](auto &&self,int u)->void
{
maxdep[eor[u]]=max(maxdep[eor[u]],dep[u]);
for(auto [v,w]:g[u])
{
if(v!=fat[u])
{
self(self,v);
}
}
};
auto erase=[&](auto &&self,int u)->void
{
maxdep[eor[u]]=0;
for(auto [v,w]:g[u])
{
if(v!=fat[u])
{
self(self,v);
}
}
};
vector<int>ans(n+1);
auto calc=[&](auto &&self,int u,int lca)->void
{
if(maxdep[eor[u]])
{
ans[lca]=max(ans[lca],maxdep[eor[u]]+dep[u]-2*dep[lca]);
}
for(int p=0;p<22;p++)
{
int s=eor[u]^(1<<p);
if(maxdep[s])
{
ans[lca]=max(ans[lca],maxdep[s]+dep[u]-2*dep[lca]);
}
}
for(auto [v,w]:g[u])
{
if(v!=fat[u])
{
self(self,v,lca);
}
}
};
auto dfs2=[&](auto &&self,int u,int keep)->void
{
for(auto [v,w]:g[u])
{
if(v!=fat[u]&&v!=son[u])
{
self(self,v,0);
ans[u]=max(ans[u],ans[v]);
}
}
if(son[u])
{
self(self,son[u],1);
ans[u]=max(ans[u],ans[son[u]]);
}
if(maxdep[eor[u]])
{
ans[u]=max(ans[u],maxdep[eor[u]]-dep[u]);
}
for(int p=0;p<22;p++)
{
int s=eor[u]^(1<<p);
if(maxdep[s])
{
ans[u]=max(ans[u],maxdep[s]-dep[u]);
}
}
maxdep[eor[u]]=max(maxdep[eor[u]],dep[u]);
for(auto [v,w]:g[u])
{
if(v!=fat[u]&&v!=son[u])
{
calc(calc,v,u);
add(add,v);
}
}
if(keep==0)
{
erase(erase,u);
}
};
dfs2(dfs2,1,0);
for(int i=1;i<=n;i++)
{
cout<<ans[i]<<" ";
}
cout<<endl;
}
void init()
{
}
signed main()
{
ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
int t=1;
//cin>>t;
init();
while(t--)
{
solve();
}
return 0;
}
首先,对于一条合法的回文路径,要么其中字母的词频全是偶数,要么就只出现一个词频为奇数的字母。而对于奇偶词频,考虑将其转化成异或问题,即对题目里这 22 个字母的词频状态压缩。之后,对于路径问题,还是考虑转化成前缀问题。所以在构建出从根节点到每个节点 u 这条路径上字母的词频异或后,此时两点 x 和 y 这条路径上的词频异或就是两点状态异或起来的结果。
在来到当前节点 u 时,首先答案可以由所有孩子子树的答案构成,之后就需要考虑跨子树的答案了。那么首先,讨论从 u 出发,和自己重儿子的子树构成的答案。那么若从根节点来到 u 的状态为 s,其就可以和重儿子子树内,状态为 s 以及和状态为 s 只有一位不同的状态异或,得到合法的状态。又因为要让路径最长,所以考虑定义 为状态为 s 的点最深出现在第几层。
那么在将 u 合并到重儿子后,此时去所有轻儿子的子树内考虑。那么对于当前到的节点 v,还是去之前合并过的集合内找状态。然后和最深出现的位置合并,通过深度计算路径长度,然后将当前子树合并到大集合里即可。
7.森林
最能体现 dsu on tree 的一道题!
cpp
#include <bits/stdc++.h>
using namespace std;
/* /\_/\
* (= ._.)
* / > \>
*/
/*
*想好再写
*注意审题 注意特判
*不要红温 不要急躁 耐心一点
*WA了不要立马觉得是思路不对 先耐心找反例
*/
#define endl '\n'
#define dbg(x) cout<<#x<<endl;cout<<x<<endl;
#define vdbg(a) cout<<#a<<endl;for(auto x:a)cout<<x<<" ";cout<<endl;
#define YES cout<<"YES"<<endl;return ;
#define Yes cout<<"Yes"<<endl;return ;
#define NO cout<<"NO"<<endl;return ;
#define No cout<<"No"<<endl;return ;
#define popcount __builtin_popcount
using ll=long long;
using ld=long double;
using pii=pair<int,int>;
using pll=pair<ll,ll>;
const int INF=1e9;
const ll INFLL=1e18;
const int dx[]={-1,1,0,0};
const int dy[]={0,0,-1,1};
const int ddx[]={-2,-1,1,2,2,1,-1,-2};
const int ddy[]={1,2,2,1,-1,-2,-2,-1};
template<typename T>
struct Persistent_SegTree{
vector<int>root;
vector<int>left;
vector<int>right;
vector<T>data;
int cnt;
//自定义
Persistent_SegTree(){}
Persistent_SegTree(int n){
root.assign(n,0);
left.assign(n*300,0);
right.assign(n*300,0);
data.assign(n*300,0);
cnt=0;
//自定义
}
//自定义
int build(int l,int r){
int cur=++cnt;
data[cur]=0;
if(l==r){
}
else{
int m=l+r>>1;
left[cur]=build(l,m);
right[cur]=build(m+1,r);
}
return cur;
}
//单点修改
int change(int jobi,T jobv,int l,int r,int i){
int cur=copy(i);
data[cur]+=jobv;
if(l==r){
}
else{
int m=l+r>>1;
if(jobi<=m){
left[cur]=change(jobi,jobv,l,m,left[cur]);
}
else{
right[cur]=change(jobi,jobv,m+1,r,right[cur]);
}
}
return cur;
}
//区间 [u,v] 查第 k 小
int kth(int jobk,int l,int r,int u,int v,int lca,int fa){
if(l==r){
return l;
}
int cnt=data[left[u]]-data[left[lca]]+data[left[v]]-data[left[fa]];
int m=l+r>>1;
if(cnt>=jobk){
return kth(jobk,l,m,left[u],left[v],left[lca],left[fa]);
}
else{
return kth(jobk-cnt,m+1,r,right[u],right[v],right[lca],right[fa]);
}
}
//自定义
int copy(int i){
int cur=++cnt;
left[cur]=left[i];
right[cur]=right[i];
data[cur]=data[i];
//自定义
return cur;
}
};
void solve()
{
int n,m,q;
cin>>n>>m>>q;
vector<int>a(n+1);
for(int i=1;i<=n;i++)
{
cin>>a[i];
}
vector<vector<int>>g(n+1);
for(int i=1,u,v;i<=m;i++)
{
cin>>u>>v;
g[u].push_back(v);
g[v].push_back(u);
}
vector<int>sorted=a;
sort(sorted.begin()+1,sorted.end());
int len=1;
for(int i=2;i<=n;i++)
{
if(sorted[len]!=sorted[i])
{
sorted[++len]=sorted[i];
}
}
auto rnk=[&](int v)->int
{
int l=1;
int r=len;
int m;
int ans;
while(l<=r)
{
m=l+r>>1;
if(sorted[m]>=v)
{
ans=m;
r=m-1;
}
else
{
l=m+1;
}
}
return ans;
};
for(int i=1;i<=n;i++)
{
a[i]=rnk(a[i]);
}
const int MAXP=20;
vector<int>deep(n+1);
vector<vector<int>>stjump(n+1,vector<int>(MAXP+1));
Persistent_SegTree<ll>st(n+1);
vector<int>head(n+1);
vector<int>size(n+1);
auto dfs=[&](auto &&self,int u,int fa,int top)->void
{
deep[u]=deep[fa]+1;
stjump[u][0]=fa;
st.root[u]=st.change(a[u],1,1,len,st.root[fa]);
head[u]=top;
size[top]++;
for(int p=1;p<=MAXP;p++){
stjump[u][p]=stjump[stjump[u][p-1]][p-1];
}
for(auto v:g[u]){
if(v!=fa){
self(self,v,u,top);
}
}
};
auto query=[&](int a,int b)->int
{
if(deep[a]<deep[b]){
swap(a,b);
}
for(int p=MAXP;p>=0;p--){
if(deep[stjump[a][p]]>=deep[b]){
a=stjump[a][p];
}
}
if(a==b){
return a;
}
for(int p=MAXP;p>=0;p--){
if(stjump[a][p]!=stjump[b][p]){
a=stjump[a][p];
b=stjump[b][p];
}
}
return stjump[a][0];
};
for(int i=1;i<=n;i++)
{
if(head[i]==0)
{
dfs(dfs,i,0,i);
}
}
int pre=0;
string op;
int u,v,k;
while(q--)
{
cin>>op;
if(op[0]=='L')
{
cin>>u>>v;
u^=pre,v^=pre;
g[u].push_back(v);
g[v].push_back(u);
int fu=head[u];
int fv=head[v];
if(size[fu]>=size[fv])
{
dfs(dfs,v,u,fu);
}
else
{
dfs(dfs,u,v,fv);
}
}
else if(op[0]=='Q')
{
cin>>u>>v>>k;
u^=pre,v^=pre,k^=pre;
int lca=query(u,v);
int fa=stjump[lca][0];
int rnk=st.kth(k,1,len,st.root[u],st.root[v],st.root[lca],st.root[fa]);
pre=sorted[rnk];
cout<<pre<<endl;
}
}
}
void init()
{
}
signed main()
{
ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
int t=1;
cin>>t;
init();
// while(t--)
// {
solve();
// }
return 0;
}
对于这个路径第 k 大的问题,如果不涉及连边的操作,那么其实就是主席树的板子题。而不管怎么连接,都不会影响两点之间的路径。那么根据启发式合并的原理,在两树合并时,考虑让小树挂到大树上,然后暴力修改小树所有节点的版本即可。那么在合并时,就需要每次查询当前节点所在树的头节点以及整棵树的大小,这个就可以通过一个类似并查集的结构实现。
注意 testcase 是编号不是数据组数,这个太他妈坑了......
总结
感觉这玩意儿维护信息的方式和莫队很像啊!!前几题完全可以把树用 dfn 序拍成数组跑莫队!!