數據結構七:遞歸+動規+分治+回溯

Datawhale 系列數據結構

本文參考鏈接:
01揹包問題:https://blog.csdn.net/chanmufeng/article/details/82955730

Task7.1 遞歸

7.1.1爬樓梯

//爬樓梯:
//假設你正在爬樓梯。需要 n 階你才能到達樓頂
//每次你可以爬 1 或 2 個臺階。你有多少種不同的方法可以爬到樓頂呢?
class Solution {
    public int climbStairs(int n) {
        int [] ways = new int[n+1];
        ways[0] = 0;
        for (int i = 1;i<ways.length;i++){
            if (i < 3 ){
                ways[i] = i;
            }else {
                ways[i] = ways[i-1] + ways[i-2];
            }
        }
        return ways[n];
    }
}
//使用最小花費爬樓梯
//數組的每個索引做爲一個階梯,第 i個階梯對應着一個非負數的體力花費值 cost[i](索引從0開始)。
//每當你爬上一個階梯你都要花費對應的體力花費值,然後你可以選擇繼續爬一個階梯或者爬兩個階梯。
//您需要找到達到樓層頂部的最低花費。在開始時,你可以選擇從索引爲 0 或 1 的元素作爲初始階梯。
class Solution {
    public int minCostClimbingStairs(int[] cost) {
       int length = cost.length;
        int[] newCost = new int[length+1];
        newCost = Arrays.copyOf(cost,length+1);
        length = length+1;
        cost = newCost;
        int[] fn = new int[length];
        fn[0] = 0;
        fn[1] = 0;
        fn[2] = Math.min(cost[0],cost[1]);
        for (int i = 3;i<length;i++){
            fn[i] = Math.min(fn[i-1] + cost[i-1],fn[i-2]+cost[i-2]);
        }
        return fn[length-1]; 
    }
}

7.1.2 0-1揹包問題(遞歸方法解決)

問題描述:
給定 n 種物品重量$ w_1,w_2,w_3,...,w_n $,價值爲$v_1,v_2,v_3,...,v_4$,一個容量爲$C$的揹包,問:應該如何選擇裝入揹包的物品,使得裝入揹包中的物品的總價值最大?
遞歸思想:
首先我們用遞歸的方式來嘗試解決這個問題
我們用$F(n,C)$ 表示將前$n$個物品放進容量爲$C$的揹包裏,得到最大的價值。
我們用自頂向下的角度來看,假如我們已經進行到了最後一步(即求解將$n$個物品放到揹包了獲得最大價值),此時我們便有兩種選擇:

1.不放第$n$個物品,此時總價值爲$F(n-1,C)$
2.放置第$n$個物品,此時總價值爲$v_n+F(n-1,C-w_n)$

兩種選擇中總價值最大的方案就是我們的最終方案,遞推式如下:

$F(i,C)=max(F(i-1,C),v(i)+F(i-1,C-w(i)))$
//遞歸解法
public static int solveKS(int[] w,int[] v,int index,int capacity ){
        if(index<0 || capacity <=0) return 0;
        
        int res = solveKS(w,v,index-1,capacity);
        if(w[index]<=capacity){
            res=Math.max(res, v[index]+solveKS(w,v,index-1,capacity-w[index]));
        }
        return res;
    }
    
    public static int knapSack(int [] w,int [] v,int C){
        int size=w.length;
        return solveKS(w,v,size-1,C);
    }

Task 7.2 回溯

7.2.1八皇后問題

public static int [][] array=new int[8][8];
    public static int map=0;
    
    public static void main(String[] args) {
        System.out.println("八皇后問題");
        findQueen(0);
        System.out.println("八皇后問題共有:"+map+"種可能");
    }
    
    public static void findQueen(int i){
        if(i>7){
            map++;
            print();//
            return;
        }
        for(int m=0;m<8;m++){
            if(check(i,m)){
                array[i][m]=1;
                findQueen(i+1);
                array[i][m]=0;
            }
        }
    }
    public static boolean check(int k, int j){
        for(int i=0;i<8;i++){//檢查行列衝突
            if(array[i][j]==1){
                return false;
            }
        }
        for(int i=k-1,m=j-1;i>=0&& m>=0;i--,m--){
            if(array[i][m]==1){//檢查左對角線衝突
                return false;
            }
        }
        for(int i=k-1,m=j+1;i>=0&&m<=7;i--,m++){
            if(array[i][m]==1){
                return false;
            }
        }
        return true;
    }
    public static void print(){
        System.out.print("方案"+map+":"+"\n");
        for(int i=0;i<8;i++){
            for(int m=0;m<8;m++){
                if(array[i][m]==1){  
                    //System.out.print("皇后"+(i+1)+"在第"+i+"行,第"+m+"列\t");
                    System.out.print("o ");
                }
                else{
                        System.out.print("+ ");
                }
            }
            System.out.println();
        }
        System.out.println();
    }

