Apr 27, 2013

生成字符串的全排列

递归的方法比较简单,略过。

非递归方法首先需要将字符串排序,然后循环查找下一个比当前字排列大的字符串。举例来说,当前字符串是1 5 7 6 4 3 2,我们知道下一个应该是1 6 2 3 4 5 7,怎么找到的呢?
从右边往左扫描,找到第一个比左边字符大的字符,本例中我们找到了7,然后将7左边的字符(5)与与右边的字符(6 4 3 2)比较,找到(6 4 3 2)中比5大的字符,本例中是6,将5与6交换,然后反转7右边的字符(将5 4 3 2反转成2 3 4 5)。

思路清楚之后,程序就很简单了,并且能处理重复字符的情况(如果用递归的方法,需要检测重复字符进行处理)。

void permutation(char *s, int n)
{
    std::sort(s, s + n);
    printf("%s\n", s);
    int i, j;
    while (1) {
        for (i = n-1; i>=1 && s[i]<=s[i-1]; --i)
            ;
        if (i == 0)
            break;
        for (j = n-1; j>i && s[j]<=s[i-1]; --j)
            ;
        std::swap(s[i-1], s[j]);
        for (j = n-1; i < j; ++i, --j)
            std::swap(s[i], s[j]);
        printf("%s\n", s);
    }
}

Apr 12, 2013

poj 1166 The Clocks

这是一道挺有意思的题目,方法也很多,值得讨论讨论。

首先想到的是搜索的方法。
分析题目可知,9种移动方法,每种方法最多使用3次(因为4次会回到初始状态),因此总的状态数为4^9=262144,不算多,因此简单的BFS就可以搞定,但是空间开销比较大,如果牺牲一点时间换取空间,可以使用限定深度的DFS方法,将深度从1开始递增到27,搜索的状态数最多是BFS的两倍,但是空间消耗少了很多。状态表示上,可以用两个bit位表示一个钟表的状态,这样只需18位就可以表示9个钟表的状态,移动方法的作用也都采用位运算来加速。

其实这是一道数学题。
不妨设钟表的初始状态为A,9种移动方法对钟表的作用可以用矩阵M表示(第一列为第一种移动方法,依此类推),将移动方法用向量x表示,则有A + M.x = 0,于是解得x = M^{-1} * (-A),注意所有的运算都需要模4,而M的逆可以事先求得,因此该解法的时间复杂度是O(1),结束!

Apr 6, 2013

算法导论-红黑树

Introduction to Algorithms 3rd Edition

Part III - Data Structures
Chapter 13: Red-Black Trees

为了解决二叉排序树在最坏情况下的插入、删除和查找等操作的性能,本质上是尽量将树的高度维持在lg(n)左右,出现了一些二叉排序树的变种,比较著名的有AVL树,红黑树等。一般认为,红黑树的平均性能较AVL树更好,插入、删除所需的旋转操作更少,因而应用更广泛。

红黑树具有以下性质:
    1. 每个节点非黑即红
    2. 根节点为黑
    3. 所有叶子节点(NIL)都为黑
    4. 如果一个节点为红,那么它的两个孩子都为黑
    5. 对任何一个节点,从它到所有它的叶子节点的路径上黑色节点个数相等

为了方便,定义一个全局空指针NIL,所有叶子节点都是NIL,颜色为黑,根节点的父节点为NIL。

可以证明,一棵有着n个内节点的红黑树的高度至多为2lg(n+1)。

为了保持红黑树的高度在O(lgn),需要在插入和删除节点时对红黑树进行调整,而调整的主要手段就是对节点进行旋转,以此来维持红黑树的平衡。当然旋转之后可能会导致违背红黑树性质的情况出现,这时就需要对红黑树做进一步的调整。对一个节点左旋,是将它与右孩子旋转;对一个节点右旋,是将它与左孩子旋转。

由于红黑树特性的复杂性,相较二叉排序树,其插入和删除操作也更为复杂,下面详细分析。

