任意合并问题

例题洛谷 P1090

问题描述

堆石子排成一排,每堆石子质量为 。现需将这些石子合并为一堆,每次可任意选择两堆合并,代价为两堆石子质量之和。要求找出使总代价最小的合并方案,并输出最小代价。

解法分析

贪心策略:每次选择质量最小的两堆石子进行合并

理论依据

  1. 二叉树模型:最优合并过程可表示为一棵二叉树,总代价等于各石子质量乘以其深度之和
  2. 深度特性:最小的两堆石子应位于最优方案的最深层(可通过反证法证明)
  3. 等价替换:同层叶子节点交换不影响总代价,因此可将最小两堆调整至最深层合并
  4. 递归应用:合并最小两堆后问题规模减1,可递归应用相同策略

代码实现

#include <cstdio>
#include <queue>
using std::priority_queue;
using std::greater;
using std::vector;

int n, ans = 0;
priority_queue<int, vector<int>, greater<int>> q;

int main() {
    scanf("%d", &n);
    for (int i = 1; i <= n; ++i) {
        int x;
        scanf("%d", &x);
        q.push(x);
    }
    while (q.size() > 1) {
        int x = q.top(); q.pop();
        int y = q.top(); q.pop();
        ans += x + y;
        q.push(x + y);
    }
    printf("%d\n", ans);
    return 0;
}

对于无限制条件的合并问题,贪心算法能够保证获得最优解。

相邻合并问题(链式结构)

例题洛谷 P1775

问题描述

堆石子排成一行,每堆质量为 。每次只能合并相邻两堆,合并代价为两堆石子质量之和,合并后的新堆与原来相邻的石子保持相邻。要求找出使总代价最小的合并方案。

解法分析

贪心策略的局限性

考虑以下测试数据:

5
2 9 5 10 6

若采用贪心策略(每次合并最小两堆):

  • 第一步:2+9=11 → [11,5,10,6],代价11
  • 第二步:5+10=15 → [11,15,6],代价15
  • 第三步:6+15=21 → [11,21],代价21
  • 第四步:11+21=32 → [32],代价32
  • 总代价:11+15+21+32=79

更优的合并方案:

  • 第一步:2+9=11 → [11,9,10,6],代价11
  • 第二步:5+11=16 → [16,10,6],代价16
  • 第三步:10+6=16 → [16,16],代价16
  • 第四步:16+16=32 → [32],代价32
  • 总代价:11+16+16+32=75

可见贪心策略无法保证获得最优解,因此需采用动态规划方法。

动态规划解法

定义状态:dp[i][j] 表示合并区间 [i,j] 内石子的最小代价

状态转移方程:

dp[i][j] = min(dp[i][k] + dp[k+1][j] + sum[i...j]), 其中 i ≤ k < j

合并代价通过前缀和数组高效计算。

代码实现

#include<bits/stdc++.h>
using namespace std;
int dp[310][310], a[310], n, sum[310];

int main() {
    cin >> n;
    memset(dp, 0x3f, sizeof(dp)); // 初始化为极大值
    for (int i = 1; i <= n; i++) {
        cin >> a[i];
        sum[i] = sum[i - 1] + a[i];
        dp[i][i] = 0; // 单堆石子合并代价为0
    }
    
    for (int len = 2; len <= n; len++) { // 枚举区间长度
        for (int i = 1; i <= n - len + 1; i++) { // 枚举区间起点
            int j = i + len - 1; // 区间终点
            for (int k = i; k < j; k++) { // 枚举分割点
                dp[i][j] = min(dp[i][j], 
                              dp[i][k] + dp[k + 1][j] + sum[j] - sum[i - 1]);
            }
        }
    }
    cout << dp[1][n];
    return 0;
}

相邻合并问题(环式结构)

问题描述

堆石子排成一个环,每堆质量为 。每次只能合并相邻两堆,合并代价为两堆石子质量之和,合并后的新堆与原来相邻的石子保持相邻。要求找出使总代价最小的合并方案。

贪心策略的局限性

考虑以下测试数据:

8
2 1 1 2 1 7 10 2 

若采用贪心策略(每次合并最小两堆):

  • 第一步:1+1=2 → [2,2,2,1,7,10,2],代价2
  • 第二步:2+1=3 → [2,2,3,7,10,2],代价3
  • 第三步:2+2=4 → [4,3,7,10,2],代价4
  • 第四步:4+2=6 → [6,3,7,10],代价6
  • 第五步:6+3=9 → [9,7,10],代价9
  • 第六步:9+7=16 → [16,10],代价16
  • 第七步:16+10=26 → [26],代价26
  • 总代价:2+3+4+6+9+16+26=66

更优的合并方案:

  • 第一步:2+1=3 → [2,1,1,3,7,10,2],代价3
  • 第二步:1+1=2 → [2,2,3,7,10,2],代价2
  • 第三步:2+3=5 → [2,5,7,10,2],代价5
  • 第四步:2+2=4 → [4,5,7,10],代价4
  • 第五步:4+5=9 → [9,7,10],代价9
  • 第六步:9+7=16 → [16,10],代价16
  • 第七步:16+10=26 → [26],代价26
  • 总代价:3+2+5+4+9+16+26=65

可见贪心策略无法保证获得最优解,因此需采用动态规划方法。

动态规划解法

破环成链技巧:将环形问题转化为长度为 的链式问题

定义状态:dp[i][j] 表示合并区间 [i,j] 内石子的最小代价

状态转移方程:

dp[i][j] = min(dp[i][k] + dp[k+1][j] + sum[i...j]), 其中 i ≤ k < j

合并代价通过前缀和数组高效计算。

代码实现

#include <iostream>
#include <vector>
#include <climits>
using namespace std;

int main() {
    int N;
    cin >> N;
    vector<int> m(N + 1);  // 修正:大小为N+1,索引从1到N
    for (int i = 1; i <= N; i++) {
        cin >> m[i];
    }

    // 破环成链
    vector<int> a(2 * N + 1);
    for (int i = 1; i <= N; i++) {
        a[i] = m[i];
        a[i + N] = m[i];
    }

    // 前缀和
    vector<int> sum(2 * N + 1, 0);
    for (int i = 1; i <= 2 * N; i++) {
        sum[i] = sum[i - 1] + a[i];
    }

    // DP 数组 - 初始化为INT_MAX
    vector<vector<int>> dp(2 * N + 1, vector<int>(2 * N + 1, INT_MAX));

    // 初始化:单个石堆的合并代价为0
    for (int i = 1; i <= 2 * N; i++) {
        dp[i][i] = 0;
    }

    // 区间 DP - 修正:len从2到N(不是2到N)
    for (int len = 2; len <= N; len++) { // 合并的堆数
        for (int i = 1; i <= 2 * N - len + 1; i++) {
            int j = i + len - 1;
            for (int k = i; k < j; k++) {
                dp[i][j] = min(dp[i][j], dp[i][k] + dp[k + 1][j] + sum[j] - sum[i-1]);
                // if(dp[i][j] == dp[i][k] + dp[k + 1][j] + sum[j] - sum[i-1]){
                //     cout << "(" << i << ',' << j << ')' <<"=" << dp[i][j] << " " << " <- " <<  "(" << i << ',' << k << ')' << "(" << k+1 << ',' << j << ')' << endl;
                // }
            }
        }
    }

    // 找环形最优 - 修正:start从1开始
    int ans = INT_MAX;
    for (int start = 1; start <= N; start++) {
        ans = min(ans, dp[start][start + N - 1]);
    }

    cout << ans << endl;
    return 0;
}