7.2.2求解0-1揹包問題(回溯方法解決)

整體思路:

用回溯法需要構造子集樹。對於每一個物品i,對於該物品只有選與不選2個決策,總共有n個物品,可以順序依次考慮每個物品,這樣就形成了一顆空間樹:基本思想就是遍歷這棵樹,以枚舉所有情況,最後進行判斷,如果重量不超過揹包容量,且價值最大的話,該方案就是最後的答案

算法設計:

利用回溯設計一個算法求出0-1揹包問題的解,也就是求出一個解向量xi(即對n個物品放或不妨的一種的方案)
其中,(xi=0或1,xi=0表示物體i不放入揹包,xi=1表示把物體i放入揹包)。
在遞歸函數Backtrack中,
    當i>n時,算法搜索到葉子節點,得到一個新的物品包裝方案。此時算法適時更新當前的最有價值。
    當i<n時,當前擴展結點位於排列樹的第(i-1)層,此時算法選擇下一個要安排的物品,以深度優先方式遞歸的對相應的子樹進行搜索,對不滿足上界約束的結點,則剪去相應的子樹
//回溯
public class KnapSack02 {
    private static int n=3;//物品數量編號,從0開始
    private static double c=5;//揹包容量
    private static double [] v={12,10,20,15};//各個物品的價值
    private static double [] w={2,1,3,2};//各個物品的重量
    private static double cw = 0.0;//當前揹包重量 current weight
    private static double cp = 0.0;//當前揹包中物品總價值 current value
    private static double bestp = 0.0;//當前最優價值best price
    private static double [] perp = new double [4];//單位物品價值(排序後) per price
    private static int [] order = new int [4];//物品編號
    private static int [] put = new int [4];//設置是否裝入,1裝入,0不裝

    //按單位價值排序
    public static  void knapsack(){
        int i,j;
        int temporder = 0;
        double temp= 0.0;
        
        for(i=1;i<=n;i++)
            perp[i]=v[i]/w[i];
          for(i=1;i<=n-1;i++)
            {
                for(j=i+1;j<=n;j++)
                    if(perp[i]<perp[j])//冒泡排序perp[],order[],sortv[],sortw[]
                {
                    temp = perp[i];  //冒泡對perp[]排序
                    perp[i]=perp[i];
                    perp[j]=temp;
         
                    temporder=order[i];//冒泡對order[]排序
                    order[i]=order[j];
                    order[j]=temporder;
         
                    temp = v[i];//冒泡對v[]排序
                    v[i]=v[j];
                    v[j]=temp;
         
                    temp=w[i];//冒泡對w[]排序
                    w[i]=w[j];
                    w[j]=temp;
                }
            }
    }
    
    public static void backtrack(int i){
        //i表示到達的層數(第幾步,從0開始),同事也只是當前選擇玩了幾個物品 
        bound( i);
        if(i>n){
            bestp=cp;
            return;
        }
         //如若左子節點可行,則直接搜索左子樹;
        //對於右子樹,先計算上界函數,以判斷是否將其減去
        if(cw+w[i]<=c)//將物品i放入揹包,搜索左子樹
        {
            cw+=w[i];//同步更新當前揹包的重量
            cp+=v[i];//同步更新當前揹包的總價值
            put[i]=1;
            backtrack(i+1);//深度搜索進入下一層
            cw-=w[i];//回溯復原
            cp-=v[i];//回溯復原
        }
        if(bound(i+1)>bestp)//如若符合條件則搜索右子樹
            backtrack(i+1);
    }
    
    //計算上界函數,功能爲剪枝
    public static double bound(int i)
    {   //判斷當前揹包的總價值cp+剩餘容量可容納的最大價值<=當前最優價值
        double leftw= c-cw;//剩餘揹包容量
        double b = cp;//記錄當前揹包的總價值cp,最後求上界
        //以物品單位重量價值遞減次序裝入物品
        while(i<=n && w[i]<=leftw)
        {
            leftw-=w[i];
            b+=v[i];
            i++;
        }
        //裝滿揹包
        if(i<=n)
            b+=v[i]/w[i]*leftw;
        return b;//返回計算出的上界
     
    }
    
    public static void main(String[] args) {
         knapsack();
         backtrack(1);
         System.out.println(bestp);
    }
    
}

Task7.3 分治

7.3.1 利用分治算法求一組數據的逆序對個數

public class ReverseOrder {
    private static int sum = 0;
    private static int []a ={5,4,2,6,3,1};
    private static int []b =new int [6]; 
    