对于插入操作,在二叉排序树的插入操作基础上,需要做一点修改。新插入的节点,如果将它的颜色设为黑,那么很显然会影响到红黑树的第5条特性;如果将它的颜色设为红,那么又可能会影响到第4条特性。如果违背第5条特性的话,需要的修正操作会更多更复杂,因而我们首先将新插入的节点的颜色设置为红。不妨记新插入的节点为z,如果z的父节点颜色为黑,那么不需要任何额外修正(除非z是根节点);如果z的父节点颜色也为红,那么就需要调整操作。由于对称性,我们只考虑z的父节点z.p为左孩子的情况(z.p = z.p.p.left),在这个条件下,根据z是左孩子还是右孩子,以及z的叔叔节点的颜色,又可以分为四种情况:
a. z为左孩子,叔节点为红
b. z为右孩子,叔节点为红
c. z为右孩子,叔节点为黑
d. z为左孩子,叔节点为黑

首先来看a和b,如下图所示。在这种情况下,只需将z的父节点和叔节点都设置成黑,z的爷节点设置成红,之后,z的爷节点可能会违背性质4,因此继续重复,将z设置成z.p.p继续。

再来看c和d,如下图所示。c可以通过对z的父节点左旋来转换成d,而d则只需对z的爷节点右旋,并改变z的父节点和爷节点的颜色来完成调整。可以看出,每次调整前后,所有叶子节点的路径长度(黑色节点个数)都没有改变。因此是正确的。


具体的调整代码如下所示。注意到第17行需要把T的根节点设置为黑,这是因为当跳出循环时,z可能恰好是T的根节点并且为红,因此需要将它设置为黑。可以看出,每次插入一个节点至多会进行两次旋转操作,而一旦执行了右旋操作,必然退出循环,调整结束。时间复杂度是树的高度O(lgn)。
RB-INSERT-FIXUP(T, z)
 1      while z.p.color == RED
 2          if z.p == z.p.p.left
 3              y = z.p.p.right
 4              if y.color == RED
 5                  z.p.color = BLACK           // case 1
 6                  y.color = BLACK             // case 1
 7                  z.p.p.color = RED           // case 1
 8                  z = z.p.p                   // case 1
 9              else
10                  if z == z.p.right
11                      z = z.p                 // case 2
12                      LEFT-ROTATE(T, z)       // case 2
13                  z.p.color = BLACK           // case 3
14                  z.p.p.color = RED           // case 3
15                  RIGHT-ROTATE(T, z.p.p)      // case 3
16          else (same as then clause with 'right' and 'left' exchanged)
17      T.root.color = BLACK


至于节点的删除,仍然是在二叉排序树节点删除的基础上修改。最重要的是要记录删除之后真正缺少的颜色是什么,如果是红色那无所谓,如果是黑色,那就会影响到它的子树。
先看删除操作的代码,之后再分析删除之后调整红黑树的代码。
RB-DELETE(T, z)
 1      y = z
 2      y-original-color = y.color
 3      if z.left == T.nil
 4          x = z.right
 5          RB-TRANSPLANT(T, z, z.right)
 6      else if z.right == T.nil
 7          x = z.left
 8          RB-TRANSPLANT(T, z, z.left)
 9      else
10          y = TREE-MINIMUM(z.right)
11          y-original-color = y.color
12          x = y.right
13          if y.p == z
14              x.p = y
15          else
16              RB-TRANSPLANT(T, y, y.right)
17              y.right = z.right
18              y.right.p = y
19          RB-TRANSPLANT(T, z, y)
20          y.left = z.left
21          y.left.p = y
22          y.color = z.color
23      if y-original-color == BLACK
24          RB-DELETE-FIXUP(T, x)


