F树形 DP

方案数 / 距离统计

联合权值·括号树

本课摘要

方案数 / 距离统计课程回答“树上方案数与距离统计如何在合并时避免重复”。内容以联合权值·括号树为主线,配合逐步推导、可编辑演示、例题与练习形成可复查的学习闭环。

  • 判断方案数 / 距离统计的适用条件与状态边界
  • 围绕“联合权值·括号树”推导转移与计算顺序
  • 用演示、复杂度分析和配套题目校验实现
本课目录 · 0 节

正在整理目录…

从「求最优」转向「数东西」

前几类都在求极值(最大权、最小造价、最长链)。这一类换一副眼镜:统计—— 数满足某种条件的点对 / 路径 / 子串有多少、或它们的某个量之和是多少。转移里 max⁡\max 常换成求和 ++ 或乘法。

先看一类清爽的:距离恰为 2 的点对统计。给树上每点一个权,要求所有距离为 2 的点对,其权乘积之和(以及最大乘积)。 直接两两枚举点对是 O(n2)O(n^2),n=2×105n=2\times10^5 不可行。关键观察一句话解决:

两点距离为 2,当且仅当它们有一个公共邻居(那个邻居是路径的中间点)。于是不枚举点对,改枚举中间点 mm——m 的任意两个邻居就构成一个距离 2 点对。

mabdist(a,b) = 21 步1 步中间点 m枚举中间点 m,它的邻居两两配对即所有距离 2 点对
距离 2 的点对 (a,b)(a,b) 必经一个中间点 mm;枚举 m,它的邻居两两配对即所有此类点对。

O(度) 一次算完一个中间点

固定中间点 mm,设它的邻居权为 x1,x2,…,xkx_1,x_2,\dots,x_k。所有有序点对的乘积之和是:

∑i≠jxixj=(∑ixi)2−∑ixi2\sum_{i\ne j} x_i x_j=\Big(\sum_i x_i\Big)^2-\sum_i x_i^2

这是一个恒等式:「和的平方」减去「平方和」,恰好去掉了 i=ji=j 的对角项,剩下的正是所有 i≠ji\ne j 的交叉乘积。 只需扫一遍 m 的邻居累加 ∑x\sum x 与 ∑x2\sum x^2,O(deg⁡m)O(\deg m) 就得到以 m 为中点的乘积和; 所有中间点加起来,总和是 ∑mdeg⁡m=O(n)\sum_m \deg m=O(n)。

最大乘积同理:维护 m 邻居里的最大与次大两个权,x(1)⋅x(2)x_{(1)}\cdot x_{(2)} 即以 m 为中点的最大乘积;全局取最大。

1523344152金色序号 = 处理次序:叶子 4、5 先算好,父亲 2 才能合并;根 1 最后收口。
在树上,m 的邻居 = 父亲 + 所有孩子;一遍 DFS 到每个点时就地统计,无需额外遍历。

本质

距离统计的通用招数是换枚举对象:不枚举「点对」而枚举「中间点 / 路径拐点」,把 O(n2)O(n^2) 的两两配对,压成每个点 O(deg⁡)O(\deg) 的局部统计。配合「和的平方 − 平方和」这类恒等式,一次算完一个中心的全部贡献。这是树上「数点对」的核心思维。

跟着算一遍

小树:根 11 带 2,3,42,3,4;11 的孩子 22 又带 5,65,6。权 w=[5,3,2,4,6,1]w=[5,3,2,4,6,1]。逐个中间点统计:

1
中间点 1(邻居 2、3、4,权 3、2、4)。∑x=9, ∑x2=9+4+16=29\sum x=9,\ \sum x^2=9+4+16=29。乘积和 =92−29=81−29=52=9^2-29=81-29=52。
2
中间点 2(邻居 1、5、6,权 5、6、1)。∑x=12, ∑x2=25+36+1=62\sum x=12,\ \sum x^2=25+36+1=62。乘积和 =144−62=82=144-62=82。
3
中间点 3、4、5、6 都是叶子,只有 1 个邻居,凑不出点对,贡献 0。
✓
总乘积和 52+82=13452+82=134;最大乘积出现在中间点 2 的「5×6=30」。全程一遍 DFS。
下面的演示让你点选中间点,高亮它的邻居并列出所有距离 2 点对与乘积和;改点权实时重算。

