四、回溯算法

四、回溯算法

可能不是最优解法,目的是为了理解回溯算法

1、总结

常见的回溯算法通用模板:

1、确定结束条件,什么时候回溯结束:

  • 通常若满足要求,会把路径(List<>)、记数(int)进行添加,然后return
  • 若不满足要求,直接return

2、选择列表:代表着每一次可以都选择的选项(不管可不可行),所以具有通用性适合于每一次:

  • 可能是一个循环for(int i ~9)进行选择
  • 也可能是某几种情况进行选择,例如有效括号,是对’(‘和’)'进行选择

3、判断可行性:可行即放,不可行就continue

  • 数组表达-这个数有没有被用过:visited[]?
  • 函数表达-数放在这里可不可行:N皇后中某个点的放置是否可行

4、迭代下一次:这个是个关键

  • 若任意的排列组合都行(全排列),直接backTrack(形参)
  • 若排列组合对全局可能性存在某种要求(若N皇后),backTrack是有boolean的返回值的,
    • 此时这一步为:if(backTrack(形参) return true;)
    • 在循环列表结束之后,要return false,相当于在这个点,所有可能的情况都装不进去(回溯到上一点,上一点重新选择)

5、去除这个可能性

public void(boolean) backTrack(选择列表){
    // 1、结束条件
    if(结束?){
        if(满足要求?) {
            添加路径or记数加一;
        }
        return (boolean);
    }
	
    // 2、选择列表
    for(int i = 0;i < grap.length;i++){
        // 3、判断当前位置可不可以是这个数
        if(isValid(当前i))或visite[]?
        
        // 添加值,该点是用这个数
        grap[curRow][i] = 1;
        // 4、下一个点的迭代
        -------backTrack(形参);
        -------if(backTrack(形参)) return true;
        
        // 5、去除该可能
        grap[curRow][i] = 0;
    }

        (return false;)
    }

2、例题分类

1、记数-循环列表-可行性函数-组合有条件

2.1 N皇后2
题目描述
n 皇后问题 研究的是如何将 n 个皇后放置在 n × n 的棋盘上,并且使皇后彼此之间不能相互攻击。
给你一个整数 n ,返回 n 皇后问题 不同的解决方案的数量。

示例 1:
输入:n = 4
输出:2
解释:如上图所示,4 皇后问题存在两个不同的解法。

示例 2:
输入:n = 1
输出:1

提示:

1 <= n <= 9
代码
class Solution {
    // 运行函数
    public int totalNQueens(int n) {
        // 创建n*n的矩阵,方便某个点放置位置是否可行的判断
        int[][] data = new int[n][n];
        // 调用回溯算法,从第0行开始
        backTrack(data,0);
        // 返回“记数”值
        return res;
    }

    int res = 0;
    
    public boolean backTrack(int[][] grap,int curRow){
        // 1、是否结束的判断--结束与条件满足是同一个判断语句
        // 因为能到grap.length行说明矩阵已经放好了n个皇后的位置
        if(curRow == grap.length){
            res++;
            // 进行下一个可能性,所以是false
            return false;
        }
		
        // 2、循环列表---这一行的所有列都可以选择
        for(int i = 0;i < grap.length;i++){
            // 3、判断当前位置可不可以是这个数,是一个函数
            if(!isValid(grap,curRow,i)) continue;
			
            // 放置
            grap[curRow][i] = 1;
            // 4、有条件的迭代
            if(backTrack(grap,curRow+1)) return true;
            // 5、去除此可能性
            grap[curRow][i] = 0;
        }
		// 此行所有位置都不能放,让上一行进行重新选择
        return false;
    }
	
    // 判断位置可不可放----皇后的攻击范围
    private boolean isValid(int[][] grap,int row,int cloum){
        int n = grap.length;
        for(int i = 0; i < row; i++){
            if(grap[i][cloum]!=0) return false;
        }

        for(int i = row - 1,j = cloum - 1; i >= 0 && j >=0;i--,j--){
            if(grap[i][j]!=0) return false;
        }

        for(int i = row - 1,j = cloum + 1; i >= 0 && j < n;i--,j++){
            if(grap[i][j]!=0) return false;
        }

        return true;
    }
}

2、路径-循环列表-可行性Visite-组合有条件

2.1 格雷编码
题目描述
n 位格雷码序列 是一个由 2n 个整数组成的序列,其中:
每个整数都在范围 [0, 2n - 1] 内(含 0 和 2n - 1)
第一个整数是 0
一个整数在序列中出现 不超过一次
每对 相邻 整数的二进制表示 恰好一位不同 ,且
第一个 和 最后一个 整数的二进制表示 恰好一位不同

给你一个整数 n ,返回任一有效的 n 位格雷码序列 。

示例 1:

输入:n = 2
输出:[0,1,3,2]
解释:
[0,1,3,2] 的二进制表示是 [00,01,11,10] 。
- 00 和 01 有一位不同
- 01 和 11 有一位不同
- 11 和 10 有一位不同
- 10 和 00 有一位不同
[0,2,3,1] 也是一个有效的格雷码序列,其二进制表示是 [00,10,11,01] 。
- 00 和 10 有一位不同
- 10 和 11 有一位不同
- 11 和 01 有一位不同
- 01 和 00 有一位不同

示例 2:

输入:n = 1
输出:[0,1]

提示:
1 <= n <= 16
``
代码

