← 首页 / permutation · 把全部 n!个排列玩通透 待审核 4 页

permutation · 把全部 n!个排列玩通透

n 个互异的数能排成 n! 种不同顺序。本系列从一个朴素的需求出发: 给定一个排列, 不重不漏地走到「字典序里紧接着的下一个」, 进而枚举出全部排列; 再换用回溯 (backtracking) 从空排列逐位生长出整棵选择树; 最后用康托展开 (Cantor expansion) 把每个排列和一个整数序号 0 … n!−1 一一对应起来。

每页都是可单步播放的 demo: 看支点 (pivot) 与后继 (successor) 如何被找出、翻转 (reverse) 如何让后缀回到最小、递归如何「选 → 递归 → 撤销」地铺满选择树、序号如何在阶乘进制下被逐位解码成排列。一条主线: 局部一步 (next) → 全局枚举 (enumerate) → 递归视角 (backtrack) → 编号双射 (rank/unrank)

生成: 一个接一个, 再到一次全部

先把「下一个」这一步拆到原子级, 再看它如何驱动整体枚举, 以及回溯给出的另一种生成视角。

下一个排列 · pivot + succ + reverse

下一个排列:让顺序「刚好大一点」

字典序里紧接着的下一个排列怎么求?从右找第一个上升沿(支点 pivot),在它右边的递减段里找大于支点的最小者(后继 succ),交换两者,再翻转支点之后的后缀使其回到最小。单步看四个动作如何精确拼出「下一个」;整段递减时则回环到最小排列。

字典序枚举 · 反复调用 next

枚举全部:从最小一路走到最大

从升序(最小排列)出发,反复调用「下一个排列」,直到抵达降序(最大排列),恰好不重不漏地走遍全部 n! 个。单步观察清单按字典序逐行点亮,留意每一步用到的支点位置在哪——它解释了相邻两个排列差在哪几位。

回溯 · 选择树 · 选/递归/撤销

回溯生成:从空排列长出选择树

换一个视角:从空排列开始,逐位选一个还没用过的数填进去,填满即得一个排列;试完一个分支就撤销(回溯) 去试别的。单步沿递归树深度优先地展开,看「选 → 递归 → 撤销」如何系统地铺满全部叶子——每片叶子正是一个完整排列。

编号: 排列 ↔ 整数序号

排列不只是一堆顺序 —— 它和区间 [0, n!) 里的整数是严格的一一对应。

康托展开 · rank / unrank · 阶乘进制

康托展开:给每个排列一个整数编号

把排列看成阶乘进制 (factorial number system) 下的一个数:第 i 位的权重是 (n-1-i)!,数位则是「右边还没用、且比当前数小的个数」。于是 rank 把排列映成序号 0 … n!-1, unrank 反向解码。单步看序号如何逐位还原成排列——排列与整数严格一一对应。

它真实跑在哪里

标准库: C++ std::next_permutation / prev_permutation、Python itertools.permutations 都在干这件事; 前者正是本系列第一页的算法。 枚举与穷举: 旅行商 (TSP) 的暴力解、座位 / 赛程 / 任务次序的全排列搜索、密码 / 组合的穷举, 都需要不重不漏地遍历排列。 字典序与排名: 把排列编号存储 / 传输 (只存一个整数而非整个数组)、按字典序生成第 k 个排列 (康托 unrank)、判断两个排列谁在前。 回溯框架: 排列是回溯法最经典的入门模型, N 皇后、数独、子集 / 组合枚举都共用「选 → 递归 → 撤销」这套骨架。

相关链接

  • 双指针 · two pointers 本站 · sequence 系列 「下一个排列」里翻转后缀用的就是对撞双指针; 找支点 / 后继也是从右向左的线性扫描 —— 同属序列扫描技巧。
  • 搜索与回溯 · backtracking 本站 · search 系列 回溯生成排列是该范式的入门模型; 同样的「选 → 递归 → 撤销」骨架支撑起 N 皇后、子集枚举与约束求解。
  • Permutation — Generation in lexicographic order en.wikipedia.org Narayana Pandita (14 世纪) 给出的字典序下一个排列算法, 即本系列第一页四步法的来源与正确性说明。
  • Factorial number system (factoradic) en.wikipedia.org 阶乘进制的定义与「factoradic ↔ 排列」的双射, 即康托展开 rank / unrank 的数学基础。