CF-1009F Dominant Indices:长链剖分+DP指针优化

Dominant Indices【长链剖分+DP指针优化】

给定一棵以 1 为根,共有 n n n 个节点的树。设 d ( u , x ) d(u,x) d(u,x) u u u 子树中到 u u u 距离为 x x x 的节点数。

对于每个点,求一个最小的 k k k,使得 d ( u , x ) d(u,x) d(u,x) 最大

题解

f u , d e p f_{u,dep} fu,dep 表示 u u u 的子树中与 u u u 的距离为 d e p dep dep 的点的个数,则有转移方程
f u , d e p = ∑ v ∈ s o n u f v , d e p − 1 f_{u,dep} = \sum_{v\in son_u} f_{v,dep-1} fu,dep=vsonufv,dep1
此时暴力求解,复杂度为 O ( n 2 ) O(n^2) O(n2),考虑用长链剖分优化。

对于一个节点 u u u,先遍历其重儿子 s o n u son_u sonu,将重儿子的结果合并到当前点 u u u 上(通过指针 O ( 1 ) O(1) O(1) 实现),然后再遍历这个点 u u u 的所有轻儿子 v v v,并将轻儿子的结果合并到当前节点 u u u 上,此时复杂度就优化为 O ( n ) O(n) O(n)

对于答案的统计,可以先令 u u u 节点的答案为其重儿子的答案加一,在暴力合并的过程中检查是否有更优的答案。如果最后发现 f u , a n s u = 1 f_{u,ans_u}=1 fu,ansu=1,即 u u u 的子树为一条链,此时答案 a n s u ans_u ansu 应设为0。

复杂度证明

u u u 为根节点,则其子树 v v v 的信息会被暴力合并,当且仅当该子树 v v v 是轻儿子,合并的代价就是这棵子树 v v v 中最长链的长度(即子树 v v v 的深度)。根据长链剖分结果,子树 v v v 的根节点必然就是这个最长链的链头,因而就转化成了——只有每条长链的链头会被暴力合并,并且合并的时间复杂度就等于链长。由于所有链长和为 n n n,因此总复杂度就是 O ( n ) O(n) O(n)

指针 O ( 1 ) O(1) O(1) 优化

继承重儿子结果的过程实际上就是把重儿子的数组复制一遍,因此放弃传统的为每个节点都申请一个空间的DP写法,而是在DP的过程中动态为节点申请内存,进而实现部分节点“共用内存”。

具体来说,只对每一个长链的链头节点申请内存,其大小就等于链长。对于长链上的所有节点,都可以共用这一片空间。例如对节点 u u u 申请了内存后,若 v v v u u u 的重儿子,那么就把 f u f_u fu 数组的起点加 1 当做 f v f_v fv 数组的起点,那么当节点 v v v 完成统计后,实际上就已经更新到了 f u f_u fu 里。

过程模拟

假设有如下一棵树,其中长链为 1 − 2 − 4 1-2-4 124 t m p tmp tmp 数组用来实现“动态申请内存”,初始情况下指针 i t it it 指向 t m p 0 tmp_0 tmp0,表示当前还未分配空间。

image-20210204223700741

最开始为根节点 1 1 1 分配空间,因为链长为 3 3 3,所以分配了 3 3 3 个空间,即从 t m p 0 tmp_0 tmp0 t m p 2 tmp_2 tmp2,并让指针 i t + 3 it+3 it+3,表示前 3 3 3 个位置已经被分配出去了。

image-20210204224018450

从根节点开始 d f s dfs dfs,沿着重儿子走,首先到节点 2 2 2。由于链头 1 1 1 号节点已经申请了足够大的内存,因此链上的节点直接使用即可。

d p 2 dp_2 dp2 指向 d p 1 + 1 dp_1+1 dp1+1,即 d p 2 dp_2 dp2 实际拥有的空间就是 t m p 1 tmp_1 tmp1 t m p 2 tmp_2 tmp2

image-20210204224322434

继续走重儿子,到达节点4。同样让 d p 4 dp_4 dp4 指向 d p 2 + 1 dp_2+1 dp2+1,即 d p 4 dp_4 dp4 实际拥有的空间就是 t m p 2 tmp_2 tmp2

image-20210204224728283

重儿子走完后回溯到节点 2 2 2,继续走轻儿子。因为轻儿子实际上可以看做是另一条长链的开端,因此需要为这条链开辟新的空间。

d p 3 dp_3 dp3 指向 i t it it,因为此时深度只有1,因此只开辟了 1 1 1 个空间,即 d p 3 dp_3 dp3 实际拥有的空间就只有 t m p 3 tmp_3 tmp3,并且让 i t + 1 it+1 it+1

image-20210204225013266

代码

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

const int maxn = 1e6 + 10;

int n;
struct Edge {
	int to, w, nxt;
}edge[maxn << 1];
int tot, head[maxn];

void addEdge(int u, int v, int w) {
	edge[tot].to = v;
	edge[tot].w = w;
	edge[tot].nxt = head[u];
	head[u] = tot++;
}

int dep[maxn], son[maxn];
int tmp[maxn], *it = tmp; // 动态开辟空间
int *dp[maxn], ans[maxn];

/* 长链剖分 */
void dfs1(int u, int fa) {
	for (int i = head[u]; ~i; i = edge[i].nxt) {
		int v = edge[i].to;
		if (v == fa) continue;
		dfs1(v, u);
		if (dep[v] > dep[son[u]]) son[u] = v;
	}
	dep[u] = dep[son[u]] + 1;
}

/* DP过程 */
void dfs2(int u, int fno) {
	dp[u][0] = 1;
	if (son[u]) {
		dp[son[u]] = dp[u] + 1; // 共享内存
		dfs2(son[u], u);
		ans[u] = ans[son[u]] + 1; // 从重儿子处继承答案
	}
	for (int i = head[u]; ~i; i = edge[i].nxt) {
		int v = edge[i].to;
		if (v == son[u] || v == fno) continue;
		dp[v] = it; it += dep[v]; // 分配新内存
		dfs2(v, u);
		for (int j = 1; j <= dep[v]; j++) {
			dp[u][j] += dp[v][j - 1]; // 暴力合并
			if (dp[u][j] > dp[u][ans[u]] || (dp[u][j] == dp[u][ans[u]] && j < ans[u])) {
				ans[u] = j; // 更新答案
			}
		}
	}
	if (dp[u][ans[u]] == 1) ans[u] = 0;
}

int main()
{
	ios::sync_with_stdio(0), cin.tie(0);
	mem(head, -1);
	cin >> n;
	for (int i = 1; i < n; i++) {
		int u, v; cin >> u >> v;
		addEdge(u, v, 1);
		addEdge(v, u, 1);
	}
	dfs1(1, 0); // 长链剖分
	dp[1] = it; it += dep[1]; // 为根节点的答案分配内存
	dfs2(1, 0); // DP
	for (int i = 1; i <= n; i++) {
		cout << ans[i] << endl;
	}
}
  • 4
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值