atc abc460F 思路分享(树的直径,线段树,点分树)
atc abc460F
题意
给定一棵树,初始全黑,\(q\) 个查询,每次将 \(x\) 节点颜色反转,求每次操作后任意两黑点距离的最大值.
\(3\le n \le 10^5\),\(1\le q \le 10^5\).
线段树维护直径做法
思路
相当于维护黑点构成的动态集合的直径,直径是可 merge 的,可以用线段树维护.
具体来说,假设两集合直径端点分别是 \((a_1,b_1)\) 和 \((a_2,b_2)\),合并后集合直径端点 \((a',b')\) 必有 \(a',b' \in \{a_1,b_1,a_2,b_2\}\),暴力枚举二元组即可.
计算任意两点距离可以通过预处理 \(depth\),\(dis(a,b) = depth_a+depth_b-2\cdot depth_{LCA(a,b)}\).
时间复杂度 \(\mathcal{O}(n\log^2 n)\),可以使用 \(\mathcal{O}(1)\) 计算 \(LCA\) 的技术优化到 \(\mathcal{O}(n\log n)\).
代码
//author:kzssCCC
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
const int INF = 1e9;
int n;
vector<vector<int>> adj,nxt;
vector<int> depth;
void dfs(int u,int par){
for (auto& v:adj[u]){
if (v==par) continue;
depth[v] = depth[u]+1;
nxt[0][v] = u;
dfs(v,u);
}
}
int lca(int a,int b){
if (depth[a]<depth[b]) swap(a,b);
int diff = depth[a]-depth[b];
for (int k=20;k>=0;k--){
if (diff>>k&1){
a = nxt[k][a];
}
}
if (a==b) return a;
for (int k=20;k>=0;k--){
if (nxt[k][a]!=-1 && nxt[k][b]!=-1 && nxt[k][a]!=nxt[k][b]){
a = nxt[k][a];
b = nxt[k][b];
}
}
return nxt[0][a];
}
int caldis(int a,int b){
if (a==-1 || b==-1) return -INF;
return depth[a]+depth[b]-2*depth[lca(a,b)];
}
struct node{
int a,b;
node(int a=-1,int b=-1):a(a),b(b){};
node operator+(const node& o)const {
vector<int> temp{a,b,o.a,o.b};
node res;
int mx = -INF;
for (int i=0;i<4;i++){
for (int j=i+1;j<4;j++){
int d = caldis(temp[i],temp[j]);
if (d>mx){
mx = d;
res.a = temp[i];
res.b = temp[j];
}
}
}
return res;
}
};
struct segmentTree{
int n;
vector<node> seg;
segmentTree(int _n){
n = _n;
seg.assign(4*n+1,{});
}
void build(int rt,int l,int r,vector<node>& a){
if (l==r){
seg[rt] = a[l];
return;
}
int mid = l+r >> 1;
build(rt<<1,l,mid,a);
build(rt<<1|1,mid+1,r,a);
seg[rt] = seg[rt<<1]+seg[rt<<1|1];
}
void build(vector<node>& a){
build(1,1,n,a);
}
void update(int rt,int l,int r,int pos,node val){
if (l==r){
seg[rt] = seg[rt]+val;
return;
}
int mid = l+r >> 1;
if (pos<=mid){
update(rt<<1,l,mid,pos,val);
}
else{
update(rt<<1|1,mid+1,r,pos,val);
}
seg[rt] = seg[rt<<1]+seg[rt<<1|1];
}
void update(int pos,node val){
if (pos<1 || pos>n) return;
update(1,1,n,pos,val);
}
void assign(int rt,int l,int r,int pos,node val){
if (l==r){
seg[rt] = val;
return;
}
int mid = l+r >> 1;
if (pos<=mid){
assign(rt<<1,l,mid,pos,val);
}
else{
assign(rt<<1|1,mid+1,r,pos,val);
}
seg[rt] = seg[rt<<1]+seg[rt<<1|1];
}
void assign(int pos,node val){
if (pos<1 || pos>n) return;
assign(1,1,n,pos,val);
}
node query(int rt,int l,int r,int pos){
if (l==r){
return seg[rt];
}
int mid = l+r >> 1;
if (pos<=mid){
return query(rt<<1,l,mid,pos);
}
else{
return query(rt<<1|1,mid+1,r,pos);
}
}
node query(int pos){
if (pos<1 || pos>n) return {};
return query(1,1,n,pos);
}
node query_range(int rt,int l,int r,int x,int y){
if (r<x || l>y){
return {};
}
if (x<=l && y>=r){
return seg[rt];
}
int mid = l+r >> 1;
return query_range(rt<<1,l,mid,x,y)+query_range(rt<<1|1,mid+1,r,x,y);
}
node query_range(int l,int r){
if (l<1 || l>n || r<1 || r>n || l>r) return {};
return query_range(1,1,n,l,r);
}
};
void solve(){
cin >> n;
adj.resize(n+1);
for (int i=0;i<n-1;i++){
int u,v;
cin >> u >> v;
adj[u].push_back(v);
adj[v].push_back(u);
}
depth.assign(n+1,0);
nxt.assign(21,vector<int>(n+1,-1));
dfs(1,-1);
for (int k=1;k<=20;k++){
for (int i=1;i<=n;i++){
if (nxt[k-1][i]==-1) continue;
nxt[k][i] = nxt[k-1][nxt[k-1][i]];
}
}
segmentTree sg(n);
vector<int> a(n+1,1);
{
vector<node> temp(n+1);
for (int i=1;i<=n;i++){
temp[i] = {i,i};
}
sg.build(temp);
}
int q;
cin >> q;
while (q--){
int x;
cin >> x;
if (a[x]==0){
sg.assign(x,{x,x});
}
else{
sg.assign(x,{-1,-1});
}
a[x]^=1;
cout << caldis(sg.seg[1].a,sg.seg[1].b) << '\n';
}
}
int main(){
ios::sync_with_stdio(false);
cin.tie(0);
int t = 1;
// cin >> t;
while (t--) solve();
return 0;
}
点分树做法
思路
核心思路是对每个重心讨论跨重心的贡献,因为带修的缘故,需要维护所有的信息而不是单单最大值.
对每个重心,维护其与子树中所有黑点的距离,其贡献是最大和次大两距离之和,注意不能来自于同一子树.
这里用到点分树中一个经典 trick,每个节点维护其父节点的信息,记为 \(fa\).
维护所有孩子 \(fa\) 的最大值,记为 \(best\),这样该节点贡献就是 \(best\) 中最大值和次大值之和.
用 multiset 维护所有节点的贡献,记为 \(st\),查询直接输出最大值即可.
\(best\) 和 \(fa\) 实际上都可以用 multiset 维护,但这样实现常数较大,难以在 \(4s\) 内通过,原因在于节点初始全黑,初始对每个节点 update 的开销是巨大的.
事实上,\(fa\) 每次只取最大值,可以用双堆懒删除的技术代替 multiset,使用堆结构的优势在于建堆时间是线性的,这样就解决了初始化成本巨大的问题.
时间复杂度 \(\mathcal{O}(n\log^2 n)\),可以把所有 multiset 都换成堆进一步优化.
代码
//author:kzssCCC
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
void solve(){
int n;
cin >> n;
vector<vector<int>> adj(n+1);
for (int i=0;i<n-1;i++){
int u,v;
cin >> u >> v;
adj[u].push_back(v);
adj[v].push_back(u);
}
vector<int> sz(n+1),a(n+1,1);
vector<bool> vis(n+1,false);
vector<vector<pair<int,int>>> path(n+1);
function<void(int,int)> getsz = [&](int u,int par){
sz[u] = 1;
for (auto& v:adj[u]){
if (v==par || vis[v]) continue;
getsz(v,u);
sz[u] += sz[v];
}
};
auto getsent = [&](int u){
getsz(u,-1);
int par = -1;
int half = sz[u] >> 1;
while (1){
bool ok = false;
for (auto& v:adj[u]){
if (v==par || vis[v]) continue;
if (sz[v]>half){
par = u;
u = v;
ok = true;
break;
}
}
if (!ok) break;
}
return u;
};
function<void(int,int,int,int)> dfs = [&](int u,int par,int sent,int dis){
path[u].emplace_back(dis,sent);
for (auto& v:adj[u]){
if (v==par || vis[v]) continue;
dfs(v,u,sent,dis+1);
}
};
function<void(int)> work = [&](int u){
vis[u] = true;
path[u].emplace_back(0,u);
for (auto& v:adj[u]){
if (vis[v]) continue;
dfs(v,u,u,1);
}
for (auto& v:adj[u]){
if (vis[v]) continue;
work(getsent(v));
}
};
work(getsent(1));
for (int i=1;i<=n;i++){
reverse(path[i].begin(),path[i].end());
}
vector<vector<int>> fa(n+1),sub(n+1);
vector<multiset<int>> best(n+1);
multiset<int> st;
auto cal = [&](int x){
if (best[x].size()<2) return 0;
return *best[x].rbegin()+(*prev(prev(best[x].end())));
};
auto pop = [&](int x){
while (!fa[x].empty() && !sub[x].empty()){
int a = fa[x].front();
int b = sub[x].front();
if (a==b){
pop_heap(fa[x].begin(),fa[x].end());
pop_heap(sub[x].begin(),sub[x].end());
fa[x].pop_back();
sub[x].pop_back();
}
else break;
}
};
auto update = [&](int x,int op){
int len = path[x].size();
st.extract(cal(x));
if (op==0){
best[x].insert(0);
}
else{
best[x].extract(0);
}
st.insert(cal(x));
for (int i=0;i<len-1;i++){
st.extract(cal(path[x][i+1].second));
pop(path[x][i].second);
if (!fa[path[x][i].second].empty()){
best[path[x][i+1].second].extract(fa[path[x][i].second].front());
}
if (op==0){
fa[path[x][i].second].push_back(path[x][i+1].first);
push_heap(fa[path[x][i].second].begin(),fa[path[x][i].second].end());
}
else{
sub[path[x][i].second].push_back(path[x][i+1].first);
push_heap(sub[path[x][i].second].begin(),sub[path[x][i].second].end());
}
pop(path[x][i].second);
if (!fa[path[x][i].second].empty()){
best[path[x][i+1].second].insert(fa[path[x][i].second].front());
}
st.insert(cal(path[x][i+1].second));
}
};
for (int x=1;x<=n;x++){
int len = path[x].size();
best[x].insert(0);
for (int i=0;i<len-1;i++){
fa[path[x][i].second].push_back(path[x][i+1].first);
}
}
for (int x=1;x<=n;x++){
make_heap(fa[x].begin(),fa[x].end());
int len = path[x].size();
if (len>=2 && !fa[x].empty()){
best[path[x][1].second].insert(fa[x].front());
}
}
for (int x=1;x<=n;x++){
st.insert(cal(x));
}
int q;
cin >> q;
while (q--){
int x;
cin >> x;
update(x,a[x]);
a[x]^=1;
cout << *st.rbegin() << '\n';
}
}
int main(){
ios::sync_with_stdio(false);
cin.tie(0);
int t = 1;
// cin >> t;
while (t--) solve();
return 0;
}

浙公网安备 33010602011771号