LeetCode47, 全排列进阶,如果有重复元素怎么办?
本文始发于个人公众号:TechFlow,原创不易,求个关注
今天是LeetCode第28篇,依然是全排列的问题。
如果对全排列不熟悉或者是最近关注的同学可以看一下上一篇文章:
LeetCode就是喜欢这样,把类似的问题放在一起,让你刷的时候一起刷,从而更加深刻地理解。今天的问题同样是全排列,不过稍稍不同的是,我们有一个限制条件不一样,给定的元素当中可能存在重复。但是元素存在重复,我们并不想最后的结果也出现重复,这个时候应该怎么办?
举个例子,比如我们有一个排列是[1, 2, 2].
它的所有排列是[1, 2, 2], [2, 1, 2], [2, 2, 1],但是注意,在之前的做法当中,我们把所有的元素看成是unique的,但是现在这个条件被破坏了。显然我们需要在实现全排列的基础上解决这个问题。
无脑解决
解决的方法有两种,第一种是无脑解决。是的你没有看错,因为我们分析一下就会发现next_permutation循环的方法在这题当中仍然奏效,原因也很简单,因为我们每次用next_permutation求得字典序+1的下一个排列的时候,显然已经去除了重复元素的情况。为什么?因为同样元素换位置字典序是不会变化的。
比如下图当中,我们更换两个2的顺序,整个序列的字典序是没有变化的,要使得字典序变化一定要交换不同的元素。所以这个解法是可行的。
next_permutation是返回当前序列字典序+1的序列的算法,这个算法在之前的文章当中出现过,所以我就不赘述它的原理了,如果你不记得或者是忘记了的话,可以点击下方的链接回顾一下:
LeetCode 31:递归、回溯、八皇后、全排列一篇文章全讲清楚
这个解法和上一题的最后一个解法完全一样,连改动都不需要,我们直接把代码copy过来把函数名匹配上就可以提交了。这也是我说这道题无脑的原因。
class Solution:
def get_next(self, nums: List[int]):
"""
Do not return anything, modify nums in-place instead.
"""
# 长度
n = len(nums)
# 记录图中i-1的位置
pos = n - 1
for i in range(n-1, 0, -1):
# 如果降序破坏,说明找到了i
if nums[i] > nums[i-1]:
pos = i-1
break
for i in range(n-1, pos, -1):
# 从最后开始找大于pos位置的
if nums[i] > nums[pos]:
# 先交换元素,在进行翻转
nums[i], nums[pos] = nums[pos], nums[i]
# 翻转[pos+1, n]区间
nums[pos+1:] = nums[n:pos:-1]
return False
return True
def permuteUnique(self, nums: List[int]) -> List[List[int]]:
ret = []
# 从小到大排序,获得最小排列
nums = sorted(nums)
ret.append(nums.copy())
# 如果还有下一个排列则继续调用
while not self.get_next(nums):
# 要.copy()是因为Python中存储的引用,如果不加copy
# 会导致当nums发生变化之后,ret中存储的数据也会变化
ret.append(nums.copy())
return ret
回溯法
显然重复之前的工作并不能让我们变得更强,所以我们还是要回到正解上来。在之前的文章我们知道,生成全排列的通常做法是使用回溯法。那么如果使用回溯法我们怎么解决重复元素的问题呢?
还是老惯例,我们要解决问题,首先来分析问题,我们知道重复的元素会干扰全排列生成的算法,那么它为什么会干扰,是怎么干扰的?
在回溯法当中,我们是顺序遍历位置,然后枚举放置的元素。于是我们大概可以想出两种可能导致重复的情况。
我们先来看情况1,情况1很简单,就是同一个位置放入同样元素的情况。比如我们原数组当中有两个4,我们在放置的过程当中放入第一个4和第二个4的情况是一样的。显然这就重复了,我们可以参考下下图,我们给当下所有的选择编号,选择2和选择3是等价的,两者只能选一个。
第二种情况也比较容易想明白,还是同样的例子,我们给数组当中的两个4编号,一个编号是,一个是,我们会发现对于两个位置,我们先放第一个再放第二个和先放第二个再放第一个是重复的。
表面上来看情况是这两种,但是如果深入分析会发现这两种情况其实说的是一回事,结果出现重复都是由于全排列的时候元素出现不稳定造成的。
这里的“稳定“其实是一个排序算法当中的术语,我们经常会讨论某一个排序算法是稳定的,某一个不是。这个稳定是什么意思呢,其实就是指的元素的相对顺序。比如在上面这张图当中的两个4,如果在排序的结果当中,后面一个4有可能出现在第一个4的前面,那么就说明这个算法是不稳定的,同样,如果不会出现这样的情况,那么可以说这个算法是稳定的。
同样,如果我们能保证执行全排列的时候元素的稳定性,那么这个问题就解决了。表面上看这似乎是一个优化问题,其实不然,考察的是稳定性这个概念。
如果能想到稳定性这点,离解决已经很近了。我们要保证稳定性,也就是说对于当前元素,我们需要保证前面的同元素已经放置了,那么在一个排列中,相同元素的摆放位置应该是递增的。我们用map记录每一个元素最后一次摆放的位置,控制它在往下递归的过程当中递增即可。
比如当数组当中有两个4的时候,第一个4如果还没有摆放,那么第二个4也不能摆。但是由于我们在判断要不要摆放第二个4的时候,并不知道前面是否还有其他的4,所以我们只能倒过来,判断在摆放第一个4的时候,是不是已经放过了后面的4,如果是的话,那么这个操作不能执行。用语言描述这个逻辑有点绕,我们来看下图就明白了:
我们摆放了第一个4之后,map[4] = 4,记录的是摆放的4的下标,当我们枚举放置第一个4的时候,发现已经放置的4下标大于当前,说明非法,放置了会引起重复。
还有一个问题是当我们回溯的时候,需要重置map里的值,比如:
在一次递归当中,我们放置了两个4,放了第二个4之后,map[4] = 4,当我们回溯弹出第二个4的时候,这个时候的map[4]应该是多少?
答案不难,应该是1,也就是第一个4的下标。所以我们在递归的时候,在更新map之前,需要记录一下之前的值,方便回溯的时候恢复。
我们整理一下,思路可以得到代码:
class Solution:
def dfs(self, nums, n, i, states, cur, ret, flag):
if i == n:
ret.append(cur.copy())
return
for p in range(n):
# 遍历所有元素
# 如果p元素已经放置过了,或者是已经放置了更后面的同元素
# 跳过
if flag[p] or (nums[p] in states and states[nums[p]] >= p):
continue
# 当前位置放置p
cur.append(nums[p])
# 更新之前,记录之前的值
tmp, states[nums[p]] = states[nums[p]] if nums[p] in states else -1, p
# flag[p]置为True
flag[p] = True
# 递归
self.dfs(nums, n, i+1, states, cur, ret, flag)
# 回溯
cur.pop()
flag[p] = False
# 恢复递归之前的值
states[nums[p]] = tmp
# states.remove((i, nums[p]))
def permuteUnique(self, nums: List[int]) -> List[List[int]]:
ret = []
n = len(nums)
# 记录元素i有没有放置过
flag = [False for _ in range(n)]
self.dfs(nums, n, 0, {}, [], ret, flag)
return ret
改进
上面这个方法其实有点复杂,理解起来有很多细节,一个比较蛋疼的点就是我们用了map去记录了位置,由于要更新map,所以还需要记录之前的值,还需要判断元素不在map当中的情况。并且map的查找存在开销,那么我们能不能不用map呢?
在想怎么不用map的替代方案之前,我们需要先搞清楚,我们为什么要使用map?
这个问题问的并不是一个充分问题,而是一个必要问题,不是用map解决了什么问题,而是我们为什么只能用map不可呢?
因为map是一个容器,它能存储数据。对于一个元素t来说,我们并不知道它在数组nums当中之前出现的位置是哪里,我们也并不知道这些之前出现的t有没有被放入当前的排列里。我们用map记录下t的下标,本质原因是这个。map只是我们实现这个功能的手段,不是目的。
所以我们如果不想使用map,必须要保证能够知道每一个元素的摆放位置才可以。要做到这点其实并不难,我们只需要对nums排序就好了。排序之后,所有相同的元素都会挨在一起。那么,对于位置p来说,我们只需要判断如果nums[p-1] 等于 nums[p]的话,必须要flag[p-1]等于true,也就是说对于元素v来说,它前面一位如果是v必须要放置好,才能放置当前的v,否则就是非法的。这样每一个v都限制前一个v,就保证了所有的v不会起冲突。这是一种链式限制的思想。
通过这种方法,我们就可以抛弃掉map的使用,进而极大地提升效率。
想清楚了原理之后,代码非常简单:
class Solution:
def dfs(self, nums, n, i, cur, ret, flag):
if i == n:
ret.append(cur.copy())
return
for p in range(n):
# 遍历所有元素
# 如果p元素已经放置过了,或者
# 如果nums[p-1] == nums[p] 并且flag[p-1] 是False
# 跳过
if flag[p] or (p > 0 and nums[p-1] == nums[p] and not flag[p-1]):
continue
# 当前位置放置p
cur.append(nums[p])
# flag[p]置为True
flag[p] = True
# 递归
self.dfs(nums, n, i+1, cur, ret, flag)
# 回溯
cur.pop()
flag[p] = False
def permuteUnique(self, nums: List[int]) -> List[List[int]]:
ret = []
n = len(nums)
# 记录元素i有没有放置过
nums = sorted(nums)
flag = [False for _ in range(n)]
self.dfs(nums, n, 0, [], ret, flag)
return ret
你看,我们只需要排序,也不需要引入新的数据结构,就可以完美地解决这个问题。其实很多时候,解决问题的途径有很多种,能不能想到更好的解法除了取决于能力之外,更重要的还是对问题的理解。
这道题也是一个Medium的问题,总体来说难度并不大。如果可以不看标程,独立通过这道题,就说明对全排列这个问题的思考已经比较到位了。
今天的文章就是这些,如果觉得有所收获,请顺手点个关注或者转发吧,你们的举手之劳对我来说很重要。
LeetCode47, 全排列进阶,如果有重复元素怎么办?的更多相关文章
- [leetcode]47. Permutations全排列(给定序列有重复元素)
Given a collection of numbers that might contain duplicates, return all possible unique permutations ...
- dfs 全排列 使用交换——含重复元素和非重复元素
15. 全排列 中文 English 给定一个数字列表,返回其所有可能的排列. 样例 样例 1: 输入:[1] 输出: [ [1] ] 样例 2: 输入:[1,2,3] 输出: [ [1,2,3], ...
- [leetcode]46. Permutations全排列(给定序列无重复元素)
Given a collection of distinct integers, return all possible permutations. Input: [1,2,3] Output: [ ...
- 【模板】 全排列 && 有重复元素的全排列
全排列 #include<bits/stdc++.h> using namespace std; ]; void print (int n){ ;i<=n;i++) cout< ...
- Concise and clear CodeForces - 991F(dfs 有重复元素的全排列)
就是有重复元素的全排列 #include <bits/stdc++.h> #define mem(a, b) memset(a, b, sizeof(a)) using namespace ...
- poj3421 X-factor Chains(重复元素的全排列)
poj3421 X-factor Chains 题意:给定正整数$x(x<=2^{20})$,求$x$的因子组成的满足任意前一项都能整除后一项的序列的最大长度,以及满足最大长度的子序列的个数. ...
- 紫书 习题 10-25 UVa 1575 (有重复元素的全排列+暴搜)
我一开始以为有什么很牛逼的方法来做,然后一直没有思路 后来看了https://blog.csdn.net/zju2016/article/details/78562932的博客 竟然是暴搜?????? ...
- 剑指 Offer 38. 字符串的排列 + 无重复元素的全排列
剑指 Offer 38. 字符串的排列 Offer_38 题目描述 解题思路 可以使用递归实现全排列,每次都确定一个数的位置,当所有位置的数都确定后即表示一个排列. 但是考虑到本题需要排除重复的排列, ...
- python经典面试算法题4.1:如何找出数组中唯一的重复元素
本题目摘自<Python程序员面试算法宝典>,我会每天做一道这本书上的题目,并分享出来,统一放在我博客内,收集在一个分类中. [百度面试题] 难度系数:⭐⭐⭐ 考察频率:⭐⭐⭐⭐ 题目描述 ...
随机推荐
- 使用CSS实现折叠面板总结
任务目的 深入理解html中radio的特性 深入理解CSS选择器以及伪元素的使用 任务描述 使用input的radio单选框特性结合CSS中的伪元素实现bootstrap中折叠面板(点击查看样例), ...
- 神奇的 SQL 之 ICP → 索引条件下推
开心一刻 楼主:来,我们先排练一遍 小伙伴们:好 嘿.哈.嚯 楼主:非常好,就是这个节奏,我们开始吧 楼主:啊.啊.啊,疼 ! 你们是不是故意的 ? 回表与覆盖索引 正式讲 ICP 之前了,我们先将相 ...
- Asp.Net Core 中IdentityServer4 授权中心之应用实战
一.前言 查阅了大多数相关资料,查阅到的IdentityServer4 的相关文章大多是比较简单并且多是翻译官网的文档编写的,我这里在 Asp.Net Core 中IdentityServer4 的应 ...
- 如何在PHP7中扩展mysql,先安装php7.2。后安装mysql
相对与PHP5,PHP7的最大变化之一是移除了mysql扩展,推荐使用mysqli或者pdo_mysql,实际上在PHP5.5开始,PHP就着手开始准备弃用mysql扩展,如果你使用mysql扩展,可 ...
- PHP mysql事务问题实例分析
本文实例分析了PHP的mysql事务问题.分享给大家供大家参考,具体如下: 对于myisam数据库,可以控制事务的进行: $mysqlrl = mysql_connect ( $db_config [ ...
- SpiningUP 强化学习 中文文档
2020 OpenAI 全面拥抱PyTorch, 全新版强化学习教程已发布. 全网第一个中文译本新鲜出炉:http://studyai.com/course/detail/ba8e572a 个人认为 ...
- Block详解一(底层分析)
本篇博客不再讲述Block的基本定义使用,最近而是看了很多的block博客讲述的太乱太杂,所以抽出时间整理下block的相关底层知识,在讲述之前,提出几个问题,如果都可以回答出来以及知道原理,大神绕过 ...
- mycli初体验
一.安装 pip install mycli 二.使用 mycli --help 三.特点 语法不全,高亮等
- WPF转换器之值转换器
WPF有两转转换器,一种是值转换器,另一种多值转换器,在开发过程中经常会从数据拉一些数据过来,比如存储性别的时候往往会用0或1,但在界面上肯定是要显示男或女,那么这个时候就可以用上值转换器 编写转换器 ...
- 2016 Multi-University Training Contest 1 T4
http://acm.hdu.edu.cn/showproblem.php?pid=5726 求不修改区间gcd可以用线段树或者倍增. 求l-n的我们注意观察gcd(al,al+1,... ...