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. 最后乘法原理统计答案。

相关推荐
Tisfy1 小时前
LeetCode 3727.最大交替平方和:排序 (双指针可选)
算法·leetcode·题解·贪心·数组·排序
Logic1012 小时前
C语言/数据结构动态规划题解:字符串变回文的最少操作次数——最长回文子序列(LPS)
c语言·数据结构·算法·动态规划·时间复杂度·回文串·最长回文子序列
不会就选b3 小时前
算法日常・每日刷题--<动态规划>24
算法·动态规划
不会就选b1 天前
算法日常・每日刷题--<动态规划>22
算法·动态规划
Coder-magician2 天前
《代码随想录》刷题打卡day51:图论-part09(dijistra堆优化,Bellman_ford算法)
算法·图论
不会就选b2 天前
算法日常・每日刷题--<动态规划>20
算法·leetcode·动态规划
垆边人似月.2 天前
华为题:全排列
算法·深度优先·图论
熊猫钓鱼>_>2 天前
大学图论第 6 讲:网络流与二分图匹配——对偶性让瓶颈自己现形
开发语言·算法·面试·职场和发展·图论·校招·面经
All for pursuit.2 天前
【动态规划-11】279.完全平方数
数据结构·c++·算法·leetcode·动态规划
All for pursuit.3 天前
【动态规划-6】96.不同的二叉搜索树
数据结构·c++·算法·leetcode·动态规划