0337-打家劫舍 III

Raphael Liu Lv10

小偷又发现了一个新的可行窃的地区。这个地区只有一个入口,我们称之为 root

除了 root 之外,每栋房子有且只有一个“父“房子与之相连。一番侦察之后,聪明的小偷意识到“这个地方的所有房屋的排列类似于一棵二叉树”。 如果
两个直接相连的房子在同一天晚上被打劫 ,房屋将自动报警。

给定二叉树的 root 。返回 _ 在不触动警报的情况下 ,小偷能够盗取的最高金额_ 。

示例 1:

**输入:** root = [3,2,3,null,3,null,1]
**输出:** 7 
**解释:**  小偷一晚能够盗取的最高金额 3 + 3 + 1 = 7

示例 2:

**输入:** root = [3,4,5,1,3,null,1]
**输出:** 9
**解释:**  小偷一晚能够盗取的最高金额 4 + 5 = 9

提示:

  • 树的节点数在 [1, 104] 范围内
  • 0 <= Node.val <= 104

方法一:动态规划

思路与算法

简化一下这个问题:一棵二叉树,树上的每个点都有对应的权值,每个点有两种状态(选中和不选中),问在不能同时选中有父子关系的点的情况下,能选中的点的最大权值和是多少。

我们可以用 $f(o)$ 表示选择 $o$ 节点的情况下,$o$ 节点的子树上被选择的节点的最大权值和;$g(o)$ 表示不选择 $o$ 节点的情况下,$o$ 节点的子树上被选择的节点的最大权值和;$l$ 和 $r$ 代表 $o$ 的左右孩子。

  • 当 $o$ 被选中时,$o$ 的左右孩子都不能被选中,故 $o$ 被选中情况下子树上被选中点的最大权值和为 $l$ 和 $r$ 不被选中的最大权值和相加,即 $f(o) = g(l) + g(r)$。
  • 当 $o$ 不被选中时,$o$ 的左右孩子可以被选中,也可以不被选中。对于 $o$ 的某个具体的孩子 $x$,它对 $o$ 的贡献是 $x$ 被选中和不被选中情况下权值和的较大值。故 $g(o) = \max { f(l) , g(l)}+\max{ f(r) , g(r) \。

至此,我们可以用哈希表来存 $f$ 和 $g$ 的函数值,用深度优先搜索的办法后序遍历这棵二叉树,我们就可以得到每一个节点的 $f$ 和 $g$。根节点的 $f$ 和 $g$ 的最大值就是我们要找的答案。

我们不难给出这样的实现:

[sol0-C++]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Solution {
public:
unordered_map <TreeNode*, int> f, g;

void dfs(TreeNode* node) {
if (!node) {
return;
}
dfs(node->left);
dfs(node->right);
f[node] = node->val + g[node->left] + g[node->right];
g[node] = max(f[node->left], g[node->left]) + max(f[node->right], g[node->right]);
}

int rob(TreeNode* root) {
dfs(root);
return max(f[root], g[root]);
}
};
[sol0-Java]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Solution {
Map<TreeNode, Integer> f = new HashMap<TreeNode, Integer>();
Map<TreeNode, Integer> g = new HashMap<TreeNode, Integer>();

public int rob(TreeNode root) {
dfs(root);
return Math.max(f.getOrDefault(root, 0), g.getOrDefault(root, 0));
}

public void dfs(TreeNode node) {
if (node == null) {
return;
}
dfs(node.left);
dfs(node.right);
f.put(node, node.val + g.getOrDefault(node.left, 0) + g.getOrDefault(node.right, 0));
g.put(node, Math.max(f.getOrDefault(node.left, 0), g.getOrDefault(node.left, 0)) + Math.max(f.getOrDefault(node.right, 0), g.getOrDefault(node.right, 0)));
}
}
[sol0-JavaScript]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var rob = function(root) {
const f = new Map();
const g = new Map();

const dfs = (node) => {
if (node === null) {
return;
}
dfs(node.left);
dfs(node.right);
f.set(node, node.val + (g.get(node.left) || 0) + (g.get(node.right) || 0));
g.set(node, Math.max(f.get(node.left) || 0, g.get(node.left) || 0) + Math.max(f.get(node.right) || 0, g.get(node.right) || 0));
}

dfs(root);
return Math.max(f.get(root) || 0, g.get(root) || 0);
};

假设二叉树的节点个数为 $n$。

我们可以看出,以上的算法对二叉树做了一次后序遍历,时间复杂度是 $O(n)$;由于递归会使用到栈空间,空间代价是 $O(n)$,哈希表的空间代价也是 $O(n)$,故空间复杂度也是 $O(n)$。

我们可以做一个小小的优化,我们发现无论是 $f(o)$ 还是 $g(o)$,他们最终的值只和 $f(l)$、$g(l)$、$f(r)$、$g(r)$ 有关,所以对于每个节点,我们只关心它的孩子节点们的 $f$ 和 $g$ 是多少。我们可以设计一个结构,表示某个节点的 $f$ 和 $g$ 值,在每次递归返回的时候,都把这个点对应的 $f$ 和 $g$ 返回给上一级调用,这样可以省去哈希表的空间。

代码如下。

代码

[sol1-C++]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
struct SubtreeStatus {
int selected;
int notSelected;
};

class Solution {
public:
SubtreeStatus dfs(TreeNode* node) {
if (!node) {
return {0, 0};
}
auto l = dfs(node->left);
auto r = dfs(node->right);
int selected = node->val + l.notSelected + r.notSelected;
int notSelected = max(l.selected, l.notSelected) + max(r.selected, r.notSelected);
return {selected, notSelected};
}

int rob(TreeNode* root) {
auto rootStatus = dfs(root);
return max(rootStatus.selected, rootStatus.notSelected);
}
};
[sol1-Java]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Solution {
public int rob(TreeNode root) {
int[] rootStatus = dfs(root);
return Math.max(rootStatus[0], rootStatus[1]);
}

public int[] dfs(TreeNode node) {
if (node == null) {
return new int[]{0, 0};
}
int[] l = dfs(node.left);
int[] r = dfs(node.right);
int selected = node.val + l[1] + r[1];
int notSelected = Math.max(l[0], l[1]) + Math.max(r[0], r[1]);
return new int[]{selected, notSelected};
}
}
[sol1-JavaScript]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var rob = function(root) {
const dfs = (node) => {
if (node === null) {
return [0, 0];
}
const l = dfs(node.left);
const r = dfs(node.right);
const selected = node.val + l[1] + r[1];
const notSelected = Math.max(l[0], l[1]) + Math.max(r[0], r[1]);
return [selected, notSelected];
}

const rootStatus = dfs(root);
return Math.max(rootStatus[0], rootStatus[1]);
};
[sol1-Golang]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
func rob(root *TreeNode) int {
val := dfs(root)
return max(val[0], val[1])
}

func dfs(node *TreeNode) []int {
if node == nil {
return []int{0, 0}
}
l, r := dfs(node.Left), dfs(node.Right)
selected := node.Val + l[1] + r[1]
notSelected := max(l[0], l[1]) + max(r[0], r[1])
return []int{selected, notSelected}
}

func max(x, y int) int {
if x > y {
return x
}
return y
}
[sol1-C]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
struct SubtreeStatus {
int selected;
int notSelected;
};

struct SubtreeStatus dfs(struct TreeNode *node) {
if (!node) {
return (struct SubtreeStatus){0, 0};
}
struct SubtreeStatus l = dfs(node->left);
struct SubtreeStatus r = dfs(node->right);
int selected = node->val + l.notSelected + r.notSelected;
int notSelected = fmax(l.selected, l.notSelected) + fmax(r.selected, r.notSelected);
return (struct SubtreeStatus){selected, notSelected};
}

int rob(struct TreeNode *root) {
struct SubtreeStatus rootStatus = dfs(root);
return fmax(rootStatus.selected, rootStatus.notSelected);
}

复杂度分析

  • 时间复杂度:$O(n)$。上文中已分析。

  • 空间复杂度:$O(n)$。虽然优化过的版本省去了哈希表的空间,但是栈空间的使用代价依旧是 $O(n)$,故空间复杂度不变。

 Comments
On this page
0337-打家劫舍 III