从零开始重学动态规划(最后更新:2021.10.3)

前言

最近发现自己 DP 太菜了,准备从零开始再学一遍(当然速度肯定较快),于是就有了这篇文章。

部分内容参考自网络。

目前学习着重于:OI Wiki 相关内容、洛谷题单【【动态规划】普及~省选的dp题】。

在 OI 中,计数等非最优化问题的递推解法也常被不规范地称作 DP,但在本文中也被列出。

动态规划理论(一个模型三个特征)

一个模型

一个模型指动态规划适合解决问题的模型。

三个特征

最优子结构

最优子结构指的是,问题的最优解包含子问题的最优解。反过来说就是,我们可以通过子问题的最优解,推导出问题的最优解。

在动态规划模型上,就是前面的状态可以推导出后面的状态。

无后效性

无后效性有两层含义:

  1. 在推导后面阶段状态的时候,我们只关心前面阶段的状态值,不关心这个状态是怎么一步步推导出来的;
  2. 某阶段状态一旦确定,就不受之后阶段的决策影响。

重复子问题

不同的决策序列,到达某个相同的阶段时,可能会产生重复的状态。

动态规划基础

网格行走问题

问题

我们有一个 n×n 的网格,每个格子都有一个权值 ai,j。定义一条路径经过的所有格子的权值和是这条路径的长度,每一步只能向下或向右走一格,求从左上角到右下角的最短路径长度。

题解

我们设 dpi,j 表示从 (1,1) 走到 (i,j) 的最短路径长度,所求即为 dpn,n

显然,到一个格子的最短路径只有两种情况:从上面过来或从左面过来。我们贪心地选取路径最短的方向即可。

for(int i=1;i<=n;i++)
    for(int j=1;j<=n;j++)
        dp[i][j] = min(dp[i-1][j], dp[i][j-1]) + a[i][j];

数字三角形问题

问题

我们有一个数字金字塔,需要求出一条从最高点到底部任意处结束的路径,使路径经过数字的和最大,输出权值即可。每一步可以走到左下方的点也可以到达右下方的点。

题解

与上一个问题类似,一个位置可以从上面两个转移过来。

for(int i=1;i<=n;i++)
    for(int j=1;j<=i;j++)
        dp[i][j] = max(dp[i-1][j-1], dp[i-1][j]) + a[i][j];

答案就是 max1indpn,i

当然,我们也可以从下往上倒推,答案为 dp1,1

最长公共子序列问题(LCS)

问题

我们有两个长度分别为 n,m 的数列 a,b,求它们的最长公共子序列长度。

题解

我们假设 dpi,j 表示 a1,,ib1,,j 的 LCS 长度,发现有三种情况,分别是 ai 失配、bj 失配、ai,bj 匹配。

for(int i=1;i<=n;i++)
    for(int j=1;j<=m;j++)
        dp[i][j] = max(max(dp[i][j-1], dp[i-1][j]), (a[i]==b[j])?(dp[i-1][j-1]+1):0);

最长上升子段问题

问题

有一个长度为 n 的数列 a,求最长的连续上升子段长度。

题解

对于当前位置,如果可以与前一个接上,肯定是接上更优,否则创建一个新的子段。

for(int i=1;i<=n;i++) {
    if(a[i-1] < a[i]) dp[i] = dp[i-1] + 1;
    else dp[i] = 1;
    ans = max(ans, dp[i]);
}

最长上升子序列问题(LIS)

问题

有一个长度为 n 的数列 a,求最长的上升子序列长度。

题解

有一个很显然的 O(n2) 做法:扫一遍更新答案。

for(int i=2;i<=n;i++)
    for(int j=1;j<i;j++)
        if(a[j] < a[i]) {
            dp[i] = max(dp[i], dp[j]+1);
            ans = max(ans, dp[i]);
        }

考虑 O(nlogn) 做法。

在插入一个元素 ai 时,假设当前的 LIS 序列为 dlen

  • 如果 aidlen:在 d 中找到第一个 不小于 ai 的元素,替换为 ai
  • 如果 ai>dlen:直接插到 d 末尾。
