洛谷P1706 全排列问题

全排列问题

题目描述

P1706 全排列问题 - 洛谷

按照字典序输出自然数 \(1\)\(n\) 所有不重复的排列,即 \(n\) 的全排列,要求所产生的任一数字序列中不允许出现重复的数字。

输入格式

一个整数 \(n\)

输出格式

\(1 \sim n\) 组成的所有不重复的数字序列,每行一个序列。

每个数字保留 \(5\) 个场宽。

样例 #1

样例输入 #1

3

样例输出 #1

    1    2    3
    1    3    2
    2    1    3
    2    3    1
    3    1    2
    3    2    1

提示

\(1 \leq n \leq 9\)

解析

前言

不要使用printf格式化输出,性能十分的低!!!

DFS回溯法求全排列

\(DFS\) 最显著的特征在于其 递归调用自身。同时与 \(BFS\) 类似,\(DFS\) 会对其访问过的点打上访问标记,在遍历时跳过已打过标记的点,以确保 每个点仅访问一次

回溯法是一种经常被用在\(DFS\) (深度优先搜索) 和\(BFS\)(广度优先搜索)的技巧。

其本质是:走不通就回头。

伪码

int ans = 最坏情况;

void dfs(传入数值) {
  if (到达目的地){
     ans = 从当前解与已有解中选最优;
  	 return...
  }
  for (遍历所有可能性)
    if (可行) {
      进行操作;
      dfs(缩小规模);
      撤回操作;
    }
}

实现

import java.util.Scanner;

public class Main {
    static Scanner sc = new Scanner(System.in);
    //book[i]代表i已经被选择过了
    static boolean[] book;
    //存储当前的排列
    static int[] num;
    static int N;

    static void dfs(int n) {
        //已经搜索到最后一个数了
        if (n > N) {
            for (int i = 1; i <= N; ++i) {
                System.out.print("    " + num[i]);
            }
            System.out.println();
        }
        //遍历所有数
        for (int i = 1; i <= N; ++i) {
            //如果i已经被选择,则跳过
            if (book[i]) continue;
            //给i这个数打上标记,代表选上i
            book[i] = true;
            //将i存入当前排列答案中
            num[n] = i;
            //继续搜索下一个数
            dfs(n + 1);
            //撤回标记
            book[i] = false;
        }
    }

    public static void main(String[] args) {
        N = sc.nextInt();
        num = new int[N + 1];
        book = new boolean[N + 1];
        dfs(1);
    }

下一个排列

下一个排列的定义是:

给定数字序列的字典序中下一个更大的排列。如果不存在下一个更大的排列,则将数字重新排列成最小的排列(即升序排列)。

对于非严格递减的序列没有下一个排列

简单来说就是,下一个数比当前数大且尽可能的小

推导

举个例子,求7562541的下一个排列

对于一个序列

越靠前的数对整个序列的影响幅度越大,越靠后的数对整个序列的影响幅度越小

对于非严格递减的数没有下一个排列

所以要找到一个靠后有下一个排列的最小单元序列,也就是最后一个相邻升序元素对

我们发现(2,5)是最后一个相邻的升序的元素对,因此,2541是满足条件的最小单元序列(这个子序列从第二个元素开始,一定是非严格递减的)

我们需要让这个子序列2541大一点点,于是在2541的后缀541中从后向前找第一个大于首元素的数字,也就是4,让它与第一个元素2进行交换

这样得到的就是以4为首的最大排列4521,对其后缀521进行逆序操作,就能得到以4为首的最小排列4125

因此得到了7562541的下一个排列7564125

描述

下一个排列的描述:

  1. 从后向前查找第一个相邻的 升序 的元素对(代表这个后缀有下一个排列),即找到最后一个arr[i]<arr[i+1]。(此时[i+1,end)是非严格递减的)
  2. 在后缀[i+1,end)中找到最小大于arr[i]的数,记为arr[k]。由于后缀[i+1,end)是非严格递减的,所以从后向前找到第一个大于arr[i]的数即可
  3. 交换arr[i]arr[k]。这时得到的后缀是以arr[k]为首的最大的排列
  4. 对后缀[i+1,end)进行逆序操作,来得到以arr[k]为首的最小的排列。
  5. 若在步骤1中无符合条件的相邻的元素对,则说明此时数组是非严格降序的,没有下一个排列了

实现

import java.util.Scanner;

public class Main {
    public static void swap(int[] arr, int x, int y) {
        arr[x] ^= arr[y] ^ (arr[y] = arr[x]);
    }

    //作用:生成下一个排列 (数组范围[l,r])
    public static boolean nextPermutation(int[] arr, int l, int r) {
        if (arr.length <= 1) return false;
        int i = r - 1;
        while (i >= l && !(arr[i] < arr[i + 1])) --i;
        if (i == l - 1) return false;
        int k = r;
        while (arr[i] >= arr[k]) --k;
        swap(arr, i, k);
        for (int left = i + 1, right = r; left < right; ++left, --right) swap(arr, left, right);
        return true;
    }

    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        int n = sc.nextInt();
        int[] arr = new int[n + 1];
        for (int i = 1; i <= n; ++i) arr[i] = i;
        do {
            for (int i = 1; i <= n; ++i) System.out.print("    " + arr[i]);
            System.out.println();
        } while (nextPermutation(arr, 1, n));
    }
}

对比

DFS回溯 下一个排列
时间复杂度 \(O(n\times n!)\) \(O(n\times n!)\)
空间复杂度 在不考虑栈消耗下,需要标记数组 \(O(n)\) 数组内原地操作 \(O(1)\)
是否支持序列包含重复元素 不支持(除非进行特判) 完全支持
适用程度 DFS回溯算法适用程度更广,应当优先掌握 仅适用于排列问题

参考资料

📝【LeetCode】31、46、47 下一个排列、全排列 (imageslr.com)

posted @ 2022-11-18 18:17  Cattle_Horse  阅读(153)  评论(0编辑  收藏  举报