第一节 树形 dp (草稿版)

偷看我题解的是哪位,为什么每次一加东西就发现浏览量变大。(没有不让看的意思,欢迎批评。)

不推荐把这玩意当题解用,废话有点多还有点不聪明不是么。

总述

很恶心的一个到现在为止基本完全不会的东西。基本思路是先进行递归,考虑最小的子树(单个节点,叶节点,或是边界情况),然后进行回溯时合并。

[USACO17DEC] Barn Painting G

染色,共三种颜色,给定一些染色完成的,要求相邻的不能同色,求方案数。

显然三种颜色应拆成三个状态处理,最终答案为根节点染成什么东西都可以。

该节点的颜色选定时,其以下任意节点均可以为剩下两种颜色任选其一,即方案数先相加后累乘。

然后套个上面的板子就做完了。

「dWoi R2」Arcade hall / 街机厅

一棵树,点权具有上限,每条边约束两个点点权关系不相等,相等或没有约束,求选择点权方案数及点权和最小的方案。

考虑先讨论第一问。如果从子节点向上考虑,会出现约束过多导致无法合理计算的情况,不妨考虑从根节点向下考虑。对于一个结点,根据其与父节点连边的种类,若为相等边则有 \(1\) 种方案,若为不相等边则有 \(R - 1\) 种方案,若不存在或没有约束则有 \(R\) 种方案。这是显然的。

每个选择相互独立且为不同步骤,故上述式相乘即为答案。

第二问考虑记 \(f_{i, j}\) 为该节点选 \(j\) 时,该子树的最小权值和。不要太刻板,从上向下递推也是可行的。

则对于节点 \(u\) 选择 \(k\) 时,考虑子节点 \(v\) 时,其中间边权为:

  • 不相等,则选取 \(f_{v, p}(p \neq k)\) 的最小值,并 \(+ k\)。
  • 相等,则选取 \(f_{v, k}\),并 \(+ k\)。
  • 无约束,则选取 \(f_{v, p}\) 的最小值,并 \(+ k\)。

最终答案为树根随便选个数的最小值。注意到显然超时了,此时时间复杂度为 \(O(nR^2)\)。

显然优化选择子节点点权较显著。考虑条件 \(p \neq k\) 是最恶心的东西,我们将其变为 \(p < k\) 或 \(p > k\),则此时发现其为两段完整区间 \([1, p - 1]\) 和 \([p + 1, R]\),显然可以用前缀和预处理。则分别递推最小前缀,对于不相等情况,改为 \(\min \{L_{p - 1}, R_{p + 1}\}\) 即可。对于相等情况,改为 \(L_{R}\) 即可。请注意前缀和需要 \(n\) 个,否则会干扰运算。

时间复杂度 \(O(nR)\)。复建训练还是要做啊。

本题题解还提到了题目“迷失森林”,可做。

[SDOI2013] 直径

某个傻子忘了直径怎么求,特此批评。

直径的求法:假设这个点为根,则其直径为其之下路径长度最大值与次大值之和,故记两个数组分别为对应子树的路径长度(注意不是直径)最大值和次大值,则在子树中其直径为两个数组对应值之和,参考如下:

void dfs(int u, int f){
	for (pair<int, int> v : e[u]){
		if (v.first == f) continue;
		dfs(v.first, u);
		long long t = dp1[v.first] + v.second;
		if (t > dp1[u]){
			dp2[u] = dp1[u];
			dp1[u] = t;
		}
		else if (t > dp2[u]){
			dp2[u] = t;
		}
	}
	ans = max(ans, dp1[u] + dp2[u]);
	return ;
}

也可 dfs 求直径,方法为两次求最长路找到的两个点必然是树直径的两端。

void dfs(int u, int fa){
    if (!fa) memset(f, 0, sizeof f);
    f[u] = fa;
    if (dis[p][u] > dis[p][r]) r = u;
    for (edge g : e[u]){
        int v = g.v, t = g.t;
        if (v == f[u]) continue;
        dis[p][v] = dis[p][u] + t;
        dfs(v, u);
    }
}