代码中需要特别注意的地方是:x取代了y原来的位置,y的颜色被设置成z的颜色,因此y的颜色缺失了。如果y的颜色是红,那么不影响红黑树性质;如果y的颜色是黑,那么就可能会影响,需要进行调整。
由于x取代了y的位置,而y为黑。如果x为红,那么直接将x设为黑即可;如果x为黑,需要将x对黑的贡献改为2。具体调整代码如下:
RB-DELETE-FIXUP(T, x)
  1  while x != T.root and x.color == BLACK
 2      if x == x.p.left
 3          w = x.p.right
 4          if w.color == RED
 5              w.color = BLACK                 // case 1
 6              x.p.color = RED                 // case 1
 7              LEFT-ROTATE(T, x.p)             // case 1
 8              w = x.p.right                   // case 1
 9          if w.left.color == BLACK and w.right.color == BLACK
10              w.color = RED                   // case 2
11              x = x.p                         // case 2
12          else
13              if w.right.color == BLACK
14                  w.left.color = BLACK        // case 3
15                  w.color = RED               // case 3
16                  RIGHT-ROTATE(T, w)          // case 3
17                  w = x.p.right               // case 3
18              w.color = x.p.color             // case 4
19              x.p.color = BLACK               // case 4
20              x.right.color = BLACK           // case 4
21              LEFT-ROTATE(T, x.p)             // case 4
22              x = T.root                      // case 4
23      else (same as then clause with 'right' and 'left' exchanged)
24  x.color = BLACK

只考虑x是左孩子的情况,又可以分为四种情况:
1. x的兄弟节点w是红色。如下图所示,由于w是红色,因此x的父节点、w的孩子节点都是黑色,对x的父节点左旋,并修改x父节点和w的颜色,这时x的兄弟节点颜色为黑,转换为其它情况。

2. x的兄弟节点w为黑,并且w的孩子都为黑。如下图所示,不管x的父节点是什么颜色,可以将w改成红色,并将x修改为父节点,这样x就往上移了一个节点,继续循环。

3. x的兄弟节点w为黑,w的左孩子为红,右孩子为黑。如下图所示,将w右旋,并修改新的w及其右孩子颜色,可以将新的w右孩子改成红,转换成case 4

4. x的兄弟节点w为黑,w的右孩子为红。如下图所示,将x的父节点左旋,并修改几个节点的颜色,可以结束调整(通过将x设置为T.root)。

Exercises

13.1-3
很显然仍然是

13.1-5
最短的情况是x到叶子节点都是黑色节点,最长的情况是x到叶子节点的路径红黑相间。后者至多是前者的两倍

13.1-6
如果所有节点都是黑色,那么内节点个数为2^k-1;如果是红黑相间,那么内节点个数为2^2k -1

13.1-7
最大的比例是2,所有黑色节点的孩子都是红节点,每个黑色节点都对应两个红色节点,因此比例为2;最小的比例为0,没有红色节点

13.2-1
将LEFT-ROTATE中的x与y对换,left与right对换即可

13.2-2
因为旋转是基于两个节点的,而两个节点必然确定一条边,n个节点的树有n-1条边,因此恰好有n-1种旋转

13.2-3
如果y为黑,那么α高度加1;如果x为黑,那么γ高度减1;β高度不变

13.2-4
任何一个树至多通过n-1次右旋可以转换成right-going chain

13.2-5
1(#, 2)不能通过右旋转换成2(1, #)
如果T1能通过右旋转换成T2,那么可以这样操作:首先通过右旋操作将T1中与T2的根节点对应的那个节点旋转成根节点,之后再递归地对左右子树操作,复杂度是:
T(n) = O(n) + T(k) + T(n-1-k),也即T(n) = O(n) + T(n-1),因此T(n) = O(n^2)
P.S. 我也不知道这样证明对不对……

13.3-4
设置成红色节点只有在case 1和case 3才出现:将z.p.p设置成红,如果z.p.p是T.nil,那么z.p就是根节点,为黑,根本就不会进入循环;再来看case 2,我们在将z.p.p设置为红之前将z和z.p交换了,但是交换之后进行了旋转操作,z.p.p仍然不变,因此case 2也没有问题。所以这种情况不会发生

13.4-7
直接看答案吧,哈哈