    public static void worksort(int l,int r){
        int mid,tmp,i,j;
        if(r>l+1){
            mid=(l+r)/2;
            worksort(l,mid-1);
            worksort(mid,r);
            tmp=l;
            for(i=l,j=mid;i<=mid-1 && j<=r;){
                if(a[i]>a[j])
                {
                    b[tmp++]=a[j++];//快速排序 
                    sum+=mid-i;//統計逆序對個數 
                }
                else
                   b[tmp++]=a[i++];
            }
             if(j<=r)
                  for(;j<=r;j++)  b[tmp++]=a[j];
             else
                  for(;i<=mid-1;i++)   b[tmp++]=a[i];
             for(i=l;i<=r;i++)  a[i]=b[i];//將排好序的b數組賦值給a數組
        }else{
            if(l+1==r)//遞歸的邊界 
                  if(a[l]>a[r])
                  { int temp = a[l];
                      a[l]=a[r];
                      a[r]=temp;
                      sum++;
                  }
        }
    }
    public static void main(String[] args) {
        worksort(0,5);
        System.out.println(sum);
    }
}

Task 7.4 動態規劃

7.4.1 0-1揹包問題

//動態規劃
int size = w.length;
        if (size == 0) {
            return 0;
        }

        int[] dp = new int[C + 1];
        //初始化第一行
        //僅考慮容量爲C的揹包放第0個物品的情況
        for (int i = 0; i <= C; i++) {
            dp[i] = w[0] <= i ? v[0] : 0;
        }

        for (int i = 1; i < size; i++) {
            for (int j = C; j >= w[i]; j--) {
                dp[j] = Math.max(dp[j], v[i] + dp[j - w[i]]);
            }
        }
        return dp[C];
    }

    public static void main(String[] args) {
        int[] w = {2, 1, 3, 2};
        int[] v = {12, 10, 20, 15};
        System.out.println(knapSack(w, v, 5));
    }

7.4.2 編程實現萊溫斯坦最短編輯距離

public static int minEditDistance(String dest,String src){
        int [][] f=new int[dest.length()+1][src.length()+1];
        f[0][0]=0;
        for(int i=1;i<dest.length()+1;i++){
            f[i][0]=i;
        }
        for(int i=1;i<src.length()+1;i++){
            f[0][i]=i;
        }
        for(int i=1;i<dest.length()+1;i++){
            for(int j=1;j<src.length()+1;j++){
                int cost=0;
                if(dest.charAt(i - 1) != src.charAt(j - 1)){
                    cost=1;
                }
                int minCost;
                if(f[i-1][j]<f[i][j-1]){
                    minCost=f[i-1][j]+cost;
                }else{
                    minCost=f[i][j-1]+cost;
                }
                f[i][j]=minCost;
            }    
        }
        return f[dest.length()][src.length()];
    }
    public static void main(String[] args) {
        
        System.out.println(minEditDistance("sot", "stop"));
    }

7.4.3 編程實現查找兩個字符串的最長子序列

//求解str1 和 str2 的最長公共子序列
    public static int LCS(String str1, String str2){
        int[][] c = new int[str1.length() + 1][str2.length() + 1];
        for(int row = 0; row <= str1.length(); row++)
            c[row][0] = 0;
        for(int column = 0; column <= str2.length(); column++)
            c[0][column] = 0;
        
        for(int i = 1; i <= str1.length(); i++)
            for(int j = 1; j <= str2.length(); j++)
            {
                if(str1.charAt(i-1) == str2.charAt(j-1))
                    c[i][j] = c[i-1][j-1] + 1;
                else if(c[i][j-1] > c[i-1][j])
                    c[i][j] = c[i][j-1];
                else
                    c[i][j] = c[i-1][j];
            }
        return c[str1.length()][str2.length()];
    }
    
    //test
    public static void main(String[] args) {
        String str1 = "BDCABA";
        String str2 = "ABCBDAB";
        int result = LCS(str1, str2);
        System.out.println(result);
    }

7.4.4編程實現一個數據序列的最長遞增子序列

class Solution {
    public int lengthOfLIS(int[] nums) {
        /**
        dp[i]: 所有長度爲i+1的遞增子序列中, 最小的那個序列尾數.
        由定義知dp數組必然是一個遞增數組, 可以用 maxL 來表示最長遞增子序列的長度. 
        對數組進行迭代, 依次判斷每個數num將其插入dp數組相應的位置:
        1. num > dp[maxL], 表示num比所有已知遞增序列的尾數都大, 將num添加入dp
           數組尾部, 並將最長遞增序列長度maxL加1
        2. dp[i-1] < num <= dp[i], 只更新相應的dp[i]
        **/
        int maxL = 0;
        int[] dp = new int[nums.length];
        for(int num : nums) {
            // 二分法查找, 也可以調用庫函數如binary_search
            int lo = 0, hi = maxL;
            while(lo < hi) {
                int mid = lo+(hi-lo)/2;
                if(dp[mid] < num)
                    lo = mid+1;
                else
                    hi = mid;
            }
            dp[lo] = num;
            if(lo == maxL)
                maxL++;
        }
        return maxL;
    }
}

