每日一题——樱桃分级优化问题:最小化标准差的动态规划与DFS解决方案
文章目录
- 一、问题描述
- 输入格式
- 输出格式
- 二、问题本质分析
- 三、解题思路
- 1. 前缀和预处理
- 2. DFS 枚举与剪枝
- 3. 剪枝策略
- 4. 标准差计算
- 四、代码实现
- 五、样例解析
- 样例 1
- 样例 2
- 六、一行行代码带你敲
- dfs
- 七、总结
一、问题描述
某大型樱桃加工厂使用自动化机械扫描了一批樱桃的尺寸大小,获得了直径范围 [L, H]
内各个区间的所有樱桃个数统计。现在需要通过 m
个等级(m < H - L
)来筛选不同尺寸大小的樱桃,筛选后需使得各等级内的樱桃数量和的标准差最小。
输入格式
- 第一行:两个整数
n
(樱桃总组数,2 < n ≤ 20
)和m
(需要的等级数,2 < m < n
)。 - 第二行:长度为
n
的整数序列A = [a_0, a_1, …, a_{n-1}]
,其中a_i
表示第i
组直径对应的樱桃个数(0 < a_i < 100
)。
输出格式
输出长度为 m
的序列 B = [b_0, b_1, …, b_{m-1}]
,其中:
b_0
表示从A
的第0
位开始,顺序取b_0
个元素作为第1
个等级;b_1
表示从A
的第b_0
位开始,顺序取b_1
个元素作为第2
个等级;- 依次类推,保证所有元素被分配且所得到的等级和序列和的标准差最小。
二、问题本质分析
这是一个将长度为 n
的序列分割成 m
段,使得每一段元素之和的标准差最小的分割优化问题。由于 n ≤ 20
,可以使用 动态规划 + 枚举 或者 DFS + 剪枝 来搜索最优分割方案。
三、解题思路
1. 前缀和预处理
预处理序列 A
的前缀和 S
,使得任意区间和 sum(i, j) = S[j + 1] - S[i]
能够在 O(1)
时间内获得。
2. DFS 枚举与剪枝
递归枚举每一段的长度,维护以下信息:
- 已分配的段数。
- 当前位置。
- 已经选取的各段和列表。
当分配到第 m
段时,将剩余元素作为最后一段,计算所有段和的标准差,并记录最小标准差的分割方案。
3. 剪枝策略
- 若当前已分配段数与剩余元素无法满足分段数时,直接剪枝。
整理如下,保持结构清晰,并将公式按 CSDN Markdown 规范用 $$
包裹:
4. 标准差计算
对于段和数组 W = [w_0, w_1, …, w_{m-1}]
,其平均值为:
μ = 1 m ∑ k = 0 m − 1 w k \mu = \frac{1}{m} \sum_{k=0}^{m-1} w_k μ=m1k=0∑m−1wk
标准差为:
σ = 1 m ∑ k = 0 m − 1 ( w k − μ ) 2 \sigma = \sqrt{ \frac{1}{m} \sum_{k=0}^{m-1} (w_k - \mu)^2 } σ=m1k=0∑m−1(wk−μ)2
在代码中,通过 accumulate
求平均值,再依次计算方差并开方,即可得出标准差,用于评估分段划分的均匀性。标准差越小,说明划分越均匀。需要注意,为避免整数除法造成精度丢失,平均值计算时应以 0.0
作为初始值确保类型为 double
。
四、代码实现
以下是完整的C++代码实现,包含详细注释:
#include <bits/stdc++.h>
using namespace std;int n, m; // 樱桃总组数和需要的等级数
vector<int> A; // 每组樱桃的数量
vector<int> S; // 前缀和数组double best_std = 1e300; // 当前最小的标准差
vector<int> best_B; // 最优分割方案// 计算区间和 [l, r)
int intervalSum(int l, int r) {return S[r] - S[l];
}// 深度优先搜索 (DFS) 枚举分割方案
void dfs(int pos, int k, vector<int>& W, vector<int>& B) {if (k == m - 1) { // 如果已经分配了 m-1 段int len = n - pos; // 剩余部分作为最后一段B.push_back(len);W.push_back(intervalSum(pos, n)); // 计算最后一段的和// 计算平均值double mu = accumulate(W.begin(), W.end(), 0.0) / m;// 计算方差double var = 0;for (double w : W) var += (w - mu) * (w - mu);var /= m;double std = sqrt(var); // 计算标准差if (std < best_std) { // 如果当前标准差更小best_std = std; // 更新最小标准差best_B = B; // 更新最优分割方案}B.pop_back(); W.pop_back(); // 回溯return;}// 枚举当前段的长度for (int len = 1; pos + len + (m - k ) <= n; ++len) {B.push_back(len); // 将当前段长度加入方案int w = intervalSum(pos, pos + len); // 计算当前段的和W.push_back(w);dfs(pos + len, k + 1, W, B); // 递归枚举下一段B.pop_back(); W.pop_back(); // 回溯}
}int main() {cin >> n >> m; // 输入樱桃总组数和等级数A.resize(n); // 初始化樱桃数量数组for (int i = 0; i < n; ++i) cin >> A[i]; // 输入每组樱桃的数量S.assign(n + 1, 0); // 初始化前缀和数组for (int i = 0; i < n; ++i) S[i + 1] = S[i] + A[i]; // 计算前缀和vector<int> W, B; // 当前段和列表和当前分割方案dfs(0, 1, W, B); // 开始深度优先搜索for (int x : best_B) cout << x << ' '; // 输出最优分割方案return 0;
}
五、样例解析
样例 1
输入
9 3
1 2 3 4 5 6 7 8 9
输出
5 2 2
解析
- 将 9 组樱桃分为 3 组,使得三组樱桃数量和的标准差最小。
- 分割方案为:
- 第一组:
1 + 2 + 3 + 4 + 5 = 15
- 第二组:
6 + 7 = 13
- 第三组:
8 + 9 = 17
- 第一组:
- 段和为
[15, 13, 17]
,平均值为15
,标准差为sqrt((15-15)^2 + (15-13)^2 + (15-17)^2) / 3
,为所有筛选方案中的最小值。
样例 2
输入
10 4
16 40 37 20 18 30 18 60 50 37
输出
3 3 2 2
解析
- 分割方案为:
- 第一组:
16 + 40 + 37 = 93
- 第二组:
20 + 18 + 30 = 68
- 第三组:
18 + 60 = 78
- 第四组:
50 + 37 = 87
- 第一组:
- 段和为
[93, 68, 78, 87]
,平均值为81.5
,标准差为sqrt((93-81.5)^2 + (68-81.5)^2 + (78-81.5)^2 + (87-81.5)^2) / 4
,为所有筛选方案中的最小值。
六、一行行代码带你敲
#include<bits/stdc++.h>
using namespace std;
零帧起手。
int n, m; // 樱桃总数和需要的等级数
vector<int> A; // 每组樱桃的数量
vector<int> S; // 前缀和数组
没啥好解释的。提前定义好全局变量
int main()
进入main函数
cin>>n>>m;
结构化输入输出,不解释
A.resize(n); // 初始化樱桃数量数组for (int i = 0; i < n; ++i) cin >> A[i]; // 输入每组樱桃的数量
确定数量组的大小并且给A[i]赋值
S.assign(n + 1, 0); // 初始化前缀和数组for (int i = 0; i < n; ++i) S[i + 1] = S[i] + A[i]; // 计算前缀和
给前缀和进行赋值。注意提前assign(n + 1, 0);否则S默认值未知
vector<int> W, B; // 当前段和当前分割方案
dfs
重头戏来了
dfs(0, 0, W, B); // 开始深度优先搜索
第一个参数是开始节点,第二个参数是长度,第三个参数是段和数组,第四个参数是分割方案。表示从0节点开始分割,长度为0,段和数组为空,分割方案为空。
void dfs(int pos, int k, vector<int>& W, vector<int>& B)
开始分割
for (int len = 1; pos + len + (m - k ) <= n; ++len)
初始长度肯定从1开始,从开始节点开始分割,一定要开始节点+长度+(m-k-1)<=数组长度。m-k-1,表示还剩下的几组,
B.push_back(len); // 将当前段长度加入方案
将当前段放进方案里面,不处理标准差,等方案确定再一起处理
int w = intervalSum(pos, pos + len); // 计算当前段的和
W.push_back(w);
计算当前段的和,并且push进去
// 计算区间和 [l, r)
int intervalSum(int l, int r) {return S[r] - S[l];
}
intervalSum函数的实现
dfs(pos + len, k + 1, W, B); // 递归枚举下一段
想象着一直递归枚举下去,把m段分好,一直分到最后一段。
if (k == m ) {return;}
分配到最后一段,也可以return了
int len = n - pos; // 剩余部分作为最后一段B.push_back(len);W.push_back(intervalSum(pos, n)); // 计算最后一段的和// 计算平均值double mu = accumulate(W.begin(), W.end(), 0.0) / m;
W:是一个 vector,存储了每个段的和。
W.begin():指向 W 的第一个元素的迭代器。
W.end():指向 W 的最后一个元素之后的位置的迭代器。
0.0:累加的初始值,这里使用 0.0 而不是 0,是为了确保计算结果是 double 类型,避免整数除法导致的精度丢失。
/ m:将累加结果除以段的数量 m,得到平均值。
// 计算方差double var = 0;for (double w : W) var += (w - mu) * (w - mu);var /= m;double std = sqrt(var); // 计算标准差
最后两行完全没必要,m是一个输入的定值,var和std是平方关系,没必要开根号。
if (std < best_std) { // 如果当前标准差更小best_std = std; // 更新最小标准差best_B = B; // 更新最优分割方案}
更新方案
double best_std = 1e300; // 当前最小的标准差
vector<int> best_B; // 最优分割方案
别忘了前面要定义一个全局变量,储存方案
B.pop_back(); W.pop_back(); // 回溯
七、总结
通过前缀和预处理和深度优先搜索(DFS)枚举所有可能的分割方案,结合剪枝策略,可以高效地找到使得各等级内樱桃数量和的标准差最小的分割方案。这种方法适用于 n
较小的情况(如本题中的 n ≤ 20
),能够在合理的时间内找到最优解。