「 洛谷 」P4539 [SCOI2006]zh_tree
小兔的話
推薦 小兔的CSDN
[SCOI2006]zh_tree
題目限制
- 記憶體限制:250.00MB
- 時間限制:1.00s
- 標準輸入輸出
題目知識點
- 思維
- 動態規劃 \(dp\)
- 區間\(dp\)
題目來源
為了方便大家閱讀通暢,題目可能略有改動,保證不會造成影響
題目
題目背景
張老師根據自己工作的需要,設計了一種特殊的二叉搜索樹
題目描述
他把這種二叉樹起名為 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;
}