“ 洛谷 ”P4539 [SCOI2006]zh_tree

小兔的话

推荐 小兔的CSDN


[SCOI2006]zh_tree

题目限制

  • 内存限制:250.00MB
  • 时间限制:1.00s
  • 标准输入输出

题目知识点

  • 思维
  • 动态规划 \(dp\)
    • 区间\(dp\)

题目来源

“ 洛谷 ”P4539 [SCOI2006]zh_tree


为了方便大家阅读通畅,题目可能略有改动,保证不会造成影响

题目

题目背景

张老师根据自己工作的需要,设计了一种特殊的二叉搜索树

题目描述

他把这种二叉树起名为 zh_tree,对于具有 \(n\) 个节点的 zh_tree,其中序遍历恰好为 \((1、2、3、···、n)\),其中数字 \(1、2、3、···、n\) 是每个节点的编号
\(n\) 个结点恰好对应于一组学术论文中出现的 \(n\) 个不同的单词

\(i\) 个单词在该组论文中出现的次数记为 \(d_i\),例如,\(d_2 = 10\) 表示第 \(2\) 个节点所对应的单词在该组论文中出现了 \(10\)
设该组论文中出现的单词总数为 \(S\),即 \(S = d_1 + d_2 + ··· + d_n = \sum _{i = 1} ^{n} d_i\)
\(f_i = \frac{d_i}{S}\) 为第 \(i\) 个单词在该组论文中出现的 概率(频率)

张老师把根节点深度规定为 \(0\),如果第 \(i\) 个节点的深度为 \(r_i\),则访问该节点的代价为 \(h_i\)\(h_i = k(r_i + 1) + c\),其中 \(k、c\) 为已知的常数 \((0 < k, c \leq 100)\)

zh_tree 是满足 \(h_1f_1 + h_2f_2 + ··· + h_nf_n = \sum _{i = 1} ^ {n} h_if_i\) 最小的一棵二叉树
我们称上式为访问 zh_tree 的代价
请你根据已知的数据为张老师设计一棵 zh_tree

格式

输入格式

输入共 \(2\) 行:
对于第 \(1\) 行,\(3\) 个用空格隔开的正数,\(n、k、c\)。其中 \(n < 30\),为整数;\(k、c\) 为不超过 \(100\) 的正实数
对于第 \(2\) 行:\(n\) 个用空格隔开的正整数,为每个单词出现的次数 \(d_i(d_i < 200)\)

输出格式

输出一行:一个正实数,保留 \(3\) 位小数,表示访问 zh_tree 的最小代价

样例

样例输入

4 2 3.5
20 30 50 20

样例输出

7.000

提示

数据范围

对于 \(100\%\) 的数据:\(1 \leq n < 30\)\(0 < k、c \leq 100\)\(d_i < 200\)


思路

对于每个节点的深度 \(r_i\),由于 \(h_i = k(r_i + 1) + c\),不妨可以把根节点的深度改为 \(1\),这样就可以直接用 现在的 \(r_i\) 表示 原来的 \(r_i + 1\)

我们可以先把 \(\sum _{i = 1} ^ {n} h_if_i\) 化简
\(\sum _{i = 1} ^ {n} (h_i * f_i) = \sum _{i = 1} ^{n} [k(r_i + c) * \frac{d_i}{S}] = k * \frac{1}{S} * \sum _{i =1} ^{n} [(r_i + c) * d_i] = \frac{k}{S} * \sum _{i = 1} ^{n} (r_i * d_i + c * d_i) = \frac{k}{S} * (\sum _{i = 1} ^{n} r_i * d_i + \sum _{i = 1} ^{n} c * d_i) = (\frac{k}{S} * \sum _{i = 1} ^{n} r_i * d_i) + [\frac{k}{S} * c * \sum _{i = 1} ^{n} d_i] = (\frac{k}{S} * \sum _{i = 1} ^{n} r_i * d_i) + [\frac{k}{S} * c * S] = k * [\frac{\sum _{i = 1} ^{n} r_i * d_i}{S} + c]\)

题目要我们求 \(\sum _{i = 1} ^ {n} h_if_i\) 的最小值,就可以转化成求 \(\sum _{i = 1} ^{n} r_i * d_i\) 的最小值了
这道题也就是道 区间 \(dp\) 的题目了


分析

\(dp[i][j]\) 表示:一棵二叉树的 中序遍历(左子树,根节点,右子树)\((i、i + 1、i + 2、···、j)\) 时,\(\sum _{k = i} ^{j} r_k * d_k\) 的最小值
最终的答案就是 \(\frac{K * dp[1][n]}{S} + C\)

首先,对于每个 \(k = i = j\)\(dp[k][k] = r_k * d_k\);而其它的每个 \(i \neq j\),值则为 \(INF\),即无穷大 —— 这是初始状态

其次,每个 \(dp[i][j] (i < j)\) 是由上一个状态转移而来的,我们可以用 \(k\) 来枚举每一个可能的子树根节点 \(->\) 就得到由 \(k\) 分成的左子树和右子树了,由于要生成新的一棵二叉树,左右子树每个节点的深度需要加 \(1\),贡献就是 \((\sum _{t = i} ^{k – 1} d_t) + (\sum _{t = k + 1} ^{j} d_t)\);同时还有当前 \(k\) 节点的贡献 \(d_k\);总贡献就是 \(\sum _{t = i} ^{j} d_t\)
所以 \(dp[i][j] = \min\{dp[i][k – 1] + dp[k + 1][j] + \sum _{t = i} ^{j} d_t\}\)
不过,需要在 \(k = i\)\(k = j\) 的时候特殊处理


代码

#include <cstdio>
#include <cstring>

#define Min(a, b) ((a) < (b) ? (a) : (b))

const int MAXN = 30;

int N, S;
int D[MAXN + 5];
int Pre[MAXN + 5]; // 前缀和优化 

double K, C;
double dp[MAXN + 5][MAXN + 5];

int main()
{
	memset(dp, 0x3f, sizeof(dp));
	scanf("%d %lf %lf", &N, &K, &C);
	for (int i = 1; i <= N; i++)
	{
		scanf("%d", &D[i]);
		Pre[i] = S += D[i];
		dp[i][i] = 1.0 * D[i];
	}
	for (int len = 2; len <= N; len++)
	{
		for (int i = 1, j = len; j <= N; i++, j++)
		{
			dp[i][j] = Min(dp[i][j - 1], dp[i + 1][j]);
			for (int k = i + 1; k <= j - 1; k++)
				dp[i][j] = Min(dp[i][j], dp[i][k - 1] + dp[k + 1][j]);
			dp[i][j] += 1.0 * (Pre[j] - Pre[i - 1]);
		}
	}
	printf("%.3lf\n", K * dp[1][N] / S + C);
	return 0;
}