题目:

  

 

  洛谷P5566

题解:

大佬的题解:

#include "cstdio"
int n, ans, k;
int main()
{
    scanf("%d", &n);
    k = n + 1;
    while (k > 1)
    {
        ans += k & 1;
        k >>= 1;
    }
    printf("%d\n", ans);
    k = n + 1;
    ans = 0;
    while (k > 1)
    {
        if (k == 2)
            ans++, k--;
        else if ((k & 3) == 1)
            ans += ((k >> 2) << 1) - 1, k >>= 2, k++;
        else if ((k & 3) == 2)
            ans += ((k >> 2) << 1), k >>= 2, k++;
        else if ((k & 3) == 3)
            ans += ((k >> 2) << 1) + 1, k >>= 2, k++;
        else
            ans += (k >> 1), k >>= 2;
    }
    printf("%d", ans);
    return 0;
}

 

题解分析:

  这个贪心法的确非常巧妙。可是大佬并没有给出详细严谨的解释。不过我基本上把代码中每一行的意思都“翻译”了出来。

  (只不过有一点前提还没有严谨地证明:满足要求的红黑树一定可以是完全二叉树

  因为我发现只有基于这个前提,以上代码才有具体的意义,才可以解释通。)

  此外还要注意一点:题目里面的N不包括空节点数!这里要注意!

  下面分别针对最小值和最大值进行分析:

  

