LeetCode 956. 最高的广告牌
题目描述


题意分析
每根钢筋可以接到左支架、右支架,或不使用,不能同时用于两侧。要求两根支架最终等高,并最大化这个共同高度;无法得到正高度时返回 0。题目限制钢筋总长不超过 5000,适合按高度差建立动态规划。
解法:高度差动态规划
核心思路
[!blue]
处理完一部分钢筋后,用
dp[d] = h表示两侧高度差为d时,较矮侧能达到的最大高度;对应两侧高度就是h和h + d。不必区分左右名称,因为目标只关心它们是否等高。同一个差值下,较矮侧更高的状态也意味着两侧都同时更高。剩余钢筋完全相同,后续选择产生的差值变化只取决于
d和新钢筋长度,与原来的h无关;对两种状态采用相同选择,原本更高的一种始终更高。因此每个差值只保留最大的h,不会丢掉最优方案。对长度为
x的钢筋,有三个选择。不用它,保留(d, h);放到较高侧,两侧变为h、h + d + x,得到(d + x, h);放到较矮侧,两侧变为h + x、h + d,新差值是abs(d - x)。放到较矮侧时还要判断是否反超:
x <= d时,新矮侧为h + x;x > d时,原高侧反而成为新矮侧,高度为h + d。两种情况合并,新矮侧高度就是h + min(d, x)。每次更新相同差值时仍只保留较大高度。每根钢筋只枚举旧表,结果写入新表;新表先复制旧表,表示“不用这根”。这样每种旧安排都恰好扩展三种选择,又不会将本根钢筋反复使用。初始只有
(0, 0)可达,全部处理后dp[0]就是两侧相等时的最大高度。
解题步骤
- 初始化
dp[0] = 0,表示两根支架都为空。- 处理一根钢筋前,复制
dp得到next,保留不使用它的选择。- 遍历旧表的每个
(diff, best),更新放到高侧的状态(diff + x, best)。- 再更新放到矮侧的状态
(abs(diff - x), best + min(diff, x))。- 所有旧状态处理完后令
dp = next;最后返回差值 0 对应的高度。高度为 0 也可能是可达状态,例如所有已用钢筋暂时都在一侧。Java 用缺键默认值
-1,Go 显式检查键是否存在,保证新产生的零高度状态也会写入,留给后续钢筋继续配平。
代码实现
class Solution {
public int tallestBillboard(int[] rods) {
// dp[diff] = 高度差为 diff 时,较矮一侧能达到的最大高度。
Map<Integer, Integer> dp = new HashMap<>();
dp.put(0, 0);
for (int x : rods) {
// 复制一份即「这根不用」的转移,同时避免同一根被重复使用。
Map<Integer, Integer> next = new HashMap<>(dp);
for (Map.Entry<Integer, Integer> e : dp.entrySet()) {
int diff = e.getKey();
int best = e.getValue();
// 放到较高的一侧:矮侧不变,差值变大。
int d1 = diff + x;
next.put(d1, Math.max(next.getOrDefault(d1, -1), best));
// 放到较矮的一侧:可能反超,新矮侧高度为 best + min(diff, x)。
int d2 = Math.abs(diff - x);
int b2 = best + Math.min(diff, x);
next.put(d2, Math.max(next.getOrDefault(d2, -1), b2));
}
dp = next;
}
return dp.getOrDefault(0, 0);
}
}
func tallestBillboard(rods []int) int {
// dp[diff] = 高度差为 diff 时,较矮一侧能达到的最大高度。
dp := map[int]int{0: 0}
for _, x := range rods {
// 复制一份即「这根不用」的转移,同时避免同一根被重复使用。
next := make(map[int]int, len(dp)*2+1)
for k, v := range dp {
next[k] = v
}
for diff, best := range dp {
// 放到较高的一侧:矮侧不变,差值变大。
d1 := diff + x
// 缺键表示状态不存在,不能与可达的零高度混为一谈。
if old, ok := next[d1]; !ok || old < best {
next[d1] = best
}
// 放到较矮的一侧:可能反超,新矮侧高度为 best + min(diff, x)。
d2 := abs(diff - x)
b2 := best + min(diff, x)
if old, ok := next[d2]; !ok || old < b2 {
next[d2] = b2
}
}
dp = next
}
return dp[0]
}
func abs(x int) int {
if x < 0 {
return -x
}
return x
}
func min(a, b int) int {
if a < b {
return a
}
return b
}
复杂度分析
- 时间复杂度:期望 $O(nS)$,S 为钢筋总长度,差值状态不超过 S+1。
- 空间复杂度:$O(S)$,前后两张状态表。
关键点总结
[!green]
- 高度差决定后续如何配平,较矮侧高度记录当前能保留的收益。
- 同差值只保留最大的较矮侧高度,因为它同时提高了两侧,不会让未来更差。
- 放到矮侧可能使两侧身份互换,必须用绝对差和较小增量统一计算。
易错点总结
[!yellow]
- 只保留正高度状态,会丢掉“一侧已有钢筋、另一侧仍为空”的必要中间状态。
- 在遍历旧表时直接往旧表加入状态,可能让同一根钢筋被重复使用。
- 放到矮侧后总把高度增加
x,会在发生反超时算错新的较矮侧。- 忘记保留“不使用”分支,会强迫每根钢筋都加入某一侧。
- 最后只取差值 0 的状态;其他差值即使高度很大,也不是两根等高支架。
相似题目
| 题目 | 难度 | 关联与区别 |
|---|---|---|
| 416. 分割等和子集 | 中等 | 同样平衡两组和,但本题允许不用部分钢筋,还要最大化相等的高度。 |
| 1049. 最后一块石头的重量 II | 中等 | 同样用两组差值作状态,原题最小化最终差,本题要求差为0并最大化支架高度。 |
转载与许可
许可
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!