LeetCode 回溯专题 3:排列问题 Permutations

体会回溯的方法在求解排列问题中的应用,掌握使用数组记录每次走过的路的技巧,体会在这样的过程中状态重置的意义。这一节的标题更确切来说,应该叫“全排列”问题。

例题1:LeetCode 第 46 题:全排列

传送门:英文网址:46. Permutations ,中文网址:46. 全排列

给定一个没有重复数字的序列,返回其所有可能的全排列。

示例:

输入: [1,2,3]
输出:
[
  [1,2,3],
  [1,3,2],
  [2,1,3],
  [2,3,1],
  [3,1,2],
  [3,2,1]
]

分析:事实上,这是一个非常典型的使用回溯法来解决的问题。首先,我们可以很容易地要发现递归结构:

LeetCode 第 46 题:全排列

理解这张图:问题做到这个地方,我们感觉到递归算法有点类似与暴力解法一样,来帮助我们寻求问题的所有可能解。

我们看到,上图第 1 层的节点有 3 条路可以走,第 2 层的节点有 2 条路可以走,第 3 层只有 1 条路可以走,于是我们就找到了答案。

那么这样的过程我们应该如何描述呢?

我们不妨这样来理解?其实在每一层,我们都有 3 条路供我们选择,只不过这里是排序问题,之前已经走过的路,我们不能再走,这是题目给出的规则,那么我们从当前节点走到下一节点的时候,就要问一问自己,哪些路已经走过了,在生活中,我们做这样的问题,可以列一张表,走过一站在上面打一个勾,而在我们的代码实现中,就可以使用一个布尔型数组,来记录之前走过的路。

代码关键部分如下:

for (int i = 0; i < nums.length; i++) {
    if (!used[i]) {
        used[i] = true;
        integers.add(nums[i]);
        generatePermutation(nums, index + 1, integers);
        used[i] = false;
        integers.remove(integers.size() - 1);
    }
}

这里的难点在于,体会为什么图中我们看到每一层往下的分支是越来越少,但是我们的递归函数每一层的 for 循环上限仍然是 nums.length , 这是因为我们使用了 used 数组记录了每一个递归层走过的路,只要之前走过了,这一层这条路就被“砍断”,这里“砍断”的意思就是“什么都不做”。在递归到底以后,需要一层一层回溯,所以状态要重置,所以代码结构看起来就是“对称的”。

  • 从分析的过程可知,这是一个典型的树形问题;下一层问题与上一层的问题是同一个问题,只不过处理的数据不同;
  • 递归回溯算法有点类似与暴力解法,来帮助我们搜索问题的所有的解,搜索的过程类似于深度优先遍历,是一个回溯搜索的过程(走到底的时候,回到上一层);
  • 对于排列问题的实现技巧:一旦我们使用了一个数字,就会影响下一步递归的范围。我们看到下一层处理的数据规模总是比上一层的少 1 ,直到叶子节点时只处理 1个数据,递归调用返回;因此我们可以使用一个 used 数组来记录走到叶子节点的时候使用了哪些数字,即递归向下的时候设置这些数字为“已经使用状态”,但是我们还要注意,回溯向上的时候,要将一层一层地将数字置为“未使用状态”,因此,整个递归回溯过程结束以后,就好像所有的数组都没有被使用过一样(事实上,我们在 debug 的时候,可以打印这 used 数组,验证我们的想法);
  • 在递归到底以后,需要一层一层回溯,所以状态要重置,所以代码结构看起来就是"对称的";
  • 画出递归结构图真的是我们发现递归关系的一个好方法,能画好树形结构的图,就可以尝试是否可以使用回溯方法解决。

Java 代码:

public class Solution {

    private List<List<Integer>> result = new ArrayList<>();
    private boolean[] used;

    public List<List<Integer>> permute(int[] nums) {
        if (nums.length == 0) {
            return result;
        }
        int numsLen = nums.length;
        used = new boolean[numsLen];
        for (int i = 0; i < numsLen; i++) {
            used[i] = false;
        }
        generatePermutation(nums, 0, new ArrayList<Integer>());
        return result;
    }

    /**
     * 树形问题使用回溯算法的一个套路是:在循环中使用递归。
     * <p>
     * integers 中保存了一个有 index-1 个元素的排列。
     * 向这个排列的末尾添加第 index 个元素, 获得一个有 index 个元素的排列
     *
     * @param nums
     * @param index
     * @param integers
     */
    private void generatePermutation(int[] nums, int index, ArrayList<Integer> integers) {
        if (index == nums.length) {
            // 这里有个坑,integers 是一个指针,所以要新建一个对象放进去
            result.add(new ArrayList<>(integers));
            return;
        }

        for (int i = 0; i < nums.length; i++) {
            if (!used[i]) {
                used[i] = true;
                integers.add(nums[i]);
                generatePermutation(nums, index + 1, integers);
                used[i] = false;
                integers.remove(integers.size() - 1);
            }
        }
    }


