回溯題目

526. 優(yōu)美的排列

image.png

題目給的n的范圍時(shí)1-15所以用回溯效率應(yīng)該是夠的,這邊思路也是很簡(jiǎn)單,主要是用一個(gè)used數(shù)組來(lái)保存每個(gè)數(shù)字在深度遍歷的時(shí)候是否被使用過(guò),然后進(jìn)一步如果是一個(gè)沒(méi)有被使用過(guò)的,然后再通過(guò)valid函數(shù)判斷他是否合法,如果也符合要求那么就加入到path中,然后最后把所有符合條件的path加入到res,返回res的長(zhǎng)度就是答案。

package 劍指0ffer.回溯;

import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;

public class 優(yōu)美的排列 {
    List<List<Integer>> res = new ArrayList<>();
    LinkedList<Integer> path = new LinkedList<>();
    public int countArrangement(int n) {
        boolean used[] = new boolean[n+1];
        bacetrack(n,used);
        System.out.println(res);
        return res.size();
    }
    void bacetrack(int n,boolean[] used){
        if(path.size()==n){
            res.add(new ArrayList<>(path));
        }
        for(int i=1;i<n+1;i++){
            if(used[i]){
                continue;
            }
            if(isValid(i,path.size()+1)){
                path.add(i);
                used[i] = true;
                bacetrack(n,used);
                path.removeLast();
                used[i] = false;
            }
        }
    }
    boolean isValid(int num,int index){
        return num%index==0||index%num==0;
    }

    public static void main(String[] args) {
        優(yōu)美的排列 so = new 優(yōu)美的排列();
        System.out.println(so.countArrangement(3));
    }

}

638. 大禮包

image.png

相信有些人應(yīng)該和我一樣看了答案一開(kāi)始以為答案里面的策略是錯(cuò)的,但是要注意的是這題里面說(shuō)了必須要嚴(yán)格滿足個(gè)數(shù)要求,是不能多買(mǎi)的。所以本題的思路其實(shí)是用need來(lái)做回溯,代碼如下:

class Solution {
    //把這些提出來(lái)是為了回溯的參數(shù)簡(jiǎn)單
    List<Integer> price = new ArrayList<>();
    List<List<Integer>> special = new ArrayList<>();
    Map<List<Integer>,Integer> map = new HashMap<>();
    int n;
    public int shoppingOffers(List<Integer> price, List<List<Integer>> special, List<Integer> needs) {
        this.price = price;
        this.special = special;
        this.n = needs.size();
        return bacetrack(needs);
    }
    int bacetrack(List<Integer> nextneed){
        //加入記憶功能,用map記錄某個(gè)需求的最小值
        if(map.containsKey(nextneed)) return map.get(nextneed);
        //先試用最貴的方法買(mǎi)下需要的物品
        int min = 0;
        for(int i=0;i<nextneed.size();i++){
            min += nextneed.get(i)*price.get(i);
        }
        //每一層遍歷所有的大禮包
        for(int i=0;i<special.size();i++){
            //List<Integer> needtemp = nextneed;不能這么寫(xiě),這樣就是引用了。
            List<Integer> needtemp = new ArrayList<>(nextneed);
            //記錄一下這個(gè)大禮包是否可以用
            boolean flag = true;
            for(int j=0;j<n;j++){
                //如果大禮包給的東西超過(guò)了我要的貨物,就不能用
                if(special.get(i).get(j)>needtemp.get(j)){
                    flag = false;
                }
                needtemp.set(j,needtemp.get(j)-special.get(i).get(j));
            }
            if(!flag){
                continue;
            }
           //計(jì)算用了大禮包之后的最小值變化
            min = Math.min(min,bacetrack(needtemp)+special.get(i).get(n));
        }
        //每次遞歸記錄這個(gè)需求下的最小值
        map.put(nextneed,min);
        return min;
    }
}

698. 劃分為k個(gè)相等的子集

image.png

本題是很有意思的回溯題,推薦參考大佬的思路把題目好好掌握一遍。
鏈接:https://leetcode.cn/problems/partition-to-k-equal-sum-subsets/solution/by-lfool-d9o7/
要注意的點(diǎn)首先是回溯的結(jié)束條件最好設(shè)置成index==nums.length;還有就是因?yàn)槭钦业揭粋€(gè)解就可以返回,所以bacetrack的類(lèi)型位boolean。

