树形 DP
树形 DP,即在树上进行的 DP。由于树固有的递归性质,树形 DP 一般都是递归进行的。
链式前向星包含两种结构:
(1)边集数组: edge[], edge[i]表示第i条边;
(2)头结点组数: head[], head[i]存以i为起点的第一条边的下标(在edge[]中的下标)
struct node {
int to;
int next;
int w;
} edge[MAXN*MAXN];
int head[MAXN];
添加一条边(u, v, w)
void add(int u, int v, int w)
{
edge[cnt].to = v;
edge[cnt].w = w;
edge[cnt].next = head[u]; // 采用头插法
head[u] = cnt++;
}
如果是有向图,每输入一条边,执行一次add(u, v, w)即可;
如果是无向图,则需要执行两次{add(u, v, w); add(v, u, w)}
/* https://www.luogu.com.cn/problem/P1352 */
#include
using namespace std;
const int MAXN = 6001;
// edge[i]表示第i条边
struct node {
int to;
int next;
} edge[MAXN];
// head[i]存以i为起点的第一条边的下标
int head[MAXN];
bool vis[MAXN];
int cnt = 0;
// f[i][]: 以i为根的子树的最优解, f[i][0]: i不参加;f[i][1]: i参加
int f[MAXN][2];
// isLeaf[i]: true, i has one leaf node
bool isLeaf[MAXN];
void add(int u, int v)
{
edge[cnt].to = v;
edge[cnt].next = head[u]; // 采用头插法
head[u] = cnt++;
}
// 从叶子节点开始计算,返回上一层时更新当前结点的最优解
void dfs(int u)
{
vis[u] = true;
for (int i = head[u]; ~i; i = edge[i].next)
{
int v = edge[i].to;
if (vis[v])
continue;
dfs(v);
f[u][0] += max(f[v][0], f[v][1]);
f[u][1] += f[v][0];
}
return;
}
int main()
{
int n, i, u, v;
cin >> n;
for (i = 1; i <= n; ++i)
{
cin >> f[i][1];
head[i] = -1;
vis[i] = false;
}
for (i = 1; i < n; ++i)
{
cin >> v >> u;
isLeaf[v] = true;
add(u, v);
}
for (i = 1; i <= n; ++i)
if (!isLeaf[i])
{
dfs(i);
cout << max(f[i][0], f[i][1]) << endl;
}
return 0;
}
/* https://www.luogu.com.cn/problem/P3574 */
/*
状态方程
f[i]: i的子树全部安装好游戏的最小时间(包括i本身自己,但不包括root)
若有cnt条边与u相连
for (int i = 1; i <= cnt; ++i)
f[u] = max(f[u], f[son[i]] + 从u遍历到son[i]所经过路径花费的时间);
遍历子节点规则:结余时间多的先访问
*/
#include
#include
using namespace std;
const int MAXN = 500001;
// edge[i]表示第i条边
struct node {
int to;
int next;
} edge[MAXN<<1];
// head[i]存以i为起点的第一条边的下标
int head[MAXN];
int cnt = 0;
int f[MAXN]; // f[i]: i的子树全部安装好游戏的最小时间(包括i本身自己,但不包括root)
int g[MAXN]; // g[i]: 遍历i子树的时间(往返路径为2)
int cost[MAXN]; // cost[i]: i节点安装游戏的时间
int son[MAXN];
void add(int u, int v)
{
edge[cnt].to = v;
edge[cnt].next = head[u]; // 采用头插法
head[u] = cnt++;
}
// f[i]-g[i]: 为遍历i子树的结余时间,按结余时间降序排列
bool cmp(int i, int j)
{
return (f[i] - g[i]) > (f[j] - g[j]);
}
// 根据结余时间从大到小的顺序,自叶节点更新f[]
void dfs(int u, int fa)
{
if ( u != 1) // u is not root
f[u] = cost[u];
for (int i = head[u]; ~i; i = edge[i].next)
{
int v = edge[i].to;
if (v != fa) // unvisited
dfs(v, u);
}
int num = 0;
for (int i = head[u]; ~i; i = edge[i].next)
{
int v = edge[i].to;
if (v != fa)
son[++num] = v;
}
sort(son+1, son+1+num, cmp);
// 按结余时间从大到小顺序遍历孩子节点,才能获取到最优解
for (int i = 1; i <= num; ++i)
{
f[u] = max(f[u], f[son[i]] + g[u] + 1);
g[u] += g[son[i]] + 2;
}
}
int main()
{
int i, n, u, v;
cin >> n;
for (i = 1; i <= n; ++i)
{
cin >> cost[i];
head[i] = -1;
}
for (i = 1; i < n; ++i)
{
cin >> u >> v;
add(u, v);
add(v, u);
}
dfs(1, 0);
cout << max(f[1], g[1] + cost[1]) << endl;
return 0;
}
树上背包
/* https://www.luogu.com.cn/problem/P2014 */
/*
dp[i][j]: i为根的子树选j门课程的最大学分
枚举x节点的每个子结点y,同时枚举以y为根的子树选了几门课程,将子树的结果合并到x上
状态方程: dp[x][j]=max(dp[x][j],dp[x][j−k]+dp[y][k])(k∈[0,j))
新增一门0学分的课程(设这个课程的编号为0),作为所有无先修课课程的先修课,
这样我们就将森林变成了一棵以0号课程为根的树。
*/
#include
#include
using namespace std;
const int MAXN = 301;
// edge[i]表示第i条边
struct node {
int to;
int next;
} edge[MAXN];
head[i]存以i为起点的第一条边的下标
int head[MAXN];
int cnt = 0, m;
// dp[i][j]: i为根的子树选j个的最大学分
int dp[MAXN][MAXN];
void add(int u, int v)
{
edge[cnt].to = v;
edge[cnt].next = head[u];
head[u] = cnt++;
}
void dfs(int u)
{
for (int i = head[u]; ~i; i = edge[i].next)
{
int v = edge[i].to;
dfs(v);
for (int j = m+1; j >= 1; --j)
for (int k = 0; k < j; ++k)
dp[u][j] = max(dp[u][j], dp[u][j-k] + dp[v][k]);
}
}
int main()
{
int n, i, p, v;
memset(head, -1, sizeof(head));
cin >> n >> m;
for (i = 1; i <= n; ++i)
{
cin >> p >> v;
dp[i][1] = v;
add(p, i);
}
dfs(0);
cout << dp[0][m+1] << endl;
return 0;
}
换根 DP
树形DP中的换根DP通常不会指定根结点,并且根结点的变化会对一些值,例如子结点深度和、点权和等产生影响。
通常需要两次DFS,第一次DFS预处理诸如深度,点权和之类的信息,在第二次DFS开始运行换根动态规划
/* https://www.luogu.com.cn/problem/P3478 */
#include
#include
using namespace std;
const int MAXN = 1000001;
// edge[i]表示第i条边
struct node {
int to;
int next;
} edge[MAXN<<1];
head[i]存以i为起点的第一条边的下标
int head[MAXN<<1];
int cnt = 0, n;
void add(int u, int v)
{
edge[cnt].to = v;
edge[cnt].next = head[u];
head[u] = cnt++;
}
// s[i]: 以i为根的子树中节点的个数
long long s[MAXN];
// f[i]: 以i为根时所有节点的深度之和
long long f[MAXN];
// depth[i]: 以i节点的深度
long long depth[MAXN];
// 初始化,计算s[], depth[]
void dfs(int u, int fa)
{
s[u] = 1;
depth[u] = depth[fa] + 1;
for (int i = head[u]; ~i; i = edge[i].next)
{
int v = edge[i].to;
if (v != fa)
{
dfs(v, u);
s[u] += s[v];
}
}
}
// 根节点u变为v,计算f[v]
void swithroot(int u, int fa)
{
for (int i = head[u]; ~i; i = edge[i].next)
{
int v = edge[i].to;
if (v != fa)
{
f[v] = f[u] + n - (s[v]<<1);
swithroot(v, u);
}
}
}
int main()
{
int i, u, v;
memset(head, -1, sizeof(head));
cin >> n;
for (i = 1; i < n; ++i)
{
cin >> u >> v;
add(u, v);
add(v, u);
}
// 假设以1作为根节点
dfs(1, 1);
for (i = 1; i <= n; ++i)
f[1] += depth[i];
swithroot(1, 1);
long long ans = f[1];
int res = 1;
for (i = 2; i <= n; ++i)
if (ans < f[i])
{
ans = f[i];
res = i;
}
cout << res << endl;
return 0;
}