1. 问题分析与动态规划思路
这个题目本质上是一个带有特殊条件的背包问题变种。我们需要从n个英雄中选择最多4个,在总cost不超过C的前提下,最大化战斗力。特殊之处在于存在"双生英雄"关系,当同时选择一对双生英雄时,可以获得额外的战斗力加成。
1.1 问题建模
我们可以将这个问题建模为一个多维约束的背包问题:
- 背包容量:总cost限制C
- 物品数量限制:最多选择4个英雄
- 特殊条件:某些物品组合有额外价值
1.2 动态规划状态设计
采用动态规划来解决这个问题,需要设计合适的状态表示。这里我们使用一个三维数组dp[p][j]:
- p:当前已选择的英雄数量(0≤p≤4)
- j:当前已消耗的cost(0≤j≤C)
- dp[p][j]:在选p个英雄、消耗j cost时的最大战斗力
1.3 状态转移分析
对于每个英雄i,我们需要考虑以下几种情况:
- 不选择该英雄:dp[p][j]保持不变
- 单独选择该英雄:从dp[p-1][j-cost[i]]转移过来
- 如果该英雄有双生关系,考虑同时选择这对英雄:从dp[p-2][j-cost[i]-cost[k]]转移过来,并加上额外战斗力
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 核心算法实现解析
2.1 数据结构设计
cpp复制int costs[1001] = {}; // 英雄的cost
long long fightings[1001] = {}; // 英雄的基础战斗力
int connections[1001] = {}; // 记录双生关系
int unionfights[1001] = {}; // 双生英雄的额外战斗力
long long dp[5][1001] = {}; // DP状态数组
2.2 输入处理
首先读取基本输入数据:
- 英雄数量n、总cost限制C、双生关系对数m
- 每个英雄的cost和基础战斗力
- 每对双生英雄及其额外战斗力
cpp复制for (int i = 1; i <= n; i++) {
cin >> costs[i] >> fightings[i];
}
for (int i = 1; i <= m ; i++) {
cin >> u >> v >> w;
if(u > v) swap(u, v); // 统一存储,保证u<v
connections[u] = v;
unionfights[u] = w;
}
2.3 动态规划主体
核心的动态规划过程分为几种情况处理:
cpp复制for (int i = 1; i <= n; i++) {
int k = connections[i]; // 检查是否有双生英雄
for (int p = 4; p > 0; p--) { // 逆序更新,避免重复选择
if (k == 0) { // 无双生关系
for (int j = C; j >= costs[i]; j--) {
dp[p][j] = max(dp[p][j], dp[p-1][j-costs[i]] + fightings[i]);
}
}
else if (k > i) { // 处理双生英雄对
int low_bound = min(costs[i], costs[k]);
for (int j = C; j >= low_bound; j--) {
// 单独选择英雄i
if (j - costs[i] >= 0) {
dp[p][j] = max(dp[p][j], dp[p-1][j-costs[i]] + fightings[i]);
}
// 单独选择英雄k
if (j - costs[k] >= 0) {
dp[p][j] = max(dp[p][j], dp[p-1][j-costs[k]] + fightings[k]);
}
// 同时选择英雄i和k
if (j - costs[i] - costs[k] >= 0 && p >= 2) {
dp[p][j] = max(dp[p][j],
dp[p-2][j-costs[i]-costs[k]] +
fightings[i] + fightings[k] + unionfights[i]);
}
}
}
}
}
2.4 结果提取
最终结果是在所有可能的选择数量(0-4)中,消耗不超过C cost的最大战斗力:
cpp复制long long ans = 0;
for(int i = 0; i < 5; i++) {
if(ans < dp[i][C]) {
ans = dp[i][C];
}
}
cout << ans;
3. 算法优化与细节处理
3.1 空间优化技巧
虽然我们使用了二维数组dp[5][1001],但实际上可以进一步优化空间。因为每次更新只依赖前一轮的状态,可以使用滚动数组技术将空间复杂度降低到O(C)。
3.2 边界条件处理
需要特别注意以下边界条件:
- 当C=0时,无法选择任何英雄,输出0
- 当m=0时,退化为普通背包问题
- 当n<4时,只需考虑实际英雄数量
3.3 时间复杂度分析
算法的时间复杂度主要取决于:
- 外层循环:O(n)
- 中间循环:O(4) ≈ O(1)
- 内层循环:O(C)
总时间复杂度为O(n*C),在题目给定的约束条件下(n,C≤1000)是完全可行的。
4. 常见问题与调试技巧
4.1 初始化问题
注意:dp数组必须初始化为0,表示初始状态下战斗力为0。忘记初始化可能导致错误结果。
4.2 双生英雄处理顺序
在处理双生英雄时,必须确保只处理一次。代码中通过保证u<v来实现这一点:
cpp复制if(u > v) swap(u, v);
connections[u] = v;
4.3 数值溢出问题
由于战斗力数值可能很大(≤1e9),使用int类型可能导致溢出。代码中使用了long long来存储战斗力和dp值:
cpp复制long long fightings[1001] = {};
long long dp[5][1001] = {};
4.4 测试用例设计
设计测试用例时应考虑以下场景:
- 无英雄可选的情况
- 无双生关系的情况(m=0)
- cost总和刚好等于C的情况
- 必须选择双生英雄才能获得最大收益的情况
- 选择不同数量英雄的组合情况
4.5 调试输出技巧
在调试时可以添加临时输出,例如:
cpp复制// 调试输出
if (i == n) {
for (int p = 0; p <= 4; p++) {
for (int j = 0; j <= C; j++) {
cout << "dp[" << p << "][" << j << "]=" << dp[p][j] << " ";
}
cout << endl;
}
}
5. 算法扩展与变种思考
5.1 英雄数量限制变化
如果题目将最多选择4个英雄的限制改为k个英雄,我们可以很容易地扩展算法,只需将dp数组的第一维改为k+1即可。
5.2 多重双生关系
如果允许一个英雄属于多对双生关系,问题会变得更加复杂。可能需要使用更复杂的状态表示,如位掩码来记录已选择的英雄组合。
5.3 负cost或战斗力
如果允许cost或战斗力为负值,需要特别注意状态转移时的边界条件,并可能需要调整初始化和更新逻辑。
5.4 其他组合加成
除了双生英雄,题目可能会引入更多类型的组合加成,如三人组合、特定职业组合等。这种情况下可以考虑使用更通用的组合处理方式。
在实际编程竞赛中,这类背包问题的变种非常常见。掌握基础背包问题的解法后,关键是要能够根据题目要求灵活调整状态设计和转移方程。这道题的特别之处在于结合了数量限制和组合加成两个约束条件,通过合理的状态设计,我们能够高效地解决问题。