int main(){
    cin >> n >> m;
    for (int i = 1; i <= m; i++){
        int u, v, t; cin >> u >> v >> t;
        e[u].push_back({v, t});
        e[v].push_back({u, t});
    }
    int st, ed;
    p = 0; dfs(1, 0); st = r;
    p = 1; dfs(st, 0); ed = r;
    p = 2; dfs(ed, 0);

答案即为 dis[2][st]。

引理:树的所有直径拥有相同的中点。证明:若其不拥有相同的中点,显然。

考虑这些满足条件的边的分布是从中点开始向两端扩散的,且其具有边界 \((x, y)\)。

所以只需要求出 \(x, y\) 之间的距离即可。

故求出直径后找到任意一条直径,并沿非直径边试图“等长替换”直径边,若与原来到左侧节点的长度相等,则右侧节点左移;若与原来到右侧节点的长度相等,则左侧端点右移;否则该点更新失败。

#include <bits/stdc++.h>
using namespace std;

const int N = 2e5 + 5;

struct edge {
    int v, t;
};

int n, f[N], st, ed, vis[N], ans = 1;
long long mx, r, depth[N];
vector<edge> e[N];

void dfs(int u, int fa){
    f[u] = fa;
    for (edge p : e[u]){
        int v = p.v, t = p.t;
        if (v == f[u]) continue;
        depth[v] = depth[u] + t;
        if (depth[v] > mx){
            mx = depth[v];
            r = v;
        }
        dfs(v, u);
    }
}

void dfs2(int u, int fa){
    for (edge p : e[u]){
        int v = p.v, t = p.t;
        if (v == f[u] || vis[v]) continue;
        depth[v] = depth[u] + t;
        mx = max(mx, depth[v]);
        dfs2(v, u);
    }
}

int main(){
    cin >> n;
    for (int i = 1; i < n; i++){
        int u, v, t; cin >> u >> v >> t;
        e[u].push_back({v, t});
        e[v].push_back({u, t});
    }
    dfs(1, 0); st = r;
    depth[st] = 0; mx = 0; dfs(st, 0); ed = r;
    cout << mx << endl;
    for (int i = ed; i; i = f[i]) vis[i] = true;
    int l = st, r = ed;
    bool flag = false;
    for (int i = f[r]; i != l; i = f[i]){
        int ld = depth[i], rd = depth[r] - depth[i];
        mx = depth[i] = 0; dfs2(i, 0);
        if (mx == rd) { ed = i; }
        if (mx == ld && !flag) { st = i; flag = true; }
    }
    for (int i = f[ed]; i != st; i = f[i]) ans++;
    cout << ans << endl;
    return 0;
}

你给我记得清空。

[HAOI2009] 毛毛虫

停课的第一天。________。(完形填空。)

感觉这是什么奇怪直径的变种,但是居然一眼看上去说不出来。(?)

考虑按照树形 dp 求直径类似的方法进行求解。

对于一个“核心节点”,考虑求出其最长的两条“毛毛虫”链。然后,以该节点为根的树的答案即为最长链、次长链与还未被选中的相邻节点之和。

怎么又有点绕不过来了是啥情况这个。

哦搞错了,只能从直径向外扩张一个。哦没搞错,我干啥呢。

OK 感觉我们的复健训练有点成功了。

核心代码如下,相信自己一定能再看懂!:

void dfs(int u, int f){
    for (int v : e[u]){
        if (v == f) continue;
        dfs(v, u);
        if (dp1[v] >= dp1[u]) { dp2[u] = dp1[u]; dp1[u] = dp1[v]; }
        else if (dp1[v] > dp2[u]) { dp2[u] = dp1[v]; }
    }
    ans = max(ans, dp1[u] + dp2[u] + (int)e[u].size() - (dp1[u] != 0) - (dp2[u] != 0) + 1);
    dp1[u] += (int)e[u].size() - (dp1[u] != 0) - (f != 0) + 1;
    return ;
}

[NOIP 2006 提高组] 金明的预算方案

感觉其实带依赖的 dp 一般是树形 dp。

考虑正向建边还是反向建边。好吧其实根本不分啥正反。一个点只可能依赖一个点,所以显然这棵树应该以被依赖的点为父节点。除此以外我们希望树是尽量完整的,故我们增加超级原点 \(0\),建立费用为 \(0\)。

啊啊啊好恶心题里给的 \(n, m\) 和习惯的是反的!!

哦这个题还可以让一块钱等于十块钱,不然貌似开不下空间。

其实这道题貌似还有别的做法,而且树深一点应该也还可以吧。

坏了有点忘了咋写了。

?好家伙破局之处在于只有两个附件。恭喜我第一句话错了。那我树形 dp 怎么办。

哦哦哦对还要把题里的东西乘回来来着差点忘了。

首先对于一个点,如果能买就给买的状态写个数。遍历子结点,先复制出来一份表示转移之前的内容,然后从子结点试图转移,并加对应的贡献,如下:

void dfs(int u){
    if (a[u] <= m)
        dp[u][a[u]] = max(dp[u][a[u]], a[u] * w[u]);
    int old[3205];
    for (int v : e[u]){
        dfs(v);
        memcpy(old, dp[u], sizeof old);
        for (int i = 0; i <= m; i++){
            if (old[i] < 0) continue;
            for (int j = 0; j <= m - i; j++){
                if (dp[v][j] < 0) continue;
                dp[u][i + j] = max(dp[u][i + j], old[i] + dp[v][j]);
            }
        }
    }
    return ;
}

好吧确实时间复杂度不太优,得亏能 \(/10\) 计算。时间复杂度 \(O(nm^2)\)。

绿题 \(300\) 祭。

[USACO10MAR] Great Cow Gathering G

很标准化的一道题啊。虽然并没有想到应该咋做才对。

哎我的天感觉被坑大了。这明显在提示换根 dp 我居然一点都没有看出来。

考虑已经计算出节点 \(1\) 作为根节点时,如果移动一下根会发生什么。这时,移动到的那个子树的大小会减小边权乘上奶牛数,而其他节点将增加边权乘剩余奶牛数,则可以先进行第一次的动态规划计算子树大小和根节点的答案,然后第二次递归转移即可。

哎看来换根动态规划也不太熟。

void dfs(int u, int f){
    sz[u] = a[u];
    for (edge g : e[u]){
        int v = g.v, t = g.t;
        if (v == f) continue;
        dfs(v, u);
        sz[u] += sz[v];
        dp[1] += 1ll * sz[v] * t;
    }
    return ;
}

void dfs2(int u, int f){
    for (edge g : e[u]){
        int v = g.v, t = g.t;
        if (v == f) continue;
        dp[v] = dp[u] + 1ll * (p - 2 * sz[v]) * t;
        dfs2(v, u);
    }
    return ;
}

迷失森林

不在题单之内,但被认为是一道好题的题。故先来完成这道题。

看到题目显然是在求直径。考虑一颗“单位树”的直径与答案有何关联。

考虑模仿一般求直径的套路先求出“单位树”上每个点所对应的最长链的大小。

这道题其实画画图应该会更好理解一些吧。

整体上,设 \(dp_i\) 表示整体第 \(i\) 棵单位树的根节点所求的的最长链,则其为其本身的最长链与任意一棵与之相连的单位树的值增加上根节点到 \(i\) 的长度。

还有一种情况没有被考虑,即为从 \(1\) 根节点出发向最深处走,另一端补充自己的节点。

不要忘了开 long long。

『JROI-5』Color

求从根节点开始的一棵完全二叉树有多少个连通块。

不是特别好想,考虑以某个节点 \(u\) 为根,则其方案数 \(dp_u = (dp_L + 1)(dp_R + 1)\),这是因为每个子结点都可以选或者不选,如果选的话会有相应的选法种方案数。

对于一棵深度为 \(m\) 的满二叉树,因其形态没有差别且非常规律,我们可以将其预处理出来,这样应该可以减小一些时间复杂度。

其方案数 \(f_m = (f_{m-1} + 1)^2, f_1 = 1\)。判定方法为一棵 \(m\) 层满二叉树的叶子节点共有 \(2^{m - 1}\) 个。

到这还没完,题里给的数的级别实在有点大的过分了,什么情况下才能判定这里存在满二叉树?我认为高精度的话会直接给出一个超时,因此绝对不能这么想。

显然如果是在左子树上的满二叉树一定是满层的,如果是在右子树上是少一层的。

考虑对于这一串二进制,本质上的某一位就是用于表示那颗不满的二叉树在哪个方向的,为 \(1\) 则表示在左子树,为 \(0\) 则表示为右子树。

则由上述引理得到本质上就是对一条链进行递归计算,时间复杂度 \(O(Tn)\)。

考虑真正的满二叉树应为全 \(1\),故 \(s - 1\)。

int dfs(int p){
    if (p == n) return 1;
    return 1ll * (dfs(p + 1) + 1) * (dp[n - p - (1 - (s[p] - '0'))] + 1) % MOD;
}

int main(){
    dp[1] = 1;
    for (int i = 2; i < N; i++){
        dp[i] = 1ll * (dp[i - 1] + 1) * (dp[i - 1] + 1) % MOD;
    }
    cin >> T;
    while (T--){
        cin >> n >> s;
        int p = 0;
        for (int i = 1; i < s.size(); i++)
            if (s[i] == '1') p = i;
        s[p] = '0';
        for (int i = p + 1; i < s.size(); i++)
            s[i] = '1';
        cout << dfs(1) << endl;
    }
    return 0;
}

[JSOI2016] 最佳团体

综合性比较强吧,考虑题目所求即为 \(\max \dfrac{\sum a_i}{\sum b_i} = x\),则 \(\sum a_i - x\sum b_i = \sum (a_i - xb_i) = 0\),最大化,求是否存在方案使得 \(\sum (a_i - xb_i) \le 0\),如果有的话就缩小 \(x\)。

然后是如何选择人,这个 \(01\) 背包就行了,每个人价格为 \(1\),价值为 \(a_i - xb_i\),然后就做完了。

void dfs(int u){
    sz[u] = 1; dp[u][0] = 0; dp[u][1] = v[u];
    for (int v : e[u]){
        dfs(v);
        for (int i = 0; i <= sz[u] + sz[v]; i++) tmp[i] = -1e18; // 辅助数组
        for (int i = 1; i <= sz[u]; i++){ // 前面的子树
            for (int j = 0; j <= sz[v]; j++){ // 当前子树
                tmp[i + j] = max(tmp[i + j], dp[u][i] + dp[v][j]); // 总数与当前答案取较大者
            }
        }
        sz[u] += sz[v]; // 补充子树大小
        for (int i = 1; i <= sz[u]; i++) dp[u][i] = tmp[i]; // 搬回去
    }
    return ;
}

double check(double x){
    memset(dp, 0x3f, sizeof dp);
    for (int i = 1; i <= n; i++){
        v[i] = a[i] - x * b[i];
    }
    dfs(0);
    return dp[0][k];
}

[蓝桥杯 2018 国 A] 采油

第一问,无论如何走,其最优答案均为 \(2\) 倍边权之和。

考虑以建立空运站的点为根,则最优情况下每条边经过且仅经过两次。

第二问,规定 \(B_i \ge S_i\),因为无论如何都需要 \(S_i\) 的人在某个油井上死守。

由数学归纳法证明得按 \(B_i - S_i\) 降序排列一定更优,此时可以“缩点”。

考虑一层的 \(n\) 个点,可以缩成 \(S = \sum S_i, B = \max (\sum_{i=1}^{j - 1} S_i + B_j)\)。

更一般的情况下,考虑如果要使路径最短,不能跨越子树,则先对子树进行缩点,再整体排序进行缩点。

需要注意,深度优先搜索开始时的节点一定是 \(S + B\) 最大或者最小的一个。

struct node {
    int b = 0, s = 0;
    node& operator += (const node &x){
        b = max(b, s + x.b); s += x.s;
        return *this;
    }
    friend bool operator <(const node &x, const node &y){
        return x.b - x.s > y.b - y.s;
    }
} a[N];

int n, s = 1, ans;
vector<int> e[N];

node dfs(int u, int f){
    vector<node> vt; vt.push_back(a[u]);
    for (int v : e[u]){
        if (v == f) continue;
        vt.push_back(dfs(v, u));
    }
    node ret; sort(vt.begin(), vt.end());
    for (node pt : vt){
        ret += pt;
    }
    return ret;
}

[POI 2014] FAR-FarmCraft

哎咱上一道题是不是刚做完走路两倍边权来着。

显然,优先分配更慢安装完成的更优。

考虑设 \(f_i\) 表示安装完成的时间,\(g_i\) 表示发放之后回到 \(i\) 的时间。

那么如果还没安装完,就需要更多时间等。按照 \(f_i - g_i\) 降序排序即可。

其他的不想写了,看下代码吧。

bool cmp(const int &x, const int &y){
    return f[x] - g[x] > f[y] - g[y];
}

void dfs(int u, int fa){
    vector<int> vt;
    for (int v : e[u]){
        if (v == fa) continue;
        dfs(v, u); vt.push_back(v);
    }
    sort(vt.begin(), vt.end(), cmp);
    for (int v : vt){
        f[u] = max(f[u], g[u] + 1 + f[v]);
        g[u] += g[v] + 2;
    }
    if (a[u] > g[u] && u != 1) f[u] = max(f[u], a[u]);
    return ;
}

[USACO02FEB] 重建道路

笑点解析:参考的题解中,表示别人都没审题建双向边,然后自己也不审题在那靠入度找根。还有 \(4 + 0\) 啥时候还能等于 \(5\) 了。

感觉背包类型的树形 dp 还是不是特别熟悉。

考虑设计状态:记 \(dp_{u, k}\) 表示以 \(u\) 为根节点的子树留了 \(k\) 个节点时最小删除的边数。

则有 \(f_{u, i + j} = \max (f_{u, i} + f_{v, j} - 1)\)。

对于边 \((u, v)\) 是不可删的,故删去边数要 \(-1\)。其实好像就做完了。最后别忘了可能还要砍掉一条边。

void dfs(int u){
    dp[u][1] = e[u].size(); sz[u] = 1;
    for (int v : e[u]){
        dfs(v);
        for (int i = 1; i <= sz[u] + sz[v]; i++){
            tmp[i] = 0x3f3f3f3f;
        }
        for (int i = 1; i <= sz[u]; i++){
            tmp[i] = min(tmp[i], dp[u][i]);
            for (int j = 1; j <= sz[v]; j++){
                tmp[i + j] = min(tmp[i + j], dp[u][i] + dp[v][j] - 1);
            }
        }
        for (int i = 1; i <= sz[u] + sz[v]; i++){
            dp[u][i] = tmp[i];
        }
        sz[u] += sz[v];
    }
    return ;
}

[TJOI2017] 城市

首先这道题说了一个叫做最大值最小的东西,它的本质是最小化而不是二分,不要被带跑偏。

我们通过观察得到,断掉后,答案可能分为多个部分:\(u\) 连通块的直径,\(v\) 连通块的直径和 \(u\) 连通块中最远到 \(u'\) 距离 + \(v\) 连通块中最远到 \(v'\) 距离 + \((u, v)\)。

前两个问题不受干扰,其实可以由此得出删除的边一定在直径上,否则删除之后,直径仍是最大距离。

最后一问里的两个可选点互相不受影响。则问题转化为“求一棵树中,与其他点最大距离最小的点”。

显然要连接的是距离最小的两个点,故考虑最大距离的取值:

  1. 在 \(p\) 对应的子树内。
  2. 其父亲对应的除了 \(p\) 子树以外的部分补上中间距离。
  3. 除了其父亲所在子树的其他部分到其父亲的最长链补上中间距离。

我去了研究了半天研究不懂了。以下内容由 AI 生成,已经确认无误。

其核心思想是:只需考虑原树直径上的边,并在动态维护子树信息时利用单调性进行优化。

1. 核心观察与问题转化

1.1 只需枚举直径上的边

可以证明,要删除的边一定在原树的直径上。因为如果删除的边不在直径上,那么原树的直径(最远点对)依然完整地存在于某棵子树中,新树的直径不会小于原直径,无法达到优化的目的。

1.2 重连的最优策略

删除一条直径边 (u, v) 后,树被分为包含 u 和包含 v 的两棵子树。新树的直径来源于三种情况:

  1. 左子树(含 u)内部的直径。
  2. 右子树(含 v)内部的直径。
  3. 跨越新连接边的路径:(u侧某点到u的最远距离) + w(u,v) + (v侧某点到v的最远距离)。

前两种情况是固定的。要使第三种情况最小,新边应连接两棵子树的“中心点”,即到该子树内所有点最大距离最小的点。这个最小距离就是该子树的半径。

因此,问题转化为:对直径上的每条边,快速求出左右两棵子树的直径和半径。

2. O(n) 做法的核心机制:动态维护与单调性

为达到 O(n),算法从左到右、再从右到左两次扫描直径,利用数据结构动态维护子树信息。

2.1 改变扫描顺序:化“删点”为“加点”

如果按边的顺序从直径一端开始删除,左子树会不断“失去”一些节点,维护起来很复杂。

O(n) 做法的巧妙之处在于反向思考。例如,当我们从左到右处理到第 i 条边时,我们考虑的是以这条边为界的左子树。随着 i 增大,左子树是在不断“增加”新的节点的。这样,我们只需要处理“新增”的节点,所有节点总共只会被处理一次。

2.2 利用“直径端点”的单调性

在“加点”的过程中,子树的直径长度是单调不降的。这意味着,子树的直径端点(最远点对)也是单调移动的。因此,在每次加入新节点后,我们不需要重新搜索整棵树来找直径,只需要检查新加入的节点能否更新当前的直径端点即可。

具体来说,假设当前子树直径为 (A, B),新加入一个节点 x。新的直径只可能是 (A, B), (A, x), (B, x) 中的一种。我们通过比较这三对点的距离,就能在 O(1) 时间内更新直径。

2.3 寻找“中心点”(半径)的单调性

同样,子树的半径(中心点到最远点的距离)也是单调不降的。并且,子树的“中心点”在直径上的位置也是单调移动的。

在每次更新直径后,我们可以从当前的中心点开始,沿着直径方向向前移动,直到找到新的中心点。由于中心点在整个扫描过程中只会向前移动,总移动次数是 O(n) 的。

#include <bits/stdc++.h>
using namespace std;

const int N = 5e3 + 5;
const int INF = 0x3f3f3f3f;

struct edge {
    int v, t;
};

int n, dis[N], f[N], fw[N];
int s, t, pre[N], prer[N];
int down[N], lcd[N], ld[N], lr[N], rd[N], rr[N];
vector<int> d, dw; 
vector<edge> e[N];
bool flagd[N];

void dfs(int u){
    for (edge g : e[u]){
        int v = g.v, t = g.t;
        if (v == f[u]) continue;
        dis[v] = dis[u] + t; fw[v] = t;
        f[v] = u; dfs(v);
    }
}

edge dfs(int u, int fa, int depth){
    int mxdep = depth, mxd = 0;
    for (edge g : e[u]){
        int v = g.v, t = g.t;
        if (v == fa || flagd[v]) continue;
        edge rt = dfs(v, u, depth + t);
        int len = mxdep + rt.v - 2 * depth;
        mxd = max({mxd, rt.t, len});
        mxdep = max(mxdep, rt.v);
    }
    return {mxdep, mxd};
}

void calr(const vector<int>& dist, const vector<int>& downArr, int m, int rad[]) {
    int preMax[m];
    preMax[0] = downArr[0] - dist[0];
    for (int i = 1; i < m; ++i)
        preMax[i] = max(preMax[i-1], downArr[i] - dist[i]);

    multiset<int> ms;
    int p = 0;
    for (int i = 0; i < m; ++i) {
        ms.insert(dist[i] + downArr[i]);
        while (p < i) {
            int curMax = *ms.rbegin();
            int curVal = max(dist[p] + preMax[p], -dist[p] + curMax);
            auto it = ms.find(dist[p] + downArr[p]);
            if (it != ms.end()) {
                ms.erase(it);
                int newMax = *ms.rbegin();
                int newVal = max(dist[p+1] + preMax[p+1], -dist[p+1] + newMax);
                if (newVal <= curVal) {
                    ++p;
                    continue;
                } else {
                    ms.insert(dist[p] + downArr[p]);
                    break;
                }
            } else break;
        }
        int curMax = *ms.rbegin();
        rad[i] = max(dist[p] + preMax[p], -dist[p] + curMax);
    }
}

int main(){
    cin >> n;
    if (n == 1){ cout << 0 << endl; return 0; }
    for (int i = 1; i < n; i++){
        int u, v, t; cin >> u >> v >> t;
        e[u].push_back({v, t});
        e[v].push_back({u, t});
    }
    dfs(1); s = 1;
    for (int i = 1; i <= n; i++) if (dis[i] > dis[s]) s = i;
    memset(dis, 0, sizeof dis);
    memset(f, 0, sizeof f);
    dfs(s); t = s;
    for (int i = 1; i <= n; i++) if (dis[i] > dis[t]) t = i;
    int cur = t;
    while (cur){ d.push_back(cur); if (cur == s) break; cur = f[cur]; }
    reverse(d.begin(), d.end());
    int m = d.size();
    for (int i = 1; i < m; i++) dw.push_back(fw[d[i]]);
    for (int v : d) flagd[v] = true;
    for (int i = 1; i < m; i++) pre[i] = pre[i-1] + dw[i-1];
    for (int i = m - 2; i >= 0; i--) prer[i] = prer[i+1] + dw[i];
    for (int i = 0; i < m; i++){
        int root = d[i], mx1 = 0, mx2 = 0, sd = 0;
        for (edge g : e[root]){
            int v = g.v, t = g.t;
            if (flagd[v]) continue;
            edge gt = dfs(v, root, t);
            sd = max(sd, gt.t);
            if (gt.v >= mx1) { mx2 = mx1; mx1 = gt.v; }
            else if (gt.v > mx2) { mx2 = gt.v; }
        }
        down[i] = mx1; lcd[i] = max(sd, mx1 + mx2);
    }
    int ml = pre[0] - down[0]; ld[0] = lcd[0];
    for (int i = 1; i < m; i++){
        int cs = (pre[i] + down[i]) - ml;
        ld[i] = max({ld[i-1], cs, lcd[i]});
        ml = min(ml, pre[i] - down[i]);
    }
    int mr = prer[m-1] - down[m-1]; rd[m-1] = lcd[m-1];
    for (int i = m-2; i >= 0; i--){
        int cs = (prer[i] + down[i]) - mr;
        rd[i] = max({rd[i+1], cs, lcd[i]});
        mr = min(mr, prer[i] - down[i]);
    }
    calr(vector<int>(pre, pre + m), vector<int>(down, down + m), m, lr);
    vector<int> distR(m), downR(m);
    for (int i = 0; i < m; ++i) { distR[i] = prer[m-1-i]; downR[i] = down[m-1-i]; }
    vector<int> radRev(m);
    calr(distR, downR, m, radRev.data());
    for (int i = 0; i < m; ++i) rr[i] = radRev[m-1-i];
    int ans = INF;
    for (int i = 0; i < m - 1; i++){
        ans = min(ans, max({ld[i], rd[i+1], lr[i] + rr[i+1] + dw[i]}));
    }
    cout << ans << endl;
    return 0;
}

[POI 2013] LUK-Triumphal arch

注意到题目答案具有单调性,故用二分答案将最优性问题转化为判定性问题。

考虑 B 走的路线一定是一条从根到叶节点的链而不会走回头路,否则相当于白送回合数给 A。

容易发现 B 在 A 没来得及将 B 走到的节点的下一步(子结点)全部染成黑色时获胜。

考虑当某一层有多余次数时,如何向下多染两个。

则采用合并思想,记 \(f_i\) 为其子树中需要的多余染色次数,则若 \(f_1 \le 0\),则合法。

对于 \(f_i\),设当前答案为 \(x\),其子结点个数为 \(p_i\),则 \(f_i = p_i - x + \sum \max(f_j, 0)\)。

然后就做完了。这题是不是水了。

[APIO2010] 巡逻

考虑本题中 \(K = 1\) 或者 \(K = 2\),猜想连接叶子节点会更优一些,不难想到 \(K = 1\) 应该是连接的直径的两个端点(不难证明端点均为叶子节点)。

不难猜测第二条应该也得和直径有点关系,我们考虑再选一条直径。此时注意到如果选的有重合会发生冲突,即原本扣除的部分需要加回来。则修改原先直径上的边权即可。

一直都没有变成有向无环图。第一次求直径用 dfs,第二次用 dp。然后就又做完了。

[POI 2014] HOT-Hotels

考虑树上的路径所具有的特性,则想到换根后统计深度相等的点有多少个。这非常不显然的错了。甚至我不知道错哪了(?)。

其实压根不明白这东西是怎么牵扯到树形 dp 上的。

三个点两两距离相等时,它们在树上的唯一公共中心一定是一个树上的节点,记为 (u)。

设三个酒店为 (A,B,C),中心 (u) 到它们的距离都为 (d),则:

所以合法三元组可以按“中心节点 (u)”和“到 (u) 的距离 (d)”来计数。

并且三个点必须分别来自 (u) 的三个不同子树,否则它们的路径交点就不是 (u)。

枚举每个点作为中心 (root)。

以 (root) 为根,遍历它的每个邻居子树,统计该子树中距离 (root) 为 (d) 的节点数。

维护两个数组:

  • cnt[d]:已经处理过的子树中,距离 (root) 为 (d) 的节点总数。
  • two[d]:已经处理过的子树中,从两个不同子树各选一个距离为 (d) 的节点的方案数。

当前子树中距离 (root) 为 (d) 的节点数为 cur[d],那么:

  • 新增合法三元组:two[d] * cur[d]
    • 前两个点来自两个不同子树,第三个点来自当前子树。
  • 更新 two[d] += cnt[d] * cur[d]
    • 当前子树选一个,之前所有子树选一个,形成新的两子树组合。
  • 更新 cnt[d] += cur[d]

所有中心枚举完后,答案就是合法三元组总数。

[COCI 2014/2015 #1] Kamp

看起来处理若干遍固然好像有点多了,只处理一遍还要处理一堆点就只能想到换根 dp 了。

换根 dp 的一般思路是:第一遍,以 \(1\) 为根处理信息;第二遍,换根并处理信息。

那么当以 \(1\) 为根节点时,注意到答案为所有边权的二倍之和减去最长链(无效位置,即子树没东西的位置不走)。

第二次要分一堆类别讨论,这里不写了吧。

void dfs(int u, int fa){
    if (p[u]) sz[u] = 1;
    for (edge y : e[u]){
        int v = y.v, t = y.t;
        if (v == fa) continue;
        dfs(v, u);
        if (!sz[v]) continue;
        g[u] += g[v] + 2 * t;
        int pt = len[v] + t;
        if (pt >= len[u]) {
            slen[u] = len[u];
            len[u] = pt;
            id[u] = v;
        }
        else if (pt > slen[u]){
            slen[u] = pt;
        }
        sz[u] += sz[v];
    }
}

void dfs2(int u, int fa){
    for (edge y : e[u]){
        int v = y.v, t = y.t;
        if (v == fa) continue;
        if (!sz[v]){ f[v] = f[u] + 2 * t; len[v] = len[u] + t; }
        else if (!(k - sz[v])) f[v] = g[v];
        else {
            f[v] = f[u];
            if (id[u] != v && len[v] < len[u] + t){
                slen[v] = len[v];
                len[v] = len[u] + t;
                id[v] = u;
            }
            else if (len[v] < slen[u] + t){
				slen[v] = len[v];
                len[v] = slen[u] + t;
                id[v] = 1;
            }
            else if (slen[v] < len[u] + t && id[u] != v)
                slen[v] = len[u] + t;
            else if (slen[v] < slen[u] + t)
                slen[v] = slen[u] + t;
        }
        dfs2(v, u);
    }
}

城市环路

不能同时染色,且还不能同时染两个,那由两个分别为起点动态规划即可。

void dfs(int u, int fa){
    dp[u][0] = 0; dp[u][1] = p[u];
    for (int v : e[u]){
        if (v == fa) continue;
        dfs(v, u);
        dp[u][0] += max(dp[v][0], dp[v][1]);
        dp[u][1] += dp[v][0];
    }
    return ;
}

int main(){
    cin >> n;
    for (int i = 1; i <= n; i++){
        cin >> p[i]; f[i] = i;
    }
    for (int i = 1; i <= n; i++){
        int u, v; cin >> u >> v; u++; v++;
        if (find(u) == find(v)) { s = u; t = v; continue; }
        f[find(v)] = find(u);
        e[u].push_back(v);
        e[v].push_back(u);
    }
    cin >> k;
    dfs(s, 0); ans = dp[s][0];
    dfs(t, 0); ans = max(ans, dp[t][0]);
    printf("%.1lf\n", ans * k);
    return 0;
}

[USACO06MAR] Milk Team Select G

这有啥区别么,不想讲了都。

void dfs(int u){
    dp[u][0][0] = 0; dp[u][0][1] = a[u];
    for (int v : e[u]){
        dfs(v);
        for (int i = sz[u]; i >= 0; i--){
			for (int j = sz[v]; j >= 0; j--){
                dp[u][i + j][0] = max(dp[u][i + j][0], dp[u][i][0] + max(dp[v][j][0], dp[v][j][1]));
                dp[u][i + j + 1][1] = max(dp[u][i + j + 1][1], dp[u][i][1] + dp[v][j][1]);
                dp[u][i + j][1] = max(dp[u][i + j][1], dp[u][i][1] + dp[v][j][0]);
            }
        }
        sz[u] += sz[v] + 1;
    }
    return ;
}

偷天换日

除了读入方法好像真没啥好讲的了。

void dfs(int u){
    int t, x; cin >> t >> x; t *= 2;
    if (x){
        for (int i = 1; i <= x; i++){
            int v, w; cin >> v >> w;
            for (int j = m; j >= t + w; j--){
                dp[u][j] = max(dp[u][j], dp[u][j - w] + v);
            }
        }
        return ;
    }
    int ls = ++n; dfs(ls);
    int rs = ++n; dfs(rs);
    for (int i = t; i <= m; i++){
        for (int j = 0; j <= i - t; j++){
            dp[u][i] = max(dp[u][i], dp[ls][j] + dp[rs][i - j - t]);
        }
    }
}

[yLCPC2024] F. PANDORA PARADOXXX

本来都打算收工了,这看下一讲讲直径我高低看看,然后成功被第一题卡住。

考虑使用技能“回溯”。

最开始的答案一定是每个连通块的直径,考虑加边时,如果有能被更新的直径,则其一定满足经过加入的这条边。

则可以更新的答案为 \(u' + d(u, v) + v'\),其中 \(u', v'\) 分别代表其对应的最长链长度。则这一段中,这条直径的端点只可能是原来两个连通块的直径端点中的 \(2\) 个。

由于我们提前知道树长什么样子,实际上可以先计算深度和倍增公共祖先,然后计算两点之间的距离。

删除所有边,倒着操作,貌似应该就做完了。

注意到加最开始的边也可以认为是在对树进行处理,故其实不用上来先求直径了。

再不清空全都飞起来。

#include <bits/stdc++.h>
using namespace std;

const int N = 1e5 + 5;

struct edge {
    int v, t;
} pe[N];

int T, n, Q, d[N], pd[N], slog2[N];
int f[N], dp[N][20], depth[N], p1[N], p2[N];
long long s[N], ans[N], res;
vector<edge> e[N];

int find(int x){
    if (f[x] == x) return x;
    return f[x] = find(f[x]);
}

void dfs(int u){
    depth[u] = depth[dp[u][0]] + 1;
    for (int i = 1; i <= slog2[depth[u]]; i++)
        dp[u][i] = dp[dp[u][i-1]][i-1];
    for (edge g : e[u]){
        int v = g.v, t = g.t;
        if (v == dp[u][0]) continue;
        s[v] = s[u] + t;
        dp[v][0] = u; dfs(v);
    }
}

int lca(int x, int y){
    if (depth[x] < depth[y])
        swap(x, y);
    while (depth[x] > depth[y])
        x = dp[x][slog2[depth[x]-depth[y]]];
    if (x == y) return x;
    for (int i = slog2[depth[x]]; i >= 0; i--){
        if (dp[x][i] != dp[y][i]){
            x = dp[x][i]; y = dp[y][i];
        }
    }
    return dp[x][0];
}

long long dist(int u, int v) { return s[u] + s[v] - 2 * s[lca(u, v)]; }

void merge(int u, int v){
    int fu = find(u), fv = find(v);
    f[fv] = fu;

    int now[4] = {p1[fu], p2[fu], p1[fv], p2[fv]};
    long long ret = 0;
    int x = p1[fu], y = p2[fu];
    
    for (int i = 0; i < 4; i++){
        for (int j = i + 1; j < 4; j++){
            long long d = dist(now[i], now[j]);
            if (d > ret){
                ret = d;
                x = now[i]; y = now[j];
            }
        }
    }
    p1[fu] = x; p2[fu] = y;
    res = max(res, ret);
    return ;
}

int main(){
    ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
    slog2[0] = -1;
    for (int i = 1; i < N; i++){
        slog2[i] = slog2[i >> 1] + 1;
    }
    cin >> T;
    while (T--){
        cin >> n >> Q; res = 0;
        for (int i = 1; i <= n; i++){
            e[i].clear(); f[i] = i; pd[i] = 0;
            p1[i] = p2[i] = i; depth[i] = 0;
            s[i] = 0;
            for (int j = 0; j < 20; j++)
                dp[i][j] = 0;
        }
        for (int i = 1; i < n; i++){
            int u, v, t; cin >> u >> v >> t;
            e[u].push_back({v, t});
            e[v].push_back({u, t});
            pe[i] = {u, v};
        }
        dfs(1);
        for (int i = 1; i <= Q; i++){
            cin >> d[i];
            pd[d[i]] = 1;
        }
        for (int i = 1; i < n; i++){
            if (pd[i]) continue;
            merge(pe[i].v, pe[i].t);
        }
        for (int i = Q; i >= 1; i--){
            ans[i] = res;
            merge(pe[d[i]].v, pe[d[i]].t);
        }
        for (int i = 1; i <= Q; i++){
            cout << ans[i] << endl;
        }
    }
    return 0;
}
posted @ 2026-08-29 10:06  _lhr  阅读(16)  评论(0)    收藏  举报