YBTOJ 5.4树形DP

A.树上求和

image
image

因为它有选/不选的状态 我们设状态的时候要考虑进去
所以设 \(f[i][0/1]\) 表示第 \(i\) 个节点没选/选的最大价值
显然就有:

  • \(f[fa][0] = \sum max(f[son][0], f[son][1])\)
  • \(f[fa][1] = \sum f[son][0]\)
    因为父亲的状态要从儿子转移过来 所以先递归后转移
点击查看代码
#include <bits/stdc++.h>
using namespace std;

const int N = 1e4 + 0721;
int head[N], to[N], nxt[N], cnt;
int f[N][2];
bool rt[N];
int n, root;

inline void cmb(int x, int y) {
	to[++cnt] = y;
	nxt[cnt] = head[x];
	head[x] = cnt;
}

void dfs(int x) {
	for (int i = head[x]; i; i = nxt[i]) {
		int y = to[i];
		dfs(y);
		f[x][0] += max(f[y][0], f[y][1]);
		f[x][1] += f[y][0];
	}
}

int main() {
	scanf("%d", &n);
	for (int i = 1; i <= n; ++i) scanf("%d", &f[i][1]);
	for (int i = 1; i < n; ++i) {
		int fa, son;
		scanf("%d%d", &son, &fa);
		cmb(fa, son);
		rt[son] = 1;
	}
	
	for (int i = 1; i <= n; ++i) {
		if (!rt[i]) {
			root = i;
			break;
		}
	}
	
	dfs(root);
	
	printf("%d", max(f[root][0], f[root][1]));
	
	return 0;
}

B.节点覆盖

image
image

很容易想到的一个思路是用 \(1/0\) 表示选/不选这个节点
但是有一个问题 转移的时候它的父亲和它的儿子必须满足至少选一个 没法转移
所以我们考虑设 \(0/1/2\) 表示被父亲/自己/儿子看守
转移很显然 可以看代码

点击查看代码
#include <bits/stdc++.h>
using namespace std;

const int N = 0x0d00;
int head[N], nxt[N], to[N], v[N];
int dp[N][3];
int cnt, n, ans;

void cmb(int x, int y) {
    to[++cnt] = y;
    nxt[cnt] = head[x];
    head[x] = cnt;
}

void dfs(int x, int fa) {
    dp[x][1] = v[x];
    int tmp = 0x7ffffff;
    for (int i = head[x]; i; i = nxt[i]) {
        int y = to[i];
        if (y == fa)
            continue;
        dfs(y, x);
        tmp = min(tmp, dp[y][1] - dp[y][2]);
        dp[x][0] += min(dp[y][1], dp[y][2]);
        dp[x][1] += min(dp[y][0], min(dp[y][1], dp[y][2]));
        dp[x][2] += min(dp[y][1], dp[y][2]);
    }
    dp[x][2] += max(tmp, 0);
    if (fa == 0)
        ans = min(dp[x][1], dp[x][2]);
}

int main() {
    scanf("%d", &n);
    for (int i = 1; i <= n; ++i) {
        int x, m;
        cin >> x;
        cin >> v[x];
        cin >> m;
        if (m != 0) {
            for (int j = 1; j <= m; ++j) {
                int u;
                scanf("%d", &u);
                cmb(x, u);
                cmb(u, x);
            }
        }
    }
    //	cout<<cnt ;
    dfs(1, 0);

    //	for( int i = 1 ; i <= cnt ; ++i )
    //	cout<<to[i]<<" " ;

    printf("%d", ans);

    return 0;
}

C.最长距离

image
image

详见YBTOJ 5.4例3 最长距离 题解


D.选课方案

image
image

详见P2014 选课 ( 树上背包 )
复杂度证明是假的不要看

点击查看代码
#include <bits/stdc++.h>
using namespace std;

const int N = 1e3 + 0721;
int head[N], nxt[N], to[N], cnt;
int dp[N][N];
int m, n;

void cmb(int x, int y) {
    to[++cnt] = y;
    nxt[cnt] = head[x];
    head[x] = cnt;
}

