排列的分解
离散数学里面大家都学过群,群里面有种很基本并且很重要的叫做置换群,置换群的元素本质上就是一个排列(英文 permutation group, 直译过来应该是排列群)。大家应该都学过当初要把 (4 1 5 2 3) 分解成 (1 4 2)(3 5),这个叫做群的 k-cycle 分解,也就是说,一个排列(置换群的一个元素)可以分解成多个 k-cycle。所以,现在问题就是:给定一个[1, n] 构成的排列,求这个排列的 k-cycle 分解,即输出每个 k-cycle,允许使用 O(n) 的额外空间。这实际上是一个循环链表问题:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
// 为了更贴近前文所述,a 的长度是 n+1,a[0] 未用,a[1..n] 是由 [1, n] 构成的排列 void k_cycle_decomp(const int* a, int n) { bitvector mark(n+1); // mark[0] 未用 for (int i = 1; i <=n; ++i) if (!mark[i]) { int j = i; do { mark[j] = 1; printf("%d ", j); j = a[j]; } while (j != i); printf("n"); } } |
由此,可以衍生出另一个问题:给定两个包含 n 个元素的平行数组 p, d, p 是下标数组,d 是值(数据)数组,p[i] 是指向 d 的下标,现在 d 已经按 p 有序了,也就是说(现在数组下标是从 0 到 n-1 ):
对于任意的 i ∈[0, n-1) ,d[p[i]] ≤ d[p[i+1]],现在要求,根据 p 将 d 原地整理成有序数组,在这个过程中,允许改变 p,不允许使用额外O(n) 空间。有了前面的 k-cycle 分解,这个问题就相当容易了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
void rearrange(int* p, Data* d, int n) { for (int i = 0; i < n; ++i) if (i != p[i]) { // need rearrange Data tmp = d[i]; int j = i; int k = p[i]; do { d[j] = d[k]; p[j] = j; // mark j = k; k = p[k]; } while (k != i); d[j] = tmp; p[j] = j; } } |
条件判断:
1 |
if (i != p[i]) |
实际上同时还是个优化手段,如果本来就 p[i] == i,就不需要任何移动,一旦移动过后,置 p[i] = i 的作用是打标记。
如果把排列的 k-cycle 看成一个循环链表,内循环实际上是将链表元素循环左移一个单位。内循环使用 do {} while 是因为我们已经知道这个内循环至少执行一次,do {} while 在循环次数较少时比 while {} 要快不少,具体原因请参考相关资料(汇编语言或编译原理或观察编译器生成的汇编码)。
与这相同的算法,可以对排列(置换群的元素)进行原地求逆(需要标记,可以将 int 最高位用来做标记),求逆如果不要求原地,那么就是简单地让 output[p[i]] = i ,最后返回 output 即可,原地求逆的具体代码我这里就不写了。