class Solution {
    public boolean canPartitionKSubsets(int[] nums, int k) {
        Arrays.sort(nums);
        int l = 0;
        int r= nums.length-1;
        //逆序的數(shù)組計(jì)算效率是更高的
        while (l<r){
            int temp = nums[l];
            nums[l] = nums[r];
            nums[r] = temp;
            l++;
            r--;
        }
        int sum = 0;
        for(int i:nums){
            sum+=i;
        }
        int[] buket = new int[k];
        int target = sum/k;
        if(sum%k!=0){
            return false;
        }
        return bacetrack(nums,k,buket,0,target);

    }
    public boolean bacetrack(int[] nums,int k,int[] bukets,int index,int target){
        if(index==nums.length){
            return true;
            
        }
        for(int i=0;i<k;i++){
            //這是剪枝策略,如果兩個(gè)籃子剩余空間是相同的,同一個(gè)index的求就不要把這兩個(gè)桶都計(jì)算一次了,不符合上一個(gè)桶這次的桶也就直接跳過(guò)了。
            if(i>0&&bukets[i]==bukets[i-1]){
                continue;
            }
            if(bukets[i]+nums[index]>target){
                continue;
            }
            bukets[i] += nums[index];
            if(bacetrack(nums,k,bukets,index+1,target)) return true;
            bukets[i]-=nums[index];
        }
        return false;
    }

}

784. 字母大小寫(xiě)全排列

image.png

本題其實(shí)就是一個(gè)求子集的問(wèn)題,然后額外需要注意的是判斷并轉(zhuǎn)化字母大小寫(xiě)的程序代碼,代碼總體如下:

class Solution {
     List<String> res = new ArrayList<>();
    public List<String> letterCasePermutation(String s) {
        bacetrack(s,0);
        return res;
    }
    void bacetrack(String s,int index){
//        if(index>=s.length()-1){
//            return ;
//        }
        res.add(s);
        for(int i=index;i<s.length();i++){
            if(s.charAt(i)>='0'&&s.charAt(i)<='9'){
                continue;
            }else {
                String s2 = isValid(s,i);
                bacetrack(s2,i+1);
            }
        }
    }
    public String isValid(String s,int index){
        String b = s.substring(0,index)+(Character.isLowerCase(s.charAt(index))?Character.toUpperCase(s.charAt(index)):Character.toLowerCase(s.charAt(index)))+s.substring(index+1);
        return b;
    }
}

將數(shù)組拆分成斐波那契序列

image.png

本題的思路是回溯,也算是回溯中比較經(jīng)典的題型,自己的繪制這個(gè)dfs樹(shù)的時(shí)候會(huì)發(fā)現(xiàn)其實(shí)每一層要做的就是劃分出2,23,234,2345,23456...這樣的類(lèi)似字符串裁剪的序列,不是普通的2,3,4,5這樣單獨(dú)的一個(gè)常量。其實(shí)也沒(méi)多大區(qū)別,唯一要注意的是你多些一個(gè)函數(shù)來(lái)生成從2到單獨(dú)常量的序列,start記錄每一層的起點(diǎn)元素索引,然后i就可以作為結(jié)束元素索引。可以先寫(xiě)出下面這樣的代碼,對(duì)啦這個(gè)題目是找到一個(gè)答案就返回,所以bacetrack返回值類(lèi)型用boolean。

class Solution {
    List<Integer> res = new ArrayList<>();
    public List<Integer> splitIntoFibonacci(String num) {
        char[] numarr = num.toCharArray();
        bacetrack(numarr,0);
        return res;
    }
    boolean bacetrack(char[] num,int start){
        if(start>=num.length&&res.size()>=3){
            return true;
        }
        
        for(int i=start;i<num.length;i++){
            if (num[start] == '0' && i > start) {
                break;
            }
            long sub = isValid(num,start,i+1);
            if (sub > Integer.MAX_VALUE) {
                break;
            }
            int size = res.size();
            //如果截取的數(shù)字大于res中前兩個(gè)數(shù)字的和,說(shuō)明這次截取的太大,直接終止,因?yàn)楹竺嬖浇厝≡酱?            if (size >= 2 && sub > res.get(size - 1) + res.get(size - 2)) {
                break;
            }
            if(res.size()<=1||res.get(res.size()-1)+res.get(res.size()-2)==sub){
                res.add((int)sub);
                if(bacetrack(num,i+1))
                    return true;
                res.remove(res.size()-1);
            }
        }
        return false;

    }
    long isValid(char[]num,int start,int end){
        long res = 0;
        for (int i = start; i < end; i++) {
            res = res * 10 + num[i] - '0';
        }
        return res;
    }
}

1079. 活字印刷

image.png
class Solution {
        LinkedList<Character> path = new LinkedList<>();