void dfs(int x) {
    for (int i = head[x]; i; i = nxt[i]) {
        int y = to[i];
        dfs(y);
        for (int j = m + 1; j > 0; --j) {
            for (int k = 0; k < j; ++k) dp[x][j] = max(dp[x][j], dp[x][j - k] + dp[y][k]);
        }
    }
}

int main() {
    scanf("%d%d", &n, &m);
    for (int i = 1; i <= n; ++i) {
        int x, y;
        scanf("%d%d", &x, &y);
        cmb(x, i);
        for (int j = 1; j <= m + 1; ++j) dp[i][j] = y;
    }

    dfs(0);

    printf("%d", dp[0][m + 1]);

    return 0;
}

E.路径求和

image
image

很适合独立思考的一道题 但是题面没看懂直接去找题解了 可惜了

应该加上一句话:对于无根树 我们定义度数为 \(1\) 的点为叶节点

暴力的想法是枚举点对 \(\text{O}(n ^ 2)\)

考虑每一条边对答案的贡献 把它断掉把树分为两个子树 \(x, y\)
那么这条边对答案的贡献就是 \(x\) 中的所有叶节点走到 \(y\) 中的所有节点 + \(y\) 中的所有叶节点走到 \(x\) 中的所有节点
所以我们直接维护出每个子树的大小以及叶节点数量即可

理论上要把 \(deg_i \neq 1\)\(i\) 作根节点开始 dfs
但实际这题直接 dfs(1, 0) 也能过

注意本题是先输入边权再输入两端点 /fn

点击查看代码
#include <bits/stdc++.h>
#define ll long long 
using namespace std;

namespace steven24 {
	
const int N = 1e5 + 0721;
int head[N], to[N << 1], nxt[N << 1], len[N << 1], cnt;
int deg[N], siz[N], num[N];
int totleaf;
int n, m;
ll ans;

inline int read() {
    int xr = 0, F = 1; 
	char cr;
    while (cr = getchar(), cr < '0' || cr > '9') if (cr == '-') F = -1;
    while (cr >= '0' && cr <= '9') 
        xr = (xr << 3) + (xr << 1) + (cr ^ 48), cr = getchar();
    return xr * F;
}

inline void add_edge(int x, int y, int z) {
	to[++cnt] = y;
	nxt[cnt] = head[x];
	head[x] = cnt;
	len[cnt] = z;
}

void dfs(int x, int fa) {
	siz[x] = 1;
	if (deg[x] == 1) {
		++totleaf;
		num[x] = 1;
	}
	for (int i = head[x]; i; i = nxt[i]) {
		int y = to[i];
		if (y == fa) continue;
		dfs(y, x);
		siz[x] += siz[y];
		num[x] += num[y];
	}
}

void get_ans(int x, int fa) {
	for (int i = head[x]; i; i = nxt[i]) {
		int y = to[i];
		if (y == fa) continue;
		ans += 1ll * len[i] * (1ll * num[y] * (n - siz[y]) + 1ll * siz[y] * (totleaf - num[y]));
		get_ans(y, x);
	}
}

void main() {
	n = read(), m = read();
//	cerr << n << " " << m << "\n";
	for (int i = 1; i <= m; ++i) {
		int x, y, z;
		z = read(), x = read(), y = read();
//		cerr << x << " " << y << " " << z << "\n";
		add_edge(x, y, z);
		add_edge(y, x, z);
		++deg[x], ++deg[y];
	}
	for (int i = 1; i <= n; ++i) {
		if (deg[i] > 1) {
			dfs(i, 0);
			get_ans(i, 0);
			break;
		}
	}
	printf("%lld\n", ans);
}
	
}

int main() {
	steven24::main();
	return 0;
}
/*
5 4
1 2 1
1 3 1
2 4 2
2 5 2
*/

F.树上移动

image
image

无根树 并且 \(S\) 是固定的 不难想到把 \(S\) 作为根节点

