1 条题解

  • 0
    @ 2025-12-1 15:47:49

    题目分析

    这是一个通过相邻交换使前 FF 个元素总和至少达到 TT 的问题。每次只能交换相邻元素,需要最小化交换次数。

    关键点:

    • 只能进行相邻交换
    • 目标:前 mm 个元素(假设 m=Fm=F)的和至少为 TT
    • 需要计算最小交换次数

    算法思路

    核心思想

    使用动态规划 + 分治策略

    1. 拆分为两个子问题
      • 前m个盒子内部移动糖果到前m个位置
      • 后n-m个盒子移动糖果到前m个位置
    2. DP记录状态:用DP记录不同交换代价下的糖果数
    3. 双指针合并:合并两个子问题的结果

    关键观察

    • 交换相邻元素相当于将一个元素向左或向右移动
    • 将一个元素从位置 ii 移动到位置 jj (i>ji>j) 需要 iji-j 次交换
    • 移动操作可以分为两类:内部移动和外部移动

    代码详解

    预处理

    cin >> n >> m >> T;
    
    for (int i = 0; i < n; i++)
        cin >> a[i];
    
    // 减去前m个盒子已有的糖果
    for (int i = 0; i < m; i++)
        T -= a[i];
    
    if (T <= 0) {
        puts("0");  // 已经满足要求
        return 0;
    }
    

    第一部分:左半区DP (fl 数组)

    memset(fl, 63, sizeof(fl));
    fl[0][0] = 0;
    
    for (int i = m - 1; i >= 0; i--) {
        for (int j = m - i - 1; j >= 0; j--)
            for (int k = 0; k < M; k++)
                if (fl[j][k] < LINF) {
                    // 状态转移
                    fl[j + 1][k + m - i - 1 - j] = min(fl[j + 1][k + m - i - 1 - j], fl[j][k] + a[i]);
                }
    }
    

    fl[j][k] 含义

    • 从前 mm 个盒子中选择 jj 个移动到前 mm 个位置
    • 代价为 kk(交换次数)
    • 糖果数的最小值

    状态转移

    • 选择第 ii 个盒子(位于位置 ii
    • 将其移动到前 mm 个位置中的某个位置
    • 代价增加:m - i - 1 - j(需要跳过的盒子数)
    • 糖果数增加:a[i]

    第二部分:右半区DP (fr 数组)

    memset(fr, -63, sizeof(fr));
    fr[0][0] = 0;
    
    for (int i = m; i < n; i++) {
        for (int j = i - m; j >= 0; j--)
            for (int k = 0; k < M; k++)
                if (fr[j][k] >= 0) {
                    // 状态转移
                    fr[j + 1][k + i - m - j] = max(fr[j + 1][k + i - m - j], fr[j][k] + a[i]);
                }
    }
    

    fr[j][k] 含义

    • 从后 nmn-m 个盒子中选择 jj 个移动到前 mm 个位置
    • 代价为 kk(交换次数)
    • 糖果数的最大值

    状态转移

    • 选择第 ii 个盒子(位于位置 ii
    • 将其移动到前 mm 个位置
    • 代价增加:i - m - j(需要跳过的盒子数)
    • 糖果数增加:a[i]

    第三部分:合并结果

    int ans = INF;
    
    for (int i = 0; i <= n; i++) {
        // 构建有序序列
        vector<pair<ll, int>> seql, seqr;
        
        // 左半区:按糖果数递增,代价递减构建
        ll lst = LINF;
        for (int j = 0; j < M; j++)
            if (fl[i][j] < lst) {
                seql.push_back(make_pair(fl[i][j], j));
                lst = fl[i][j];
            }
        
        // 右半区:按糖果数递增,代价递增构建
        lst = -LINF;
        for (int j = 0; j < M; j++)
            if (fr[i][j] > lst) {
                seqr.push_back(make_pair(fr[i][j], j));
                lst = fr[i][j];
            }
        
        // 双指针扫描寻找最优解
        for (int j = (int)seql.size() - 1, k = 0; j >= 0; j--) {
            // 找到满足糖果数要求的右半区元素
            while (k < seqr.size() && -seql[j].F + seqr[k].F < T)
                k++;
            
            if (k < seqr.size())
                // 总代价 = 左代价 + 右代价 + 内部交换代价(i*i)
                ans = min(ans, seql[j].S + seqr[k].S + i * i);
        }
    }
    

    合并逻辑

    • i:从前 mm 个盒子中选择了 i 个盒子保留
    • seql:左半区选择的糖果数(负值)和代价
    • seqr:右半区选择的糖果数(正值)和代价
    • 条件:-seql[j].F + seqr[k].F ≥ T(总糖果数满足要求)
    • 总代价:左代价 + 右代价 + 内部调整代价(i*i)

    输出结果

    if (ans >= INF)
        puts("NO");
    else
        cout << ans << '\n';
    

    算法原理

    为什么需要 i*i 的额外代价?

    当从前 mm 个盒子中选择 ii 个保留时,需要考虑:

    1. ii 个盒子需要占据前 ii 个位置
    2. 其他 mim-i 个位置由外部盒子占据
    3. 内部盒子之间的相对顺序调整需要额外交换

    i*i 是对内部交换代价的一个估计(实际可能更复杂,但这个估计足够)。

    双指针扫描的正确性

    • seql 按糖果数递增排序,代价递减(更少的糖果需要更少的代价)
    • seqr 按糖果数递增排序,代价递增(更多的糖果需要更多的代价)
    • 对于每个左半区选择,找到满足条件的最小右半区代价

    复杂度分析

    时间复杂度

    • DP部分:O(n×m×M)O(n × m × M),其中 M=5010M=5010 是最大代价
    • 合并部分:O(n×M×logM)O(n × M × \log M)
    • 对于 n100n ≤ 100,可以接受

    空间复杂度

    • DP数组:O(n×M)O(n × M)
    • 辅助数组:O(M)O(M)

    样例分析

    样例1

    输入:

    6 2 27
    10 4 20 6 3 3
    
    • 前2个盒子已有糖果:10+4=14
    • 还需要:27-14=13
    • 算法找到:交换4和20(代价1)
    • 输出:1

    样例2

    输入:

    6 5 5000000000
    1000000000 1000000000 0 1000000000 1000000000 1000000000
    
    • 需要将0移动到前面
    • 代价:从位置2移动到位置0需要2次交换
    • 输出:3(考虑内部调整)

    算法优势

    1. 分治策略:将复杂问题分解为可管理的子问题
    2. 动态规划:高效记录所有可能状态
    3. 双指针优化:快速找到最优组合
    4. 精确计算:考虑所有可能的交换方案

    关键技巧

    1. 正负值处理:左半区用负值,右半区用正值,便于判断总和
    2. 有序序列构建:维护单调性,加速查询
    3. 代价估计:使用 i*i 近似内部交换代价
    4. 边界处理:处理已满足条件和不可能情况
    • 1

    信息

    ID
    5703
    时间
    1000ms
    内存
    256MiB
    难度
    10
    标签
    递交数
    1
    已通过
    1
    上传者