对于红色节点最小值:

  首先根据红黑树的性质可以得到这样的一个理解:将一个黑色节点涂成红色,可以使得经过这个节点的从根到叶的黑路径长度-1(黑路径就是这条路径上黑色节点的个数)。

  所以这个地方贪心的意思就慢慢出来了。

  不如假设N个节点都是黑色,然后我们要做的是把黑色涂成红色,问题就变成了最少要涂多少个。

  为了使得涂的个数最少,则

    1.应当使这棵树尽量平衡。(比如一个满二叉树,每条路径的长度都一样——为树的高度)

    2.所涂的节点尽量高度要高(因为越高的节点影响的黑路径就越多)

  关于第一点,因为,比如一个节点的左右子树的高度不一样,那么可以通过给较高的子树中的某个或某些节点涂红色,从而达到降低其高度的效果。

  所以我们考虑的范围就可以缩小到只考虑完全二叉树了。(因为完全二叉树叶子节点的深度最多相差1,而且相同高度的叶子结点都互相邻近(靠在一起这样可以更便于涂红、即更利于第二点的实现)。

  emm可能我表述得并不是很好,下面举个例子来加深理解:

  比如这样一棵二叉树(空节点没画出来,后面也都不会画出来):

  

 

 

 

 

  

  显然,经过节点2的路径要比经过3的长1,所以我们来把一些黑色节点涂成红色来使其符合每条从根要叶子节点的黑路径相同(红黑树的条件也是题目里面的条件)。

  我们可以涂4、5:

 

 

  也可以涂2:

 

  

 

 

  显然,为了使红色节点数最少,此处就会选择涂2号

 

  (慢慢就会发现,每次涂尽量涂高处,这样红色节点影响的路径数才多,红色节点用的自然也就更少了。这里就用了贪心的思想)。

 

  再看个例子:

 

 

 

  两棵树看起来都挺平衡的(子树高度差不超过1)(左边为完全二叉树,右边不是)

 

    但是左边的树只要涂一个节点就行了,右边的至少要涂两个。

 

  这也是为什么我们选择完全二叉树来考虑的原因。

  

  然后我们来思考如何求出要涂红的最少的节点数。

  不如从例子出发,有这样一棵完全二叉树:

  

 

 

 

 

  我们要做的就是通过将1、2、3节点或其祖先节点涂红,把节点1、2、3的深度调整到和右半棵树相同(即深度-1)。

  为了贪心,我们从上往下考虑:

  A节点:不行,因为他会影响到不需要调整的节点。

  B节点:不可行,因为他会影响到不需要调整的节点(图中看不出来是因为,它影响的是空节点(空节点算是黑色,但是上图我没把他画出来))。

  C节点:可行

  D节点:可行

  此时以及全部符合要求

  综上只最少需要涂两个。

  通过自己具大量的例子,可以发现这样一个规律:完全二叉树,设最下面一层节点数为m,且最下面一层未满。

  当m=1时,只需要涂1个,

  当m=2时,只需要涂1个,

  当m=4时,只需要涂1个,

  ······

  当m = 2的n次方时,只需要涂1个。

  此时可以停下来,慢慢结合满二叉树的性质来思考思考这件事情。

  没错,这里就利用到了,满二叉树的叶子节点数等于2的幂。

  例如下面的例子,此时红色涂的就是这连续的2^2个节点作为叶子节点的一个满二叉树的根节点:

  

 

 

  那么下面就可以考虑m不为2的幂的情况了。

 

  例如前一个例子,m=3;

 

  此时可以发现3=1+2 = 2^0 + 2^1   ,此时的答案是  涂2个

 

  再举个例子,这棵树m=7:

 

  

 

 

  7 = 1 + 2+7  = 2^0 + 2 ^ 1 + 2^2  ,而此时的答案是3;

 

  所以,看到这里,即使我的废话可能表述不清,这么多的例子应该能让你有个大概的理解了。

 

  那就是:最少涂的红色节点数量= 未满层的叶子节点数能够分解为2的不同次幂的个数。

  熟悉二进制的话,上面这句话就能很轻松地翻译成:最少红色节点数量=未满层叶子节点数的二进制中1的个数。

  这也就是大佬代码中求第一问的简洁过程的来源了。

  不过也许你会疑惑为什么代码中令k =n+1,这里稍微解释一下:

  满二叉树节点数=2^h-1

  而完全二叉树节点数=一个满二叉树节点数+未满层节点数

  所以:完全二叉树节点数+1 = 2^h + 未满层节点数

  而2^h在 k的二进制中的表达便是最高位的那个1,所以循环的条件是k>1

  好的,第一小问就这么愉快的结束了。

第二问,对于红色节点数的最大值:

  说实话这一小问的贪心做法挺难理解的。

  首先我无法证明为什么这一问同样考虑完全二叉树即可(还是说是我的一厢情愿实际上对非完全二叉树也有说得通的说法?)。

  我姑且从完全二叉树的前提去考虑这一小问了。

  (实际上,多举几个例子,发现其它的情况都可以变换为完全二叉树的情况)。。

  然后,对于这一问我们制定的贪心策略是:

    尽量从叶子节点开始涂,能涂上的叶子都涂上。

    (为什么没有考虑每条路径的黑长度都相等呢?慢慢看看例子就懂了)

  为啥呢?多举几个例子就懂了(啪!) 

  但是尽管知道了策略,代码依然很让人费解。

  没关系,从例子出发就行,

  说是这边来了个20个节点的完全二叉树:

 

 

  那我们来看看第一回合涂了哪些叶子节点:

 

  

 

 

  去掉红色节点和与红色节点上下相邻的黑色节点后(因为一条路径上不能出现相邻的红色节点),二叉树为:

 

 

第二回合涂完后,二叉树为:

 

 

 

 

去掉节点后,第三次,最后一次涂了:

 

 

 

所以结果为:

 

 

 

 

结束

 

  

       所以我们的涂法策略归结为:

 

       当最下面一层叶子节点深度>=3时,涂掉尽可能多的叶子节点,

 

同时要保证:整个二叉树左右的黑深度总是相差1,且不破坏红黑树正确性

 

(因为不是把所有叶子节点都涂起来就是正确的做法,要从四个情况考虑,

 

而这四个情况需要当前叶子节点深度>=3)。

 

下面四个情况的图,分别是将二叉树从上到下从左到右编号中,编号最后的几个节点的局部图:

  

 

 

 

 

 

 

       同时,这四种情况分别对应代码中的对(k&3)的讨论的操作。

 

       (其实代码为什么那么写可能和作者的思维方式有关,讨论部分的代码可以等效为:   

  

while (k > 1)
    {
        if (k == 2)
            ans++, k--;
        else if ((k & 3) == 1)
            ans += (k >> 1) - 1, k >>= 2, k++;
        else if ((k & 3) == 2)
            ans += (k >> 1) - 1, k >>= 2, k++;
        else if ((k & 3) == 3)
            ans += (k >> 1) , k >>= 2, k++;
        else
            ans += (k >> 1), k >>= 2;
    }

  这就时对四种情况的讨论了。

  其中k>>=2的操作就是将涂色的红节点和无法涂色的黑节点删去的过程(文章最后的代码就是按照这个过程用减法写的,可行)。这里巧妙地利用了完全二叉树叶子节点数等于(总结点数+1)/2,所以除以了2次(常数由于整数除法,除以完了就变成了0)。或者理解为给整个树修剪了两次叶子。

  其他一些小细节,举一举例子就能够写出来了。

总结:

  关于这条题目的贪心方法相较于常规的动态规划解法可以说是非常简洁了。但是其中许多地方的正确性证明起来并不容易。能力有限,只能靠直觉意会了。

顺便附上我依照我所理解得到的思路写下的代码(已AC):

 

 1 #include<iostream>
 2 using namespace std;
 3 int main() {
 4     int N;
 5     cin >> N;
 6     int a = N + 1;
 7     int ans1 = 0;
 8     int ans2 = 0;
 9     while (a > 1) {
10         if (a & 1) {
11             ans1++;
12         }
13         a = a >> 1;
14     }
15     cout << ans1 << endl;//问题一的解答
16     a = N + 1;
17     while (a>1) {
18         //cout << '=' << a << '=' << endl;
19         if (a == 2) {
20             ans2++;
21             break; 
22         }
23         else if ((a & 3) == 3 || (a & 3)== 0) {
24             ans2 += a >> 1;
25             a = a - (a >> 1);
26             a = a >> 1;
27             //a++;
28         }
29         else {//a & 3 == 2 ||a & 3 ==  1
30             ans2 += (a >> 1) - 1;
31             a = a - ((a >> 1) - 1);
32             a = a >> 1;
33             //a++;
34         }
35         //cout << '-' << a << '-' << endl;
36     }
37     cout << ans2 << endl;
38     return 0;
39 }