\(f_{i, 0/1/2}\) 表示从 \(i\) 出发遍历子树内所有节点 回到 / 一个点出发不回到 / 两个点出发不回到该节点的最短距离
转移的时候 对于 \(f_{x, 0}\)\(f_{x, 0} = \sum\limits_{y \in son_x} f_{y, 0} + 2 \times dis_{x, y}\)
对于 \(f_{x, 1}\)\(f_{x, 1} = \min\limits_{y \in son_x}(f_{y, 1} + dis_{x, y} + \sum\limits_{z \in son_x, z \ne y} f_{z, 0} + 2 \times dis_{x, z})\)

对于第二个转移 我们把 \(\sum\limits_{z \in son_x, z \ne y} f_{z, 0} + 2 \times dis_{x, z}\) 转化成 \(f_{x, 0} - f_{y, 0} - dis_{x, y} \times 2\) 来保证复杂度

对于 \(f_{x, 2}\) 可以是一个儿子的 \(f_{y, 2} + 2 \times dis_{x, y}\) + 剩余儿子的 \(f_{y, 0} + 2 \times dis_{x, y}\)
也可以是两个儿子的 \(f_{y, 1}\) + 剩余儿子的 \(f_{y, 0} + 2 \times dis_{x, y}\)
前面那个转移还是用上面那个优化来保证复杂度
对于下面那个 我们考虑取到 \(f_{y, 1}\)\(f_{x, 0}\) 的差值 那就是 \(f_{x, 0} - f_{y, 0} + f_{y, 1} - dis_{x, y}\)
那么我们直接维护 $ - f_{y, 0} + f_{y, 1} - dis_{x, y}$ 的最小值和次小值即可

那么对于第一问 答案就是 \(f_{s, 1}\)
对于第二问 答案就是 \(f_{s, 2}\)

点击查看代码
#include <bits/stdc++.h>
#define ll long long 
using namespace std;

namespace steven24 {
	
const int N = 1e5 + 0721;
const ll inf = 0x7ffffffffffffff;
int head[N], to[N << 1], nxt[N << 1], len[N << 1], cnt;
ll f[N][3];
int n, s;

inline int read() {
    int xr = 0, F = 1; 
	char cr;
    while (cr = getchar(), cr < '0' || cr > '9') if (cr == '-') F = -1;
    while (cr >= '0' && cr <= '9') 
        xr = (xr << 3) + (xr << 1) + (cr ^ 48), cr = getchar();
    return xr * F;
}

inline void add_edge(int x, int y, int z) {
	to[++cnt] = y;
	nxt[cnt] = head[x];
	head[x] = cnt;
	len[cnt] = z;
}

void dfs(int x, int fa) {
	bool isleaf = 1;
	for (int i = head[x]; i; i = nxt[i]) {
		int y = to[i];
		if (y == fa) continue;
		isleaf = 0;
		dfs(y, x);
		f[x][0] += f[y][0] + 2 * len[i];
	}
	
	if (!isleaf) {
		f[x][1] = inf;
		f[x][2] = inf;
	}
	ll min1 = inf, min2 = inf;
	for (int i = head[x]; i; i = nxt[i]) {
		int y = to[i];
		if (y == fa) continue; 
		f[x][1] = min(f[x][1], f[y][1] - len[i] + f[x][0] - f[y][0]);
		f[x][2] = min(f[x][2], f[y][2] + f[x][0] - f[y][0]);
		
		if (f[y][1] - f[y][0] - len[i] < min1) {
			min2 = min(min2, min1);
			min1 = f[y][1] - f[y][0] - len[i];
		} else min2 = min(min2, f[y][1] - f[y][0] - len[i]);
	}
	if (min1 != inf && min2 != inf) f[x][2] = min(f[x][2], f[x][0] + min1 + min2);
	
} 

void main() {
	n = read(), s = read();
	for (int i = 1; i < n; ++i) {
		int x, y, z;
		x = read(), y = read(), z = read();
		add_edge(x, y, z);
		add_edge(y, x, z);
	}
	dfs(s, 0);
	printf("%lld\n", f[s][1]);
	printf("%lld\n", f[s][2]);
}
	
}

int main() {
	steven24::main();
	return 0;
}
/*
5 1
1 2 8
1 3 10
3 4 10
4 5 7
*/

G.块的计数

image
image

