海亮 7.10 树论
Hailiang 7.10 树论/树上问题
树上问题:
- 树形\(dp\) 技巧:在状态里面加上父亲/儿子/祖先的状态
- 重心,直径的性质
- 链问题:树上差分/树剖/\(LCA\)(树上倍增)
- 树上路径统计问题:淀粉质/动态淀粉质/淀粉树
- 基环树
Trick:
-
求某个点到根节点的权值和
\(DFS\)时维护数组 进点时++ 回溯时-- 在进点时查询即可
-
求某个子树权值和
\(DFS\) 进的时候把当前权值挂到节点上(比如说颜色信息\(c\)值 开一个新数组记录下来这个状态)
再\(dfs\)子树并更新权值 那么我们在出去的时候将现在的新权值和老权值相减即为答案
P5838 [USACO19DEC] Milk Visits G
一道比较板的树剖题 显然将询问离线 每次加入一种颜色并回答这个颜色的所有询问
开始以为用\(vector\)暴力维护插入的点会\(T\) 所以打了一个\(build\)函数+区间修改线段树 但是挂了
最后改用\(vector\)过的()
代码一:
#include <bits/stdc++.h>
using namespace std;
#define endl '\n'
#define mkp make_pair
#define mid ((l+r)>>1)
#define ls p<<1
#define rs p<<1|1
#define lson ls,l,mid
#define rson rs,mid+1,r
#define int long long
const int N = 2e5 + 5;
const int inf = 9e18;
inline int read ()
{
int x = 0 , f = 1;
char ch = cin.get();
while ( !isdigit ( ch ) ) { if ( ch == '-' ) f = -1; ch = cin.get(); }
while ( isdigit ( ch ) ) { x = ( x << 1 ) + ( x << 3 ) + ( ch ^ 48 ); ch = cin.get(); }
return x * f;
}
int n , m , a[N] , test , testcol , ans[N];
vector<int> v[N];
int head[N] , cnt;
struct node { int to , nxt; } e[N<<1];
void add ( int u , int v ) { e[++cnt] = { v , head[u] } , head[u] = cnt; }
struct que { int x , y , id , col , ans; } q[N];
int son[N] , fa[N] , sz[N] , dep[N];
int pos[N] , top[N] , rev[N] , timer;
void dfs1 ( int u , int f )
{
fa[u] = f , sz[u] = 1 , dep[u] = dep[f] + 1;
for ( int i = head[u] ; i ; i = e[i].nxt )
{
int v = e[i].to;
if ( v == f ) continue;
dfs1 ( v , u );
sz[u] += sz[v];
if ( sz[son[u]] < sz[v] ) son[u] = v;
}
}
void dfs2 ( int u , int tp )
{
top[u] = tp , pos[u] = ++timer , rev[timer] = pos[u];
if ( son[u] ) dfs2 ( son[u] , tp );
for ( int i = head[u] ; i ; i = e[i].nxt )
{
int v = e[i].to;
if ( fa[u] == v || son[u] == v ) continue;
dfs2 ( v , v );
}
}
struct kk { int cov , val; } t[N<<2];
void up ( int p ) { t[p].val = max ( t[ls].val , t[rs].val ); }
void upd ( int p , int l , int r , int x , int val )
{
if ( l == r ) return t[p].val = val , void();
if ( x <= mid ) upd ( lson , x , val );
else upd ( rson , x , val );
up(p);
}
int query ( int p , int l , int r , int x , int y )
{
if ( x <= l && r <= y ) return t[p].val;
int res = -inf;
if ( x <= mid ) res = max ( res , query ( lson , x , y ) );
if ( mid + 1 <= y ) res = max ( res , query ( rson , x , y ) );
return res;
}
int querymax ( int u , int v )
{
int res = -inf;
while ( top[u] != top[v] )
{
if ( dep[top[u]] < dep[top[v]] ) swap ( u , v );
res = max ( res , query ( 1 , 1 , n , pos[top[u]] , pos[u] ) );
// cout << "test:" << test << " col=" << testcol << " top=" << pos[top[u]] << " bottom=" << pos[u] << " ans=" << query ( 1 , 1 , n , pos[top[u]] , pos[u] ) << endl;
u = fa[top[u]];
}
if ( dep[u] < dep[v] ) swap ( u , v );
res = max ( res , query ( 1 , 1 , n , pos[v] , pos[u] ) );
// cout << "test:" << test << " col=" << testcol << " top=" << pos[v] << " bottom=" << pos[u] << " ans=" << query ( 1 , 1 , n , pos[v] , pos[u] ) << endl;
return res > 0 ? 1 : 0;
}
signed main ()
{
ios::sync_with_stdio(false);
cin.tie(0) , cout.tie(0);
n = read() , m = read();
for ( int i = 1 ; i <= n ; i ++ ) a[i] = read() , v[a[i]].push_back(i);
for ( int i = 1 , u , v ; i < n ; i ++ ) u = read() , v = read() , add ( u , v ) , add ( v , u );
dfs1 ( 1 , 0 ) , dfs2 ( 1 , 1 );
for ( int i = 1 ; i <= m ; i ++ ) q[i].x = read() , q[i].y = read() , q[i].col = read() , q[i].id = i;
// for ( int i = 1 ; i <= n ; i ++ ) cout << pos[top[i]] << ' ' << pos[i] << endl;
sort ( q + 1 , q + m + 1 , [](const que a , const que b) { return a.col < b.col; } );
int now = 1;
for ( int i = 1 ; i <= q[m].col ; i ++ )
{
for ( int j = 0 ; j < v[i].size() ; j ++ ) upd ( 1 , 1 , n , pos[v[i][j]] , i );
while ( q[now].col == i ) ans[q[now].id] = querymax ( q[now].x , q[now].y ) , now ++;
for ( int j = 0 ; j < v[i].size() ; j ++ ) upd ( 1 , 1 , n , pos[v[i][j]] , 0 );
}
for ( int i = 1 ; i <= m ; i ++ ) cout << ans[i];
return 0;
}
代码二:(主席树版本)
比较好理解的在线做法 树剖求\(lca\) 主席树维护这个点到根节点的颜色 最后查询用差分查询即可
#include <bits/stdc++.h>
using namespace std;
#define endl '\n'
#define mkp make_pair
#define mid ((l+r)>>1)
#define ls(p) t[p].son[0]
#define rs(p) t[p].son[1]
#define lson ls(p),l,mid
#define rson rs(p),mid+1,r
#define int long long
const int N = 2e5 + 5;
const int inf = 9e18;
inline int read ()
{
int x = 0 , f = 1;
char ch = cin.get();
while ( !isdigit ( ch ) ) { if ( ch == '-' ) f = -1; ch = cin.get(); }
while ( isdigit ( ch ) ) { x = ( x << 1 ) + ( x << 3 ) + ( ch ^ 48 ); ch = cin.get(); }
return x * f;
}
int n , m , a[N] , test , testcol , ans[N] , root[N];
int head[N] , cnt;
struct node { int to , nxt; } e[N<<1];
void add ( int u , int v ) { e[++cnt] = { v , head[u] } , head[u] = cnt; }
int son[N] , fa[N] , sz[N] , dep[N];
int pos[N] , top[N] , rev[N] , timer;
struct tree
{
void dfs1 ( int u , int f )
{
fa[u] = f , sz[u] = 1 , dep[u] = dep[f] + 1;
for ( int i = head[u] ; i ; i = e[i].nxt )
{
int v = e[i].to;
if ( v == f ) continue;
dfs1 ( v , u );
sz[u] += sz[v];
if ( sz[son[u]] < sz[v] ) son[u] = v;
}
}
void dfs2 ( int u , int tp )
{
top[u] = tp , pos[u] = ++timer , rev[timer] = pos[u];
if ( son[u] ) dfs2 ( son[u] , tp );
for ( int i = head[u] ; i ; i = e[i].nxt )
{
int v = e[i].to;
if ( fa[u] == v || son[u] == v ) continue;
dfs2 ( v , v );
}
}
int lca ( int u , int v )
{
while ( top[u] != top[v] )
{
if ( dep[top[u]] < dep[top[v]] ) swap ( u , v );
u = fa[top[u]];
}
if ( dep[u] < dep[v] ) swap ( u , v );
return v;
}
}L;
struct DQY
{
struct kk { int son[2] , val; } t[N<<5];
int tot = 0;
int new_node ( int p ) { t[++tot] = t[p]; return tot; }
void upd ( int &p , int l , int r , int x , int val )
{
p = new_node(p);
t[p].val ++;
if ( l == r ) return;
if ( x <= mid ) upd ( lson , x , val );
else upd ( rson , x , val );
}
int query ( int u , int v , int l , int r , int col )
{
if ( l == r ) return t[v].val - t[u].val;
if ( col <= mid ) return query ( ls(u) , ls(v) , l , mid , col );
else return query ( rs(u) , rs(v) , mid + 1 , r , col );
}
}T;
void dfs ( int u , int f )
{
root[u] = root[f];
T.upd ( root[u] , 1 , n , a[u] , 1 );
for ( int i = head[u] ; i ; i = e[i].nxt )
{
int v = e[i].to;
if ( v == f ) continue;
dfs ( v , u );
}
}
int query ( int x , int y , int col )
{
int f = L.lca ( x , y );
int ff = fa[f];
int res = 0;
res += T.query ( root[ff] , root[x] , 1 , n , col );
res += T.query ( root[f] , root[y] , 1 , n , col );
return res > 0 ? 1 : 0;
}
signed main ()
{
ios::sync_with_stdio(false);
cin.tie(0) , cout.tie(0);
n = read() , m = read();
for ( int i = 1 ; i <= n ; i ++ ) a[i] = read();
for ( int i = 1 , u , v ; i < n ; i ++ ) u = read() , v = read() , add ( u , v ) , add ( v , u );
L.dfs1 ( 1 , 0 ) , L.dfs2 ( 1 , 1 );
dfs ( 1 , 0 );
for ( int i = 1 ; i <= m ; i ++ )
{
int x = read() , y = read() , col = read();
cout << query ( x , y , col );
}
return 0;
}
Trees of Tranquillity
很明显我们第一棵树上的点都应该是一条链上的节点
所以我们\(dfs\)第一棵树 同时维护第二棵树上的信息
那么对于第二棵树 节点之间不应该存在子树关系 又因为子树区间在\(dfn\)序上是连续的 那么策略就是:
如果第一个区间被第二个区间包含 那么选择更小的区间
首先一个\(DFS\)处理每个节点在第二棵树中的区间 再\(DFS\)第一棵树
必须注意回溯时的区间置0操作顺序
\(upd\):袁神给出了一组\(hack\) 但是题目中有父亲节点的编号小于子节点的编号的性质 这样可以保证父亲节点在第二棵树上维护的\(dfn\)序列一定大于子节点 \(hack\)未成功
感谢他的贡献让我对题目加深了理解()
#include <bits/stdc++.h>
using namespace std;
#define endl '\n'
#define mkp make_pair
#define mid ((l+r)>>1)
#define ls p<<1
#define rs p<<1|1
#define lson ls,l,mid
#define rson rs,mid+1,r
#define pii pair<int,int>
#define fi first
#define se second
#define mkp make_pair
const int N = 3e5 + 5;
const int inf = 2e9;
inline int read ()
{
int x = 0 , f = 1;
char ch = cin.get();
while ( !isdigit ( ch ) ) { if ( ch == '-' ) f = -1; ch = cin.get(); }
while ( isdigit ( ch ) ) { x = ( x << 1 ) + ( x << 3 ) + ( ch ^ 48 ); ch = cin.get(); }
return x * f;
}
int n , m;
int st[N] , ed[N] , timer , sum , ans;
int head1[N] , head2[N] , cnt;
struct node { int to , nxt; } e[N<<2];
void add1 ( int u , int v ) { e[++cnt] = { v , head1[u] } , head1[u] = cnt; }
void add2 ( int u , int v ) { e[++cnt] = { v , head2[u] } , head2[u] = cnt; }
struct DQY
{
struct node { int tag , val; } t[N<<2];
void up ( int p ) { t[p].val = max ( t[ls].val , t[rs].val ); }
void changedown ( int p , int val ) { t[p].tag = t[p].val = val; }
void down ( int p ) { if ( t[p].tag != -1 ) changedown ( ls , t[p].tag ) , changedown ( rs , t[p].tag ) , t[p].tag = -1; }
void build ( int p , int l , int r )
{
t[p].tag = -1 , t[p].val = 0;
if ( l == r ) return;
build ( lson ) , build ( rson );
}
void upd ( int p , int l , int r , int x , int y , int val )
{
if ( x <= l && r <= y ) return changedown ( p , val ) , void();
down(p);
if ( x <= mid ) upd ( lson , x , y , val );
if ( mid + 1 <= y ) upd ( rson , x , y , val );
up(p);
}
int query ( int p , int l , int r , int x , int y )
{
if ( x <= l && r <= y ) return t[p].val;
down(p); int res = -inf;
if ( x <= mid ) res = max ( res , query ( lson , x , y ) );
if ( mid + 1 <= y ) res = max ( res , query ( rson , x , y ) );
return res;
}
}T;
void dfs2 ( int u )
{
st[u] = ++timer;
for ( int i = head2[u] ; i ; i = e[i].nxt ) dfs2(e[i].to);
ed[u] = timer;
}
void dfs1 ( int u )
{
int tt = T.query ( 1 , 1 , n , st[u] , ed[u] );
if ( tt ) T.upd ( 1 , 1 , n , st[tt] , ed[tt] , 0 ) , T.upd ( 1 , 1 , n , st[u] , ed[u] , u );
else sum ++ , T.upd ( 1 , 1 , n , st[u] , ed[u] , u );
ans = max ( ans , sum );
for ( int i = head1[u] ; i ; i = e[i].nxt ) dfs1(e[i].to);
if ( tt ) T.upd ( 1 , 1 , n , st[u] , ed[u] , 0 ) , T.upd ( 1 , 1 , n , st[tt] , ed[tt] , tt );//这里的更新顺序必须注意 否则置0操作会将赋值tt操作顶掉
else sum -- , T.upd ( 1 , 1 , n , st[u] , ed[u] , 0 );
}
void solve()
{
memset ( head1 , 0 , sizeof head1 ) , memset ( head2 , 0 , sizeof head2 ) , cnt = 0;
n = read();
for ( int i = 2 , fa ; i <= n ; i ++ ) fa = read() , add1 ( fa , i );
for ( int i = 2 , fa ; i <= n ; i ++ ) fa = read() , add2 ( fa , i );
timer = 0 , dfs2 ( 1 );
ans = sum = 0 , dfs1 ( 1 );
cout << ans << endl;
}
signed main ()
{
ios::sync_with_stdio(false);
cin.tie(0) , cout.tie(0);
int T = read();
while ( T -- ) solve();
return 0;
}
P2491 [SDOI2011] 消防
一个性质:如果一个树网有多个直径 那么必然交于直径的中点
\(O(n^3)\)做法显然 枚举两个直径上的端点 为这一段打上标记 并枚举这一段上的每一个点\(DFS\)即可
一个优化:对于一个端点\(p\) \(q\)必然贪心地取最远点 那么省掉了\(i\)这一维度 时间复杂度\(O(n^2)\)
\(O(n)\)做法:还是枚举\(p\) 但是我们对于每一对\(p,q\)处理(\(O(1)\))
离这个树网的核最远的点只可能是 直径两端的点到\(p,q\)的最远距离 或者是其他点到直径的最远距离
因为对于\(p,q\),从它们出发最远的点一定是直径两端的点 不存在其他点 否则就不是直径了
对于取点可以用单调队列\(O(n)\)维护
打了两遍 乐 第一遍抄太多了
#include <bits/stdc++.h>
using namespace std;
#define endl '\n'
#define mkp make_pair
#define mid ((l+r)>>1)
#define ls p<<1
#define rs p<<1|1
#define lson ls,l,mid
#define rson rs,mid+1,r
#define pii pair<int,int>
#define fi first
#define se second
#define mkp make_pair
const int N = 6e5 + 5;
const int inf = 2e9;
inline int read ()
{
int x = 0 , f = 1;
char ch = cin.get();
while ( !isdigit ( ch ) ) { if ( ch == '-' ) f = -1; ch = cin.get(); }
while ( isdigit ( ch ) ) { x = ( x << 1 ) + ( x << 3 ) + ( ch ^ 48 ); ch = cin.get(); }
return x * f;
}
int n , s , s1 , s2 , vis[N] , dis[N] , fa[N] , maxs , maxdis , ans = inf;
int head[N] , cnt;
struct node { int to , nxt , w; } e[N<<2];
void add ( int u , int v , int w ) { e[++cnt] = { v , head[u] , w }; head[u] = cnt; }
void dfs ( int u , int f )
{
fa[u] = f;
if ( dis[u] > dis[s2] ) s2 = u;
for ( int i = head[u] ; i ; i = e[i].nxt )
{
int v = e[i].to;
if ( v == f ) continue;
dis[v] = dis[u] + e[i].w;
dfs ( v , u );
}
}
void mark_len()//标记直径 顺便统计点到直径一个端点的距离
{
memset ( dis , 0 , sizeof dis );//初始值设置为0
int u = s2; vis[u] = 1;
while ( u != s1 )
{
for ( int i = head[u] ; i ; i = e[i].nxt )
{
int v = e[i].to;
if ( v == fa[u] ) dis[v] = dis[u] + e[i].w , vis[v] = 1;
}
u = fa[u];
}
}
int find ( int u , int s )//u向右s步能走到的最远节点
{
for ( int i = head[u] ; i ; i = e[i].nxt )
{
int v = e[i].to;
if ( v == fa[u] && s >= e[i].w ) return find ( v , s - e[i].w );
}
return u;
}
void dis_len()//枚举一个起点 处理直径两端点到这条链的距离最大值的最小值
{
int u = s2;
while ( u != s1 ) ans = min ( ans , max ( dis[u] , dis[s1] - dis[find(u,s)] ) ) , u = fa[u];
ans = min ( ans , max ( dis[u] , dis[s1] - dis[find(u,s)] ) );
}
void query ( int u , int f )
{
for ( int i = head[u] ; i ; i = e[i].nxt )
{
int v = e[i].to;
if ( vis[v] || v == f ) continue;
dis[v] = dis[u] + e[i].w;
query ( v , u );
}
}
void node_len ()//处理每一个点到直径的距离
{
memset ( dis , 0 , sizeof dis );
int u = s2;
while ( u != s1 ) query ( u , 0 ) , u = fa[u];
query ( u , 0 );
}
signed main ()
{
ios::sync_with_stdio(false);
cin.tie(0) , cout.tie(0);
n = read() , s = read();
for ( int i = 1 , u , v , w ; i < n ; i ++ ) u = read() , v = read() , w = read() , add ( u , v , w ) , add ( v , u , w );
dfs(1,0) , s1 = s2 , s2 = 0 , dis[s1] = 0;
dfs(s1,0);
mark_len();
dis_len();
node_len();
for ( int i = 1 ; i <= n ; i ++ ) ans = max ( ans , dis[i] );
cout << ans << endl;
return 0;
}
P1600 [NOIP2016 提高组] 天天爱跑步
天天将会臭名昭著!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
我们首先考虑让天天换一个爱好
考虑对于每一个观察员在满足什么条件的时候会收到贡献
每一次询问 都可以拆成两条链(\(u->lca\)和\(lca->v\))
考虑第一条链
对于观察员\(i\) 如果它的子树中有节点满足\(dep[u]=dep[i]+a[i]\) 那么满足条件
考虑第二条链
可以推出 如果\(dep[i]-a[i]=dep[v]-dis(u,v)\) 那么会产生贡献
\(dep[i]-a[i]\)有可能小于\(0\) 那么我们对两边都加上\(n\)即可(因为题目中限制了\(w[i]\le n\))
所以 在上行途中 符合条件的起点可以做贡献 下行中 符合条件的终点可以做出贡献
那么我们开一个桶\(cnt\) 里面装着贡献 那么我们\(dfs\)一遍子树 差值就是这个节点的贡献
特别要注意:
在统计当前结点作为起点和终点所产生的贡献 继而计算出当前结点作为“根”上的差值后
在回溯过程中 一定要减去以当前结点为LCA的起点终点在桶里产生的贡献 这部分贡献在离开这个子树后就没有意义了 因为如果不删去的话 它上面的节点会错误地接收到这些路径的贡献
#include <bits/stdc++.h>
using namespace std;
#define int long long
const int N = 3e5 + 5;
inline int read ()
{
int x = 0 , f = 1;
char ch = cin.get();
while ( !isdigit ( ch ) ) { if ( ch == '-' ) f = -1; ch = cin.get(); }
while ( isdigit ( ch ) ) { x = ( x << 1 ) + ( x << 3 ) + ( ch ^ 48 ); ch = cin.get(); }
return x * f;
}
int n , m , cnt1[N<<1] , cnt2[N<<1] , a[N] , ans[N];
vector<int> q1[N] , q2[N] , q3[N] , q4[N];
int head[N] , cnt;
struct node { int to , nxt; } e[N<<1];
void add ( int u , int v ) { e[++cnt] = { v , head[u] } , head[u] = cnt; }
int fa[N] , dep[N] , son[N] , sz[N] , top[N];
struct DQY
{
void dfs1 ( int u , int f )
{
dep[u] = dep[fa[u]=f] + 1 , sz[u] = 1;
for ( int i = head[u] , v ; v = e[i].to , i ; i = e[i].nxt )
if ( v != f )
{
dfs1 ( v , u ) , sz[u] += sz[v];
if ( sz[son[u]] < sz[v] ) son[u] = v;
}
}
void dfs2 ( int u , int tp )
{
top[u] = tp;
if ( son[u] ) dfs2 ( son[u] , tp );
for ( int i = head[u] , v ; v = e[i].to , i ; i = e[i].nxt )
if ( v != son[u] && v != fa[u] ) dfs2 ( v , v );
}
int lca ( int u , int v )
{
while ( top[u] != top[v] )
{
if ( dep[top[u]] < dep[top[v]] ) swap ( u , v );
u = fa[top[u]];
}
if ( dep[u] < dep[v] ) swap ( u , v );
return v;
}
}L;
struct query { int u , v , lca , dis; } q[N];
void dfs1 ( int u )//搜dep[u]=dep[i]+a[i]
{
int now = dep[u] + a[u] , cur = cnt1[now];
for ( int i = head[u] , v ; v = e[i].to , i ; i = e[i].nxt ) if ( v != fa[u] ) dfs1(v);
for ( int i = 0 ; i < q1[u].size() ; i ++ ) ++cnt1[q1[u][i]];
ans[u] += cnt1[now] - cur;//当前节点的答案计入
for ( int i = 0 ; i < q2[u].size() ; i ++ ) --cnt1[q2[u][i]];//将这棵子树中的所有点都清理掉
}
void dfs2 ( int u )//搜dep[v]-dis(u,v)+n=dep[i]-a[i]+n
{
int now = dep[u] - a[u] + n , cur = cnt2[now];
for ( int i = head[u] , v ; v = e[i].to , i ; i = e[i].nxt ) if ( v != fa[u] ) dfs2(v);
for ( int i = 0 ; i < q3[u].size() ; i ++ ) ++cnt2[q3[u][i]];
for ( int i = 0 ; i < q4[u].size() ; i ++ ) --cnt2[q4[u][i]]; //相当于我们忽略掉了lca这个节点
ans[u] += cnt2[now] - cur;
}
signed main ()
{
ios::sync_with_stdio(false);
cin.tie(0) , cout.tie(0);
n = read() , m = read();
for ( int i = 1 , u , v ; i < n ; i ++ ) u = read() , v = read() , add ( u , v ) , add ( v , u );
L.dfs1(1,0) , L.dfs2(1,1);
for ( int i = 1 ; i <= n ; i ++ ) a[i] = read();
for ( int i = 1 ; i <= m ; i ++ ) q[i].u = read() , q[i].v = read() , q[i].lca = L.lca ( q[i].u , q[i].v ) , q[i].dis = dep[q[i].u] + dep[q[i].v] - 2 * dep[q[i].lca];
for ( int i = 1 ; i <= m ; i ++ ) q1[q[i].u].push_back(dep[q[i].u]) , q2[q[i].lca].push_back(dep[q[i].u]);
dfs1(1); //搜dep[u]=dep[i]+a[i]
for ( int i = 1 ; i <= m ; i ++ ) q3[q[i].v].push_back(dep[q[i].v]-q[i].dis+n) , q4[q[i].lca].push_back(dep[q[i].v]-q[i].dis+n);
dfs2(1); //搜dep[v]-dis(u,v)+n=dep[i]-a[i]+n
for ( int i = 1 ; i <= n ; i ++ ) cout << ans[i] << ' ';
return 0;
}