• 算法题解之动态规划


    Arithmetic Slices

    算术片的个数

    思路(最优解):序列型dp。每次记录一下以当前数为末尾的算术片的最大长度以及数的等差值。下一次就能求出算术片增加的个数。使用滚动指针来优化。时间复杂度O(n),空间复杂度O(1)。

     1 public class Solution {
     2     public int numberOfArithmeticSlices(int[] A) {
     3         if (A.length < 3) {
     4             return 0;
     5         }
     6         
     7        int prev_res = 0;
     8        int prev_length = 2;
     9        int prev_diff = A[1] - A[0];
    10        
    11        for (int i = 2; i < A.length; i++) {
    12            if (prev_diff == A[i] - A[i - 1]) {
    13                prev_res += prev_length - 1;
    14                prev_length++;
    15            } else {
    16                prev_length = 2;
    17                prev_diff = A[i] - A[i - 1];
    18            }
    19        }
    20        return prev_res;
    21     }
    22 }
    View Code

    Climbing Stairs

    爬楼梯

    思路:滚动指针优化到O(1)空间。开一个大小为2的数组,用模2方式来求解。

     1 public class Solution {
     2     public int climbStairs(int n) {
     3         if (n == 1) {
     4             return 1;
     5         }
     6         int[] dp = new int[2];
     7         dp[0] = 1;
     8         dp[1] = 2;
     9         
    10         for (int i = 2; i < n; i++) {
    11             dp[i % 2] = dp[(i - 1) % 2] + dp[(i - 2) % 2];
    12         }
    13         return dp[(n - 1) % 2];
    14     }
    15 }
    View Code

    Drop Eggs II

    扔鸡蛋II

    思路:状态:设一种丢法在最坏情况下需要丢p次能确定结果,状态dp[i][j]表示i个鸡蛋和j层楼的所有丢法的最小p值。

       方程:对于i个蛋,j层楼,要确定它的所有丢法的最小p值。先假定第一次在第k层丢(1<=k<=j),如果蛋碎了,那么我们只需要用剩下的鸡蛋测试k层以下的楼层,所以问题简化为k-1层和i-1个鸡蛋,因此对应的次数是dp[i-1][k-1]+1;如果蛋不碎,那么我们只需要检查比k较高的楼层; 所以问题简化为 j-k 和i个鸡蛋,即dp[i][j-k]+1。最坏情况应该是两者的最大值。而所有k(1~j)对应的次数的最小值就是最小p值。

       初始化:对于一个蛋的问题,p值是楼层数j。即dp[1][j] = j.

       结果:dp[m][n]

    public class Solution {
        /**
         * @param m the number of eggs
         * @param n the umber of floors
         * @return the number of drops in the worst case
         */
        public int dropEggs2(int m, int n) {
            // Write your code here
            int[][] dp = new int[m+1][n+1];
            
            for (int j = 1; j <= n; j++) {
                dp[1][j] = j;
            }
            
            for (int i = 2; i <= m; i++) {
                for (int j = 1; j <= n; j++) {
                    dp[i][j] = Integer.MAX_VALUE; 
                    for (int first = 1; first <= j; first++) {
                        dp[i][j] = Math.min(Math.max(dp[i-1][first - 1] + 1, dp[i][j - first] + 1),
                        dp[i][j]);
                    }
                }
            }
            
            return dp[m][n];
        }
    }
    View Code

    Is Subsequence

    判断是否是子序列

    思路1:典型的双序列动态规划。 状态dp[i][j]表示s的前i个字符是否是t的前j个字符的子序列。时间复杂度O(s*t),空间复杂度O(s*t),使用滚动数组可以讲空间优化到O(t)。

     1 public class Solution {
     2     public boolean isSubsequence(String s, String t) {
     3         boolean[][] dp = new boolean[s.length() + 1][t.length() + 1];
     4         
     5         for (int j = 0; j <= t.length(); j++) {
     6             dp[0][j] = true;
     7         }
     8         
     9         for (int i = 1; i <= s.length(); i++) {
    10             for (int j = 1; j <= t.length(); j++) {
    11                 if (dp[i][j - 1]) {
    12                     dp[i][j] = true;
    13                 } else {
    14                     dp[i][j] = dp[i - 1][j - 1] && s.charAt(i - 1) == t.charAt(j - 1);
    15                 }
    16             }
    17         }
    18         return dp[s.length()][t.length()];
    19     }
    20 }
    View Code

    思路2:使用two pointers。时间复杂度O(t),空间复杂度O(1)。

     1 public class Solution {
     2     public boolean isSubsequence(String s, String t) {
     3         if (s.length() == 0) return true;
     4         int indexS = 0, indexT = 0;
     5         while (indexT < t.length()) {
     6             if (t.charAt(indexT) == s.charAt(indexS)) {
     7                 indexS++;
     8                 if (indexS == s.length()) return true;
     9             }
    10             indexT++;
    11         }
    12         return false;
    13     }
    14 }
    View Code 

    思路3(最优解):改进的two pointers,使用indexOf函数,时间复杂度仍然是O(t)。但是思路2调用了t次charAt函数,而这个方法只调用了s次函数。虽然时间复杂度相同,但是实际运行起来更快。

     1 public class Solution {
     2     public boolean isSubsequence(String s, String t) {
     3         if (s.length() == 0) return true;
     4         int pt = 0;
     5         for (int ps = 0; ps < s.length(); ps++) {
     6             char c = s.charAt(ps);
     7             pt = t.indexOf(c, pt);
     8             if (pt == -1) {
     9                 return false;
    10             }
    11             pt++;
    12         }
    13         return true;
    14     }
    15 }
    View Code

    k Sum

    k数和

    思路:一道三维动态规划,状态和方程都比较容易想,初始化要想一下。

    public class Solution {
        /**
         * @param A: an integer array.
         * @param k: a positive integer (k <= length(A))
         * @param target: a integer
         * @return an integer
         */
        public int kSum(int A[], int k, int target) {
            // write your code here
            int[][][] dp = new int[A.length + 1][k + 1][target + 1];
            for (int i = 1; i < A.length; i++) {
                for (int m = 1; m < target; m++) {
                    if (A[i - 1] == m) {
                        dp[i][1][m] = 1;
                    } else {
                        dp[i][1][m] = dp[i - 1][1][m];
                    }
                }
            }
            
            for (int i = 2; i <= A.length; i++) {
                for (int j = 2; j <= Math.min(i, k); j++) {
                    for (int m = 1; m <= target; m++) {
                        if (A[i - 1] < m) {
                            dp[i][j][m] = dp[i - 1][j - 1][m - A[i - 1]];
                        }
                        dp[i][j][m] += dp[i - 1][j][m];
                    }
                }
            }
            return dp[A.length][k][target];
        }
    }
    View Code

     

    Longest Increasing Subsequence

    最长递增子序列

    思路1:LIS是一道经典的dp题。用O(n^2)来做是一道简单的序列型动态规划。

     1 public class Solution {
     2     public int lengthOfLIS(int[] nums) {
     3         if (nums == null || nums.length == 0) {
     4             return 0;
     5         }
     6         
     7         int[] dp = new int[nums.length];
     8         dp[0] = 1;
     9         for (int i = 1; i < dp.length; i++) {
    10             dp[i] = 1;
    11             for (int j = 0; j <= i - 1; j++) {
    12                 if (nums[j] < nums[i]) {
    13                     dp[i] = Math.max(dp[i], 1 + dp[j]);
    14                 }
    15             }
    16         }
    17         int res = 0;
    18         for (int i = 0; i < dp.length; i++) {
    19             res = Math.max(res, dp[i]);
    20         }
    21         return res;
    22     }
    23 }
    View Code

    思路2(最优解):这道题还存在O(nlgn)的解法。在思路1中,每次我们计算dp[i]的时候,都要往前面遍历找到一个nums[j],使得nums[j] < nums[i]并且dp[j]值最大。在这个过程中其实有些数我们不用找。比如说长度为2的子序列有(3,5),(3,10),(2,4),那么我们只需要与4比较就可以,而不需要与5和10比较。因此我们定义一个数组B,其中B[i]表示长度为i的所有子序列中最小的末尾数。可以证明B是一个单调递增的数组。因此每次我们只需要使用二分查找找到小于nums[i]的最大值,返回它的长度i即可。

     1 public class Solution {
     2     public int lengthOfLIS(int[] nums) {
     3         if (nums == null || nums.length == 0) {
     4             return 0;
     5         }
     6         
     7         int res = 0;
     8         int[] B = new int[nums.length + 1];
     9         for (int i = 0; i < B.length; i++) {
    10             B[i] = Integer.MAX_VALUE;
    11         }
    12         
    13         for (int i = 0; i < nums.length; i++) {
    14             int len = binarySearch(B, nums[i]) + 1;
    15             res = Math.max(res, len);
    16             B[len] = Math.min(B[len], nums[i]);
    17         }
    18         
    19         return res;
    20     }
    21     
    22     public int binarySearch(int[] B, int k) {
    23         int start = 0;
    24         int end = B.length - 1;
    25         while (start + 1 < end) {
    26             int mid = (start + end) / 2;
    27             if (B[mid] >= k) {
    28                 end = mid;
    29             } else {
    30                 start = mid;
    31             }
    32         }
    33         if (B[end] < k) {
    34             return end;
    35         }
    36         if (B[start] < k) {
    37             return start;
    38         }
    39         return 0;
    40     }
    41 }
    View Code

    Minimum Adjustment Cost

    最小调整代价

    思路:状态:dp[i][j]表示对0~i的数做调整,最后一个数调整到数j的最小代价。

       方程:dp[i][j] = min(dp[i-1][m]) + cost(i,j),即序列0~i-1做调整,最后一个数调整到m的代价的最小值,加上第i个数调整到j的代价。为保证满足条件,m的范围是(j - taget ~ j + target)。

       初始化:对于dp[0][j],就是与j的差值。

         结果:dp矩阵最后一行的最小值。

    public class Solution {
        /**
         * @param A: An integer array.
         * @param target: An integer.
         */
        public int MinAdjustmentCost(ArrayList<Integer> A, int target) {
            // write your code here
            
            int[][] dp = new int[A.size()][101];
            for (int j = 1; j < 101; j++) {
                dp[0][j] = Math.abs(j - A.get(0));
            }
            
            for (int i = 1; i < A.size(); i++) {
                for (int j = 1; j < 101; j++) {
                    int cost = Math.abs(j - A.get(i));
                    
                    int min = Math.max(1, j - target);
                    int max = Math.min(100, j + target);
                    
                    int pre_min_cost = Integer.MAX_VALUE;
                    for (int m = min; m <= max; m++) {
                        pre_min_cost = Math.min(dp[i-1][m], pre_min_cost);
                    }
                    dp[i][j] = pre_min_cost + cost;
                }
            }
            
            int res = Integer.MAX_VALUE;
            for (int j = 1; j < 101; j++) {
                res = Math.min(dp[A.size() - 1][j], res);
            }
            return res;
        }
    }
    View Code

    Partition Equal Subset Sum

    和相同的二划分

    思路1:每个划分的和应该是总和的一半,问题转化为01背包问题。状态dp[i][j]表示前j个数能否恰好装满容量为j的背包。

     1 public class Solution {
     2     public boolean canPartition(int[] nums) {
     3         int sum = 0;
     4         for (int num : nums) {
     5             sum += num;
     6         }
     7         if (sum % 2 != 0) {
     8             return false;
     9         }
    10         
    11         //转化为01背包问题
    12         int capacity = sum / 2;
    13         boolean[][] dp = new boolean[capacity + 1][nums.length + 1];
    14         
    15         dp[0][0] = true;
    16         for (int i = 0; i <= capacity; i++) {
    17             for (int j = 1; j <= nums.length; j++) {
    18                 if (nums[j - 1] <= i) {
    19                     dp[i][j] = dp[i - nums[j - 1]][j - 1] || dp[i][j - 1];
    20                 } else {
    21                     dp[i][j] = dp[i][j - 1];
    22                 }
    23             }
    24         }
    25         
    26         return dp[capacity][nums.length];
    27         
    28     }
    29 }    
    View Code

    思路2(最优解):将思路1用滚动数组优化,这里在循环时要将背包容量i改为从大到小循环(见01背包问题的空间优化)。

     1 public class Solution {
     2     public boolean canPartition(int[] nums) {
     3         int sum = 0;
     4         for (int num : nums) {
     5             sum += num;
     6         }
     7         if (sum % 2 != 0) {
     8             return false;
     9         }
    10         
    11         int capacity = sum / 2;
    12         boolean[] dp = new boolean[capacity + 1];
    13         
    14         dp[0] = true;
    15         for (int j = 1; j <= nums.length; j++) {
    16             for (int i = capacity; i >= 0; i--) {
    17                 if (nums[j - 1] <= i) {
    18                     dp[i] = dp[i - nums[j - 1]] || dp[i];
    19                 } 
    20             }
    21         }
    22         
    23         return dp[capacity];
    24         
    25     }
    26 }
    View Code

    Regular Expression Matching 

    正则表达式匹配

    思路:是一道双序列型动态规划,注意方程与通配符匹配的不同之处。

     1 public class Solution {
     2     public boolean isMatch(String s, String p) {
     3         int m = s.length(), n = p.length();
     4         char[] ws = s.toCharArray();
     5         char[] wp = p.toCharArray();
     6         boolean[][] dp = new boolean[m+1][n+1];
     7         
     8         dp[0][0] = true;
     9         if (m > 0 && n > 0) {
    10             dp[1][1] = wp[0] == '.' || wp[0] == ws[0];
    11         }
    12         
    13         for (int j = 1; j <= n; j++) {
    14             if (wp[j - 1] == '*') {
    15                 dp[0][j] = j == 1 ? true : dp[0][j - 2] || dp[0][j - 1];
    16             } 
    17         }
    18         for (int i = 1; i <= m; i++) {
    19             dp[i][0] = false;
    20         }
    21         
    22         for (int i = 1; i <= m; i++) {
    23             for (int j = 2; j <= n; j++) {
    24                 if (wp[j-1] == '.' || ws[i-1] == wp[j-1]) {
    25                     dp[i][j] = dp[i-1][j-1];
    26                 } else if (wp[j-1] == '*') {
    27                     if (wp[j-2] == '*') {
    28                         dp[i][j] = dp[i][j-1];
    29                     } else {
    30                         dp[i][j] = dp[i][j-2];
    31                         if (wp[j-2] == ws[i-1] || wp[j-2] == '.') {
    32                             dp[i][j] |= dp[i-1][j];
    33                         }
    34                     }
    35                 }
    36             }
    37         }
    38         return dp[m][n];
    39     }
    40 }
    View Code

    Word Break II

    切分单词II

    思路:用wordbreak I 动态规划的结果,来为搜索做剪枝,避免不必要的搜索,保留isWord[i][j]数组可以避免每次重复的切割字符串。如果用动态规划中保留中间结果的方法会超时,因为保留了很多不必要的中间结果,而保留中间结果既耗时又耗空间。

    public class Solution {
        /**
         * @param s a string
         * @param wordDict a set of words
         */
        public List<String> wordBreak(String s, Set<String> wordDict) {
            // Write your code here
            boolean[] dp = new boolean[s.length() + 1];
            
            int maxLen = 0;
            for (String word : wordDict) {
                maxLen = Math.max(word.length(), maxLen);
            }
            
            boolean[][] isWord = new boolean[s.length()][s.length()];
            for (int i = 0; i < s.length(); i++) {
                for (int j = i; j < s.length(); j++) {
                    String word = s.substring(i, j + 1);
                    isWord[i][j] = wordDict.contains(word);
                }
            }
            
            dp[0] = true;
            
            for (int i = 1; i <= s.length(); i++) {
                for (int j = Math.max(0, i - maxLen); j < i; j++) {
                    if (dp[j]) {
                        if (isWord[j][i - 1]) {
                            dp[i] = true;
                            break;
                        }
                    }   
                }
            }
            List<String> res = new ArrayList<String>();
            search(res, isWord, dp, s.length() - 1, new ArrayList<String>(), s);
            return res;
        }
        
        public void search(List<String> res, boolean[][] isWord, 
                boolean[] dp, int end, List<String> cur, String s) {
            if (end < 0) {
                StringBuilder sb = new StringBuilder();
                for (int i = cur.size() - 1; i >= 0; i--) {
                    sb.append(cur.get(i));
                    if (i != 0) {
                        sb.append(" ");
                    }
                }
                res.add(sb.toString());
            }
            
            for (int i = end; i >= 0; i--) {
                if (isWord[i][end] && dp[i]) {
                    cur.add(s.substring(i, end + 1));
                    search(res, isWord, dp, i - 1, cur, s);
                    cur.remove(cur.size() - 1);
                }
            }
        }
    }
    View Code

    Wildcard Matching

    通配符匹配

    思路:属于双序列型动态规划,(注意题目中假设了只有p中含有通配符)。

       状态:dp[i][j]表示s的前i个字符组成的前缀串与p的前j个字符组成的前缀串是否匹配。

       方程:判断两个字符串是否匹配,有两种情况:

          1 p的最后一个字母不为'*',则要求最后一个字母匹配(相等或其中一个字母为'?'),且前i-1字串与前j-1字串匹配;

          2 p的最后一个字母为'*',如果它匹配s的最后一个空字符串,则等价于看dp[i][j-1],如果它匹配s末尾某个非空字符串,则等价于看dp[i-1][j]。

       初始化:初始化第0行第0列;

       结果:dp[slen][plen]。

     1 public class Solution {
     2     public boolean isMatch(String s, String p) {
     3         int m = s.length(), n = p.length();
     4         char[] ws = s.toCharArray();
     5         char[] wp = p.toCharArray();
     6         boolean[][] dp = new boolean[m+1][n+1];
     7         dp[0][0] = true;
     8         for (int j = 1; j <= n; j++) {
     9             dp[0][j] = dp[0][j-1] && wp[j-1] == '*';
    10         }
    11         for (int i = 1; i <= m; i++) {
    12             dp[i][0] = false;
    13         }
    14         for (int i = 1; i <= m; i++) {
    15             for (int j = 1; j <= n; j++) {
    16                 if (wp[j-1] == '?' || ws[i-1] == wp[j-1]) {
    17                     dp[i][j] = dp[i-1][j-1];
    18                 }
    19                 else if (wp[j-1] == '*') {
    20                     dp[i][j] = dp[i-1][j] || dp[i][j-1];
    21                 }
    22             }
    23         }
    24         return dp[m][n];
    25     }
    26 }
    View Code

      

  • 相关阅读:
    通用应用程序设计
    咫尺与天涯
    SQL SERVER 自定义函数 split
    asp.net 页面实践执行顺序
    asp.net UpdatePanel 不能局部刷新问题汇总
    DataGrid GridView 单页javascript 表头排序
    存储过程分页方案
    数据库名、数据表名、字段名、主键、标识列相关查询
    c# web 缓存管理
    js笔记---拖动元素
  • 原文地址:https://www.cnblogs.com/coldyan/p/6016016.html
Copyright © 2020-2023  润新知