\(f_{i, 0/1}\) 表示以 \(i\) 为根节点的合法 / 不合法联通块个数
下意识觉得不合法联通块个数不是很好转移
进而考虑设 \(f_{i, 0/1}\) 表示以 \(i\) 为根节点的合法 / 合不合法都行的联通块个数 剩下那个减一下就行了
然后写假了 寄。

实际上不好转移的是合法连通块个数
所以设 \(f_{i, 0/1}\) 表示以 \(i\) 为根节点的不合法 / 合不合法都行的连通块个数
那么就有 \(f_{x, 0} = \prod\limits_{y \in son_x} (f_{y, 0} + 1)\) (可以不选)
\(f_{x, 1} = \prod\limits_{y \in son_x} (f_{y, 1} + 1)\) (可以不选)
答案就是 \(\sum\limits_{i = 1}^n (f_{i, 1} - f_{i, 0})\)

点击查看代码
#include <bits/stdc++.h>
#define ll long long

using namespace std;

namespace steven24 {
	
const int N = 1e5 + 0721;
const int mod = 998244353;

ll f[N][2];
int val[N], maxn;
int head[N], nxt[N << 1], to[N << 1], cnt;
int n;	

inline int read() {
    int xr = 0, F = 1; 
	char cr;
    while (cr = getchar(), cr < '0' || cr > '9') if (cr == '-') F = -1;
    while (cr >= '0' && cr <= '9') 
        xr = (xr << 3) + (xr << 1) + (cr ^ 48), cr = getchar();
    return xr * F;
}

inline void add_edge(int x, int y) {
	to[++cnt] = y;
	nxt[cnt] = head[x];
	head[x] = cnt;
}
void dfs(int x, int fa) {
	f[x][1] = 1;
	if (val[x] != maxn) f[x][0] = 1;
	for (int i = head[x]; i; i = nxt[i]) {
		int y = to[i];
		if (y == fa) continue;
		dfs(y, x);
		f[x][1] = f[x][1] * (f[y][1] + 1) % mod;
		f[x][0] = f[x][0] * (f[y][0] + 1) % mod;
	}
}

void main() {
	n = read();
	for (int i = 1; i <= n; ++i) val[i] = read();
	maxn = *max_element(val + 1, val + 1 + n);
	for (int i = 1; i < n; ++i) {
		int x, y;
		x = read(), y = read();
		add_edge(x, y);
		add_edge(y, x);
	}
	dfs(1, 0);
	ll ans = 0;
	for (int i = 1; i <= n; ++i) ans = (mod + ans + f[i][1] - f[i][0]) % mod;

	printf("%lld\n", ans);
}
	
}

int main() {
	steven24::main();
	return 0;
}
/*
5
1 1 1 1 1
1 2
2 3
3 4
4 5
*/

H.树的合并

image
image

考虑枚举第一颗树上的点 把它们和第二颗树上所有点连边的情况
设两点为 \(u, v\)\(f_i\) 表示 \(i\) 在它所在树里能走到的最远点距离
那么显然新树直径就是 \(\max(第一棵树直径, 第二棵树直径, f_u + f_v + 1)\)

我们枚举 \(u\) 点 那么我们就需要讨论 \(f_u + f_v + 1\)\(\max(第一棵树直径, 第二棵树直径)\) 的大小
\(f_u\) 是给定的 那么如果我们把 \(f_v\) 排序 我们就可以二分一个位置 使它前面的 \(v\) 都取 \(\max(第一棵树直径, 第二棵树直径)\) 后面的 \(v\) 都取 \(f_u + f_v + 1\)

那么前面那些直接就是点数 \(\times \max(第一棵树直径, 第二棵树直径)\)
后面那些预处理一个后缀和即可

总复杂度 \(\text{O}(n \log n + n \log m + m \log m)\)

