【赛前复习】树形DP

前言:

一时 \(DP\) 一时爽, 一直 \(DP\) 一直爽!

推荐博客:

多叉树的树形背包常见建模方法

树形dp+树形结构总结

【DP_树形DP专辑】 (题目合集)

简单介绍:

顾名思义,在树上的 \(DP\)

因为其具有传递性,可以向子树传递信息,或者子树向父亲传递信息,就对于具有一定规律的树上问题求解起到了很大帮助。

一般都是给定一些限制,然后求什么树上的最大最小花费。

很多树形结构都可以用到, \(such\) \(as\) 二叉树、三叉树、静态搜索树、AVL树,线段树、SPLAY树,后缀树 等等(当然我都不会

题目

Fire

\(Fire\)

给定一棵树,对于任意节点 \(i\) 需要满足至少一个条件。

1.可以花费一定代价 \(w_i\) 建消防站;
2.离它最近的消防站的之间的距离不超过 \(D_i\)

求满足条件的最小花费。

定义 \(dp_{i, j}\) 表示城市 \(i\) 的负责点是 \(j\) 的情况, \(g_i\) 表示 \(i\) 的子树内负责点不定的最小代价。

\(dp_{i, j}\) 向上传递,由儿子传递到父亲,最后计算出答案。

对于一棵树来说,父亲为 \(x\), 儿子为 \(y\)

  • 如果 \(x\), \(y\) 由同一个节点 \(a\) 负责,

\(dp_{x, a} += dp_{y,a} - w_a\) (直接减去重复计算的 \(w_a\) 即可)

  • 如果 \(x\), \(y\) 不由同一个节点负责,那就不用管 \(y\) 具体由哪个节点负责,直接用 \(g_y\) 转移

\(dp_{x,a} += g_y\)

关于 \(g_x\) 的转移就是,每次枚举 负责 \(x\) 的节点 \(i\) , 处理完后取最小值

\(g_x = \min\) { \(dp_{x,i}\) } \((dis_{x, i} <= D_x)\)

点击查看代码

#include<cstdio>
#include<algorithm>
#include<cstring>
#include<queue>
using namespace std;
const int N = 1005;
int t, n, D[N], W[N], tot, head[N], nex[N << 1], to[N << 1], w[N << 1], dis[N][N];
void add(int x, int y, int z) {
	to[++tot] = y, nex[tot] = head[x], head[x] = tot, w[tot] = z; 
}
void dfs(int st, int x, int f) {
	int ver;
	for(int i = head[x]; i; i = nex[i]) {
		ver = to[i];
		if(ver == f) continue;
		dis[st][ver] = dis[st][x] + w[i];
		dfs(st, ver, x);
	}
}
int dp[N][N], g[N];
void solve(int x, int f) {
	int ver;
	for(int i = head[x]; i; i = nex[i]) {
		ver = to[i];
		if(ver == f) continue;
		solve(ver, x);
	}
	for(int i = 1; i <= n; i ++) {
		if(dis[i][x] > D[x]) continue;
		dp[x][i] = W[i];
		for(int j = head[x]; j; j = nex[j]) {
			ver = to[j];
			if(ver == f) continue;
			dp[x][i] += min(dp[ver][i] - W[i], g[ver]);
		} 
		g[x] = min(g[x], dp[x][i]);
	}
}

int main() {
	int u, v, ww;
	scanf("%d", &t);
	while(t --) {
		scanf("%d", &n);
		tot = 0;
		memset(head, 0, sizeof(head));
		memset(dp, 0x3f, sizeof(dp));
		memset(g, 0x3f, sizeof(g));
		for(int i = 1; i <= n; i ++) scanf("%d", &W[i]);
		for(int i = 1; i <= n; i ++) scanf("%d", &D[i]);
		for(int i = 1; i < n; i ++) {
			scanf("%d %d %d", &u, &v, &ww);
			add(u, v, ww), add(v, u, ww);
		}
		for(int i = 1; i <= n; i ++) {
			dis[i][i] = 0;
			dfs(i, i, 0);
		}
		solve(1, 0);
		printf("%d\n", g[1]);
	}
	
	return 0;
}

Rebuilding Roads

\(Rebuilding\) \(Roads\) \(or\) \(Luogu\) 重建道路

给定一棵 \(n\) 个节点的树,问最少删去多少条边,能够得到一个节点个数为 \(p\) 的连通块。

定义 \(dp_{i,j}\) 表示以 \(i\) 为根的子树保留 \(j\) 个点联通的最小代价,

重点在 \(dp\) 的初始条件的设置以及式子的小细节处理上。

\(dp_{i, j} = min(dp_{i, j}, dp_{i, j - k} + dp_{ver, k} - 1)\)

解释:因为 \(x\)\(ver\) 之间相连是存在一条边的, 而这条边并没有算进 \(dp_{ver, k}\) 中, 还可能会在 \(dp_{x, j - k}\) 中被当成删边, 所以要 \(-1\)

而且初始值的设置也很灵活,值得学习。

注意最后的答案统计,因为除树根之外的点在 \(DP\) 时都没有考虑与其父亲的连边, 所以最后还要额外删去和父亲相连的那条边,即 \(-1\)

点击查看代码

#include<cstdio>
#include<algorithm>
#include<cstring>
using namespace std;
const int N = 155;
int n, p, rt, ans = 1e9, fa[N], tot, head[N], nex[N], to[N], dp[N][N], in[N], out[N];

void add(int x, int y) {
	to[++tot] = y, nex[tot] = head[x], head[x] = tot;
}
int siz[N];
void dfs(int x) {
	int ver;
	siz[x] = 1;
	for(int i = head[x]; i; i = nex[i]) {
		ver = to[i];
		dfs(ver);
		siz[x] += siz[ver];
	}
	for(int i = head[x]; i; i = nex[i]) {
		ver = to[i];
		for(int j = siz[x]; j; j --) {
			for(int k = 1; k < j; k ++) {
				dp[x][j] = min(dp[x][j], dp[x][j - k] + dp[ver][k] - 1);
			}
		}
	}
}

int main() {
	int u, v;
	memset(dp, 0x3f, sizeof(dp));
	scanf("%d %d", &n, &p);
	for(int i = 1; i < n; i ++) {
		scanf("%d %d", &u, &v);
		out[u] ++, in[v] ++;
		add(u, v);
	}
	for(int i = 1; i <= n; i ++) {
		if(!in[i]) rt = i;
		dp[i][1] = out[i];
	} 
	dfs(rt);
	ans = dp[rt][p];
	for(int i = 1; i <= n; i ++) ans = min(ans, dp[i][p] + 1);
	printf("%d", ans);
	return 0;
} 

Road Improvement

\(Road\) \(Improvement\)

给定一棵 \(n\) 个节点的树,所有树边的初始状态均为不良,现在要求你改良道路,使得从节点 \(x\) 到任意节点的路径上最多只有一条不良路径。

对于每一个可能的x,求出能够满足条件的方案总数。

定义 \(dp_i\) 表示以 \(x\) 为根的子树满足条件的方案数;

\(g_i\) 表示 使以 \(x\) 为根的整棵树(不包含原属于 \(x\) 子树的那部分) 满足条件的方案数;

关于 \(dp_i\) 部分:

假设 存在一条边 \(u -> v\) (即 \(v\) 的父亲节点为 \(u\))

  • \((u, v)\) 不修时, 意味着 \(v\) 子树内的边都要修,只有一种方法,即答案贡献为 \(1\);

  • \((u, v)\) 修时, 意味着 \((u, v)\) 这条边对贡献无影响,只要 \(v\) 子树内的边满足条件即可,则对答案的贡献为 \(dp[v]\);

总贡献为 \((dp_v + 1)\), 因为对于一个顶点的任意两棵子树而言,子树与子树之间是互不影响的,所以是乘法原理。

所以,$$dp_u = \prod_{v \in sonu} (dp_v + 1) $$

关于 \(g_i\) 部分:

这一 \(part\) 就是换根啦。

真的女子女少

依旧假设存在一条边 \(u -> v\)

  • \((u, v)\) 修时

意味着只要以 \(v\) 为根的整棵树(不包含原以 \(v\) 为根的子树)满足条件即可,即贡献为 \(g_v\)

又因为以 \(u\) 为根的原子树部分也要满足条件, 贡献为 \(dp_u\), 然而根据 \(g_x\) 的定义, 要除去原子树的方案数, 所以真正的贡献应该是 \(dp_u / (dp_v + 1)\);

  • \((u, v)\) 不修时

意味着 \(v\) 的上面部分和 \(u\) 的子树部分都要全修,那贡献只能为 \(1\);

综上,总贡献为 \((dp_u / dp_v + 1)\), 依旧是乘法原理。

\[g_v = g_u \times \tfrac{dp_u}{dp_v + 1} \]

注意除法要用逆元来算,前缀积后缀积维护。

点击查看代码

#include<cstdio>
#include<cstring>
#include<algorithm>
#include<vector>
using namespace std;
#define int long long
const int N = 2e5 + 5, mod = 1e9 + 7;
int n, tot, head[N], Nex[N << 1], to[N << 1], dp[N], g[N];
vector<int> pre[N], nex[N];
void add(int x, int y) {
	to[++tot] = y, Nex[tot] = head[x], head[x] = tot;
}
void DP(int x, int f) {
	int ver;
	dp[x] = 1, pre[x].push_back(1), nex[x].push_back(1);
	for(int i = head[x]; i; i = Nex[i]) {
		ver = to[i];
		if(ver == f) continue;
		DP(ver, x);
		dp[x] = dp[x] * (dp[ver] + 1) % mod;
		pre[x].push_back(dp[ver] + 1), nex[x].push_back(dp[ver] + 1);
	}
	pre[x].push_back(1), nex[x].push_back(1);
	for(int i = 1; i < pre[x].size() - 1; i ++) pre[x][i] = pre[x][i] * pre[x][i - 1] % mod;
	for(int i = nex[x].size() - 2; i; i --) nex[x][i] = nex[x][i] * nex[x][i + 1] % mod;
}

void DP2(int x, int f) {
	int ver;
	for(int i = head[x], j = 1; i; i = Nex[i], j ++) {
		ver = to[i];
		if(ver == f) continue;
		g[ver] = (g[x] * pre[x][j - 1] % mod * nex[x][j + 1] % mod + 1) % mod;
		DP2(ver, x);
	}
}

signed main() {
	scanf("%lld", &n);
	int x;
	for(int i = 2; i <= n; i ++) scanf("%lld", &x), add(i, x), add(x, i);
	g[1] = 1, DP(1, 0), DP2(1, 0);
	for(int i = 1; i <= n; i ++) printf("%lld ", dp[i] * g[i] % mod);
	return 0;
}

Drazil and Morning Exercise

\(Drazil\) \(and\) \(Morning\) \(Exercise\)

好家伙,黑题。

那我先咕了。ヾ(•ω•`)o

posted @ 2021-10-18 10:49  Spring-Araki  阅读(46)  评论(2编辑  收藏  举报