[探究] 浅析一类较为简单的树上边权操作题

好的,今天整理一类较为简单的树上针对边权的修改、查询、操作题:

问题大概有两类:只有查询\(\&\)修改查询都有

\(\mathscr{Task1}\)普通的查询问题

大概问题就类似于这个典型的\(XOR\)问题

其实说白了,这个题的解法十分简单——\(LCA\)

但其实……我一开始考虑\(LCA\),没有细致考虑,只是觉得如果从\(u,v\)分别向\(LCA\)跳,这并不可以提前处理或者提前维护,所以和暴力没有区别,于是就弃疗。

但是这个地方,\(LCA\)的作用十分巧妙——我们试图去拓展这个题。我们定义\(D(\bigodot,r,u)\)表示在运算\(\bigodot\)下,从根\(r\)到某一固定点\(u\)的边权运算结果,其中限定了运算\(\bigodot\)必须为可逆运算,比如说异或、加法、减法——注意,此时的可逆运算,可以是自可逆(自己对自己运算是逆运算,比如\(A~XOR ~A = 0\)或者他可逆(即存在另一种运算,是这种运算的逆运算,比如加法之于减法),那么我们就可以很方便地得出它的一般形式$$D(\bigodot,u,v) = D(\bigodot,r,u) \bigodot D(\bigodot,r,v) ~\bigoplus (2 \cdot D(\bigodot,r,LCA(u,v)))$$ ,其中我们假设运算\(\bigodot\)\(\bigoplus\)互逆。树上前缀和大抵上就是这个意思。

那么回到这个题,我们对于每一个点的\(D\),都是十分方便计算的,因为无权时,\(D(r,t) = depth[t]\)是一个相当平凡的结论;有权时,则直接\(dfs\)一遍即可。每次查询是\(log\)级别的,所以时间复杂度的渐进上界就是比较显然的\(O(\max(m \log_2n,n))\)

代码大概是这样

#include<cmath>
#include<cstdio>
#include<iostream>
#define il inline
#define MAXN 120100

using namespace std ;
int N, M ;
struct edge{
    int next, t, v;
}e[MAXN << 1] ;
int cnt, head[MAXN << 1] ;
int Up, dep[MAXN], XOR[MAXN] ,fa[MAXN][18] ;
int pre, i, j, f1, f2, in1, in2, in3, in4 ;

il int qr(){
    int k = 0;
    char c = getchar() ;
    while(!isdigit(c)) c = getchar() ;
    while(isdigit(c)) k = (k << 1 )+ (k << 3) + c - 48, c = getchar() ;
    return k ;
}
il void add(int u, int v, int w){
    e[++cnt].t = v ;
    e[cnt].next = head[u] ;
    e[cnt].v = w ;
	head[u] = cnt ;
    e[++cnt].t = u ;
    e[cnt].next = head[v] ;
    e[cnt].v = w ;
	head[v] = cnt ;
}
void _build(int deep, int now, int f, int _xor){
    fa[now][0] = f, dep[now] = deep, XOR[now] = _xor ;
    for(int k = head[now]; k ;k = e[k].next){
        if(e[k].t == f) continue ;
        _build(deep + 1, e[k].t, now, _xor ^ e[k].v) ;
    }
}
il void init(){
    Up = log(N) / log(2) + 1 ;
    for(i = 1; i <= Up; i ++)
        for(j = 1; j <= N; j ++)
            fa[j][i] = fa[fa[j][i - 1]][i - 1] ;
}
il int LCA(int u, int v){
    if(dep[u] < dep[v]) swap(u, v) ;
    pre = dep[u] - dep[v] ;
    for(j = 0; j <= Up; j ++) if((1 << j) & pre) u = fa[u][j] ;
    if(u == v) return u ;
    for(j = Up; j >= 0; j --) if(fa[u][j] != fa[v][j]) u = fa[u][j], v = fa[v][j] ;
    return fa[v][0] ;
}
int main(){
    N = qr() ;
    for(i = 1; i < N; i ++){
        in1 = qr(), in2 = qr(), in3 = qr();
        add(in1, in2, in3) ;
    }
    M = qr() ;
    _build(1, 1, 0, 0), init( );
    for(i = 1; i <= M; i ++){
        in1 = qr(), in2 = qr() ;
        f1 = LCA(in1, in2) ;
        int t = XOR[in1]^XOR[f1]^XOR[in2]^XOR[f1] ;
        cout << t << endl ;
    }
    return 0 ;
}

\(\mathscr{Task~2}\)带有修改的查询问题:

之后我们紧接着尝试去探讨一类带有修改边权的树上操作,大概题目就是某年国家集训队的板子题和一道经典的边权修改启蒙题

我们考虑,纷繁的操作,此时好像树剖比较合适些。但无论怎样,树剖是剖点的,不是剖边的。所以我们自然而然地想到要去把边权转移到点权上面。转移到哪儿去呢?显然只能转移到深度大的点上,因为在树这种结构里面,一对多决定了每个节点如果想要只保留一个属性(权值),就不能让父节点保留边权——否则父节点就会同时保有许多属性,并且叶子结点会没有属性,导致逻辑关系十分混乱。

所以,我们就应该把边权放到深度大的节点上,对节点进行操作。事实上,此处唯一需要注意的问题就是,我们在操作的时候要忽略\(LCA(u,v)\),这是显然的。关于这一点,我们的方法是把之前树剖里面的这一句:

_query(1, 1, N, Id[u], Id[v]) ;	
_update(1, 1, N, Id[u], Id[v], d) ;

改成

_query(1, 1, N, Id[u] + 1, Id[v]) ;	
_update(1, 1, N, Id[u] + 1, Id[v], d) ;

即可。

而对于如何边权转点权,我用的总时间复杂度大约\(O(n)\)的区间赋值函数,好像比较方便:

for (i = 1 ; i < N ; ++ i)
    n[i].u = qr(), n[i].v = qr(), n[i].c = qr(), _Add(n[i].u, n[i].v, n[i].c) ;
dfs1(1, 1), dfs2(1, 1), _build(1, 1, N) ;
for (i = 1 ; i < N ; ++ i){
    if (Dep[n[i].u] > Dep[n[i].v]) swap(n[i].u,n[i].v);
    _change(1, 1, N, Id[n[i].v], Id[n[i].v], n[i].c);
}

嗯,题解好啊

那么上面两个题就比较简单了:

\(\mathscr{T1 ~\text{の} ~code}\)

#include <cstdio>
#include <cstring>
#include <iostream>
#define MAX 200010
#define Inf 192608170
#define Ls(x) (x << 1)
#define Rs(x) (x << 1 | 1)
#define Mid ((L + R) >> 1)

using namespace std ;
struct Node{
    int u, v, c ;
}n[MAX << 1] ;
struct Edge{
    int v, next, to ;
}e[MAX << 1] ;  
int Id[MAX], M, N, tot, A, B, C, i ; char STR[50] ;
int Top[MAX], Fa[MAX], Sum[MAX], Son[MAX], Dep[MAX], head[MAX] ;
int Max[MAX << 1], Min[MAX << 1], S[MAX << 1], T[MAX << 1], cnt ;

inline int qr(){
    int res = 0, f = 1 ; char c = getchar() ;
    while (!isdigit(c)) {if (c == '-') f = -1 ; c = getchar() ;}
    while (isdigit(c)) res = (res << 1) + (res << 3) + c - 48, c = getchar() ;
    return res * f ;
}
inline void _Add(int u, int v, int c){
    e[++ cnt].v = c, e[cnt].to = v ;
    e[cnt].next = head[u], head[u] = cnt ;
    e[++ cnt].v = c, e[cnt].to = u ;
    e[cnt].next = head[v], head[v] = cnt ;
}
void dfs1(int F, int now){
    Sum[now] = 1, Fa[now] = F, Dep[now] = Dep[F] + 1 ;
    for (int k = head[now] ; k ; k = e[k].next){
        if (e[k].to == F) continue ;
        dfs1(now, e[k].to) ;
        Sum[now] += Sum[e[k].to] ;
        if (Sum[e[k].to] > Sum[Son[now]]) Son[now] = e[k].to ;
    }
}
void dfs2(int now, int Tp){
    Id[now] = ++ tot, Top[now] = Tp ;
    if (Son[now]) dfs2(Son[now], Tp) ;
    for (int k = head[now] ; k ; k = e[k].next){
        if (e[k].to == Fa[now] || e[k].to == Son[now]) continue ;
        dfs2(e[k].to, e[k].to) ;
    }
}
inline void P_u(int rt){
    Max[rt] = max(Max[Rs(rt)], Max[Ls(rt)]) ;
    Min[rt] = min(Min[Rs(rt)], Min[Ls(rt)]) ;
    S[rt] = S[Ls(rt)] + S[Rs(rt)] ;
}
inline void P_d(int rt, int L, int R){
    if (T[rt]){
        int t1 = Max[Ls(rt)], t2 = Max[Rs(rt)] ;
        T[Ls(rt)] ^= 1, T[Rs(rt)] ^= 1 ;
        Max[Ls(rt)] = -Min[Ls(rt)], Max[Rs(rt)] = -Min[Rs(rt)] ;
        Min[Ls(rt)] = -t1, Min[Rs(rt)] = -t2, S[Ls(rt)] = -S[Ls(rt)], S[Rs(rt)] = -S[Rs(rt)] ;
        T[rt] = 0 ;  
    }
}
void  _build(int rt, int L, int R){
    if (L == R) return ;
    _build(Ls(rt), L, Mid), _build(Rs(rt), Mid + 1, R) ;
    P_u(rt) ;
}
void _change(int rt, int L, int R, int l, int r, int k){
    if (l <= L && R <= r){
        S[rt] = Max[rt] = Min[rt] = k, T[rt] = 0 ; return ;
    }
    P_d(rt, L, R) ;
    if (Mid >= l) _change(Ls(rt), L, Mid, l, r, k) ;
    if (Mid < r) _change(Rs(rt), Mid + 1, R, l, r, k) ;
    P_u(rt) ;
}
void _update(int rt, int L, int R, int l, int r){
    if (l <= L && R <= r){
        int t = Max[rt] ;
        Max[rt] = -Min[rt], Min[rt] = -t ; 
        T[rt] ^= 1, S[rt] = -S[rt] ; return ;
    }
    P_d(rt, L, R) ;
    if (Mid >= l) _update(Ls(rt), L, Mid, l, r) ;
    if (Mid < r) _update(Rs(rt), Mid + 1, R, l, r) ;
    P_u(rt) ;   
}
int _query(int rt, int L, int R, int l, int r){
    if (l <= L && R <= r) return Max[rt] ;
    P_d(rt, L, R) ; int Maxxxxx = -Inf ;
    if (Mid >= l) Maxxxxx = max(Maxxxxx, _query(Ls(rt), L, Mid, l, r)) ;
    if (Mid < r) Maxxxxx = max(Maxxxxx, _query(Rs(rt), Mid + 1, R, l, r)) ;
    return Maxxxxx ;
}
int __query(int rt, int L, int R, int l, int r){
    if (l <= L && R <= r) return Min[rt] ;
    P_d(rt, L, R) ; int Minnnnn = Inf ;
    if (Mid >= l) Minnnnn = min(Minnnnn, __query(Ls(rt), L, Mid, l, r)) ;
    if (Mid < r) Minnnnn = min(Minnnnn, __query(Rs(rt), Mid + 1, R, l, r)) ;
    return Minnnnn ;
}
int ___query(int rt, int L, int R, int l, int r){
    if (l <= L && R <= r) return S[rt] ;
    P_d(rt, L, R) ; int Ssssss = 0 ;
    if (Mid >= l) Ssssss += ___query(Ls(rt), L, Mid, l, r) ;
    if (Mid < r) Ssssss += ___query(Rs(rt), Mid + 1, R, l, r) ;
    return Ssssss ;
}
inline void _Update(int u, int v){
    while(Top[u] != Top[v]){
        if(Dep[Top[u]] < Dep[Top[v]]) swap(u, v) ;
        _update(1, 1, N, Id[Top[u]], Id[u]) ;
        u = Fa[Top[u]] ;
    }
    if (Dep[u] > Dep[v]) swap(u, v) ;
    _update(1, 1, N, Id[u] + 1, Id[v]) ;
}
inline int Get_Max(int u, int v){
    int ret = -Inf ;
    while(Top[u] != Top[v]){
        if(Dep[Top[u]] < Dep[Top[v]]) swap(u, v) ;
        ret = max(ret, _query(1, 1, N, Id[Top[u]], Id[u])) ;
        u = Fa[Top[u]] ;
    }
    if (Dep[u] > Dep[v]) swap(u, v) ;
    ret = max(ret, _query(1, 1, N, Id[u] + 1, Id[v])) ;
    return ret ;
}
inline int Get_Min(int u, int v){
    int ret = Inf ;
    while(Top[u] != Top[v]){
        if(Dep[Top[u]] < Dep[Top[v]]) swap(u, v) ;
        ret = min(ret, __query(1, 1, N, Id[Top[u]], Id[u])) ;
        u = Fa[Top[u]] ;
    }
    if (Dep[u] > Dep[v]) swap(u, v) ;
    ret = min(ret, __query(1, 1, N, Id[u] + 1, Id[v])) ;
    return ret ;
}
inline int Get_Sum(int u, int v){
    int ret = 0 ;
    while(Top[u] != Top[v]){
        if(Dep[Top[u]] < Dep[Top[v]]) swap(u, v) ;
        ret += ___query(1, 1, N, Id[Top[u]], Id[u]) ;
        u = Fa[Top[u]] ;
    }
    if (u == v) return ret ;
    if (Dep[u] > Dep[v]) swap(u, v) ;
    ret += ___query(1, 1, N, Id[u] + 1, Id[v]) ;
    return ret ;
}
int main(){
    cin >> N ;
    for (i = 1 ; i < N ; ++ i)
        n[i].u = qr() + 1, n[i].v = qr() + 1, n[i].c = qr(), _Add(n[i].u, n[i].v, n[i].c) ;
    dfs1(1, 1), dfs2(1, 1), _build(1, 1, N) ;
    for (i = 1 ; i < N ; ++ i){
        if (Dep[n[i].u] > Dep[n[i].v]) swap(n[i].u,n[i].v);
        _change(1, 1, N, Id[n[i].v], Id[n[i].v], n[i].c);
    }
    cin >> M ;
    for (i = 1 ; i <= M ; ++ i){
        scanf("%s", STR) ;
        if (STR[0] == 'N') A = qr() + 1, B = qr() + 1, _Update(A, B) ;
        if (STR[0] == 'S') A = qr() + 1, B = qr() + 1, printf("%d\n", Get_Sum(A, B)) ;
        if (STR[0] == 'M' && STR[1] == 'A') A = qr() + 1, B = qr() + 1, printf("%d\n", Get_Max(A, B)) ;
        if (STR[0] == 'M' && STR[1] == 'I') A = qr() + 1, B = qr() + 1, printf("%d\n", Get_Min(A, B)) ;
        if (STR[0] == 'C') A = qr(), B = qr(), n[A].c = B, _change(1 ,1, N, Id[n[A].v], Id[n[A].v], B) ;
    }
    return 0 ;
}

这个题有一个取相反数的操作,遇到这种正反都需要考虑的,\(tag\)一定是异或而不是覆盖 ……是一个坑点

\(\mathscr{T2 ~\text{の} ~code}\)

// luogu-judger-enable-o2
#include <cstdio>
#include <cstring>
#include <iostream>
#define MAX 200010
#define Ls(x) (x << 1)
#define Rs(x) (x << 1 | 1)
#define Mid ((L + R) >> 1)

using namespace std ;
struct Node{
    int u, v, c ;
}n[MAX << 1] ;
struct Edge{
    int v, next, to ; //0 = Change, 1 = Add .
}e[MAX << 1] ; char STR[50] ; int A, B, C ;
int Rec[MAX], Id[MAX], Aft[MAX] ;int N, tot ;
int M[MAX << 1], T[MAX << 1][2], head[MAX], cnt, i ;
int Top[MAX], Fa[MAX], Sum[MAX], Son[MAX], Dep[MAX];

inline int qr(){
    int res = 0, f = 1 ; char c = getchar() ;
    while (!isdigit(c)) {if (c == '-') f = -1 ; c = getchar() ;}
    while (isdigit(c)) res = (res << 1) + (res << 3) + c - 48, c = getchar() ;
    return res * f ;
}
inline void _Add(int u, int v, int c){
    e[++ cnt].v = c, e[cnt].to = v ;
    e[cnt].next = head[u], head[u] = cnt ;
    e[++ cnt].v = c, e[cnt].to = u ;
    e[cnt].next = head[v], head[v] = cnt ;
}
void dfs1(int F, int now){
    Sum[now] = 1, Fa[now] = F, Dep[now] = Dep[F] + 1 ;
    for (int k = head[now] ; k ; k = e[k].next){
        if (e[k].to == F) continue ;
        dfs1(now, e[k].to) ;
        Sum[now] += Sum[e[k].to] ;
        if (Sum[e[k].to] > Sum[Son[now]]) Son[now] = e[k].to ;
    }
}
void dfs2(int now, int Tp){
    Id[now] = ++ tot, Top[now] = Tp ;
    if (Son[now]) dfs2(Son[now], Tp) ;
    for (int k = head[now] ; k ; k = e[k].next){
        if (e[k].to == Fa[now] || e[k].to == Son[now]) continue ;
        dfs2(e[k].to, e[k].to) ;
    }
}
inline void P_u(int rt){M[rt] = max(M[Rs(rt)], M[Ls(rt)]) ;}
void P_d(int rt, int L, int R){
    if(L == R) return;
    if (T[rt][0] != -1){
        M[Ls(rt)] = T[rt][0], M[Rs(rt)] = T[rt][0] ;
        T[Rs(rt)][0] = T[rt][0], T[Ls(rt)][0] = T[rt][0]; 
        T[rt][0] = -1, T[Rs(rt)][1] = 0, T[Ls(rt)][1] = 0 ;
    }
    if (T[rt][1]){
        M[Ls(rt)] += T[rt][1], M[Rs(rt)] += T[rt][1] ;
        T[Rs(rt)][1] += T[rt][1], T[Ls(rt)][1] += T[rt][1] ; 
        T[rt][1] = 0 ;  
    }
}
void  _build(int rt, int L, int R){
    T[rt][0] = -1 ;
    if (L == R) {M[rt] = 0 ; return ;}
    _build(Ls(rt), L, Mid), _build(Rs(rt), Mid + 1, R) ;
    P_u(rt) ;
}
void _change(int rt, int L, int R, int l, int r, int k){
    if (l <= L && R <= r){T[rt][0] = M[rt] = k, T[rt][1] = 0 ; return ;}
    P_d(rt, L, R) ;
    if (Mid >= l) _change(Ls(rt), L, Mid, l, r, k) ;
    if (Mid < r) _change(Rs(rt), Mid + 1, R, l, r, k) ;
    P_u(rt) ;
}
void _update(int rt, int L, int R, int l, int r, int k){
    if (l <= L && R <= r){T[rt][1] += k, M[rt] += k ; return ;}
    P_d(rt, L, R) ;
    if (Mid >= l) _update(Ls(rt), L, Mid, l, r, k) ;
    if (Mid < r) _update(Rs(rt), Mid + 1, R, l, r, k) ;
    P_u(rt) ;   
}
int _query(int rt, int L, int R, int l, int r){
    if (l <= L && R <= r) return M[rt] ;
    P_d(rt, L, R) ; int Maxxxxx = -1 ;
    if (Mid >= l) Maxxxxx = max(Maxxxxx, _query(Ls(rt), L, Mid, l, r)) ;
    if (Mid < r) Maxxxxx = max(Maxxxxx, _query(Rs(rt), Mid + 1, R, l, r)) ;
    return Maxxxxx ;
}
inline void _Update(int u, int v, int d){
    while(Top[u] != Top[v]){
        if(Dep[Top[u]] < Dep[Top[v]]) swap(u, v) ;
        _update(1, 1, N, Id[Top[u]], Id[u], d) ;
        u = Fa[Top[u]] ;
    }
    if (u == v) return ;
    if (Dep[u] > Dep[v]) swap(u, v) ;
    _update(1, 1, N, Id[u] + 1, Id[v], d) ;
}
inline int Get_Max(int u, int v){
    int ret = -1 ;
    while(Top[u] != Top[v]){
        if(Dep[Top[u]] < Dep[Top[v]]) swap(u, v) ;
        ret = max(ret, _query(1, 1, N, Id[Top[u]], Id[u])) ;
        u = Fa[Top[u]] ;
    }
    if (u == v) return ret ;
    if (Dep[u] > Dep[v]) swap(u, v) ;
    ret = max(ret, _query(1, 1, N, Id[u] + 1, Id[v])) ;
    return ret ;
}
inline void _Cover(int u, int v, int d){
    while(Top[u] != Top[v]){
        if(Dep[Top[u]] < Dep[Top[v]]) swap(u, v) ;
        _change(1, 1, N, Id[Top[u]], Id[u], d) ;
        u = Fa[Top[u]] ;
    }
    if (u == v) return ;
    if (Dep[u] > Dep[v]) swap(u, v) ;
    _change(1, 1, N, Id[u] + 1, Id[v], d) ;
}
int main(){
    cin >> N ;
    for (i = 1 ; i < N ; ++ i)
        n[i].u = qr(), n[i].v = qr(), n[i].c = qr(), _Add(n[i].u, n[i].v, n[i].c) ;
    dfs1(1, 1), dfs2(1, 1), _build(1, 1, N) ;
    for (i = 1 ; i < N ; ++ i){
        if (Dep[n[i].u] > Dep[n[i].v]) swap(n[i].u,n[i].v);
        _change(1, 1, N, Id[n[i].v], Id[n[i].v], n[i].c);
    }
    cin >> STR ;
    while(STR[0] != 'S'){
        if (STR[0] == 'A') A = qr(), B = qr(), C = qr(), _Update(A, B, C) ;
        if (STR[0] == 'M') A = qr(), B = qr(), printf("%d\n", Get_Max(A, B)) ;
        if (STR[0] == 'C' && STR[1] == 'o') A = qr(), B = qr(), C = qr(), _Cover(A, B, C) ;    
        if (STR[0] == 'C' && STR[1] == 'h') A = qr(), B = qr(), n[A].c = B, _change(1 ,1, N, Id[n[A].v], Id[n[A].v], B) ;
        scanf("%s", STR) ;
    }
    return 0 ;
}

这个题也有一个坑点,不过我似乎注意到了,就是它实际上有两种需要下传的标记,一种是赋值,一种是加,在算标记的时候注意一下就好了 。

与树形\(\mathbb{DP}\) 的巧妙结合

这个地方其实说的是一类问题。。。比如一道我根本不会的 经典题:

\(Description\)

一棵带权树,\(n\)个点\(q\)次询问,每次询问一个点的\(\sum dis_{odd}\)\(\sum dis_{even}\)

\(\mathcal{Solution}\)

其实这个题的题目意思是让我们求:

\(R\)君想知道对于每个点来说,到这个点是距离奇数的节点的距离和,与到这个点距离是偶数的节点的距离和。

那么我们不妨先从一个比较简单的问题开始考虑:如何求一个点到它子树的距离和呢?

这个东西比较简单,因为我们可以直接\(dfs\)

那么如果是求所有点到所有点的距离和呢?

换句话说,我们可以把刚才这个十分简单的问题,升华成为一个稍微困难一些的问题:对于每个点\(u\),求所有点与它的距离之和,距离依边权而定。

我首先忽略边权,令边权都是\(1\),那么我们思考,我们根据已知条件,很容易知道的是什么呢?我们可以通过上一个问题知道根节点到所有点的距离之和,同时我们也可以知道每个节点子树内的边权之和,那么如何利用这些呢?

我们会发现,此时我们知道的量,同一般的树形\(DP\)或者说树形结构不同,我们现在已经知道了每一个子树的根节点的讯息;而平常的树形结构,我们知道的则是子节点的讯息\(or\)子树的讯息,这一点提示我们:反向\(DP\)

我们不妨设\(dp_u\)表示\(\sum \limits_{i \in T}^{}{dist(i,u)}\) 那么我们思考如何构造相邻两层的状态转移方程:

首先,我们已经决定反向\(DP\),换句话说就是用父亲推出儿子;并且我们了解到,对于某一个点\(u\),他的所有子节点到他的距离要比到他的父亲的距离少\(1\)(假设边权\(=1\)),同时所有除其子树之外的所有点到他的距离会多\(1\).那么转移方程旧顺水推舟地:

\(dp_v = dp_u - subsize_v + (n - subsize_v), v \in son_u\)

那么接下来我们思考,当边权不为\(1\)的时候呢?我们可以稍微魔改一下上式,于是就得到

\(dp_v = dp_u - 2 \cdot pre_v \cdot subsize_v + N \cdot pre_v\)

其中\(pre_i\)表示节点\(i\)的上行边,即父亲与它相连的那条边。

那么以上简单版本的代码:

#include <cstdio>
#include <iostream> 
#define MAX 100010
#define to(k) E[k].to

using namespace std ;
struct Edge{
	int to, next ;
}E[MAX] ; int head[MAX], cnt ; 
int N, A, B, i, j, dp[MAX], sub[MAX], Pre[MAX] ; 

inline void _Add(int u, int v){
	E[++ cnt].to = v, E[cnt].next = head[u], head[u] = cnt ;
	E[++ cnt].to = u, E[cnt].next = head[v], head[v] = cnt ;
}
void build(int now, int f){
	sub[now] = 1 ;
	for (int k = head[now] ; k ; k = E[k].next){
		if (to(k) == f) continue ;
		build(to(k), now) ;  
		sub[now] += sub[E[k].to], dp[1] += sub[E[k].to] ;
	}
	return ;
}
void fwork(int now, int f){
	for (int k = head[now] ; k ; k = E[k].next){
		if (E[k].to == f) continue ;
		dp[E[k].to] = dp[now] - 2 * sub[E[k].to] + N ;
		fwork(to(k), now) ; 
	}
}
int main(){
	cin >> N ;
	for (i = 1 ; i < N ; ++ i) 
		scanf("%d%d", &A, &B), _Add(A, B) ;
	build(1, 0) ; fwork(1, 0) ; 
	for (i = 1 ; i <= N ; ++ i) printf("%d\n", dp[i]) ;
}

唯一值得注意的地方就是其中\(dp_1\)或者说\(dp_{root}\)的处理。那么其实这个地方我们只需要不断加\(size\)即可。

高端版本的问题,代码类似:

#include <cstdio>
#include <iostream>
#define MAX 200010
#define to(k) E[k].to

using namespace std ;
struct Edge{
	int to, next, v ;
}E[MAX << 1] ; int head[MAX], cnt, i ;
int N, Sub[MAX], Pre[MAX], dp[MAX], Sum, A, B, C ;

void build(int now, int f){
	Sub[now] = 1 ;
	for (int k = head[now] ; k ; k = E[k].next){
		if (to(k) == f) continue ;
		build(to(k), now) ;  Pre[to(k)] = E[k].v ;
		Sub[now] += Sub[to(k)], dp[1] += Pre[to(k)] * Sub[to(k)] ;
	}
	return ;
}
void dp_work(int now, int f){
	for (int k = head[now] ; k ; k = E[k].next){
		if (to(k) == f) continue ;
		dp[to(k)] = dp[now] - 2 * Pre[to(k)] * Sub[to(k)] + N * Pre[to(k)] ; dp_work(to(k), now) ;
	}
}
inline void Add(int u, int v, int w){
	E[++ cnt].to = v, E[cnt].v = w, E[cnt].next = head[u], head[u] = cnt ;
	E[++ cnt].to = u, E[cnt].v = w, E[cnt].next = head[v], head[v] = cnt ;
}
int main(){
	cin >> N ; 
	for (i = 1 ; i < N ; ++ i) 
		scanf("%d%d%d", &A, &B, &C), Add(A, B, C) ;;
	build(1, 0) ; dp_work(1, 0) ; 
	for (i = 1 ; i <= N ; ++ i) cout << dp[i] << endl ;
}

那么回归到我们的问题,我们要求的是奇数距离\(\&\)偶数距离,很简单,再加一维即可。

我们致力于维护这样一个东西:对于每个\(u \in T\), 我们试图确定除\(u\)及其子树外,到\(u\)点距离是奇数的点的个数\(S0\)\(\&\)距离是偶数的点的个数\(S1\),以便于状态转移时,作为第二部分。那么平均树高下,直接在\(dfs/bfs\)里面来回赋值即可。但是还是有需要注意的地方——比如说边为奇数的时候,父子节点的两个值需要\(swap\)一下,并且边权为奇数的时候,是交叉转移的(奇数状态转移给偶数状态):

\[dp[to(k)][0] = dp[now][1] - Sub[to(k)][0] * Pre[to(k)] \\ dp[to(k)][1] = dp[now][0] - Sub[to(k)][1] * Pre[to(k)] ,\\ dp[to(k)][0] += S0 * Pre[to(k)] , \\ dp[to(k)][1] += S1 * Pre[to(k)] ; \]

其中\(to(k) \in son_{now}\)

而偶数的时候,显然不是交叉转移的(奇数状态转移给奇数状态):

\[dp[to(k)][1] = dp[now][1] - Sub[to(k)][1] * Pre[to(k)] ;\\ dp[to(k)][0] = dp[now][0] - Sub[to(k)][0] * Pre[to(k)] ;\\ dp[to(k)][1] += S1 * Pre[to(k)],\\ dp[to(k)][0] += S0 * Pre[to(k)] ; \]

于是就结束了。其实这个地方转移方式十分的多,但是关键的装套转移方程其实就是一句:

\(dp_v = dp_u - 2 \cdot pre_v \cdot subsize_v + Count \cdot pre_v\)

其中\(Count\)表示某种神秘的计数。。。。

那么其实这种问题还可以拓展到“求对于每一个点\(u\)\(dist(u,v) \mod n =k\)的点的个数,其中\(k \in [0,k-1]\),我感觉做法应该会类似吧(但是我肯定不会做因为根本不可能调的出来)

结语

  • 没啥好说的,只觉得第三模块的例题十分难调!十分难调!!!并且……我一开始没用\(S0\)或者\(S1\),直接用的根节点的\(sub_0\)\(sub_1\),最后才法案根本不和逻辑。。。但他居然过样例了。。。
  • 转移的时候还是需要有一个清醒的思路再code啊!
  • DP好啊!
posted @ 2018-10-24 20:55  皎月半洒花  阅读(353)  评论(0编辑  收藏  举报