leetcode高頻題筆記之分治與回溯

Pow(x,n)

在這裏插入圖片描述
分治法
採用分治的思想,求x的n次方,可以先求x的n/2次方

如果n爲偶數x^n = x^(n/2)* x^(n/2)
如果n爲奇數x^n = x^(n/2)* x^(n/2) *x

public class Main {
    public double myPow(double x, int n) {
        if (x == 1) return 1;//特殊用例,加快速度
        if (x == -1) return n % 2 == 0 ? 1 : -1;//特殊用例,加快速度
        if (n == -2147483648) return 0;//一個特殊的坑人的用例
        if (n < 0) {//負數次方處理
            n = -n;
            x = 1 / x;
        }
        return pow(x, n);
    }

    double pow(double x, int n) {
        if (n == 0) return 1;
        //求n/2次方的值
        double half = myPow(x, n / 2);
        return n % 2 == 0 ? half * half : half * half * x;
    }
}

子集

在這裏插入圖片描述
分治法
將添加值的問題分爲加值不加值兩種

public class Main {
    List<List<Integer>> res;
    public List<List<Integer>> subsets(int[] nums) {
        res = new ArrayList<>();
        if (nums == null) return res;
        dfs(nums, new ArrayList<Integer>(), 0);
        return res;
    }

    /**
     * @param nums  入參數組
     * @param list  傳入每層的數組
     * @param level 層數
     */
    private void dfs(int[] nums, List<Integer> list, int level) {

        if (level == nums.length) {
            res.add(new ArrayList<>(list));//因爲list是傳入的引用,所以需要new一個list加入
            return;
        }
        //不需要這個參數的集合
        dfs(nums, list, level + 1);
        //將值加入列表,再進行下探
        list.add(nums[level]);
        dfs(nums, list, level + 1);
        //reserve
        list.remove(list.size() - 1);//真的list 引用,操作完了之後必須reserve
    }
}

迭代法
這個題還有一個比較經典的玩法就是迭代法
遍歷入參集合,每次都把已有的集合複製一份,然後把值加入到複製的集合中

public class Main {
    public List<List<Integer>> subsets(int[] nums) {
        List<List<Integer>> res = new ArrayList<>();
        if (nums == null) return res;
        res.add(new ArrayList<>());
        for (int i = 0; i < nums.length; i++) {
            List<List<Integer>> tmp_list = new ArrayList<>();//使用tmp_list,保證遍歷時取的數都是上一輪的
            for (List list : res) {
                List<Integer> curlist = new ArrayList<>(list);
                curlist.add(nums[i]);
                tmp_list.add(curlist);
            }
            res.addAll(tmp_list);
        }
        return res;
    }
}

電話號碼的字母組合

在這裏插入圖片描述
回溯法

public class Main {
    Map<Character, String> map;
    List<String> res;

    public List<String> letterCombinations(String digits) {
        if (digits.length() == 0 || digits == null) return new ArrayList<>();
        //初始化
        map = new HashMap<>();
        map.put('2', "abc");
        map.put('3', "def");
        map.put('4', "ghi");
        map.put('5', "jkl");
        map.put('6', "mno");
        map.put('7', "pqrs");
        map.put('8', "tuv");
        map.put('9', "wxyz");

        res = new LinkedList<>();

        search("", digits, 0);
        return res;

    }

    private void search(String s, String digits, int level) {
        //退出條件,長度夠了
        if (level == digits.length()) {
            res.add(s);
            return;
        }
        //獲取第level個數字對應的字母
        String cur = map.get(digits.charAt(level));
        //遍歷字母並依次下探
        for (int i = 0; i < cur.length(); i++) {
            search(s + cur.charAt(i), digits, level + 1);
        }
    }
}

組合

在這裏插入圖片描述
在這裏插入圖片描述
回溯法