    public static void main(String[] args) {
        Solution solution = new Solution();
        List<List<Integer>> permute = solution.permute(new int[]{1, 2, 3, 4});
        System.out.println(permute);
        System.out.println(permute.size());
    }
}

总结:使用一个数组进行状态的记录,也是我们在算法过程中常常使用到的技巧。这道题的解答具体一定代表性,希望认真体会其中用到的技巧。在递归层一层一层减少的时候,我们可以使用一个数组来记录已经走过的路的状态。在递归到底以后,需要一层一层回溯,所以状态要重置,所以代码结构看起来就是"对称的"。

这里的代码包括了打印 used 数组的输出的代码,提交到 LeetCode 的时候,应该删去。

Python 代码:

class Solution:

    def permute(self, nums):
        """
        :type nums: List[int]
        :rtype: List[List[int]]
        """
        if len(nums) == 0:
            return []

        used = [False] * len(nums)
        res = []
        self.__dfs(nums, 0, [], used, res)
        return res

    def __dfs(self, nums, index, pre, used, res):
        # 先写递归终止条件
        if index == len(nums):
            res.append(pre.copy())
            return

        for i in range(len(nums)):
            if not used[i]:
                # 如果没有用过,就用它
                used[i] = True
                pre.append(nums[i])
                self.__dfs(nums, index + 1, pre, used, res)
                used[i] = False
                pre.pop()

练习1:LeetCode 第 47 题:全排列 II

传送门:英文网址:47. Permutations II ,中文网址:47. 全排列 II

给定一个可包含重复数字的序列,返回所有不重复的全排列。

示例:

输入: [1,1,2]
输出:
[
  [1,1,2],
  [1,2,1],
  [2,1,1]
]

求解关键:找到重复的原因,对树进行剪枝。

1、首先将数组排序,这一步很关键,是后面剪枝的基础;

2、只处理第 1 次遇到的那个数,举个具体的例子画个图。

重点理解:

1、 i > 0

2、 nums[i] == nums[i - 1]

3、之前那个数还没有使用,即 marked[i-1] = false

LeetCode 第 47 题:全排列 II

其实就是在 46 题的基础上做了重复检测。之前一个比较复杂的版本。下面是一个较简单的判断去重的版本,使用 Python 编写。

Python 代码:

# LeetCode 第 47 题:全排列问题 2
# 47. 全排列 II,要求:给定一个可包含重复数字的序列,返回所有不重复的全排列。
# 要去除重复,首先排序是肯定的


class Solution:
    def permuteUnique(self, nums):
        """
        :type nums: List[int]
        :rtype: List[List[int]]
        """
        if len(nums) == 0:
            return []
        nums.sort()
        used = [False] * len(nums)
        res = []
        self.__dfs(nums, 0, [], used, res)
        return res

    def __dfs(self, nums, index, pre, used, res):
        if index == len(nums):
            res.append(pre.copy())
            return
        for i in range(len(nums)):
            if not used[i]:
                # 如果没有用过
                if i != 0 and nums[i] == nums[i - 1] and not used[i - 1]:
                    continue
                used[i] = True
                pre.append(nums[i])
                self.__dfs(nums, index + 1, pre, used, res)
                used[i] = False
                pre.pop()


if __name__ == '__main__':
    s = Solution()
    nums = [1, 1, 2]
    result = s.permuteUnique(nums)
    print(result)

Python 代码:

# LeetCode 第 47 题:全排列问题 2
# 47. 全排列 II,要求:给定一个可包含重复数字的序列,返回所有不重复的全排列。
# 要去除重复,首先排序是肯定的


class Solution:
    def permuteUnique(self, nums):
        """
        :type nums: List[int]
        :rtype: List[List[int]]
        """
        if len(nums) == 0:
            return []
        nums.sort()
        used = [False] * len(nums)
        res = []
        self.__dfs(nums, 0, [], used, res)
        return res

    def __dfs(self, nums, index, pre, used, res):
        if index == len(nums):
            res.append(pre.copy())
            return
        for i in range(len(nums)):
            if not used[i]:
                # 如果没有用过
                if i != 0 and nums[i] == nums[i - 1] and not used[i - 1]:
                    continue
                used[i] = True
                pre.append(nums[i])
                self.__dfs(nums, index + 1, pre, used, res)
                used[i] = False
                pre.pop()

特别注意:1、排序;

2、if i != 0 and nums[i] == nums[i - 1] and not used[i - 1]: 这行代码中的 not used[i - 1] 这个条件很容易忘记;

3、continue 不是 break

(本节完)

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,132评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,802评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,566评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,858评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,867评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,695评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,064评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,705评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,915评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,677评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,796评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,432评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,041评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,992评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,223评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,185评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,535评论 2 343

推荐阅读更多精彩内容