点选中间点,看点对怎么冒出来

改点权,点任意节点当「中间点」
1
点 1 · 权
5
2
点 2 · 权
3
3
点 3 · 权
2
4
点 4 · 权
4
5
点 5 · 权
6
6
点 6 · 权
1
距离恰为 2 的点对 ⇔ 有公共中间点。点选中间点 1,它的邻居两两配对就是所有以它为中点的距离 2 点对。 全树联合权值总和 = 134,最大 = 30。
1w=52w=33w=24w=45w=66w=1
以 1 为中点的点对: (2,3)、(2,4)、(3,4)。乘积之和 = 3×2 + 3×4 + 2×4 = 52(有序对,正反各算一次)。 O(度) 一次算完,无需两两枚举。

沿根链递推:括号树的 O(1) 计数

另一支是沿「根到点」的链递推方案计数。括号树:每个节点写着一个 (( 或 )), 从根到某点的路径拼成一个括号串。要数出所有节点对应的根链里,合法括号子串的总数。

暴力对每个点重扫根链是 O(n2)O(n^2)。妙处在于:设 f[u]f[u] = 「以 u 这个字符结尾的合法括号子串数」, 它能从父亲 O(1) 递推——若 uu 是 )) 且能与链上某个 (( 配对(设那个 (( 的前一位是 pp),则

f[u]=f[p]+1f[u]=f[p]+1

读作:以 u 结尾的合法子串 = 「以 p 结尾的合法子串」全部各自向右接上这对括号,再加「刚配好的这一对」本身。 用一个栈沿 DFS 维护未匹配的 ((,进入子树时压栈 / 匹配,回溯时撤销。答案 = ∑uf[u]\sum_u f[u]。

(f=0(f=0)f=1)f=2沿根到点的链:每个 ) 若配对成功,f[u] = f[配对(的前驱] + 1「(())」在最后一位结尾有 2 个合法子串:() 与 (())——f 逐位 O(1) 累进,无需重扫
根链「(())」:每位的 ff 由父亲 O(1)O(1) 递推,末位结尾有 2 个合法子串(()() 与 (())(()))。

常见陷阱:DFS 上的栈必须回溯撤销

括号树是在树上而非一条链上递推——从一个子树退回父亲、再进入另一个子树时,前一支压入栈的 (( 必须弹出还原,否则会串味。标准写法是「进入时记下本层对栈的修改,递归返回后原样撤销」(可回滚栈)。另外 f[u]f[u] 与答案都可能超 int,用 long long\texttt{long long}。

这两支——枚举中间点做距离统计与沿根链 O(1) 递推计数——覆盖了树上「数东西」的两大范式: 前者靠「换枚举对象 + 恒等式」摊平代价,后者靠「父到子的增量递推」避免重扫。它们和 覆盖 / 染色 里的方案计数一脉相承,只是把极值算子换成了求和 / 乘法。

例题

P1351[NOIP2014] 联合权值NOIP 2014 提高组普及+/提高
题意
无根树每点有权 wiw_i。距离恰为 2 的有序点对 (u,v)(u,v) 的「联合权值」= wu⋅wvw_u\cdot w_v。求所有联合权值的最大值与之和(对 10007 取模)。
对应关系
距离 2 ⇔ 有公共中间点。枚举中间点 m,其邻居两两配对;乘积和用 (∑w)2−∑w2(\sum w)^2-\sum w^2,最大乘积用「最大 × 次大」。
为什么选它
用最小的状态把「距离统计」讲透的 NOIP 真题——不需要复杂 DP 数组,只需一个恒等式 + 一遍 DFS,是距离统计入门的最佳载体。
转移 · 复杂度
每个中间点 O(deg⁡)O(\deg),总 O(n)O(n)。
参考代码(枚举中间点)
#include <iostream>
#include <vector>
using namespace std;

const int N = 200005;
const long long MOD = 10007;
vector<int> g[N];
long long w[N];
long long sumAns, maxAns;

void dfs(int u, int fa)
{
    long long s1 = 0, s2 = 0;         // 邻居权和、平方和
    long long mx1 = 0, mx2 = 0;       // 最大、次大邻居权

    for (int v : g[u])                // ★邻居 = 所有相连点(父 + 孩子)
    {
        s1 = (s1 + w[v]) % MOD;
        s2 = (s2 + w[v] * w[v]) % MOD;
        if (w[v] > mx1) { mx2 = mx1; mx1 = w[v]; }
        else if (w[v] > mx2) mx2 = w[v];
    }

    // 以 u 为中间点的所有距离 2 有序点对:乘积和 = (Σw)² − Σw²
    sumAns = (sumAns + (s1 * s1 - s2) % MOD + MOD) % MOD;
    maxAns = max(maxAns, mx1 * mx2);  // 最大乘积 = 最大 × 次大

    for (int v : g[u])
        if (v != fa) dfs(v, u);
}

int main()
{
    int n;
    cin >> n;
    for (int i = 1; i < n; i++)
    {
        int a, b;
        cin >> a >> b;
        g[a].push_back(b);
        g[b].push_back(a);
    }
    for (int i = 1; i <= n; i++)
        cin >> w[i];

    dfs(1, 0);
    cout << maxAns << " " << sumAns << endl;
    return 0;
}
P5658[CSP-S2019] 括号树CSP-S 2019提高+/省选-
题意
nn 个节点的树,每点标 (( 或 ))。对每个点 uu,数根到 u 的字符串里合法括号子串的个数 kuk_u,输出所有 kuk_u(题目要求异或和形式)。
为什么选它
「父到子 O(1)O(1) 递推方案计数」的漂亮范例,CSP 真题热度高。f[u]=f[p]+1f[u]=f[p]+1 的递推 + DFS 上可回滚栈,把 O(n2)O(n^2) 降到 O(n)O(n)。
转移 · 复杂度
f[u]=f[p]+1f[u]=f[p]+1(u 为 )) 且成功匹配时,pp 是与之配对的 (( 的前驱),累加得答案;O(n)O(n)。
参考代码(DFS + 可回滚栈,主干示意)
#include <iostream>
#include <vector>
#include <string>
using namespace std;

const int N = 500005;
vector<int> g[N];
string s;                     // 每个点上是 '(' 或 ')'
long long f[N];               // f[u]:以 u 结尾、向上到根方向的合法括号子串数
long long ans;
int stk[N], top;              // 用栈匹配括号(栈存节点编号)
int match[N];                 // match[u]:与 u 配对的那个 '(' 的父亲上一位

void dfs(int u, int fa)
{
    int saved = -1;           // 记录本层对栈的修改,回溯时撤销
    if (s[u - 1] == '(')      // '(' 入栈
    {
        stk[++top] = u;
        // f[u] 继承父亲:新的 '(' 自身不能结尾合法串
        f[u] = f[fa];
    }
    else                      // ')' 尝试与栈顶配对
    {
        if (top > 0)
        {
            int p = stk[top--]; // 弹出配对的 '('
            saved = p;
            // p 的父亲那条链上的 f + 本次新增的 1 个(p..u 这一对)
            f[u] = f[/*p 的父亲*/ fa] + 1;   // 示意:真实实现用 match 链递推
        }
        else
            f[u] = 0;
    }

    ans += f[u];              // ★累加:每个点贡献「以它结尾的合法子串数」

    for (int v : g[u])
        if (v != fa) dfs(v, u);

    if (s[u - 1] == '(') top--;          // 撤销入栈
    else if (saved != -1) stk[++top] = saved;  // 撤销出栈
}

int main()
{
    int n;
    cin >> n >> s;
    for (int i = 2; i <= n; i++)
    {
        int fa;
        cin >> fa;
        g[fa].push_back(i);
    }

    top = 0;
    dfs(1, 0);
    cout << ans << endl;
    return 0;
}

练习

P2585[ZJOI2006] 三色二叉树(计数向)把「求绿点极值」改成「数合法染色方案」:内层枚举 (a,b) 合法颜色对时,方案数相乘、对颜色求和。同树同约束,算子从 max 换成累乘累加。在洛谷打开
P1131[ZJOI2007] 时态同步统计/合并型:f[u] = u 子树内到叶子的最长链,每条子边补齐到最长的增量累加。是「沿子树统计路径长度」的练习。在洛谷打开
P1352没有上司的舞会(回顾)回到选点:把它当计数思维的对照——同样一遍 DFS 合并子树,只是聚合的是「最大权」而非「计数」。对比体会算子之别。在洛谷打开

已进入 方案数 / 距离统计 · 树形 DP · DP大师