如果用 C 题那个做法来预处理 \(f\) 数组的话就是 \(\text{O}(n \log m + m \log m)\)
然后如果 \(m > n\) 的话就把两棵树 swap 一下 就可以做到极致复杂度 虽然最开始那个复杂度就随便过了(

注意二分边界为 \(\left[1, m + 1\right]\) 炸了一发

点击查看代码
/*
两次dfs求出直径两端点
维护树上距离
求出每个点的最长距离 然后全拿下来放到两个数组里
在b中二分找f[i] + g[j] + 1>max(d1, d2)的最小j 
给g弄个后缀和
ans+=max(d1,d2)*(j-1)+h[j]+(f[i]+1)*(n2-j+1) 
*/
#include <bits/stdc++.h>
#define ll long long

using namespace std;

inline int read() {
	int xr = 0, F = 1;
	char cr;
	while (cr = getchar(), cr < '0' || cr > '9') if (cr == '-') F = -1;
	while (cr >= '0' && cr <= '9')
		xr = (xr << 3) + (xr << 1) + (cr ^ 48), cr = getchar();
	return xr * F;
}

void write(ll x) {
	char ws[51];
	int wt = 0;
	if (x < 0) putchar('-'), x = -x;
	do {
		ws[++wt] = x % 10 + '0';
		x /= 10;
	} while (x);
	for (int i = wt; i; --i) putchar(ws[i]);
}

namespace steven24 {

const int N = 1e5 + 0721;
int f[N], g[N];
ll h[N], ans;
int d;

struct tree {

int dis[N], dep[N], fa[21][N];
int head[N], nxt[N << 1], to[N << 1], cnt;
int u1, u2, d;
int n;	

inline void add_edge(int x, int y) {
	to[++cnt] = y;
	nxt[cnt] = head[x];
	head[x] = cnt;
}

void dfs1(int x, int f) {
	dep[x] = dep[f] + 1;
	fa[0][x] = f;
	for (int i = head[x]; i; i = nxt[i]) {
		int y = to[i];
		if (y == f) continue;
		dfs1(y, x);
	}
}

void dfs2(int x, int f, bool opt) {
	dis[x] = dis[f] + 1;
	if (!opt) {
		if (dis[x] > dis[u1]) u1 = x;
	}
	else {
		if (dis[x] > dis[u2]) u2 = x;
	}
	
	for (int i = head[x]; i; i = nxt[i]) {
		int y = to[i];
		if (y == f) continue;
		dfs2(y, x, opt);
	}
}

void init() {
	for (int j = 1; j <= 20; ++j) {
		for (int i = 1; i <= n; ++i) fa[j][i] = fa[j - 1][fa[j - 1][i]];
	}
}

int lca(int x, int y) {
	if (dep[x] < dep[y]) swap(x, y);
	for (int j = 20; j >= 0; --j) if (dep[fa[j][x]] >= dep[y]) x = fa[j][x];
	if (x == y) return x;
	for (int j = 20; j >= 0; --j) if (fa[j][x] != fa[j][y]) x = fa[j][x], y = fa[j][y];
	return fa[0][x];
}

int query(int x, int y) {
	return dep[x] + dep[y] - (dep[lca(x, y)] << 1);
}
	
} tr1, tr2;


void init(tree &x) {
	x.dfs1(1, 0);
	x.init();
	x.dfs2(1, 0, 0);
	memset(x.dis, 0, sizeof x.dis);
	x.dfs2(x.u1, 0, 1);
	x.d = x.query(x.u1, x.u2);
}

int binary_search(int val, int l, int r) {
	int mid, ret = -1;
	while (l <= r) {
		mid = (l + r) >> 1;
		if (g[mid] + val + 1 > d) {
			ret = mid;
			r = mid - 1;
		} else 
			l = mid + 1;
	}
	if (ret == -1) return r + 1;
	else return ret;
}

void main() {
	tr1.n = read(), tr2.n = read();
	int n1 = tr1.n, n2 = tr2.n;
	for (int i = 1; i < n1; ++i) {
		int x = read(), y = read();
		tr1.add_edge(x, y);
		tr1.add_edge(y, x); 
	}
	for (int i = 1; i < n2; ++i) {
		int x = read(), y = read();
		tr2.add_edge(x, y);
		tr2.add_edge(y, x);
	}
	init(tr1);
	init(tr2);
	for (int i = 1; i <= n1; ++i) f[i] = max(tr1.query(i, tr1.u1), tr1.query(i, tr1.u2));
	for (int i = 1; i <= n2; ++i) g[i] = max(tr2.query(i, tr2.u1), tr2.query(i, tr2.u2));
	sort(g + 1, g + 1 + n2);
	for (int i = n2; i; --i) h[i] = h[i + 1] + g[i];
	
	d = max(tr1.d, tr2.d);
	for (int i = 1; i <= n1; ++i) {
		int loc = binary_search(f[i], 1, n2);
		ans += 1ll * d * (loc - 1) + h[loc] + 1ll * (f[i] + 1) * (n2 - loc + 1);
	}
	write(ans), putchar('\n');
}

}

int main() {
	steven24::main();
	return 0;
}
/*
4 3
1 2
2 3
2 4
1 3
2 3
*/

I.权值统计

image
image

想到个换根的做法结果被卡模数 怎么会是呢

\(f_i\) 表示以 \(i\) 为根的子树内所有以 \(i\) 为终点的路径权值和
\(g_i\) 表示以 \(i\) 为根的子树内所有经过 \(i\) 的路径权值和

考虑转移
对于 \(f_i\) 显然就是所有以它儿子为终点的路径都加上它自己 还有只包含它自己一个点的路径
\(f_i = (\sum\limits f_{son_i} + 1) \times v_i\)

对于 \(g_i\) 需要额外统计从一个子树出来经过 \(i\) 再进入另一个子树的路径权值和
实际上这部分就是 \((\sum\limits 儿子的f值两两相乘) \times v_i\) 具体可以自己拆一下
注意到 \(2(x_1x_2 + x_2x_3 + x_1x_3) = (x_1 + x_2 + x_3) ^ 2 - (x_1^2 + x_2^2 + x_3^2)\) 并且这个柿子对于更多的 \(x_i\) 也成立
所以就可以换成 \(\frac{(\sum\limits f_{son_i})^2 - \sum\limits f_{son_i}^2}{2}\)
那么就有 \(g_i = f_i + \frac{(\sum\limits f_{son_i})^2 - \sum\limits f_{son_i}^2}{2} \times v_i\)

最终答案为 \(\sum\limits_{i=1}^n g_i\)

点击查看代码
/*
*/
#include <bits/stdc++.h>
#define ll long long


using namespace std;

inline int read() {
	int xr = 0, F = 1;
	char cr;
	while (cr = getchar(), cr < '0' || cr > '9') if (cr == '-') F = -1;
	while (cr >= '0' && cr <= '9')
		xr = (xr << 3) + (xr << 1) + (cr ^ 48), cr = getchar();
	return xr * F;
}

void write(ll x) {
	char ws[51];
	int wt = 0;
	if (x < 0) putchar('-'), x = -x;
	do {
		ws[++wt] = x % 10 + '0';
		x /= 10;
	} while (x);
	for (int i = wt; i; --i) putchar(ws[i]);
}

namespace steven24 {

const int N = 1e5 + 0721;
const int mod = 10086;	
ll f[N];
int v[N];
int head[N], nxt[N << 1], to[N << 1], cnt;
int n;
ll ans;

inline void add_edge(int x, int y) {
	to[++cnt] = y;
	nxt[cnt] = head[x];
	head[x] = cnt;
}

void dfs(int x, int fa) {
	ll sum = 0, tot = 0;
	for (int i = head[x]; i; i = nxt[i]) {
		int y = to[i];
		if (y == fa) continue;
		dfs(y, x);
		sum += f[y];
		tot += f[y] * f[y];
	}
	f[x] = 1ll * (sum + 1) * v[x] % mod;
	ans = (ans + f[x] + (sum * sum - tot) / 2 * v[x]) % mod;
}

void main() {
	n = read();
	for (int i = 1; i <= n; ++i) v[i] = read();
	for (int i = 1; i < n; ++i) {
		int x = read(), y = read();
		add_edge(x, y);
		add_edge(y, x);
	}
	dfs(1, 0);
	write(ans), putchar('\n');
}

}

int main() {
	steven24::main();
	return 0;
}
/*
4 3
1 2
2 3
2 4
1 3
2 3
*/
posted @ 2023-07-02 21:52  Steven24  阅读(53)  评论(0编辑  收藏  举报