     int res = 0;
    public int numTilePossibilities(String tiles) {
        char[] arr = tiles.toCharArray();
        boolean[] used = new boolean[arr.length];
        Arrays.sort(arr);
        bacetrack(arr,used);
        return res;
    }
    void bacetrack(char[] arr,boolean[] used){
        if(!path.isEmpty()){
            res++;
        }
        for(int i=0;i<arr.length;i++){
            //同一個(gè)位置元素用過(guò)不能再用
            if(used[i] == true){
                continue;
            }
            //同一層遍歷到想同值的元素用過(guò)不能再用
            if(i>0&&arr[i-1]==arr[i]&&used[i-1]==false){
                continue;
            }
            used[i] =true;
            path.add(arr[i]);
            bacetrack(arr,used);
            path.removeLast();
            used[i] = false;
        }
    }
}

1219. 黃金礦工

image.png

本題有點(diǎn)像n宮格的島嶼題目,dfs的時(shí)候返回值類(lèi)型定義為int,然后手動(dòng)寫(xiě)一個(gè)方向數(shù)組,每次遍歷的時(shí)候把遍歷過(guò)的點(diǎn)變成0就不需要額外建立一個(gè)visit數(shù)組(也可以用)。

class Solution {
    int res = 0;
    int[][] nums;
    final int[][] dirs = {{0,1},{0,-1},{1,0},{-1,0}};
    public int getMaximumGold(int[][] grid) {
        nums = grid;
        for(int i=0;i<grid.length;i++){
            for(int j=0;j<grid[0].length;j++){
                res = Math.max(res,dfs(i,j));
            }
        }
        return res;
    }
    int dfs(int i,int j){
        if(!isInarea(i,j)||nums[i][j]==0){
            return 0;
        }
        int cur = nums[i][j];
        int max = 0;
        nums[i][j] = 0;
        for(int [] d:dirs){
            max = Math.max(max,dfs(i+d[0],j+d[1]));
        }
        nums[i][j] = cur;
        return cur+max;
    }
    boolean isInarea(int i,int j){
        return (i>=0&&i<nums.length)&&(j>=0&&j<nums[0].length);
    }
}

2375. 根據(jù)模式串構(gòu)造最小數(shù)字

image.png

本題用拓?fù)渑判虻乃悸窌?huì)更好,但是是一道很不錯(cuò)的dfs題目,因?yàn)楸绢}要根據(jù)當(dāng)前的字母是i考慮下一個(gè)層的范圍,也就是下一層的范圍不只是簡(jiǎn)單的i++或者從0開(kāi)始。所以首先遍歷1到9,模擬以1到9開(kāi)頭的每種情況。然后開(kāi)始回溯,回溯的時(shí)候判斷當(dāng)前字母是不是I,如果是I,則i從pre+1繼續(xù),然后結(jié)束就是10,但是如果當(dāng)前字母是D,則i從1開(kāi)始,結(jié)束則是pre。

class Solution {
     private String ans = "";
        private boolean[] vis;
        public void dfs(int index, int pre, StringBuilder sb, char[] pattern){
            if (index == pattern.length){
                // 比較結(jié)果取較小值
                if (ans == "" || ans.compareTo(sb.toString()) > 0) ans = sb.toString();
                return;
            }

            // 根據(jù)pattern的值控制枚舉范圍,'I':(pre, 9], 'D':[1, pre)
            for (int i = (pattern[index] == 'I' ? pre + 1 : 1); i < (pattern[index] == 'I' ? 10 : pre); ++i){
                if (!vis[i]){
                    sb.append(i);
                    vis[i] = true;
                    dfs(index + 1, i, sb, pattern);
                    vis[i] = false;
                    sb.deleteCharAt(sb.length() - 1);
                }
            }
        }

        public String smallestNumber(String pattern) {
            vis = new boolean[10];
            // 枚舉第一個(gè)數(shù)字
            for (int i = 1; i <= 9; ++i){
                vis[i] = true;
                dfs(0, i, new StringBuilder(String.valueOf(i)), pattern.toCharArray());
                vis[i] = false;
            }
            return ans;
        }
}

698. 劃分為k個(gè)相等的子集

image.png

本題回溯的思路就不是一般的情況,一般下每層都是把nums里面的元素,現(xiàn)在則是根據(jù)k來(lái)確定桶的數(shù)量,然后每層是遍歷桶的數(shù)量放入元素。