class Solution {
     HashSet<Integer> visit = new HashSet<>();
    LinkedList<Integer> res = new LinkedList<>();
    public List<Integer> grayCode(int n) {
        // 初始化,把第一个条件加入
        res.addLast(0);
        visit.add(0);
        backTrack(n,0);
        return res;
    }

    private boolean backTrack(int n,int val){
        // 1、结束条件,res里面已经装了2^n个值
        if(res.size() == (int)Math.pow(2, n)){
            // 2、组合是有一定条件的,所以返回时true和false
            // 最后一位与第一位也是相差1
            for(int i = 0;i < n;i++){
                if((val^(1<<i)) == 0){
                    return true;
                }
            }
            return false;
        }

        // 3、选择列表:每一个数之后的另一个数,只有一位不同,所以对每个位置进行按位异或,取不同的操作
        for(int i = 0; i < n+1; i++){
            int cur = val^(1<<i);
            
            // 4、不能放置的条件:这个数已经被使用了
            if(visit.contains(cur)) continue;

            // 5、加入res并标记访问
            res.addLast(cur);
            visit.add(cur);
            // 6、迭代下一个
            if(backTrack(n,cur)) return true;
            res.removeLast();
            visit.remove(val);
        }
        return false;
    }
}

3、路径-循环列表可行函数-组合无条件

3.1 复原IP地址
题目描述
有效 IP 地址 正好由四个整数(每个整数位于 0 到 255 之间组成,且不能含有前导 0),整数之间用 '.' 分隔。

例如:"0.1.2.201" 和 "192.168.1.1" 是 有效 IP 地址,但是 "0.011.255.245"、"192.168.1.312" 和 "192.168@1.1" 是 无效 IP 地址。

给定一个只包含数字的字符串 s ,用以表示一个 IP 地址,返回所有可能的有效 IP 地址,这些地址可以通过在 s 中插入 '.' 来形成。你 不能 重新排序或删除 s 中的任何数字。你可以按 任何 顺序返回答案。

示例 1:

输入:s = "25525511135"
输出:["255.255.11.135","255.255.111.35"]

示例 2:

输入:s = "0000"
输出:["0.0.0.0"]

提示:

1 <= s.length <= 20
s 仅由数字组成
代码

class Solution {
     List<String> res = new LinkedList<>();
    public List<String> restoreIpAddresses(String s) {
        // K是创建的结束条件,代表着做了几次划分(需要4次划分)
        backTrack(s,0,new StringBuffer(),4);
        return res;
    }

    private void backTrack(String s,int start,StringBuffer sb,int k){
        // 1、结束条件,就是做了是次划分
        if(k == 0){
            // 2、符合条件的就是把数据全利用完了的sb
            if(start == s.length()){
                // 3、因为每次都会加一个‘.’,所以需要把最后的.给删除了
                sb.deleteCharAt(sb.length()-1);
                res.add(sb.toString());
            }
            return;
        }

        // 4、循环列表是s中能取得数,因为有顺序,所以给予start依次顺下去
        for(int i = start; i < start + 3 && i < s.length(); i++){
            // 5、取出当前值的string与int,是因为得取出0打头得非0项
            String next = s.substring(start,i+1);
            int cur = Integer.parseInt(next);

            // 6、取得数字是否合理,break得原因:
            // 前面的不合适后续一定不合适:在001中取 第二次00不行 后一次001一定不合适
            if(cur < 0 || cur > 255) break;
            if(next.charAt(0) == '0'&&next.length() > 1) break;

            // 7、加入路径中,进行迭代,注意退路的时候得去除点位是有变化的
            sb.append(next+".");
            backTrack(s,i + 1,sb,k - 1);
            sb.delete(start + (4 - k),sb.length());
        }
    }
}
3.2 分割字符串
题目描述
给你一个字符串 s,请你将 s 分割成一些子串,使每个子串都是 回文串 。返回 s 所有可能的分割方案。

回文串 是正着读和反着读都一样的字符串。

示例 1:

输入:s = "aab"
输出:[["a","a","b"],["aa","b"]]

示例 2:

输入:s = "a"
输出:[["a"]]

提示:

1 <= s.length <= 16
s 仅由小写英文字母组成
代码

class Solution {
    List<List<String>> res;
    public List<List<String>> partition(String s) {
        res = new LinkedList<>();
        backTrack(s,0,new LinkedList<>());
        return res;
    }

    private void backTrack(String s,int start,LinkedList<String> list){
        // 1、结束条件,已没有可选字母
        if(start == s.length()){
            res.add(new LinkedList<>(list));
            return ;
        }
		
        // 2、可选列表,就是String s,不能重复且连续
        // (1)从start开始,而不是0;
        // (2)连续取子集
        for(int i = start;i<s.length();i++){
            String cur = s.substring(start,i+1);
			// 3、可行性函数:判断这个子串是不是回文字符串
            if(!isValid(cur)) continue;
			
            // 4、加入路径
            list.addLast(cur);
            // 5、下一次迭代,从下一个i开始
            backTrack(s,i + 1,list);
            // 6、返回加入的值
            list.removeLast();
        }

    }
	// 判断子集是否是回文字符串
    private boolean isValid(String s){
        int i = 0;int j = s.length() - 1;
        while(i < j){
            if(s.charAt(i)!=s.charAt(j)) return false;
            i++;
            j--;
        }
        return true;
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值