1 条题解
-
0
题解:
这道题是一个带权匹配问题,可以通过构建最小生成树来解决。以下是详细的题解分析。
问题分析:
我们有 N 件物品,每件物品有两种运输方式:
- 单独运输:费用为 A[i]
- 配对运输:费用为 B[i] + B[j],需要满足 |W[i] - W[j]| ≤ D
由于 B[i] < A[i],我们总是希望尽可能配对运输以节省费用。
关键观察
- 排序重要性:将物品按重量 W 排序后,可配对的物品一定是连续的(因为重量差限制 D)。
- 配对结构:对于排序后的物品,如果 i 和 j 配对(i < j),那么区间 [i, j] 内的所有物品也必须配对(形成若干对)。
- 图论模型:我们可以构建一个带权完全图:
- 每个节点对应一个物品
- 节点 i 有一个自环,权重为 A[i](表示单独运输)
- 节点 i 和 j 之间有一条边,权重为 B[i] + B[j](表示配对运输),但这条边只有在 |W[i] - W[j]| ≤ D 时才存在
解法思路:
对于固定的 D,问题等价于在带限制的图中找最小生成森林:
- 每个连通分量要么是单个节点(单独运输),要么是一对节点(配对运输)
- 由于每艘船最多运两件,所以没有更大的连通分量
动态规划解法(对于固定 D)
设 dp[i] 表示处理完前 i 个物品(排序后)的最小费用:
- 物品 i 单独运输:dp[i] = dp[i-1] + A[i]
- 物品 i 与 i-1 配对(如果 |W[i] - W[i-1]| ≤ D): dp[i] = min(dp[i], dp[i-2] + B[i] + B[i-1])
对于每个 D,我们可以 O(N) 计算答案。但 Q 最大可达 10^5,不能对每个 D 都重新计算。
关键优化:离线处理
注意到 D 越大,可配对的边越多,总费用越小(单调不减)。我们可以:
- 计算所有可能的配对边(i, j,其中 i < j)的"激活阈值" D_ij = |W[i] - W[j]|
- 当 D ≥ D_ij 时,边 (i, j) 可用
- 随着 D 增加,可用边增多,费用减少
算法步骤
-
预处理:
- 将物品按重量 W 排序
- 计算所有相邻物品的配对边(i, i+1),因为最优配对通常是相邻的
-
Kruskal-like 算法:
- 将所有可能的边(只考虑相邻物品)按 |W[i] - W[j]| 排序
- 初始状态:所有物品单独运输,总费用 = sum(A)
- 按 D 从小到大处理边,当 D 达到边的权重时,考虑合并两个物品
- 维护当前最小费用
-
并查集优化:
- 使用并查集维护当前哪些物品已配对
- 每个集合要么是单个物品,要么是一对物品
- 合并时更新总费用:减少 A[i]+A[j],增加 B[i]+B[j]
时间复杂度
- 排序:O(N log N)
- 边排序:O(N log N)(只考虑相邻物品)
- 查询处理:O((N+Q) log N)
C 实现
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <stdint.h> #define MAXN 100010 typedef struct { int w, a, b; } Item; typedef struct { int diff, idx; } Edge; typedef struct { int d, idx; } Query; Item items[MAXN]; Edge edges[MAXN]; Query queries[MAXN]; long long answers[MAXN]; int parent[MAXN], size[MAXN]; int find(int x) { while (parent[x] != x) { parent[x] = parent[parent[x]]; x = parent[x]; } return x; } int cmp_item(const void *a, const void *b) { return ((Item*)a)->w - ((Item*)b)->w; } int cmp_edge(const void *a, const void *b) { return ((Edge*)a)->diff - ((Edge*)b)->diff; } int cmp_query(const void *a, const void *b) { return ((Query*)a)->d - ((Query*)b)->d; } int64_t calculate_costs(int* W, int* A, int* B, int* E, int N, int Q) { // 初始化物品 for (int i = 0; i < N; i++) { items[i].w = W[i]; items[i].a = A[i]; items[i].b = B[i]; } // 按重量排序 qsort(items, N, sizeof(Item), cmp_item); // 计算初始总费用(全部单独运输) int64_t total = 0; for (int i = 0; i < N; i++) { total += items[i].a; } // 构建边(只考虑相邻物品,这是最优的) for (int i = 0; i < N - 1; i++) { edges[i].diff = items[i+1].w - items[i].w; edges[i].idx = i; } // 按重量差排序边 qsort(edges, N-1, sizeof(Edge), cmp_edge); // 处理查询 for (int i = 0; i < Q; i++) { queries[i].d = E[i]; queries[i].idx = i; } qsort(queries, Q, sizeof(Query), cmp_query); // 初始化并查集 for (int i = 0; i < N; i++) { parent[i] = i; size[i] = 1; } // 当前处理的边索引 int edge_idx = 0; // 处理每个查询 for (int q = 0; q < Q; q++) { int d = queries[q].d; // 激活所有满足条件的边 while (edge_idx < N - 1 && edges[edge_idx].diff <= d) { int i = edges[edge_idx].idx; int j = i + 1; // 检查是否可以配对 int root_i = find(i); int root_j = find(j); // 如果两个物品都未配对,则配对它们 if (root_i != root_j && size[root_i] == 1 && size[root_j] == 1) { // 更新总费用 total -= items[i].a + items[j].a; total += items[i].b + items[j].b; // 合并集合 if (root_i < root_j) { parent[root_j] = root_i; size[root_i] = 2; } else { parent[root_i] = root_j; size[root_j] = 2; } } edge_idx++; } answers[queries[q].idx] = total; } return (int64_t)answers; }算法正确性证明
- 最优配对性质:在排序后的物品序列中,最优配对不会交叉。即如果 i 与 j 配对,k 与 l 配对,且 i < k < j < l,那么这不是最优的。
- 相邻配对最优性:最优解中,配对的物品在排序后的序列中一定是相邻的,或者可以通过调整变成相邻而不增加费用。
- 单调性:随着 D 增大,可用边增多,总费用单调不减(实际是单调不增)。
注意事项
- 数据类型:总费用可能很大,需要使用 64 位整数(long long)。
- 边界情况:N=1 时,只能单独运输。
- 查询排序:通过离线处理,我们可以一次处理所有查询,避免重复计算。
这个算法可以在 O((N+Q) log N) 时间内解决本题,满足题目约束条件。
- 1
信息
- ID
- 5486
- 时间
- 1000ms
- 内存
- 256MiB
- 难度
- 8.5
- 标签
- 递交数
- 9
- 已通过
- 1
- 上传者