public class Main {
    List<List<Integer>> res;
    public List<List<Integer>> combine(int n, int k) {
        res = new ArrayList<>();
        if (n <= 0 || k <= 0 || n < k) {
            return res;
        }
        findCombinations(n, k, 1, new Stack<>());
        return res;
    }
    private void findCombinations(int n, int k, int begin, Stack<Integer> pre) {
        //當數目夠了k就加入結果集並退出
        if (pre.size() == k) {
            res.add(new ArrayList<>(pre));
            return;
        }

        //進一步下探,添加一個數,並下探它後面的數繼續進行添加
        for (int i = begin; i <= n; i++) {
            pre.add(i);
            findCombinations(n, k, i + 1, pre);
            pre.pop();
        }
    }
}

for 循環裏 i 從 start 到 n,其實沒必要到 n。比如,n = 5,k = 4,temp.size( ) == 1,此時代表我們還需要(4 - 1 = 3)個數字,如果 i = 4 的話,以後最多把 4 和 5 加入到 temp 中,而此時 temp.size() 纔等於 1 + 2 = 3,不夠 4 個,所以 i 沒必要等於 4,i 循環到 3 就足夠了。
所以,需要對下探條件進行修改,對整個遞歸樹進行剪枝,減去圖中綠色的數(圖自題解
在這裏插入圖片描述
推算出剪枝規則是i <= n - (k -temp.size()) + 1

於是優化後的代碼如下

public class Main {
    List<List<Integer>> res;
    public List<List<Integer>> combine(int n, int k) {
        res = new ArrayList<>();
        if (n <= 0 || k <= 0 || n < k) {
            return res;
        }
        findCombinations(n, k, 1, new Stack<>());
        return res;
    }
    private void findCombinations(int n, int k, int begin, Stack<Integer> pre) {
        //當數目夠了k就加入結果集並退出
        if (pre.size() == k) {
            res.add(new ArrayList<>(pre));
            return;
        }

        //進一步下探,添加一個數,並下探它後面的數繼續進行添加
        for (int i = begin; i <= n - (k - pre.size()) + 1; i++) {
            pre.add(i);
            findCombinations(n, k, i + 1, pre);
            pre.pop();
        }
    }

}

全排列

在這裏插入圖片描述
這個題組合一樣,都是標準的回溯的模板解決

public class Main {

    List<List<Integer>> res;

    public List<List<Integer>> permute(int[] nums) {
        res = new ArrayList<>();
        backtrack(new ArrayList<>(), nums);
        return res;
    }

    private void backtrack(List<Integer> tmpList, int[] nums) {

        if (tmpList.size() == nums.length) {
            res.add(new ArrayList<>(tmpList));//切記要新new一個加入
            return;
        }
        for (int i = 0; i < nums.length; i++) {
            if (tmpList.contains(nums[i])) continue;//已經存在了,就跳過
            //當前層業務
            tmpList.add(nums[i]);
            //下探
            backtrack(tmpList, nums);
            //reserve
            tmpList.remove(tmpList.size() - 1);
        }
    }
}

全排列II

在這裏插入圖片描述
與上一個題全排列相比,本題存在兩個新的問題

  1. if(tempList.contains(nums[i])) continue;這句代碼是通過比較值不同跳過的,而現在值可以相同
  2. 如果有兩個相同的值,則會產生兩次一樣的結果

針對上述兩個問題,進行如下的升級
首先將nums進行排序,重新定義個一個old集合存放訪問過的值的下標,使即使值相同也能判斷是否訪問過

針對重複現象,改爲如下代碼去重

			//判斷上一個元素和自己是不是一樣的,一樣說明上次就加進去了,不用重複操作了
            // !old.contains(i - 1)是爲了保證前一個數是因爲回溯被還原了的才跳過,否則不能跳過
            if (i > 0 && !old.contains(i - 1) && nums[i - 1] == nums[i]) {
                continue;
            }

視頻講解剪枝原理參考大佬的題解

public class Main {

    List<List<Integer>> res;

    public List<List<Integer>> permuteUnique(int[] nums) {
        res = new ArrayList<>();
        Arrays.sort(nums);
        List<Integer> old = new ArrayList<>();
        backtrack(new ArrayList<>(), nums, old);
        return res;
    }

    private void backtrack(List<Integer> tmpList, int[] nums, List<Integer> old) {

        if (tmpList.size() == nums.length) {
            res.add(new ArrayList<>(tmpList));//切記要新new一個加入
            return;
        }
        for (int i = 0; i < nums.length; i++) {
            //當前下標的值已經被使用過了
            if (old.contains(i)) {
                continue;
            }

            //判斷上一個元素和自己是不是一樣的,一樣說明上次就加進去了,不用重複操作了
            // !old.contains(i - 1)是爲了保證前一個數是因爲回溯被還原了的才跳過,否則不能跳過
            if (i > 0 && !old.contains(i - 1) && nums[i - 1] == nums[i]) {
                continue;
            }
			//當前層業務	
            old.add(i);        
            tmpList.add(nums[i]);
            //下探
            backtrack(tmpList, nums, old);
            //reserve
            old.remove(old.size() - 1);
            tmpList.remove(tmpList.size() - 1);
        }
    }
}

N皇后

在這裏插入圖片描述
超經典回溯題,思路見代碼註解

public class Main {

    List<List<String>> res;

    //標記列是否被攻擊
    int[] rows;
    //標記次對角線是否被攻擊
    int[] secondMain;
    //標記主對角線是否被攻擊
    int[] main;
    //存儲皇后的位置
    int[] queens;
    //n皇后
    int n;

    public List<List<String>> solveNQueens(int n) {
        //初始化
        res = new ArrayList<>();
        rows = new int[n];
        secondMain = new int[2 * n - 1];
        main = new int[2 * n - 1];
        queens = new int[n];
        this.n = n;

        //回溯算法從第0行開始查找皇后的位置
        backtrack(0);

        return res;
    }

    //回溯算法放置皇后
    private void backtrack(int row) {
        if (row >= n) return;
        //從第一列開始嘗試放皇后
        for (int col = 0; col < n; col++) {
            //檢測是否會被攻擊,如果沒有攻擊再進行下一步操作
            if (isNotUnderAttrack(row, col)) {
                //在當前位置放置皇后
                placeQueen(row, col);
                // 放置完成判斷現在是否是最後一行,如果是就存儲這個方案
                if (row == n - 1) addSolution();
                //下探 在下一行放皇后
                backtrack(row + 1);
                //回溯,將上一個皇后去掉
                removeQueen(row, col);

            }
        }

    }

    //回溯,去掉皇后和皇后的攻擊範圍
    private void removeQueen(int row, int col) {

        //移除row行的皇后
        queens[row] = 0;
        //更新列攻擊範圍
        rows[col] = 0;
        //更新主對角線攻擊範圍
        main[row - col + n - 1] = 0;
        //更新次對角線攻擊範圍
        secondMain[row + col] = 0;

    }

    //將滿足條件的皇后位置放入結果集
    private void addSolution() {
        List<String> solution = new ArrayList<>();
        for (int i = 0; i < n; i++) {
            //記錄當前行皇后位置
            int col = queens[i];
            StringBuilder builder = new StringBuilder();
            for (int j = 0; j < col; j++) builder.append(".");
            builder.append("Q");
            for (int j = 0;j<n-col-1;j++)builder.append(".");
            solution.add(builder.toString());
        }
        res.add(solution);
    }

    //指定位置放置皇后
    private void placeQueen(int row, int col) {
        //存放皇后
        queens[row] = col;
        //更新列攻擊範圍
        rows[col] = 1;
        //更新主對角線攻擊範圍
        main[row - col + n - 1] = 1;
        //更新次對角線攻擊範圍
        secondMain[row + col] = 1;
    }

    //檢驗位置是否會被攻擊
    private boolean isNotUnderAttrack(int row, int col) {
        //對於列,直接判斷列位置是否爲0
        //對於主對角線,row的值恆爲col-n+1,所以將row - col + n - 1=0表示
        //對於次對角線,row+col的值可以表示一條線
        return rows[col] + main[row - col + n - 1] + secondMain[row + col] == 0;
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章