Task 7.5 練習

7.5.1 實戰遞歸

//Letter Combinations of a Phone Number(17)
String[] button=new String[]{"","","abc","def","ghi","jkl","mno","pqrs","tuv","wxyz"};

    List<String> list=new ArrayList<>();
    public List<String> letterCombinations(String digits) {
        if (digits==null||digits.length()==0)
            return list;
        letterCombinations(digits,0,new String());
        return list;
    }
    public void  letterCombinations(String digits,int index,String temp) {
       if(index==digits.length()){
            list.add(temp);
            return;
        }
        int position=digits.charAt(index)-'0';
        String str=button[position];
        for (int i=0;i<str.length();i++){
            letterCombinations(digits,index+1,temp+str.charAt(i));
        }
    }

//permutations(46)
 List<List<Integer>> res = new ArrayList<>();
    public List<List<Integer>> permute(int[] nums) {
        permutation(nums, 0, nums.length - 1);
        return res;
    }
    
    private void permutation(int[] nums, int p, int q) {
        if (p == q) {
            res.add(arrayToList(nums));
        }
        for (int i = p; i <= q; i++) {
            swap(nums, i, p);
            permutation(nums, p + 1, q);
            swap(nums, i, p);  // 這裏要交換回來,免得出現重複的情況
        }
    }
    
    private List<Integer> arrayToList(int[] nums) {
        List<Integer> res = new ArrayList<>();
        for (int i = 0; i < nums.length; i++) {
            res.add(nums[i]);
        }
        return res;
    }
    
    private void swap(int[] nums, int i, int j) {
        int temp = nums[i];
        nums[i] = nums[j];
        nums[j] = temp;
    }

7.5.2 實戰dp

//完成0-1揹包問題實現(自我實現)及Leetcode
參考7.4.1
//Palindrome Partitioning II(132)
 public int minCut(String s) {
        if(s == null || s.isEmpty()){
            return 0;
        }
        int len = s.length();
        boolean[][] dp = new boolean[s.length()][s.length()];
        int[] cut = new int[s.length()];

        for(int i = 0; i < len; i++){
            //最大劃分就是i次
            cut[i]= i;
            for(int j = 0; j <= i; j++){
                if(s.charAt(i) == s.charAt(j) &&(i-j <= 1 || dp[j+1][i-1])){
                    dp[j][i] = true;
                    if(j == 0) {
                        //0-i直接是迴文
                        cut[i] = 0;
                    } else {
                        cut[i] = Math.min(cut[j-1]+1, cut[i]);
                    }
                }
            }
        }
        return cut[len-1];
    }

7.5.2 可選練習

//Regular Expression Matching(正則表達式匹配)
class Solution {
    public boolean isMatch(String text, String pattern) {
        if (pattern.isEmpty()) return text.isEmpty();
        boolean first_match = (!text.isEmpty() &&
                               (pattern.charAt(0) == text.charAt(0) || pattern.charAt(0) == '.'));

        if (pattern.length() >= 2 && pattern.charAt(1) == '*'){
            return (isMatch(text, pattern.substring(2)) ||
                    (first_match && isMatch(text.substring(1), pattern)));
        } else {
            return first_match && isMatch(text.substring(1), pattern.substring(1));
        }
    }
}
//Minimum Path Sum(最小路徑和)
public int minPathSum(int[][] grid) {
        int rows=grid.length;
        int cols=grid[0].length;
        
        int [] dp=new int[cols];
        dp[0]=grid[0][0];
        
        for(int col=1;col<cols;col++){
            dp[col]=dp[col-1]+grid[0][col];
        }
        for(int row = 1; row < rows; row++) {
            for(int col = 0; col < cols; col++) {
                if(col > 0){
                    dp[col] = Math.min(dp[col-1], dp[col]) + grid[row][col];
                }else{
                    dp[col] += grid[row][col];
                }
            }
        }
         return dp[cols - 1];
    }
//Coin Change (零錢兌換)[作爲可選]
//Best Time to Buy and Sell Stock(買賣股票的最佳時機)[作爲可選]
//Maximum Product Subarray(乘積最大子序列)[作爲可選]
//Triangle(三角形最小路徑和)[作爲可選]
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章