二叉树着色游戏 图解题解
这道题到底在问什么
- 输入
- root=[1..11], n=11, x=3
- 输出
- true
- 输入
- root=[1,2,3], n=3, x=1
- 输出
- false
最优解:一步一步想明白
- 3记牢这一句:x 把树切成左子树、右子树、父亲方向三块,二号去堵最大的一块,只要它比 n 的一半还大就赢。下面每一帧都在量这三块。
- 4n = 11 个节点,值互不相同先看清这棵树。一共 11 个节点,值是 1 到 11 互不相同的整数(题目规定值是 1 到 n 的排列)。游戏规则是一号先在某个节点上落子,你二号再落一子,然后轮流向相邻节点蔓延。现在轮到一号先选了。
- 5一号选 x = 3(橙色那个)一号把值为 3 的节点染了色,就是橙色这个。注意它一落子,整棵树就被它隔成了三块互不相通的区域:它的左子树、它的右子树、以及从它往上通向父亲的那一大片。我们要做的就是量清楚这三块各有多大。
- 6DFS 找值为 3 的节点先得在树里把这个 x 找出来。从根 6 开始一路深搜:根的值是 6,不是我们要的 3,那就往孩子走,先去左边看看。
- 7锁定 x = 3走到根的左孩子,值正好是 3,这就是一号落子的 x,锁定它。接下来分别量它的左子树、右子树,再算父亲方向那一块。
- 8左子树从 2 开始数,l = 0先量左子树。从 3 的左孩子 2 进去,这一整块以后都归左子树。计数器 l 先记成 0,每数到一个节点就加一。
- 9节点 2 计入左子树,l = 1节点 2 算进左子树,l 变成 1,把它标成蓝色表示已经数过。再看看节点 2 下面还有没有节点,有的话接着数。
- 10节点 2 的左孩子是 1节点 2 还有个左孩子 1,走下去。1 是叶子,下面再没有节点了,但它仍然属于左子树,也得数进去。
- 11节点 1 计入左子树,l = 2节点 1 算进去,l 变成 2。节点 1 没有孩子了,左子树这一支也就到头了。
- 12左子树 = 两个节点,l = 2左子树数完了,一共节点 2 和 1 两个节点,l 等于 2。蓝色这一小块就是二号如果选左孩子能独占的区域。记住这个 2,接着量右边。
- 13右子树从 4 开始数,r = 0再量右子树。左块的蓝色先留着别动,从 3 的右孩子 4 进去,计数器 r 从 0 起步,同样数一个加一个。
- 14节点 4 计入右子树,r = 1节点 4 算进右子树,r 变成 1,把它标成绿色,跟左块的蓝色区分开。再看节点 4 下面还挂着谁。
- 15节点 4 的右孩子是 5节点 4 有个右孩子 5,走下去。5 也是叶子,下面没有别的节点,但它仍归右子树,继续数进去。
- 16节点 5 计入右子树,r = 2节点 5 算进去,r 变成 2。节点 5 没有孩子,右子树这一支也到头了。
- 17右子树 = 两个节点,r = 2右子树数完,节点 4 和 5 两个,r 等于 2。绿色这一块就是二号如果选右孩子能独占的区域。现在左块 2、右块 2,只剩最后一块还没算。
- 18x 加左右子树 = 1 + 2 + 2 = 5把 x 自己,加上它的左子树和右子树,1 加 2 加 2 等于 5,这 5 个节点是「x 这一坨」。整棵树 11 个,刨掉这 5 个,剩下的全都在 x 往上的父亲方向。
- 19父亲方向 = 11 - 2 - 2 - 1 = 6父亲方向那一块不用一个个数,直接用总数 11 减掉 x 这一坨 5,等于 6。也就是图上那 6 个还没上色的节点:6、9、7、10、8、11,它们都在 x 的上方。
- 20三块大小:2 / 2 / 6三块全量出来了:左子树 2 个,右子树 2 个,父亲方向 6 个。一眼就看出来,父亲方向那块最大。二号要赢,就该往最大的这块去堵。
- 21max(2, 2, 6) = 6,n // 2 = 5取三块里最大的 6,再看 n 的一半:11 除以 2 向下取整是 5。6 严格大于 5,说明只要二号占住父亲方向那 6 个,就已经过了半数,这局稳赢。
- 22y 选 3 的父亲 6,独占父亲方向 6 个具体怎么落子?二号把 y 放在 3 的父亲、也就是根 6 上。这一子像一道闸门,把一号死死封在它自己那 5 个节点里,而二号顺着父亲方向能把那 6 个全染蓝。
- 23二号 6 个 > 一号 5 个 ➜ true最后一清点:二号 6 个,一号 5 个,6 比 5 多,二号赢。所以这道题答案是 true。整个过程没真去模拟一回合一回合的染色,只靠量出三块、堵最大那块,一步就判了输赢。
- 24量三块,堵最大,过半即赢回放一遍:x 把树切成 2、2、6 三块,最大的一块 6 超过了一半,二号就把 y 贴在那一块的入口堵上,稳赢。要是三块最大的都不过半,那二号怎么下都赢不了,返回 false。
⚠️ 容易写错的地方
✗ 错:只比左右子树,忘了父亲方向那一块
✓ 对:三块都要算:左子树、右子树、还有 n 减左减右减 1
父亲方向常常是最大的一块,漏掉它会把能赢判成不能赢
✗ 错:把判赢写成大于等于 n 的一半
✓ 对:必须严格大于 n // 2
n 是奇数,最大块恰好等于 n // 2 时二号仍不过半,剩下的归一号、一号至少多 1 个,所以必须严格大于 n // 2 才算赢
✗ 错:真去一回合一回合模拟染色过程
✓ 对:只需量三块大小,谁占住最大块过半谁赢
蔓延的结果已经被三块大小决定了,模拟既慢又容易写错
完整代码(Python / C++ / Java)
Python
from __future__ import annotations
from typing import *
from collections import *
from functools import *
from itertools import *
from math import *
from heapq import *
from bisect import *
class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
class ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = next
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, val=0, left=None, right=None):
# self.val = val
# self.left = left
# self.right = right
class Solution:
def btreeGameWinningMove(self, root: Optional[TreeNode], n: int, x: int) -> bool:
def dfs(root):
if root is None or root.val == x:
return root
return dfs(root.left) or dfs(root.right)
def count(root):
if root is None:
return 0
return 1 + count(root.left) + count(root.right)
node = dfs(root)
l, r = count(node.left), count(node.right)
return max(l, r, n - l - r - 1) > n // 2C++
#include <algorithm>
#include <array>
#include <cctype>
#include <climits>
#include <cmath>
#include <deque>
#include <functional>
#include <iostream>
#include <limits>
#include <map>
#include <numeric>
#include <optional>
#include <queue>
#include <set>
#include <sstream>
#include <stack>
#include <string>
#include <tuple>
#include <unordered_map>
#include <unordered_set>
#include <utility>
#include <vector>
using namespace std;
struct TreeNode {
int val;
TreeNode *left;
TreeNode *right;
TreeNode() : val(0), left(nullptr), right(nullptr) {}
TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
};
struct ListNode {
int val;
ListNode *next;
ListNode() : val(0), next(nullptr) {}
ListNode(int x) : val(x), next(nullptr) {}
ListNode(int x, ListNode *next) : val(x), next(next) {}
};
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
bool btreeGameWinningMove(TreeNode* root, int n, int x) {
auto node = dfs(root, x);
int l = count(node->left), r = count(node->right);
return max({l, r, n - l - r - 1}) > n / 2;
}
TreeNode* dfs(TreeNode* root, int x) {
if (!root || root->val == x) {
return root;
}
auto node = dfs(root->left, x);
return node ? node : dfs(root->right, x);
}
int count(TreeNode* root) {
if (!root) {
return 0;
}
return 1 + count(root->left) + count(root->right);
}
};Java
import java.util.*;
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class TreeNode { int val; TreeNode left; TreeNode right; TreeNode(){} TreeNode(int val){this.val=val;} TreeNode(int val, TreeNode left, TreeNode right){this.val=val;this.left=left;this.right=right;} }
class ListNode { int val; ListNode next; ListNode(){} ListNode(int val){this.val=val;} ListNode(int val, ListNode next){this.val=val;this.next=next;} }
class Solution {
public boolean btreeGameWinningMove(TreeNode root, int n, int x) {
TreeNode node = dfs(root, x);
int l = count(node.left);
int r = count(node.right);
return Math.max(Math.max(l, r), n - l - r - 1) > n / 2;
}
private TreeNode dfs(TreeNode root, int x) {
if (root == null || root.val == x) {
return root;
}
TreeNode node = dfs(root.left, x);
return node == null ? dfs(root.right, x) : node;
}
private int count(TreeNode root) {
if (root == null) {
return 0;
}
return 1 + count(root.left) + count(root.right);
}
}复杂度
时间
O(n)
找 x 的 dfs 最坏扫遍全树 O(n);找到后数左右子树合计也只覆盖 x 子树里的节点,不超过 O(n)。两段相加仍是 O(n),n 是节点总数
空间
O(h)
只用了几个整数变量,额外结构没有;真正占空间的是递归栈,深度等于树高 h。平衡树约 O(log n),退化成链时最坏 O(n)
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 二叉树着色游戏 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
为什么二号把 y 紧贴 x 放,就能独占一整块、而不会被一号抢走?+
因为 x 已经是一道墙了。比如二号把 y 放在 x 的父亲上,那么一号想往父亲方向蔓延,唯一的通道就是经过 x 再到父亲,可父亲已经被二号占了,一号过不去,只能困在 x 的左右子树那 5 个节点里。二号则从父亲这个点出发,把父亲方向那 6 个节点全部收入囊中。谁也插不进对方的地盘,所以三块大小就直接决定了最终各自染到多少。
为什么是和 n 除以 2 比,而且要严格大于?+
n 是奇数,二号要赢就得染到严格超过一半的节点。最大那块的大小若严格大于 n 除以 2 向下取整,它就至少是从一半往上多一个,二号占住它必然过半;如果最大那块恰好等于 n 除以 2 向下取整,二号还差一点没过半,剩下的 n // 2 加 1 个节点都归一号,一号反而至少多 1 个,并不算赢,所以不能取等号、必须严格大于。
能不能不去找 x、直接对每个节点试一遍?+
没必要。题目已经把 x 给定了,我们只要一次 dfs 定位到这个唯一的 x,再数它左右子树即可,父亲方向用减法得到,整体一趟 O(n)。如果对每个候选 y 都去真模拟一局,复杂度会爆炸,而本题的结论恰好只取决于 x 切出的三块大小,所以定位一次就够了。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄声线把 二叉树着色游戏 一步步讲透(全站 0 节语音精讲,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。