1. 题目背景与需求分析
P6093 [JSOI2015] 套娃是江苏省信息学奥林匹克竞赛(JSOI)的一道经典题目,考察选手对数据结构与算法的综合应用能力。题目描述了一组俄罗斯套娃,每个套娃有固定的外径和价格,要求我们找出最优的嵌套方案,使得总价值最大。
这道题的核心在于理解套娃的嵌套规则:一个套娃A可以放入另一个套娃B中,当且仅当A的外径严格小于B的外径。我们的目标是选择一组套娃,通过合理的嵌套安排,使得最终形成的套娃序列总价值最高。
从算法角度看,这属于典型的动态规划问题,与最长递增子序列(LIS)问题有相似之处。但相比基础LIS问题,本题增加了价格这一额外维度,使得单纯的O(n²)解法可能无法通过全部测试用例,需要考虑更高效的优化方案。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 数据结构设计与预处理
2.1 套娃结构体定义
首先我们需要定义合适的数据结构来存储套娃信息。在C++中,使用结构体是最直观的选择:
cpp复制struct Matryoshka {
int outer; // 外径
int price; // 价格
// 重载小于运算符便于排序
bool operator<(const Matryoshka& other) const {
return outer < other.outer;
}
};
2.2 输入数据预处理
题目输入通常给出无序的套娃列表,我们需要先对其进行排序。根据嵌套规则,按外径升序排列是合理的预处理步骤:
cpp复制vector<Matryoshka> dolls;
// 假设输入数据已读入dolls向量
sort(dolls.begin(), dolls.end());
排序后,我们可以保证对于任意i < j,dolls[i].outer ≤ dolls[j].outer。这样在后续处理时,只需要考虑前面的套娃能否放入后面的套娃中。
3. 基础动态规划解法
3.1 状态定义与转移方程
定义dp[i]表示以第i个套娃为最外层时的最大总价值。初始时,每个dp[i]至少包含自身的价格:
cpp复制vector<int> dp(n);
for (int i = 0; i < n; ++i) {
dp[i] = dolls[i].price;
}
状态转移方程需要考虑所有可以放入当前套娃的较小套娃:
cpp复制for (int i = 1; i < n; ++i) {
for (int j = 0; j < i; ++j) {
if (dolls[j].outer < dolls[i].outer) {
dp[i] = max(dp[i], dp[j] + dolls[i].price);
}
}
}
3.2 算法复杂度分析
这种解法的时间复杂度为O(n²),空间复杂度为O(n)。对于n≤1000的规模,这种解法通常可以接受。但考虑到竞赛中可能存在更大的数据规模,我们需要考虑优化方案。
4. 优化解法:基于权值线段树
4.1 离散化处理
为了使用线段树优化,首先需要对价格进行离散化处理:
cpp复制vector<int> prices;
for (const auto& doll : dolls) {
prices.push_back(doll.price);
}
sort(prices.begin(), prices.end());
prices.erase(unique(prices.begin(), prices.end()), prices.end());
4.2 线段树结构设计
我们设计一个线段树,用于维护价格区间内的最大dp值:
cpp复制class SegmentTree {
private:
vector<int> tree;
int size;
void update(int node, int l, int r, int idx, int val) {
if (l == r) {
tree[node] = max(tree[node], val);
return;
}
int mid = (l + r) / 2;
if (idx <= mid) {
update(2 * node, l, mid, idx, val);
} else {
update(2 * node + 1, mid + 1, r, idx, val);
}
tree[node] = max(tree[2 * node], tree[2 * node + 1]);
}
int query(int node, int l, int r, int ql, int qr) {
if (qr < l || ql > r) return 0;
if (ql <= l && r <= qr) return tree[node];
int mid = (l + r) / 2;
return max(query(2 * node, l, mid, ql, qr),
query(2 * node + 1, mid + 1, r, ql, qr));
}
public:
SegmentTree(int n) : size(1) {
while (size < n) size <<= 1;
tree.assign(2 * size, 0);
}
void update(int idx, int val) {
update(1, 0, size - 1, idx, val);
}
int query(int l, int r) {
return query(1, 0, size - 1, l, r);
}
};
4.3 优化后的DP实现
利用线段树,我们可以将内层循环的O(n)查询优化为O(logn):
cpp复制SegmentTree st(prices.size());
for (int i = 0; i < n; ++i) {
int price_idx = lower_bound(prices.begin(), prices.end(), dolls[i].price) - prices.begin();
int max_val = st.query(0, price_idx - 1);
dp[i] = max_val + dolls[i].price;
st.update(price_idx, dp[i]);
}
这种优化将整体时间复杂度降为O(nlogn),能够处理更大规模的数据。
5. 边界条件与特殊测试用例
5.1 空输入情况
需要处理没有套娃的特殊情况:
cpp复制if (dolls.empty()) {
cout << 0 << endl;
return 0;
}
5.2 所有套娃外径相同
当所有套娃外径相同时,无法进行任何嵌套,此时最大价值就是单个套娃的最大价格:
cpp复制bool all_same = true;
for (int i = 1; i < n; ++i) {
if (dolls[i].outer != dolls[0].outer) {
all_same = false;
break;
}
}
if (all_same) {
int max_price = 0;
for (const auto& doll : dolls) {
max_price = max(max_price, doll.price);
}
cout << max_price << endl;
return 0;
}
5.3 性能测试用例
设计极端测试用例验证算法效率:
- 严格递增序列:外径和价格都严格递增
- 严格递减序列:外径严格递减,价格随机
- 大规模随机数据:n=1e5,外径和价格在合理范围内随机
6. 完整代码实现与测试
6.1 主函数框架
cpp复制#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
// 之前定义的Matryoshka结构体和SegmentTree类
int main() {
int n;
cin >> n;
vector<Matryoshka> dolls(n);
for (int i = 0; i < n; ++i) {
cin >> dolls[i].outer >> dolls[i].price;
}
// 处理特殊情况
if (dolls.empty()) {
cout << 0 << endl;
return 0;
}
// 排序预处理
sort(dolls.begin(), dolls.end());
// 离散化价格
vector<int> prices;
for (const auto& doll : dolls) {
prices.push_back(doll.price);
}
sort(prices.begin(), prices.end());
prices.erase(unique(prices.begin(), prices.end()), prices.end());
// DP优化解法
SegmentTree st(prices.size());
vector<int> dp(n);
int max_total = 0;
for (int i = 0; i < n; ++i) {
int price_idx = lower_bound(prices.begin(), prices.end(), dolls[i].price) - prices.begin();
int max_val = (price_idx > 0) ? st.query(0, price_idx - 1) : 0;
dp[i] = max_val + dolls[i].price;
st.update(price_idx, dp[i]);
max_total = max(max_total, dp[i]);
}
cout << max_total << endl;
return 0;
}
6.2 测试用例验证
提供几组测试用例验证代码正确性:
- 基础测试用例:
code复制3
5 10
3 5
4 7
预期输出:17(5→4→10)
- 无法嵌套的情况:
code复制3
5 10
5 8
5 12
预期输出:12(取最大值)
- 性能测试用例(生成100000个随机套娃):
cpp复制// 生成随机测试数据
vector<Matryoshka> large_test(100000);
for (int i = 0; i < 100000; ++i) {
large_test[i].outer = rand() % 1000000 + 1;
large_test[i].price = rand() % 1000 + 1;
}
// 应能在1秒内完成计算
7. 算法扩展与变种思考
7.1 多维度嵌套问题
如果题目增加更多限制条件,比如:
- 同时限制内径和外径
- 考虑套娃高度限制
- 引入颜色匹配等额外约束
这类扩展问题可能需要更复杂的数据结构或算法,如多维线段树或KD树。
7.2 最小代价嵌套问题
变种题目可能要求找到满足某种条件的最小代价嵌套方案,这时需要调整状态转移方程,可能需要使用优先队列等数据结构进行优化。
7.3 输出具体嵌套方案
如果需要输出具体的套娃选择方案而不仅仅是最大价值,我们需要在DP过程中记录路径:
cpp复制vector<int> prev(n, -1);
// 在DP更新时记录前驱
if (dp[j] + dolls[i].price > dp[i]) {
dp[i] = dp[j] + dolls[i].price;
prev[i] = j;
}
// 反向追踪路径
int max_idx = max_element(dp.begin(), dp.end()) - dp.begin();
while (max_idx != -1) {
cout << dolls[max_idx].outer << " ";
max_idx = prev[max_idx];
}
8. 竞赛实战技巧与注意事项
- 输入输出优化:对于大规模数据,使用更快的IO方法:
cpp复制ios::sync_with_stdio(false);
cin.tie(nullptr);
-
内存管理:预先分配足够内存,避免动态扩容带来的性能损耗。
-
调试技巧:对于DP问题,可以打印中间状态帮助调试:
cpp复制// 调试打印DP数组
for (int i = 0; i < n; ++i) {
cerr << "dp[" << i << "] = " << dp[i] << endl;
}
- 常见错误:
- 忘记处理空输入情况
- 没有考虑所有外径相同的情况
- 线段树实现时边界条件处理不当
- 离散化时没有去重导致索引错误
- 性能调优:
- 使用更紧凑的数据结构
- 减少不必要的拷贝操作
- 使用位运算替代除法等耗时操作
在实际竞赛中,建议先实现基础DP解法确保正确性,再根据时间限制决定是否需要优化。同时要注意题目可能存在的特殊限制条件,如外径范围、价格范围等,这些信息可能提示更优的解法方向。
