算法-排列问题【回溯、交换元素】

本文介绍了解决字符全排列问题的递归算法及八皇后问题的深度优先搜索策略。针对字符全排列问题,文章提供了一种通过位置互换实现的高效递归算法模板,并特别考虑了重复字符的处理。对于八皇后问题,文章详细阐述了如何利用剪枝条件避免冲突,并给出了将一维数组转换为二维棋盘布局的具体方法。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >


前言

全排列问题大部分都是一个模板的。 以前一直以为是通过一层递归一次添加一个元素这样,但是写起来没有位置互换这样方便,要考虑的情况也太多。 记录一下这种调换式的解题模板

一、剑指offer-38:字符的全排列问题

题目描述:输入一个字符串,打印出该字符串中字符的所有排列。

你可以以任意顺序返回这个字符串数组,但里面不能有重复元素。

示例:

输入:s = “abc”
输出:[“abc”,“acb”,“bac”,“bca”,“cab”,“cba”]


思路:
从1到n,每次固定第一个位置的字母。
从下一个字母到最后一个字母,与第一个字母分别互换.
固定完后,下标后移一位,递归的对剩下n-1的字母进行同样的操作。

需要处理的特殊情况:

  1. 重复字母
    如abb
    固定a后,第二位与a互换和第三位与a互换实际上是等价的,为了防止递归进入,需要进行剪枝。

使用的秘诀是通过HashSet将每次已经互换过的字符添加进入set,当轮到下一个字母时,看看HashSet中有没有这个字符,若有,直接跳过。

  1. 递归后还原处理
    防止这次的迭代造成数组元素变动,需要将数组元素返还
 char[] p = null;
    List<String> result = null;
    public String[] permutation(String s) {
        if (s == null || s.length() == 0) return new String[]{""};
        p = s.toCharArray();
        result = new ArrayList<String>();
        combine(0);
        String[] res = new String[result.size()];
        result.toArray(res);
        return res;
    }
    
    public void  combine(int index) {
        //后移到只有一个字母的时候,就不需要再互换了,直接将字符数组组成的字符串返回
        if (index == p.length - 1) {
            result.add(String.valueOf(p));
            return;
        }
        Set<Character> set = new HashSet<>();
        for (int i = index; i < p.length; i++) {
            //若当前字母与之前已经互换过的字母相等,不再互换
            if (set.contains(p[i])) continue;
            
            //存储下当前交换的位置的字母
            set.add(p[i]);
            
            swap(index, i);
            
            //递归地进行剩下 n - index的字母的排列
            combine(index + 1);
        
//             还原数组,防止分支污染
            swap(i, index);
        }
    }
    
    public void swap(int i, int j) {
            char temp = p[j];
            p[j] = p[i];
            p[i] = temp;
    }

二、LeetCode-51:八皇后问题

题目表述:

n 皇后问题 研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击。

给你一个整数 n ,返回所有不同的 n 皇后问题 的解决方案。

每一种解法包含一个不同的 n 皇后问题 的棋子放置方案,该方案中 ‘Q’ 和 ‘.’ 分别代表了皇后和空位。
在这里插入图片描述


分析:

先不计较输出。

将皇后的位置抽象为一维数组:q[n]

  • 第一点,每个皇后的位置不在同一行。
    因为直接使用数组的下标作为行号,下标自然不可能重合。

  • 第二,要保证列号不一样。
    q[i] != q[j]

  • 第三点要求不在同一个斜线
    设 i < j,则满足等式
    i - j != abs(q[i] - q[j])
    将这个等式作为分支的剪枝条件。

可以专门设置一个函数,去判断当前插入的元素合不合法。若不合法,直接跳过。


好吧,还有一个沙雕的输出问题,这个应该不是什么问题吧?
其实真挺难得。

当迭代到第n + 1个位置后,将一维数组的信息转化为结果。
观察结果,使用List<List<String>>.
外层的List可以认为是方案,每次n + 1次位置只需要添加一次。

内层的List为当前每一行的信息转为字符串的效果。

我们的设定是:
将一维数组q的下标作为行号,将q的数值作为列号。
因此可设计两层循环,外层遍历行,一共n次。【因为每行元素是一个String,所以在外层循环开始时设置一个StringBuilder】
内存循环遍历列,我们可以这样:
对列的每个可能值遍历一遍,当列标遍历到刚好是q[i]中所存的那个列标时,添加一个“Q”

代码就很简单了

 int[] q = null;
    List<List<String>> result = null;
    public List<List<String>> solveNQueens(int n) {
        //舍弃下标为0的单元
        q = new int[n + 1];

        System.out.println(Arrays.toString(q));
        result = new ArrayList<>();

        dfs(1);

        System.out.println(result);
        return result;
    }

    public void dfs(int x) {
        if (x == q.length) {
//         运行到棋盘外了
//            此时应该收集战果,将之前的结果变成字符串返回
//            观察返回值的样子,发现是一个字符串的列表的列表,因此要用双重循环
            /*
                外层表示每一行;内层表示每一列。当数组元素的该列有值时,添加为Q
                [
                [".Q..","...Q","Q...","..Q."],
                ["..Q.","Q...","...Q",".Q.."]
                ]
            * */

            List<String> res = new ArrayList<>();
            for (int i = 1; i < q.length; i++) {
                StringBuilder sb = new StringBuilder();
                for (int j = 1; j < q.length; j++) {
                    if (q[i] == j) sb.append("Q");
                    else sb.append(".");
                }
                res.add(sb.toString());
            }

            result.add(res);

            return;
        }

        for (int i = 1; i < q.length; i++) {
            q[x] = i; //将当前行x行的皇后摆放在第i列

            if (check(x)) {
                //为true,表示当前的插入方法不会导致失败,可以插入[x, i]这一格皇后,继续向下查看
                dfs(x + 1);
            } else {
                //为false,不能插入,应该跳出循环
                //写不写无所谓
                continue;
            }
        }
    }

    /**
     * 当方法运行到第i行时,当前的插入方法是否可以保证与前面的i-1行即不重合列,又不在同一个斜边
     * @param i
     */
    public boolean check(int i) {
        for (int j = 1; j < i; j++) {
            if (q[i] == q[j] || Math.abs(i - j) == Math.abs(q[i] - q[j])) {
                return false;
            }
        }
        return true;
    }


后话

开篇说是回溯调换法,第二题八皇后还是没用上。我试了一下,发现异常的复杂,最后我自己都糊涂了,还没有这种添加元素式的简单。看来什么套路也不是万能的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值