• 红黑树


      AVL自平衡二叉树在教科书上比较常见,因为是最先提出的自平衡二叉树,自然是学术价值比较的高,但是目前工业环境觉得名为红黑二叉树RBT(Red-Black Tree)的自平衡二叉树使用的更为的广泛,比如C++标准库中的有序容器(std::set、std::map),Linux内核中的很多数据结构等,都是用的RBTree来维护管理的。

      当看完RBTree后发现,其实相对来说AVL自平衡树比RBTree更加的平衡,理论上访问效果也会更好,但是为此AVL自平衡树在插入、删除修改树结构的时候,会引入更多的旋转操作来保持平衡,所以对于经常需要添加、删除的高动态数据来说,维护这种数据结构的代价显得十分高昂,而RBTree对于树的高度限制相对要宽松的多,等于是在牺牲了部分完全性(平衡性)的条件下,以换取插入、删除操作时少量的旋转操作(但是一调整起来复杂的情况麻烦的要死~~~)。

    一、红黑二叉树简介

      说到红黑二叉树,不得不先请出红黑树的基本法则,虽然简单,但是维护起来还是挺复杂的:

      (1). 节点都有颜色标记,且只能是红色或黑色。

      (2). 根是黑色。

      (3). 所有叶子都是黑色(叶子是NIL/nill_节点,不保存实际的数据)。

      (4). 每个红色节点必须有两个黑色的子节点,也可以表述为从每个叶子到根的所有路径上不能有两个连续的红色节点。

      (5). 从任一节点到其每个叶子的所有简单路径都包含相同数目的黑色节点。

      上述这些条件中,(1)(3)是很容易遵守的,(2)只有确实在操作到根节点的时候需要注意调整一下就行了,而(4)(5)是维持红黑树结构中常常会用到的准则。

      之所以要有上面的条件和规则,就是为了这么一个保证:从根到任何一个叶子,最好的情况是整条路径全部都是黑色,假设为N,最坏的情况是黑色和红色交替的情况,那也不会超过2N,因此红黑二叉树对操作的复杂度作出了最差的保证。而维护这种数据结构,需要少量的颜色变更和不超过三次树旋转(对于插入操作最多是两次),虽然插入和删除很复杂,但操作时间复杂度仍可以保持为O(log n)而不会因为元素的数目增加而性能恶化。下面是一个典型的红黑二叉树的样子。

        

    二、红黑二叉树实现

      此处还需要事先强调一下,红黑树再复杂也是一个基本的BST,所以和AVL自平衡二叉树一样,所有的插入和删除操作,也是先按照操作BST的基本方式进行插入、删除,然后再检查是否平衡而做出相应调整,RBTree的调整操作包括着色重绘和旋转操作。

    2.1 插入操作

      规定,新增加的节点着色必须是红色。插入操作分以下情况:

      (1). 如果此时还是空的红黑树,则该节点为根节点,将其重绘为黑色,然后返回;否则进行步骤(2)

      (2). 根据BST递归查找,找到可以插入的位置时候,创建新节点后进行BST插入(更新parent_、left_、right_指针域),然后进行步骤(3)

      (3). 如果父亲节点P是黑色,此时红色节点作为孩子是允许的,红色节点的加入不会影响黑色路径的计数,原先父亲的叶子节点黑色由新插入节点的叶子节点继承,对于(4)(5)规则没有任何影响,操作完成直接返回;否则进行步骤(4)

      (4). 父亲节P点是红色的,如果此时叔父节点U也是红色的,而且此刻确定祖父节点G是黑色的,进行如下操作:将祖父节点G重绘为红色,将父亲P和叔父节U点重绘为黑色,此处操作虽然子树平衡了,但是修改了祖父节点G可能导致祖父节点G和其他节点不平衡,以祖父节点G为参数重新进入步骤(3)检查递归;否则进行(5)

        

      (5). 此时父节点P是红色、叔父节点U是黑色、祖父节点G是黑色、新增节点N是红色,根据插入节点N是父亲节点G的左孩子还是右孩子,以及父亲节点P是祖父节点G的左孩子还是右孩子分别组合,一共形成四种情况,依次处理

        a. 如果祖父节点G、父亲节点P、插入节点N在一条直线上,即插入节点N是父亲节点P的左孩子且父节点P是祖父节点G的左孩子,或者镜像情况下插入节点N是父亲节点P的右孩子且父亲节点P是祖父节点G的右孩子,此时只需将祖父节点G改为红色,父亲节点P改为黑色,然后以祖父亲节点G为中心做一个相反方向的旋转就可以了;

        

        b. 如果祖父节点G、父亲节点P、插入节点N不在一条直线上,此时需要以父亲节点P为中心,事先进行一次旋转使得祖父节点、父亲节点、插入节点三者在一条直线上,然后就可以按照a的情况所对应的步骤进行处理了;

        至此,红黑树的插入操作完成。

        

    2.2 删除操作

      红黑树的删除操作算是比较复杂的数据结构操作了,分出的情况比较的多,而且操作过程中涉及到的亲戚节点也比较多。

      此处需要说明的是,所有BST的删除操作,都可以转换看作是单个子树的删除操作,因为删除的节点只可能有三种情况:叶子节点,这时候可以将任意一个NIL节点看做单个子树;含有一个子树的节点;含有两个子树的节点,此时按照BST的删除操作,还是会寻找一个左子树最大值或者右子树最小值替换他,并将替换节点重绘成删除节点的颜色,然后问题实质上就转化成替换节点的删除了,而替换节点不可能有两个子树的情况。

      (1). 如果整个RBTree为空,直接返回;否则进入(2)

      (2). 查找当前节点是否待删除节点,如果不是递归进行左树或者右树删除,如果遍历完了还未找到直接返回,否则当前就是待删除节点,进入步骤(3)

      (3). 如果当前待删除节点的右子树为空,表明无法找到一个用于替换的右子树最小值节点,直接进入步骤(4),否则查找右子数最小节点,和当前节点进行数据部分的交换(而位置关系、着色得以保留),然后将待删除节点设置为替换节点,进入步骤(4),至此我们找到了需要真正进行删除操作的节点N

      (4). 寻找当前删除节点node的非NIL子树(如果当前节点两个孩子都是NIL那就选随便选一个假设为非NIL)作为child,然后判断当前节点是否是根节点而需要做特殊处理(此处不展开这种情况,只考虑一般的删除节点),对于一般的删除情况,通过将node父节点指针引用到child而使用child顶替当前删除节点node,node的引用计数减少(后续会被自动析构释放),而且此时如果删除掉的节点node是红色的,那么表明被删除节点的父亲和孩子child都是黑色的,最主要的是删除一个红色节点不影响RBTree的规则,此处就可以直接返回了;否则进入步骤(5)

      (5). 此处删除掉的节点node是黑色的,而如果替换的child是红色的,那么将孩子重绘为黑色,这样原来通过黑色删除节点的路线现在都由孩子去作为黑色节点顶替了,红黑树的特性没有被破坏,直接返回;否则进入步骤(6)

      (6). 进入步骤(6)就是传说中最复杂的double black情况了,在所有讨论之前这里先声明,到达这里节点的删除工作已经完成,接下来的都是调整工作了。我们将主角命名为N,其本质就是(4)操作中顶替的child节点,原先的祖父节点现在是父亲节点,原先的叔父节点现在是兄弟节点,且此时节点N是黑色的。检测兄弟节点S如果是红色,那么父亲节点P肯定是黑色,此时重绘父亲节点P成红色,重绘兄弟节点S为黑,并且如果N是父亲节点P的左儿子,则以父亲节点P为中心进行左旋操作,否则进行右旋操作,经过这一步调整,N有了一个黑色的兄弟节点和一个红色的父亲节点,但是N和现在兄弟节点S原来的儿子(现在的兄弟)挂着子树上黑色节点的数目肯定不一样,子树是不平衡的,所以还需要继续进行下面的处理,进入步骤(7)。

        

      (7). 无论是原生的还是经过步骤(6)修改得到的,此处兄弟节点S是黑色、N是黑色,然后检查兄弟节点S的两个孩子的颜色,如果兄弟节点S的两个孩子都是黑色,那么就根据父亲节点P的颜色进行讨论:

        a.如果此时父亲节点P的颜色是红色,则重绘兄弟节点S成红色、重绘父亲节点P成黑色,通过这样后原先删除掉的黑色节点就由父亲节点P补偿回来了,而兄弟节点S的整个分支没有改变,满足红黑树条件,就直接返回;

        

        b.如果父亲节点P的颜色是黑色,则重绘兄弟节点S成红色,此时虽然得到的整个子树是平衡的,但是原先经过兄弟节点S的子树都减少了一个黑色,此处需要以父亲节点P为参数步入步骤(4)重新调整;

        

        如果兄弟节点S的两个孩子不都是黑色,此时步入步骤(8)

      (8). 此时兄弟节点S是黑色、N是黑色,而且兄弟节点S的两个孩子至少有一个是红色的,但是父亲节点P多次递归已经不确定颜色了,然后当Parnet-Sibling-r_child不在一条线上面时(此时兄弟节点S的孩子由一个红色和一个黑色构成的时候,假设红色孩子记为r_child),需要先旋转成一条线,同时进行颜色的修正,把兄弟节点S改成红色且r_child改成黑色,经过这个旋转后的子树是满足二叉树性质的,但是N和新的兄弟节点S不平衡(本身这个操作不会涉及到N和父亲节点P),而且这个不平衡的情况刚好会fall through到下面步骤(9)的情况处理;而如果Parnet-Sibling-r_child在一条线上面(这其实就是前面旋转着色后的结果),直接进入步骤(9)处理

        

      (9). 此时兄弟节点S是黑色,且依次挂了红色孩子、黑色孙子一条线的子树,操作是通过以父亲节点P为中心进行旋转,让原来的兄弟节点S代替父亲节点P的颜色,同时重绘原来父亲节点P成黑色,重绘原来兄弟节点S的孩子成黑色。

      由于原先的父亲节点P和现在兄弟节点S的颜色是不确定的,无非做两种情况进行讨论:a. 父亲节点P原先是黑色的;b. 父亲节点P原先是红色的,看图可以直接分析出来,修改之后这条子树到其所有子叶的黑色节点数目和原先都是一样的,满足红黑树条件,删除结束。

        

    三、代码实现

      1 import java.util.List;
      2 
      3 /**
      4  * 红黑树 <br/>
      5  * 1)每个节点要么是红色要么是黑色 2)根节点是黑色的 3)每个叶子节点(NIL)是黑色 4)红色节点的的子节点都是黑色的
      6  * 5)对每个节点,从该节点到其后代叶子节点的简单路径上,均包含数目相同的黑色节点
      7  *
      8  * 通常我们认为树末梢的节点还有两个为空的节点,这些空节点是黑色的,所以不必检测第三条
      9  *
     10  */
     11 public class RedBlackTree<K, V> extends BinarySearchTree<K, V> {
     12     private static final boolean RED = true;
     13     private static final boolean BLACK = false;
     14 
     15     @Override
     16     public BSTNode<K, V> insert(K key, V value) {
     17         BSTNode<K, V> newNode = super.insert(key, value);
     18         fixAfterInsert2(newNode.parent, newNode);
     19         colorBlack(root);// 根节点染黑
     20         size++;
     21         return newNode;
     22     }
     23 
     24     /**
     25      * 保持树的平衡,这里采用模式匹配的写法: y x z A B C D
     26      *
     27      * @param parent 新增节点的父节点
     28      * @param newNode 新增节点
     29      * @return
     30      */
     31     private void fixAfterInsert(BSTNode parent, BSTNode newNode) {
     32         if (parent == null) {
     33             root = newNode;
     34             return;
     35         }
     36         if (colorOf(parent) == RED && colorOf(newNode) == RED) {
     37             // 虚位以待,把四种情况的ABC和xyz定好,然后统一处理
     38             BSTNode A = null, B = null, C = null, D = null, x = null, y = null, z = null;
     39             // case 1 左左
     40             /*
     41              *
     42              * z y D x C A B
     43              */
     44             if (parent.isLeftChild && newNode.isLeftChild) {
     45                 x = newNode;
     46                 A = x.left;
     47                 B = x.right;
     48 
     49                 y = parent;
     50                 C = y.right;
     51 
     52                 z = y.parent;
     53                 D = z.right;
     54 
     55                 changePeek(y, z);
     56             }
     57             // case 2 右右
     58             /*
     59              *
     60              * x A y B z C D --> A x B y C z D
     61              */
     62             else if (!parent.isLeftChild && !newNode.isLeftChild) {
     63                 z = newNode;
     64                 C = z.left;
     65                 D = z.right;
     66 
     67                 y = z.parent;
     68                 B = y.left;
     69 
     70                 x = y.parent;
     71                 A = x.left;
     72 
     73                 changePeek(y, x);
     74             }
     75             // case 3 左右
     76             else if (parent.isLeftChild && !newNode.isLeftChild) {
     77                 y = newNode;
     78                 B = y.left;
     79                 C = y.right;
     80 
     81                 x = y.parent;
     82                 A = x.left;
     83 
     84                 z = x.parent;
     85                 D = z.right;
     86 
     87                 changePeek(y, z);
     88             }
     89             // case 4 右左
     90             else if (!parent.isLeftChild && newNode.isLeftChild) {
     91                 y = newNode;
     92                 B = y.left;
     93                 C = y.right;
     94 
     95                 z = y.parent;
     96                 D = z.right;
     97 
     98                 x = z.parent;
     99                 A = x.left;
    100 
    101                 changePeek(y, x);
    102             }
    103             // ------------------统一变为一种形式,换父子链接并染色----------------------
    104             x.parent = y;
    105             z.parent = y;
    106             y.left = x;
    107             y.right = z;
    108             x.left = A;
    109             if (A != null) {
    110                 A.parent = x;
    111                 A.isLeftChild = true;
    112             }
    113             x.right = B;
    114             if (B != null) {
    115                 B.parent = x;
    116                 B.isLeftChild = false;
    117             }
    118             z.left = C;
    119             z.right = D;
    120             if (C != null) {
    121                 C.parent = z;
    122                 C.isLeftChild = true;
    123             }
    124             if (D != null) {
    125                 D.parent = z;
    126                 D.isLeftChild = false;
    127             }
    128             x.isLeftChild = true;
    129             z.isLeftChild = false;
    130             colorBlack(x);
    131             colorBlack(z);
    132             colorRed(y);
    133             // 递归向上追溯
    134             fixAfterInsert(y.parent, y);
    135         }
    136 
    137     }
    138 
    139     private void fixAfterInsert2(BSTNode parent, BSTNode newNode) {
    140         if (parent == null) {
    141             root = newNode;
    142             return;
    143         }
    144         if (colorOf(parent) == RED) {
    145             // uncle存在且为红色
    146             BSTNode grand = parent.parent;
    147             BSTNode uncle = parent.isLeftChild ? grand.right : grand.left;
    148             // uncle为红
    149             if (uncle != null && colorOf(uncle) == RED) {
    150                 colorRed(grand);
    151                 colorBlack(parent);
    152                 colorBlack(uncle);
    153                 fixAfterInsert2(grand.parent, grand);
    154             } else {// uncle为空=uncle为黑
    155                 if (parent.isLeftChild && newNode.isLeftChild) {// 左左型
    156                     colorRed(grand);
    157                     colorBlack(parent);
    158                     rightRotate(grand, parent);
    159                 } else if (parent.isLeftChild && !newNode.isLeftChild) {// 左右型
    160                     leftRotate(parent, newNode);
    161                     colorRed(grand);
    162                     colorBlack(newNode);
    163                     rightRotate(grand, newNode);
    164                 } else if (!parent.isLeftChild && !newNode.isLeftChild) {// 右右型
    165                     colorRed(grand);
    166                     colorBlack(parent);
    167                     leftRotate(grand, parent);
    168                 } else {// 右左型
    169                     rightRotate(parent, newNode);
    170                     colorRed(grand);
    171                     colorBlack(newNode);
    172                     leftRotate(grand, newNode);
    173                 }
    174             }
    175         }
    176     }
    177 
    178     /**
    179      * 切换顶点,设施newPeek为新顶点
    180      *
    181      * @param newPeek
    182      *            新顶点
    183      * @param oldPeek
    184      *            旧顶点
    185      */
    186     private void changePeek(BSTNode newPeek, BSTNode oldPeek) {
    187         newPeek.parent = oldPeek.parent;
    188         newPeek.isLeftChild = oldPeek.isLeftChild;
    189         if (oldPeek.parent != null) {
    190             if (oldPeek.isLeftChild)
    191                 oldPeek.parent.left = newPeek;
    192             else
    193                 oldPeek.parent.right = newPeek;
    194         } else {
    195             root = newPeek;
    196         }
    197     }
    198 
    199     private void colorRed(BSTNode node) {
    200         if (null != node)
    201             node.isRed = true;
    202     }
    203 
    204     private void colorBlack(BSTNode node) {
    205         if (null != node)
    206             node.isRed = false;
    207     }
    208 
    209     /**
    210      * 红黑树删除及修复 1、双支转单支 2、删除D,并顶替N 3、D为黑,需修复 4、N为红,很简单(N变黑即可) N为黑,系列复杂的修复
    211      * 
    212      * @param key
    213      */
    214     @Override
    215     public void remove(K key) {
    216         BSTNode toDelete = lookupNode(key);
    217         if (toDelete == null)
    218             return;
    219         size--;
    220 
    221         // 如果是严格的内部节点,拷贝后继元素的内容到待删节点,然后toDelete指向后继,合并到后面一同处理
    222         if (toDelete.left != null && toDelete.right != null) {
    223             BSTNode s = successor(toDelete);// 后继右子树的最左端
    224             toDelete.key = s.key;
    225             toDelete = s; // p指向其后继,是待删除的
    226         } // toDelete has 2 children
    227 
    228         // 此时,toDelete一定是单支,或者是叶子
    229         // 用于顶替待删节点的
    230         BSTNode N = (toDelete.left != null ? toDelete.left : toDelete.right);
    231         // N是用来顶替toDelete的
    232         if (N != null) {
    233             // -------这一段是顶替操作-----------
    234             // Link N to parent
    235             N.parent = toDelete.parent;
    236             if (toDelete.parent == null) {
    237                 root = N;
    238                 colorBlack(N);
    239             } else if (toDelete.isLeft()) { // p是左孩子
    240                 toDelete.parent.left = N;
    241                 N.isLeftChild = true;
    242             } else { // p是右孩子
    243                 toDelete.parent.right = N;
    244                 N.isLeftChild = false;
    245             }
    246 
    247             // Null out links so they are OK to use by fixAfterDeletion.
    248             toDelete.left = toDelete.right = toDelete.parent = null;
    249             // -------这一段是顶替操作 end-----------
    250 
    251             // toDelete为黑才需要修复
    252             if (colorOf(toDelete) == BLACK)
    253                 fixAfterDeletion(N);
    254         } // toDelete has 1 children
    255         else if (toDelete.parent == null) { // toDelete是叶子:1.它是根—— if it is the
    256                                             // only node.
    257             root = null;// 变成空树
    258         } else { // toDelete是叶子:2.不是根,没有顶替者.
    259             // toDelete为黑才需要修复
    260             if (colorOf(toDelete) == BLACK)
    261                 fixAfterDeletion(toDelete);// 先修复再cut掉
    262 
    263             // 以下代码执行切掉叶子的动作
    264             if (toDelete.parent != null) {
    265                 if (toDelete == toDelete.parent.left)
    266                     toDelete.parent.left = null;
    267                 else if (toDelete == toDelete.parent.right)
    268                     toDelete.parent.right = null;
    269                 toDelete.parent = null;
    270             }
    271         }
    272 
    273     }
    274 
    275     private void fixAfterDeletion(BSTNode N) {
    276         if (colorOf(N) == RED) {// N为红,简单变黑即可
    277             colorBlack(N);
    278         }
    279         // N为黑,即double black,删除节点和顶替节点都为黑,进行若干种情况的讨论
    280         // case1:N是新的根节点,且N为黑色,没有任何破坏
    281         else if (N.parent == null) {
    282         } else {// 为黑,且不是根节点
    283             case2(N);
    284         }
    285     }
    286 
    287     /*-------情况2:兄弟为红色,转移为兄弟为黑-------*/
    288     private void case2(BSTNode N) {
    289         BSTNode parent = N.parent;
    290         BSTNode sib = sib(N, parent);
    291         if (colorOf(sib) == RED) {
    292             colorBlack(sib);
    293             colorRed(parent);
    294             if (N.isLeft())
    295                 leftRotate(parent, N);
    296             else
    297                 rightRotate(parent, N);
    298         }
    299         case3(N);// sib must be black.
    300     }
    301 
    302     private BSTNode sib(BSTNode N, BSTNode parent) {
    303         BSTNode sib;
    304         if (N.isLeft()) {
    305             sib = parent.right;
    306         } else {
    307             sib = parent.left;
    308         }
    309         return sib;
    310     }
    311 
    312     /*-------情况3:兄弟为黑的前提下,讨论兄弟的双子为黑(兄弟可以被染红)
    313     * 1.父为红色,双子为黑或者不为黑都走向case4
    314     * 2.父为黑色,兄弟染红,递归父节点*/
    315     private void case3(BSTNode N) {
    316         BSTNode parent = N.parent;
    317         BSTNode sib = sib(N, parent);
    318         /* 2.父为黑色,兄弟染红,递归父节点 */
    319         if (colorOf(parent) == BLACK && (sib.left == null || colorOf(sib.left) == BLACK)
    320                 && (sib.right == null || colorOf(sib.right) == BLACK)) {
    321             colorRed(sib);
    322             fixAfterDeletion(parent);
    323         } else {
    324             case4(N);
    325         }
    326     }
    327 
    328     /*-------情况4.1:P为红,兄弟为黑,且兄弟的双子为黑——父兄反色,即可
    329     * 4.2 P红或者黑,兄弟为黑,无论哪一子为红,都转移到case5*/
    330     private void case4(BSTNode N) {
    331         BSTNode parent = N.parent;
    332         BSTNode sib = sib(N, parent);
    333         if (colorOf(parent) == RED && colorOf(sib.left) == BLACK && colorOf(sib.right) == BLACK) {
    334             colorRed(sib);
    335             colorBlack(parent);// 刚好平衡
    336         } else {
    337             case5(N);
    338         }
    339     }
    340 
    341     /*-------情况5,兄弟向内的孩子为红,通过旋转转移为case6:向外的孩子为红*/
    342     private void case5(BSTNode N) {
    343         BSTNode parent = N.parent;
    344         BSTNode sib = sib(N, parent);
    345         if (N.isLeft() && colorOf(sib.right) == BLACK) {
    346             // s->color = RED;
    347             // s->left->color = BLACK;
    348             // rotate_right(s);
    349             colorBlack(sib.left);
    350             colorRed(sib);
    351             rightRotate(sib, sib.left);// 兄弟的外侧孩子变为红色
    352         } else if (N.isRight() && colorOf(sib.left) == BLACK) {
    353             colorRed(sib);
    354             colorBlack(sib.right);
    355             leftRotate(sib, sib.right);
    356         }
    357         case6(N);
    358     }
    359 
    360     /*-------情况6兄弟向外的孩子为红
    361     * 兄弟染为父节点的颜色
    362     * 父节点染黑
    363     * 父节点旋转*/
    364     private void case6(BSTNode N) {
    365         BSTNode parent = N.parent;
    366         BSTNode sib = sib(N, parent);
    367         setColor(sib, colorOf(parent));
    368         colorBlack(parent);
    369         if (N.isLeft()) {
    370             colorBlack(sib.right);
    371             leftRotate(parent, sib);
    372         } else {
    373             colorBlack(sib.left);
    374             rightRotate(parent, sib);
    375         }
    376     }
    377 
    378     private void setColor(BSTNode sib, boolean colorOf) {
    379         if (sib != null)
    380             sib.isRed = colorOf;
    381     }
    382 
    383     private BSTNode rightOf(BSTNode parent) {
    384         return parent.right;
    385     }
    386 
    387     private BSTNode leftOf(BSTNode parent) {
    388         return parent.left;
    389     }
    390 
    391     private boolean colorOf(BSTNode x) {
    392         if (x == null)
    393             return false;
    394         return x.isRed;
    395     }
    396 
    397     @Override
    398     public String toString() {
    399         StringBuilder sb = new StringBuilder();
    400         List<List<BSTNode<K, V>>> lists = super.levelOrder();
    401         for (List<BSTNode<K, V>> l : lists) {
    402             for (BSTNode<K, V> n : l) {
    403                 sb.append(n.toString() + "	");
    404             }
    405             sb.append("
    ");
    406         }
    407         return sb.toString();
    408     }
    409 }
    View Code

      原文链接:http://www.sohu.com/a/131380870_479559

  • 相关阅读:
    [HNOI2006]鬼谷子的钱袋
    一日游与两道题
    [HNOI2009]梦幻布丁
    [Ahoi2009]self 同类分布
    50 days before NOI2017
    Topcoder SRM 606 div1题解
    Topcoder SRM 605 div1 题解
    Topcoder SRM 604 div1题解
    Topcoder SRM 603 div1题解
    Topcoder SRM 602 div1题解
  • 原文地址:https://www.cnblogs.com/xiaoyh/p/10402436.html
Copyright © 2020-2023  润新知