第七章 基于排列、图的DFS
- 全排列问题如何使用深度优先搜索来实现?和全子集问题的异同在哪儿?
- 全排列问题的 Follow up: Permutation II。如何去重?
- 如何求一个排列的下一个排列?
- 如何求一个排列是第几个排列?
如何求下一个排列
问题描述
给定一个若干整数的排列,给出按整数大小进行字典序从小到大排序后的下一个排列。若没有下一个排列,则输出字典序最小的序列。
例如1,2,3
→ 1,3,2
,3,2,1
→ 1,2,3
,1,1,5 → 1,5,1
原题一个要求原地修改,一个要求返回新的排列
算法描述
如果上来想不出方法,可以试着找找规律,我们关注的重点应是原数组末尾。
从末尾往左走,如果一直递增,例如...9,7,5
,那么下一个排列一定会牵扯到左边更多的数,直到一个非递增数为止,例如...6,9,7,5
。对于原数组的变化就只到6
这里,和左侧其他数再无关系。6
这个位置会变成6
右侧所有数中比6
大的最小的数,而6
会进入最后3个数中,且后3个数必是升序数组。
所以算法步骤如下:
- 从右往左遍历数组
nums
,直到找到一个位置i
,满足nums[i] > nums[i - 1]
或者i
为0
。 i
不为0时,用j
再次从右到左遍历nums
,寻找第一个nums[j] > nums[i - 1]
。而后交换nums[j]
和nums[i - 1]
。注意,满足要求的j
一定存在!且交换后nums[i]
及右侧数组仍为降序数组。- 将
nums[i]
及右侧的数组翻转,使其升序。
Q:i为0怎么办?
A:i为0说明整个数组是降序的,直接翻转整个数组即可。
Q:有重复元素怎么办?
A:在遍历时只要严格满足nums[i] > nums[i - 1]
和nums[j] > nums[i - 1]
就不会有问题。
Q:元素过少是否要单独考虑?
A:当元素个数小于等于1个时,可以直接返回。
全排列问题
全排列问题是“排列式”深度优先搜索问题的鼻祖。很多搜索的问题都可以用类似全排列的代码来完成。包括我们前面学过的全子集问题的一种做法。
这一小节中我们需要掌握:
- 普通的全排列问题怎么做
- 有重复的全排列问题怎么做?如何在搜索类问题中去重?
- 从1做极小改动
- 如何实现一个非递归的全排列算法?
基本思路
非递归的全排列,采用的是迭代方式,在如何求下一个排列中,我们讲过如何求下一个排列,那么我们只需要不断调用这个nextPermutation
方法即可。
一些可以做得更细致的地方:
- 为了确定何时结束,建议在迭代前,先对输入
nums
数组进行升序排序,迭代到降序时,就都找完了。有心的同学可能还记得在nextPermutation
当中,当且仅当数组完全降序,那么从右往左遍历的指针i
最终会指向0。所以可以为nextPermutation
带上布尔返回值,当i
为0时,返回false
,表示找完了。要注意,排序操作在这样一个NP问题中,消耗的时间几乎可以忽略。 - 当数组长度为1时,
nextPermutation
会直接返回false
;当数组长度为0时,nextPermutation
中i
会成为-1,所以返回false
的条件可以再加上i
为-1
。
如何求一个排列是第几个排列?
题目描述
给出一个不含重复数字的排列,求这些数字的所有排列按字典序排序后该排列的编号,编号从1开始。
例如排列[1,2,4]
是第1
个排列。
算法描述
只需计算有多少个排列在当前排列A
的前面即可。如何算呢?举个例子,[3,7,4,9,1]
,在它前面的必然是某位置i
对应元素比原数组小,而i
左侧和原数组一样。也即[3,7,4,1,X]
,[3,7,1,X,X]
,[3,1或4,X,X,X]
,[1,X,X,X,X]
。
而第i
个元素,比原数组小的情况有多少种,其实就是A[i]
右侧有多少元素比A[i]
小,乘上A[i]
右侧元素全排列数,即A[i]
右侧元素数量的阶乘。i
从右往左看,比当前A[i]
小的右侧元素数量分别为1,1,2,1
,所以最终字典序在当前A
之前的数量为1×1!+1×2!+2×3!+1×4!=39,故当前A
的字典序为40。
具体步骤:
- 用
permutation
表示当前阶乘,初始化为1,result
表示最终结果,初始化为0。由于最终结果可能巨大,所以用long
类型。 i
从右往左遍历A
,循环中计算A[i]
右侧有多少元素比A[i]
小,计为smaller
,result += smaller * permutation
。之后permutation *= A.length - i
,为下次循环i
左移一位后的排列数。- 已算出多少字典序在
A
之前,返回result+1
。
Q:为了找寻每个元素右侧有多少元素比自己小,用了O(n^2)的时间,能不能更快些?
A:可以做到O(nlogn)!但是很复杂,这是另外一个问题了,可以使用BST,归并排序或者线段树
Q:元素有重复怎么办?
A:好问题!元素有重复,情况会复杂的多。因为这会影响A[i]
右侧元素的排列数,此时的排列数计算方法为总元素数的阶乘,除以各元素值个数的阶乘,例如[1,1,1,2,2,3]
,排列数为6!÷(3!×2!×1!)。
为了正确计算阶乘数,需要用哈系表记录A[i]
及右侧的元素值个数,并考虑到A[i]
与右侧比其小的元素A[k]
交换后,要把A[k]
的计数减一。用该哈系表计算正确的阶乘数。
而且要注意,右侧比A[i]
小的重复元素值只能计算一次,不要重复计算!