每日一题——樱桃分级优化问题:最小化标准差的动态规划与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=0m1wk

标准差为:

σ = 1 m ∑ k = 0 m − 1 ( w k − μ ) 2 \sigma = \sqrt{ \frac{1}{m} \sum_{k=0}^{m-1} (w_k - \mu)^2 } σ=m1k=0m1(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),能够在合理的时间内找到最优解。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

tt555555555555

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值