P6534 [COCI 2015/2016 #1] UZASTOPNI 等差树列 题解

P6534 COCI 2015/2016 #1 UZASTOPNI 等差树列 题解

一、题目描述

给定一棵以 1 为根的树,每个节点有一个权值 \(v_i\)(\(1 \le v_i \le 100\))。需要选择一个点集,满足以下条件:

  1. 祖先闭包:如果选择了一个节点,则其所有祖先都必须被选择。
  2. 权值互异:被选择的节点权值互不相同。
  3. 子树连续性 :对于每个被选择的节点 \(u\),在 \(u\) 的子树中被选择的所有节点的权值构成一个公差为 1 的连续整数区间。

方案按权值集合区分,即两个方案只要选出的权值集合相同,就算同一种方案(不关心具体是哪些节点提供了这些权值)。

求满足条件的方案总数。


二、关键性质

性质1:根节点必须被选择

由于祖先闭包条件,如果根节点不被选择,那么任何其他节点都不能被选择,因为它们的祖先链最终都指向根节点。

性质2:全局选中权值集合是一个连续区间

因为根节点被选择,而整棵树就是根节点的子树,根据条件3,根节点子树中所有被选点的权值必须构成连续整数区间。所以全局选中的权值集合必然是形如 \(L, R\) 的连续区间。

性质3:孩子节点的权值区间不能跨越父节点权值 (核心)

考虑任意被选择的节点 \(u\) 和它的一个孩子 \(v\)(\(v\) 也被选择)。假设 \(v\) 子树中选中的权值集合为 \(l_v, r_v\)。

  • 如果 \(l_v < v_u < r_v\),那么为了保持连续性,\(v\) 子树中必须选中权值 \(v_u\)。
  • 但 \(u\) 已经被选中,权值为 \(v_u\),这违反了权值互异条件。
  • 因此,孩子的贡献区间只能完全小于 \(v_u\) 或完全大于 \(v_u\)。

这个性质是我们设计 DP 的基础,它使得我们可以将孩子节点的贡献独立地拼接到父节点的左侧或右侧


三、DP 状态设计

权值范围很小 (\(1 \le v_i \le 100\)),这启发我们使用 bool 数组进行 DP。

定义两个布尔数组:

  • \(Lul\) :在以 \(u\) 为根的子树中,能否选出权值集合恰好为 \(l, v_u\)(\(1 \le l \le v_u\))。

    含义:从权值 \(v_u\) 向左扩展,能覆盖到左端点 \(l\)。

  • \(Rur\) :在以 \(u\) 为根的子树中,能否选出权值集合恰好为 \(v_u, r\)(\(v_u \le r \le 100\))。

    含义:从权值 \(v_u\) 向右扩展,能覆盖到右端点 \(r\)。

初始化 :只选择节点 \(u\) 自身是合法的,所以:

\(Luv_u = Ruv_u = \text{true}\)


四、状态转移

考虑如何将孩子节点 \(v\) 的 DP 信息合并到父节点 \(u\)。

4.1 右侧扩展(\(v_v > v_u\))

当孩子权值大于父节点权值时,孩子只能贡献右侧区间。

假设:

  • \(v\) 能覆盖区间 \(v_v, r\),即 \(Rvr = \text{true}\)
  • \(u\) 当前已经能覆盖区间 \(v_u, x\),即 \(Rux = \text{true}\)
  • 需要两个区间无缝拼接,即 \(x + 1 = v_v\)

因此,必须存在 \(x \in v_u, v_v-1\),使得:

R\[u\]\[x\] == true && L\[v\]\[x+1\] == true

其中 \(Lvx+1\) 表示 \(v\) 能覆盖 \(x+1, v_v\)。

如果条件成立,则 \(v\) 的所有右侧扩展能力都可以继承给 \(u\):

\(\forall r \ge v_v,\ Rvr \Rightarrow Rur\)

4.2 左侧扩展(\(v_v < v_u\))

对称地,当孩子权值小于父节点权值时,孩子只能贡献左侧区间。

必须存在 \(y \in v_v+1, v_u\),使得:

\(Luy == true\) && \(Rvy-1 == true\)

其中 \(Rvy-1\) 表示 \(v\) 能覆盖 \(v_v, y-1\)。

如果条件成立,则:

\(\forall l \le v_v,\ Lvl \Rightarrow Lul\)


五、处理顺序

为什么需要排序?

对于有多个孩子的节点,孩子之间也必须能够无缝拼接 。例如,根节点权值为 5,有三个孩子权值分别为 2、3、4。如果先处理权值 2 的孩子,再处理权值 4 的孩子,会导致区间不连续

正确的处理顺序

  • 右侧扩展 (权值大于父节点):按权值升序 处理,先合并权值最接近 \(v_u\) 的孩子,逐步向外扩展。
  • 左侧扩展 (权值小于父节点):按权值降序 处理,同样从 \(v_u\) 逐步向左扩展。

六、答案统计

根节点 1 必须被选择。全局权值集合为 \(l, r\),其中:

  • 左端点 \(l\) 可以是任意满足 \(L1l = \text{true}\) 的值(\(1 \le l \le v_1\))
  • 右端点 \(r\) 可以是任意满足 \(R1r = \text{true}\) 的值(\(v_1 \le r \le 100\))

左端点和右端点的选择相互独立,所以总方案数为:

\(\text{ans} = \left(\sum_{l=1}^{v_1} L1l\right) \times \left(\sum_{r=v_1}^{100} R1r\right)\)


七、复杂度

  • 点数:\(n \le 10^4\)
  • 权值范围:\(100\)
  • 每个节点处理孩子时,检查条件需要扫描最多 100 个位置
  • 合并也扫描最多 100 个位置
  • 总时间复杂度:\(O(n \cdot 100)\)
  • 空间复杂度:\(O(n \cdot 100)\)

AC code

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;

using ll = long long;

const int N = 1e4 + 5;

ll n;
ll val[N];

bool L[N][105], R[N][105];

vector<vector<int> > edg;

bool cmp(int a, int b) { return val[a] < val[b]; }

void dfs(int u, int fa) {
	memset(L[u], 0, sizeof(L[u]));
	memset(R[u], 0, sizeof(R[u]));
	L[u][val[u]] = R[u][val[u]] = true;
	
	for (auto v : edg[u]) {
		if (v == fa) continue;
		dfs(v, u);
	}
	
	for (auto v : edg[u]) {
		if (v == fa) continue;
		
		if (val[v] > val[u]) {
			bool flag = false;
			for (int r = val[u]; r < val[v]; r ++) {
				if (R[u][r] && L[v][r + 1]) {
					flag = true;
					break;
				}
			}
			if (flag) {
				for (int r = val[v]; r <= 100; r ++) {
					if (R[v][r]) R[u][r] = 1;
				}
			}
		}
	}
	
	for (int i = edg[u].size() - 1; i >= 0; i --) {
		int v = edg[u][i];
		if (v == fa) continue;
		
		if (val[v] < val[u]) {
			bool flag = false;
			for (int l = val[v] + 1; l <= val[u]; l ++) {
				if (L[u][l] && R[v][l - 1]) {
					flag = true;
					break;
				}
			}
			if (flag) {
				for (int l = 1; l <= val[v]; l ++) {
					if (L[v][l]) L[u][l] = 1;
				}
			}
		}
	}
}

int main () {
	freopen ("uzastopni.in", "r", stdin);
	freopen ("uzastopni.out", "w", stdout);
	
	ios::sync_with_stdio(false);
	cin.tie(0);
	
	cin >> n;
	for (int i = 1; i <= n; i ++) cin >> val[i];
	
	edg.resize(n + 1);
	for (int i = 1; i < n; i ++) {
		int a, b; cin >> a >> b;
		edg[a].push_back(b);
		edg[b].push_back(a);
	}
	
	for (int i = 1; i <= n; i ++) {
		sort(edg[i].begin(), edg[i].end(), cmp);
	}
	
	dfs(1, 0);
	
	ll cnt1 = 0, cnt2 = 0;
	for (int i = 1; i <= 100; i ++) {
		if (L[1][i]) cnt1 ++;
		if (R[1][i]) cnt2 ++;
	}
	
	cout << cnt1 * cnt2 << '\n';
	
	return 0;
}

总结

本题的关键在于:

  1. 利用权值范围小的特点,设计左右分离的 DP。

  2. 根据"孩子不跨越父权值"的性质确定拼接条件。

  3. 距离排序处理,保证区间从中心向外合并

  4. 最后乘法原理统计答案。

相关推荐
小玮看世界1 天前
[Python]动态规划三步走:从爬楼梯到打家劫舍,再到最大子数组和
开发语言·python·动态规划
Tisfy1 天前
LeetCode 2058.找出临界点之间的最小和最大距离:遍历+遇到极值则更新(这种题谁空间复杂度不是O(1)啊)
linux·数据库·leetcode·链表·题解·模拟·遍历
positive_zpc3 天前
进阶数据结构图——关键路径(四)
数据结构·图论·关键路径
云淡风轻~窗明几净4 天前
宇宙管理学猜想
算法·图论
小星星闪亮登场4 天前
图论--最小生成树(内含二分图)
数据结构·算法·图论·迭代加深·图搜索算法
2601_956121974 天前
线性DP(入门)
c++·算法·动态规划
positive_zpc4 天前
进阶数据结构图——最短路径(二)
数据结构·算法·图论·最短路径
positive_zpc4 天前
进阶数据结构图——最小生成树(一)
数据结构·算法·图论
无定义_5 天前
Dijkstra——单源最短路径
算法·图论