d[1] = a[1]; len = 1;
for(int i=2;i<=n;i++) {
    if(a[i] <= d[len]) *lower_bound(d+1, d+1+len, a[i]) = a[i];
    else d[++len] = a[i];
}

答案即为 len

记忆化搜索

在搜索过程中,很可能会多次搜到同一个子问题,我们可以在第一次搜索到的时候将答案记录下来,后面每一次直接获取记录的答案即可。

背包 DP

0/1 背包问题(采药

问题

n 件物品,每件物品都只有一个。第 i 件的价值为 vi,重量为 wi,有一个最多可以装重量为 m 的物品的背包,最大化价值。

题解

dpi,j 表示只能选前 i 个物品、背包容量为 j 的最大价值。

则有两种情况:不选第 i 个物品,或者选了第 i 个物品。

发现每次只加进来一个物品,可以将二维状态压缩为一维,去掉物品维度。

for(int i=1;i<=n;i++)
    for(int j=m;j>=w[i];j--)
        dp[j] = max(dp[j], dp[j-w[i]]+v[i]);

完全背包问题(疯狂的采药

问题

n 件物品,每件物品有无穷多个。第 i 件的价值为 vi,重量为 wi,有一个最多可以装重量为 m 的物品的背包,最大化价值。

题解

与 0/1 背包类似,但是可以选无穷多个。上面的第二维循环逆序枚举是为了保证每件物品只选一次,这里改为正序即可。

for(int i=1;i<=n;i++)
    for(int j=w[i];j<=m;j++)
        dp[j] = max(dp[j], dp[j-w[i]]+v[i]);

多重背包问题(宝物筛选

问题

n 件物品,第 i 件物品有 ki 个,价值为 vi,重量为 wi,有一个最多可以装重量为 m 的物品的背包,最大化价值。

题解

多重背包就是把“第 i 种物品有 ki 个”转化为了“有 ki 个相同的物品”,然后进行 0/1 背包。

二进制拆分

把每个物品按照 1 个、2 个、4 个……和剩余部分拆分即可。

混合背包问题(樱花

问题

与上面的背包基本相同,只是有的物品有有限个,有的物品有无穷多个。

题解

把有限个的进行二进制拆分,更新时如果当前物品有有限个,直接按照 0/1 背包和多重背包的方式更新,否则按完全背包的方式更新。

二维费用背包问题(榨取 kkksc03

问题

与上面的背包基本相同,只是多了第二维费用(类似于重量、体积、时间)。

题解

多开一维数组即可。

分组背包问题(通天之分组背包

问题

与上面的背包基本相同,只是每种物品属于一组,同一组中物品互相冲突。

题解

其实就是变为了“在每一组中选择一件”,对每一组跑一遍 0/1 背包即可。

tk,i 表示第 k 组第 i 件物品的编号,cntk 表示第 k 组物品个数。

没写过这题,放一份从 背包 DP - OI Wiki 贺过来的代码:

for (int k = 1; k <= ts; k++)          // 循环每一组
  for (int i = m; i >= 0; i--)         // 循环背包容量
    for (int j = 1; j <= cnt[k]; j++)  // 循环该组的每一个物品
      if (i >= w[t[k][j]])
        dp[i] = max(dp[i],
                    dp[i - w[t[k][j]]] + c[t[k][j]]);  // 像0-1背包一样状态转移

有依赖的背包(金明的预算方案

没写过,搬运自 背包 DP - OI Wiki

这种背包问题其实就是如果选第 i 件物品,就必须选第 j 件物品,保证不会循环引用,一部分题目甚至会出现多叉树的引用形式。为了方便,就称不依赖于别的物品的物品称为「主件」,依赖于某主件的物品称为「附件」。

对于包含一个主件和若干个附件的集合有以下可能性:仅选择主件,选择主件后再选择一个附件,选择主件后再选择两个附件……需要将以上可能性的容量和价值转换成一件件物品。因为这几种可能性只能选一种,所以可以将这看成分组背包。

如果是多叉树的集合,则要先算子节点的集合,最后算父节点的集合。

泛化物品的背包

没写过,搬运自 背包 DP - OI Wiki

这种背包,没有固定的费用和价值,它的价值是随着分配给它的费用而定。在背包容量为 V 的背包问题中,当分配给它的费用为 vi 时,能得到的价值就是 h(vi)。这时,将固定的价值换成函数的引用即可。

区间 DP

什么是区间 DP

区间类动态规划是线性动态规划的扩展,它在分阶段地划分问题时,与阶段中元素出现的顺序和由前一阶段的哪些元素合并而来有很大的关系。

区间 DP 的特点:

  • 合并:即将两个或多个部分进行整合,当然也可以反过来;
  • 特征:能将问题分解为能两两合并的形式;
  • 求解:对整个问题设最优值,枚举合并点,将问题分解为左右两个部分,最后合并两个部分的最优值得到原问题的最优值。

石子合并

首先断环成链,设 dpi,j,[0/1] 表示把 [i,j] 合并起来的最大/小代价,我们枚举合并点 k[i,j),得到转移方程:

{dpi,j,0=maxk[i,j){dpi,k,0+dpk+1,j,0+p[i,j]ap}dpi,j,1=mink[i,j){dpi,k,1+dpk+1,j,1+p[i,j]ap}

其中区间和用前缀和预处理。答案即为任意一段长度为 n 区间的 max/min

其他题目

树上 DP(树形 DP)

基础树上 DP

没有上司的舞会

问题

一棵有根树的每个节点都有权值,现在选出若干个节点,但是一个节点和它的父亲不能同时被选中,最大化权值和。

题解

假设第 i 个点的权值为 wi,我们可以设 dpi,[0/1] 表示在以 i 为根的子树内,不选/选第 i 个节点,能得到的最大权值。

根据题意,可以写出转移方程:

{dpu,0=v is son of umax{dpv,0,dpv,1}dpu,1=wu+v is son of udpv,0

这显然是递归形式,结合树的递归性质,我们从每个节点向下递归,更新完每个儿子后用它们的 dp 值更新自己。

其他题目

树上背包

简介

树上的背包问题,简单来说就是背包问题与树形 DP 的结合。

选课

问题

n 门课程,第 i 门有 ai 学分,每门课程有 01 门先修课,必须先学完先修课才能学习一个课程。

要学习 m 门课程,求最大可能学分。

题解

我们设 dpu,x,k 表示在以 u 为根的子树内,考虑前 x 个子树,学了 k 门课程的最大学分。

假设 u 的儿子个数为 su,子树大小为 szu,则有如下转移方程:

dpu,i,j=maxv is son of u,kj,kszv(dpu,i1,jk+dpv,sv,k)

根据背包的思想,显然第二维可以压缩掉,最终得到如下解法:

初始状态 dpu,1=au,递归进行如下转移:

for(int i=m+1;i>=1;i--)
    for(int j=0;j<i;j++)
        dp[u][i] = max(dp[u][i], dp[v][j]+dp[u][i-j]);

其他题目

换根 DP

简介

树形 DP 中的换根 DP 问题又被称为二次扫描,通常不会指定根结点,并且根结点的变化会对一些值,例如子结点深度和、点权和等产生影响。

通常需要两次 DFS,第一次 DFS 预处理诸如深度,点权和之类的信息,在第二次 DFS 开始运行换根动态规划。

STA-Station

问题

给定一个 n 个点的树,请求出一个结点,使得以这个结点为根时,所有结点的深度之和最大。

题解

初始先钦定一个根节点(我钦定为 1),设 su 表示以 u 为根的子树大小,则显然有 su=v is son of usv+1,我们需要一次 dfs 预处理出所有的 su

然后设 dpu 表示以 u 为根时所有节点的深度之和,当 dpvdpu 时就是换根过程。此时深度的具体改变为:

  • v 子树上的点深度减少一;
  • 不在 v 子树上的点深度增加一。

因此总深度和增加 n2×sv,转移方程为 dpv=dpu+n2×sv

其他题目

基环树问题

咕咕咕。

虚树问题

不会。

长链剖分优化

咕咕咕。

杂题

DAG DP(拓扑排序 DP)

咕咕咕。

状压 DP

此类问题的特征一般是至少一个关键变量的 数据规模较小

暴力搜索

我们以 还是全排列 一题为例【私题请勿提交】,考虑如何进行暴力搜索。

我们记录一个全局数组表示每个位置的状态(可以放/不能放/因为同列有棋子而不能放),然后枚举每一行中放在哪个位置,标记该位置和该列,搜索下一行,在回溯时取消标记。

大概是这样:

void dfs(u): /* row id */
    if u > n: 
        ans += 1;
        return;
    for (r, c) in row(u):
        if canPlace(r, c) == True: /* check if this cell is empty */
            for (r1, c1) in column(c): canPlace(r1, c1) = False;
            dfs(u+1);
            for (r1, c1) in column(c): 
                if initiallyCanPlace(x, y) == True:
                    canPlace(r1, c1) = True;
    return;

然而上述写法是 不被推荐 的:因为它没有返回值,即使在状态压缩后也 无法记忆化 ,因此更推荐以下写法:

int dfs(u): /* row id */
    if u > n: return 1;
    now = 0;
    for (r, c) in row(u):
        if canPlace(r, c) == True: /* check if this cell is empty */
            for (r1, c1) in column(c): canPlace(r1, c1) = False;
            now += dfs(u+1);
            for (r1, c1) in column(c): 
                if initiallyCanPlace(x, y) == True:
                    canPlace(r1, c1) = True;
    return now;

时间复杂度是 O(n!×n)(希望不要分析错),显然不够理想。

状态压缩搜索

我们发现上面解法中每次标记一列太慢了,可以存一个数组表示这一列是否可以放,于是进行优化:

int dfs(u): /* row id */
    if u > n: return 1;
    now = 0;
    for (r, c) in row(u):
        if canPlace(r, c) == True and canPlaceColumn(c) == True: /* check if this cell is empty */
            canPlaceColumn(c) = False;
            now += dfs(u+1);
            canPlaceColumn(c) = True;
    return now;

然后以上写法依然是 不推荐的 ,因为依然不能记忆化,我们需要传一个数组表示当前的 canPlaceColumn[] 的状态:

int dfs(canPlaceColumn[], u): /* canPlaceColumn and row id */
    if u > n: return 1;
    now = 0;
    for (r, c) in row(u):
        if canPlace(r, c) == True and canPlaceColumn(c) == True: /* check if this cell is empty */
            canPlaceColumn_nxt = canPlaceColumn;
            canPlaceColumn_nxt(c) = False;
            now += dfs(canPlaceColumn_nxt[], u+1);
    return now;

此时我们发现每次 dfs 都要传一个 int[],空间复杂度完全不可接受,考虑对数组进行压缩。

想到 canPlaceColumn[] 的定义,只会存 0 或者 1,可以将它视为一个二进制数,从右向左存原来的 canPlaceColumn[](我的代码一般从下标 0 开始存),然后通过简单的位运算就可以获取和更改状态。

int chkColumn(sta, c):
    return (sta >> (c - 1)) & 1;
int dfs(sta, u): /* status and row id */
    if u > n: return 1;
    now = 0;
    for (r, c) in row(u):
        if canPlace(r, c) == True and chkColumn(sta, c) == True: /* check if this cell is empty */
            sta_nxt = sta | (1 << (c - 1));
            now += dfs(sta_nxt, u+1);
    return now;

时间复杂度是 O(n!)(希望不要分析错),显然依然不够理想。

记忆化的状态压缩搜索

上面的代码就非常好,可以开数组存 dfs(sta, u) 的答案,完成记忆化。

代码很简单不放了,时间复杂度是 O(2n×n)

状态压缩动态规划

就是把记忆化搜索改造成动态规划。

吃奶酪 为例,假设当前走过的路径为 (0,0)Su,其中 S 是一个集合,表示已经吃了哪些奶酪,这里我们规定 S 不包含 u(其实包含也可以,但我没有)。

于是很好定义出 dpS,u 表示从 (0,0) 经过集合 S 中的点最后到达 u 的最短距离,枚举集合 S 以及最后一条路的起点和终点进行 DP 即可。

//By: Luogu@rui_er(122461)
#include <bits/stdc++.h>
#define rep(x,y,z) for(int x=y;x<=z;x++)
#define per(x,y,z) for(int x=y;x>=z;x--)
#define debug printf("Running %s on line %d...\n",__FUNCTION__,__LINE__)
using namespace std;
typedef long long ll;
typedef tuple<double, double> dot;
const int N = 25, K = 131072;
const double inf = 1e100;

int n;
dot a[N];
double dp[K][N], ans = inf;
template<typename T> void chkmin(T &x, T y) {if(x > y) x = y;}
template<typename T> void chkmax(T &x, T y) {if(x < y) x = y;}
double dis(const dot& a, const dot& b) {
	double dx = fabs(get<0>(a) - get<0>(b));
	double dy = fabs(get<1>(a) - get<1>(b));
	return sqrt(dx * dx + dy * dy);
}

int main() {
	scanf("%d", &n);
	rep(i, 1, n) {
		double x, y;
		scanf("%lf%lf", &x, &y);
		a[i] = make_tuple(x, y);
	}
	rep(sta, 0, (1<<n)-1) {
		rep(i, 1, n) dp[sta][i] = inf;
	}
	rep(i, 1, n) dp[0][i] = dis(a[0], a[i]);
	rep(sta, 0, (1<<n)-1) {
		rep(s, 1, n) if((sta >> (s - 1)) & 1) {
			rep(t, 1, n) if(!((sta >> (t - 1)) & 1)) {
				chkmin(dp[sta][t], dp[sta^(1<<(s-1))][s]+dis(a[s], a[t]));
			}
		}
	}
	rep(i, 1, n) chkmin(ans, dp[((1<<n)-1)^(1<<(i-1))][i]);
	printf("%.2lf\n", ans);
    return 0;
}

时间复杂度为 O(2n×n2)

轮廓线 DP

短期不更。

计数 DP

咕咕咕。

数位 DP

咕咕咕。

概率期望 DP

概率 DP

咕咕咕。

期望 DP

咕咕咕。

动态 DP

短期不更。

单调队列/单调栈

咕咕咕。

斜率优化

咕咕咕。

四边形不等式

咕咕咕。

DS 优化

树状数组/线段树优化 DP

以题目 Ezzat and Grid 为例(我赛时做出了线段树优化 DP,好开心好开心[狗头])。

题意:有一个 n×109 的 0/1 矩阵,最少删掉多少行,使得每两个相邻的行都有至少一列均为 1。(1n3×105)。

考虑留下尽量多的行,然后把剩下的全删了。

dpi 表示必须保留第 i 行最多能留下多少行,于是转移方程显然:dpi=max1j<i{[rowi|rowj>0]dpj}+1

但是时间复杂度根据写法至少为 O(n2),无法通过此题。

发现这里 DP 用了区间修改和查询区间 max,可以用线段树来维护,于是就做完了。

输出方案强行增加码量很恶心,DP 的时候还要记录从哪里更新来。

时间复杂度根据写法为 O(nlog109)O(nlogn)

//By: Luogu@rui_er(122461)
#include <bits/stdc++.h>
#define rep(x,y,z) for(int x=y;x<=z;x++)
#define per(x,y,z) for(int x=y;x>=z;x--)
#define debug printf("Running %s on line %d...\n",__FUNCTION__,__LINE__)
using namespace std;
typedef long long ll;
typedef pair<int, int> pii;
const int N = 2e6+5;
const pii z = make_pair(0, 0);

int n, m;
pii dp[N];
vector<int> buc;
vector<pii> seg[N];
set<int> ans;
template<typename T> void chkmin(T &x, T y) {if(x > y) x = y;}
template<typename T> void chkmax(T &x, T y) {if(x < y) x = y;}
struct Node {
	pii w, tag;
	Node(pii a=z, pii b=z) : w(a), tag(b) {}
	~Node() {}
}t[N<<2];
#define lc(u) (u<<1)
#define rc(u) (u<<1|1)
void pushup(int u) {t[u].w = max(t[lc(u)].w, t[rc(u)].w);}
void pushdown(int u) {
	if(t[u].tag == z) return;
	t[lc(u)].tag = t[rc(u)].tag = t[u].tag;
	t[lc(u)].w = t[rc(u)].w = t[u].tag;
	t[u].tag = z;
}
void modifyMax(int u, int l, int r, int ql, int qr, pii k) {
	if(ql <= l && r <= qr) {
		t[u].tag = t[u].w = k;
		return;
	}
	pushdown(u);
	int mid = (l + r) >> 1;
	if(ql <= mid) modifyMax(lc(u), l, mid, ql, qr, k);
	if(qr > mid) modifyMax(rc(u), mid+1, r, ql, qr, k);
	pushup(u);
}
pii queryMax(int u, int l, int r, int ql, int qr) {
	if(ql <= l && r <= qr) return t[u].w;
	pushdown(u);
	int mid = (l + r) >> 1;
	pii rs = z;
	if(ql <= mid) chkmax(rs, queryMax(lc(u), l, mid, ql, qr));
	if(qr > mid) chkmax(rs, queryMax(rc(u), mid+1, r, ql, qr));
	return rs;
}
int getId(int x) {return lower_bound(buc.begin(), buc.end(), x) - buc.begin() + 1;}
void prod() {
	sort(buc.begin(), buc.end());
	auto it = unique(buc.begin(), buc.end());
	buc.erase(it, buc.end());
	rep(i, 1, n) {
		int sz = seg[i].size();
		rep(j, 0, sz-1) {
			seg[i][j].first = getId(seg[i][j].first);
			seg[i][j].second = getId(seg[i][j].second);
		}
	}
}

int main() {
	scanf("%d%d", &n, &m);
	rep(i, 1, n) ans.insert(i);
	rep(i, 1, m) {
		int p, l, r;
		scanf("%d%d%d", &p, &l, &r);
		buc.push_back(l);
		buc.push_back(r);
		buc.push_back(l+1);
		buc.push_back(r+1);
		seg[p].push_back(make_pair(l, r));
	}
	prod();
	rep(i, 1, n) {
		int sz = seg[i].size();
		rep(j, 0, sz-1) {
			int l = seg[i][j].first, r = seg[i][j].second;
			pii now = queryMax(1, 1, N-1, l, r);
			++now.first;
			chkmax(dp[i], now);
		}
		rep(j, 0, sz-1) {
			int l = seg[i][j].first, r = seg[i][j].second;
			pii now = make_pair(dp[i].first, i);
			modifyMax(1, 1, N-1, l, r, now);
		}
	}
	int now = 0;
	rep(i, 1, n) if(dp[i] > dp[now]) now = i;
	for(;now;now=dp[now].second) ans.erase(now);
	int sz = ans.size();
	printf("%d\n", sz);
	for(set<int>::iterator it=ans.begin();it!=ans.end();it++) printf("%d ", *it);
	puts("");
	return 0;
}

平衡树优化 DP

以题目 Game on Tree 2 为例(我赛时做出了平衡树优化 DP,好开心好开心[狗头])。

题意:两个人(为了方便下面称呼为 A 和 B)在以 1 为根的树上玩游戏,每个节点有权值。棋子一开始在根节点,A 和 B 轮流移动棋子,只能由祖先向后代移动,移动到叶子为止。将所有经过的节点的权值插入可重集中,可重集的中位数即为最终得分。A 想要得分尽量大,B 想要得分尽量小,A 先手,问他们均按最优策略移动时的得分。(树的大小 1n105

假的博弈论。

dpi,0/1 表示 i 节点的子树中按照最优策略可以得到的最大/小得分。因为轮流移动且都是最优策略,所以要交叉进行更新。

显然我们需要动态维护根节点到每个叶子路径的可重集,还要支持查询中位数,容易想到使用平衡树。

感觉细节也不多,就做完了,复杂度 O(nlogn)

//By: Luogu@rui_er(122461)
#include <bits/stdc++.h>
#define rep(x,y,z) for(ll x=y;x<=z;x++)
#define per(x,y,z) for(ll x=y;x>=z;x--)
#define debug printf("Running %s on line %d...\n",__FUNCTION__,__LINE__)
using namespace std;
typedef long long ll;
const ll N = 1e5+5, inf = 0x3f3f3f3f3f3f3f3fll;

ll n, a[N], tot, rt, L, M, R, dp[N][2];
template<typename T> void chkmin(T &x, T y) {if(x > y) x = y;}
template<typename T> void chkmax(T &x, T y) {if(x < y) x = y;}
struct Edge {
	ll v, nxt;
	Edge(ll a=0, ll b=0) : v(a), nxt(b) {}
	~Edge() {}
}e[N<<1];
ll h[N], ne = 1;
void add(ll u, ll v) {
	e[++ne] = Edge(v, h[u]); h[u] = ne;
	e[++ne] = Edge(u, h[v]); h[v] = ne;
}

struct Node {
	ll val, rnd, sz, fa, lc, rc;
	Node(ll a=0, ll b=0) : val(a), rnd(rand()), sz(b), fa(0), lc(0), rc(0) {}
	~Node() {}
}t[N];
ll newNode(ll w) {t[++tot] = Node(w, 1); return tot;}
void pushup(ll u) {t[u].sz = t[t[u].lc].sz + t[t[u].rc].sz + 1;}
void split(ll u, ll lim, ll &x, ll &y) {
	if(!u) x = y = 0;
	else {
		if(t[u].val <= lim) {
			x = u;
			split(t[u].rc, lim, t[u].rc, y);
		}
		else {
			y = u;
			split(t[u].lc, lim, x, t[u].lc);
		}
		pushup(u);
	}
}
ll merge(ll u, ll v) {
	if(!u || !v) return u | v;
	if(t[u].rnd < t[v].rnd) {
		t[u].rc = merge(t[u].rc, v);
		pushup(u);
		return u;
	}
	else {
		t[v].lc = merge(u, t[v].lc);
		pushup(v);
		return v;
	}
}
void insert(ll w) {
	split(rt, w, L, R);
	rt = merge(L, merge(newNode(w), R));
}
void erase(ll w) {
	split(rt, w-1, L, R);
	split(R, w, M, R);
	M = merge(t[M].lc, t[M].rc);
	rt = merge(L, merge(M, R));
}
ll rk(ll w) {
	split(rt, w-1, L, R);
	ll ans = t[L].sz + 1;
	rt = merge(L, R);
	return ans;
}
ll kth(ll u, ll k) {
	while(true) {
		if(k <= t[t[u].lc].sz) {u = t[u].lc; continue;}
		if(k == t[t[u].lc].sz + 1) return u;
		k -= t[t[u].lc].sz + 1;
		u = t[u].rc;
	}
}
ll pre(ll w) {
	split(rt, w-1, L, R);
	ll ans = t[kth(L, t[L].sz)].val;
	rt = merge(L, R);
	return ans;
}
ll suc(ll w) {
	split(rt, w, L, R);
	ll ans = t[kth(R, 1)].val;
	rt = merge(L, R);
	return ans;
}
void dfs(ll u, ll f, ll sz) {
	insert(a[u]);
	dp[u][0] = -inf;
	dp[u][1] = inf;
	ll isLeaf = 1;
	for(ll i=h[u];i;i=e[i].nxt) {
		ll v = e[i].v;
		if(v != f) {
			dfs(v, u, sz+1);
			isLeaf = 0;
			chkmax(dp[u][0], dp[v][1]);
			chkmin(dp[u][1], dp[v][0]);
		}
	}
	if(isLeaf) {
		ll now = 0;
		if(sz & 1) now = t[kth(rt, (sz+1)/2)].val << 1;
		else now = t[kth(rt, sz/2)].val + t[kth(rt, sz/2+1)].val;
		dp[u][0] = dp[u][1] = now;
	}
	erase(a[u]);
}

int main() {
	srand(time(0));
	scanf("%lld", &n);
	rep(i, 1, n) scanf("%lld", &a[i]);
	rep(i, 1, n-1) {
		ll u, v;
		scanf("%lld%lld", &u, &v);
		add(u, v);
	}
	dfs(1, 0, 1);
	printf("%lld\n", dp[1][0]>>1);
	return 0;
}

更优的状态

咕咕咕。

一些笔记

关于判断是否为动态规划

咕咕咕。

关于状态设计

咕咕咕。

关于转移方程

咕咕咕。

posted @   rui_er  阅读(389)  评论(12编辑  收藏  举报
编辑推荐:
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!
点击右上角即可分享
微信分享提示