监控二叉树
思路
這道題目首先要想,如何放置,才能讓攝像頭數最小呢?
然后我們發現題目示例中的攝像頭都沒有放在葉子節點上!
這是很重要的一個線索,攝像頭可以覆蓋上中下三層,如果把攝像頭放在葉子節點上,就浪費的一層的覆蓋。
所以把攝像頭放在葉子節點的父節點位置,才能充分利用攝像頭的覆蓋面積。
為什么不從頭結點開始看起呢,為啥要從葉子節點看呢?
因為頭結點放不放攝像頭也就省下一個攝像頭, 葉子節點放不放攝像頭省下了的攝像頭數量是指數級別的。
所以我們要從下往上看,局部最優:讓葉子節點的父節點安攝像頭,所用攝像頭最少,整體最優:全部攝像頭數量所用最少!
局部最優推出全局最優,找不出反例,那么就按照貪心來!
此時,大體思路就是從下到上,先給葉子節點父節點放個攝像頭,然后隔兩個節點放一個攝像頭,直至到二叉樹頭結點。
此時這道題目還有兩個難點:
二叉樹的遍歷
如何隔兩個節點放一個攝像頭
確定遍歷順序
在二叉樹中如何從下向上推導呢?
可以使用后序遍歷也就是左右中的順序,這樣就可以在回溯的過程中從下到上進行推導了。
int traversal(TreeNode* cur) {// 空節點,該節點有覆蓋if (終止條件) return ;int left = traversal(cur->left); // 左int right = traversal(cur->right); // 右邏輯處理 // 中return ;}以上代碼中我們取了左孩子和右孩子的返回值,即left 和 right, 方便以后推導中間節點的狀態。
如何隔兩個節點放一個攝像頭
此時需要狀態轉移。
這個狀態應該如何轉移,我們首先看看每個節點可能有幾種狀態:
1、本節點無覆蓋
2、本節點有攝像頭
3、本節點有覆蓋
我們分別有三個數字來表示:
0:本節點無覆蓋
1:本節點有攝像頭
2:本節點有覆蓋
注意:本節點無攝像頭,其實無攝像頭就是 無覆蓋 或者 有覆蓋的狀態,所以一共還是三個狀態。
那么在遍歷樹的過程中,就會遇到空節點,那么問題來了,空節點究竟是哪一種狀態呢?空節點表示無覆蓋?表示有攝像頭?還是有覆蓋呢?
回歸本質,為了讓攝像頭數量最少,我們要盡量讓葉子節點的父節點安裝攝像頭,這樣才能攝像頭的數量最少。
那么空節點不能是無覆蓋的狀態,這樣葉子節點就要放攝像頭了,空節點也不能是有攝像頭的狀態,這樣葉子節點的父節點就沒有必要放攝像頭了,而是可以把攝像頭放在葉子節點的爺爺節點上。
所以空節點的狀態只能是有覆蓋,這樣就可以在葉子節點的父節點放攝像頭了
接下來就是遞推關系。
那么遞歸的終止條件應該是遇到了空節點,此時應該返回2(有覆蓋),原因上面已經解釋過了。
// 空節點,該節點有覆蓋 if (cur == NULL) return 2;單層邏輯處理
主要有如下四類情況:
情況1:左右節點都有覆蓋
左孩子有覆蓋,右孩子有覆蓋,那么此時中間節點應該就是無覆蓋的狀態了。
情況2:左右節點至少有一個無覆蓋的情況
如果是以下情況,則中間節點(父節點)應該放攝像頭:
left == 0 && right == 0 左右節點無覆蓋
left == 1 && right == 0 左節點有攝像頭,右節點無覆蓋
left == 0 && right == 1 左節點有無覆蓋,右節點攝像頭
left == 0 && right == 2 左節點無覆蓋,右節點覆蓋
left == 2 && right == 0 左節點覆蓋,右節點無覆蓋
這個不難理解,畢竟有一個孩子沒有覆蓋,父節點就應該放攝像頭。
此時攝像頭的數量要加一,并且return 1,代表中間節點放攝像頭
if (left == 0 || right == 0) {result++;return 1; }情況3:左右節點至少有一個有攝像頭
如果是以下情況,其實就是 左右孩子節點有一個有攝像頭了,那么其父節點就應該是2(覆蓋的狀態)
left == 1 && right == 2 左節點有攝像頭,右節點有覆蓋
left == 2 && right == 1 左節點有覆蓋,右節點有攝像頭
left == 1 && right == 1 左右節點都有攝像頭
情況4:頭結點沒有覆蓋
以上都處理完了,遞歸結束之后,可能頭結點 還有一個無覆蓋的情況,如圖:
所以遞歸結束之后,還要判斷根節點,如果沒有覆蓋,result++,代碼如下:
最后的代碼
// 版本一 class Solution { private:int result;int traversal(TreeNode* cur) {// 空節點,該節點有覆蓋if (cur == NULL) return 2;int left = traversal(cur->left); // 左int right = traversal(cur->right); // 右// 情況1// 左右節點都有覆蓋if (left == 2 && right == 2) return 0;// 情況2// left == 0 && right == 0 左右節點無覆蓋// left == 1 && right == 0 左節點有攝像頭,右節點無覆蓋// left == 0 && right == 1 左節點有無覆蓋,右節點攝像頭// left == 0 && right == 2 左節點無覆蓋,右節點覆蓋// left == 2 && right == 0 左節點覆蓋,右節點無覆蓋if (left == 0 || right == 0) {result++;return 1;}// 情況3// left == 1 && right == 2 左節點有攝像頭,右節點有覆蓋// left == 2 && right == 1 左節點有覆蓋,右節點有攝像頭// left == 1 && right == 1 左右節點都有攝像頭// 其他情況前段代碼均已覆蓋if (left == 1 || right == 1) return 2;// 以上代碼我沒有使用else,主要是為了把各個分支條件展現出來,這樣代碼有助于讀者理解// 這個 return -1 邏輯不會走到這里。return -1;}public:int minCameraCover(TreeNode* root) {result = 0;// 情況4if (traversal(root) == 0) { // root 無覆蓋result++;}return result;} };本題的難點首先是要想到貪心的思路,然后就是遍歷和狀態推導。
總結