class Solution {
    public boolean canPartitionKSubsets(int[] nums, int k) {
        Arrays.sort(nums);
        int l = 0;
        int r= nums.length-1;
        while (l<r){
            int temp = nums[l];
            nums[l] = nums[r];
            nums[r] = temp;
            l++;
            r--;
        }
        int sum = 0;
        for(int i:nums){
            sum+=i;
        }
        int[] buket = new int[k];
        int target = sum/k;
        if(sum%k!=0){
            return false;
        }
        return bacetrack(nums,k,buket,0,target);

    }
    public boolean bacetrack(int[] nums,int k,int[] bukets,int index,int target){
        if(index==nums.length){
            return true;
            
        }
        for(int i=0;i<k;i++){
            if(i>0&&bukets[i]==bukets[i-1]){
                continue;
            }
            if(bukets[i]+nums[index]>target){
                continue;
            }
            bukets[i] += nums[index];
            if(bacetrack(nums,k,bukets,index+1,target)) return true;
            bukets[i]-=nums[index];
        }
        return false;
    }

}

2305. 公平分發(fā)餅干](https://leetcode.cn/problems/fair-distribution-of-cookies/)

image.png

本題雖然不是dfs最優(yōu)解,但是考驗(yàn)了剪枝的想法。大家都可以嘗試一下?。?!

class Solution {
    
    int ans = Integer.MAX_VALUE;
    int[] cookies;
    int n;
    int k;
    
    public int distributeCookies(int[] cookies, int k) {
        
        //技巧:先發(fā)餅干較多的包,這樣讓回溯過(guò)程更快。下面的回溯代碼是從最后一個(gè)餅干包開(kāi)始發(fā)所以這里是從小到大排序
        Arrays.sort(cookies);
        
        this.cookies = cookies;
        n = cookies.length;
        this.k = k;
        
        //啟動(dòng)回溯
        backtrack(new int[k], n-1);
        
        return ans;
    }
    
    //bucket數(shù)組存放k個(gè)小朋友每個(gè)人當(dāng)前的餅干數(shù)量,start為下一個(gè)要分發(fā)的餅干包下標(biāo)
    public void backtrack(int[] bucket, int start){
        
        //餅干發(fā)完了,統(tǒng)計(jì)哪個(gè)小朋友獲得的餅干最多,更新答案。
        if (start < 0){
            int curAns = Integer.MIN_VALUE;
            for (int count : bucket){
                curAns = Math.max(curAns, count);
            }
            ans = Math.min(ans, curAns);
            return;
        }
        
        //剪枝1:如果剩余的餅干包不夠空手的小朋友分了,直接返回。
        int zeroCount = 0;
        for (int count : bucket){
            if (count == 0) zeroCount++;
        }
        if (zeroCount > start + 1) return;
        
        //剪枝2:如果某位小朋友的餅干數(shù)量比當(dāng)前的答案還多,顯然繼續(xù)回溯下去也無(wú)法成為最優(yōu)答案,直接返回。
        for (int i = 0; i < k; i++){
            if (bucket[i] > ans) return;
        }
        
        for (int i = 0; i < k; i++){
            //剪枝3:第一個(gè)零食包不管給哪個(gè)小朋友,所開(kāi)啟的回溯樹(shù)都一樣,只要給一個(gè)小朋友就行了,這樣的回溯樹(shù)一下子就少了很多。
            if (start == n - 1 && i > 0) return;
            
            //標(biāo)準(zhǔn)回溯代碼
            bucket[i] += cookies[start];
            backtrack(bucket, start - 1);
            bucket[i] -= cookies[start];
        }
        return;
    }
}

2002. 兩個(gè)回文子序列長(zhǎng)度的最大乘積

image.png

本題可能一看不會(huì)想回溯,不過(guò)確實(shí)看到了一個(gè)大神的思路,所有字母有三個(gè)選擇,加入第一個(gè)子序列,加入第二個(gè)子序列,或者都不加入任何序列。

class Solution {
    int ans = 0;

    public int maxProduct(String s) {
        StringBuilder sb1 = new StringBuilder();
        StringBuilder sb2 = new StringBuilder();
        dfs(s, sb1, sb2, 0);
        return ans;
    }

    public void dfs(String s, StringBuilder sb1, StringBuilder sb2, int index) {
        if(check(sb1) && check(sb2)) {
            ans = Math.max(ans, sb1.length() * sb2.length());
        }

        if(index == s.length()) return;

        dfs(s, sb1.append(s.charAt(index)), sb2, index + 1); // 子序列s1 使用
        sb1.setLength(sb1.length() - 1);

        dfs(s, sb1, sb2.append(s.charAt(index)), index + 1); // 子序列 s2 使用
        sb2.setLength(sb2.length() - 1);

        dfs(s, sb1, sb2, index + 1); // 子序列 sb1 和 sb2 者均不使用
    }

    public boolean check(StringBuilder sb1) {  // 檢查是否為回文字符串
        int left = 0, right = sb1.length() - 1;
        while(left < right) {
            if(sb1.charAt(left) == sb1.charAt(right)) {
                left++;
                right--;
            } else {
                return false;
            }
        }
        return true;
    }
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容