摘要:本文将对面试热门题最后一块石头的重量进行介绍,通过分析论证动态规划思路的可行性,以及如何将其转换为一个0-1背包问题。给出的代码基于golang语言实现,且已经通过所有测试用例,且在执行用时上超过了100%的提交,本文最后给出相关的复杂度分析。
关键词:算法,LeetCode,动态规划,0-1背包问题,Golang
原题描述
给定一堆石头,其重量用数组stones来描述,现在可以从这堆石头中任意取两块石头,如果这两块石头重量一样,则可以完全抵消,否则其差作为一块新石头的重量加入到石堆中,现在要求这样操作后,当石堆中只剩下一块石头的时候,这块石头可能的最小重量。
思路简述
首先考虑到任何操作过后只剩下一块石头下的重量一定可以表示如下的式子:
W
m
i
n
=
∑
i
=
1
n
s
t
o
n
e
s
i
∗
k
,
k
∈
{
−
1
,
1
}
W_{min}=\sum_{i=1}^{n}stones_{i} * k, k \in \{-1, 1\}
Wmin=i=1∑nstonesi∗k,k∈{−1,1}
那么基于此,可以将所有石头分为两堆,其中一堆就是前面运算符为+1的,另外一堆就是前面运算符为-1的。假设两堆石头重量的最小非负差为D,则可以证明这个D就是最终的答案,证明如下:
依次从两堆中分别取当前堆中最重的石头,如果重量一样,则被抵消,否则,记重量更大的石头所在的堆为A,则将差出来的石头放入A中,最后一定是A中剩下了一块石头,否则,可以从A中取出一块给B,那么就和D是最小的非负差矛盾了。
那么如何去求取这样的最小非负差呢?假设总的石头重量和为sum,负堆石头重量和为neg,则一方面
n
e
g
<
s
u
m
/
2
neg < sum / 2
neg<sum/2,且neg越大的话,
D
=
s
u
m
−
n
e
g
−
n
e
g
=
s
u
m
−
2
∗
n
e
g
D=sum-neg-neg=sum-2 * neg
D=sum−neg−neg=sum−2∗neg才能最小化。因此,上述问题可以转换为一个0-1背包问题,即从所有石头中取石头,背包的容量为sum / 2,石头的价值就是其本身的重量。0-1背包问题考虑的就是每一块石头要不要选,具体过程可以参考代码实现。
代码实现及优化
func lastStoneWeightII(stones []int) int {
n := len(stones)
sum := 0
for _, stone := range stones {
sum += stone
}
limit := sum / 2
dp := make([][]bool, n + 1)
for i := 0; i <= n; i++ {
// dp[i][j] 表示用stones的前i个能否凑出重量为j的石头堆
dp[i] = make([]bool, limit + 1)
}
dp[0][0] = true // 初始值,用0块石头肯定能凑出重量为0的石头堆
for i := 1; i <= n; i++ {
for j := 0; j <= limit; j++ {
// 不选当前石头
dp[i][j] = dp[i - 1][j]
if j >= stones[i - 1] { // 当前石头重量小于目标值,才能选当前石头
dp[i][j] = dp[i][j] || dp[i - 1][j - stones[i - 1]]
}
}
}
res := -1
for i := 0; i <= limit; i++ {
if dp[n][i] {
res = i
}
}
return sum - 2 * res
}
LeetCode运行截图如下:
再观察一下代码实现,可以看到在状态转移过程中,dp[i]系列之和dp[i-1]系列有关,因此可以用滚动数组来优化内存复杂度。实现代码如下:
func lastStoneWeightII(stones []int) int {
n := len(stones)
sum := 0
for _, stone := range stones {
sum += stone
}
limit := sum / 2
// dp[i] 表示用stones的前若干个能否凑出重量为i的石头堆
dp := make([]bool, limit + 1)
dp[0] = true // 初始值,用0块石头肯定能凑出重量为0的石头堆
for i := 1; i <= n; i++ {
for j := limit; j >= 0; j-- { // 遍历顺序
// 不选当前石头
// dp[j] = dp[j], 可以省略
if j >= stones[i - 1] { // 当前石头重量小于目标值,才能选当前石头
dp[j] = dp[j] || dp[j - stones[i - 1]]
}
}
}
res := -1
for i := 0; i <= limit; i++ {
if dp[i] {
res = i
}
}
return sum - 2 * res
}
LeetCode运行截图如下:
复杂度分析
- 时间复杂度: O ( n ∗ s u m ) O(n * sum) O(n∗sum),其中n为数组长度,sum为数组的和
- 空间复杂度: O ( s u m ) O(sum) O(sum),如果是第一种解法,空间复杂度为 O ( n ∗ s u m ) O(n * sum) O(n∗sum)