18

Speaking of dynamic programming, I don't know if you have such troubles. After you have mastered some basic algorithms and data structures, you are still unable to start when you encounter some more complex problems, and you are naturally apprehensive during the interview. If I say that dynamic programming is a fantasy problem, it is not an exaggeration. The reason, I think it can be attributed to the following two points:

  • You have not fully grasped the routines and ideas of dynamic programming related problems;
  • You don't systematically summarize what problems can be solved with dynamic programming.

Know yourself and the enemy. If you want to use dynamic programming as one of your interview weapons, you have to understand it enough; and dealing with interviews, summarizing and classifying questions is actually a good choice, which can actually be felt when we brush the questions. Then, in view of the above two points, we will systematically talk about what kind of problems can be solved by dynamic programming.

1. Dynamic programming is an idea

Dynamic programming algorithm, I think you should hear it often. Well, I think it's right to call it that in principle. First of all, dynamic programming is not a data structure, there is no doubt about it, and it is strictly an algorithm. But a more accurate or more appropriate formulation should be to say that dynamic programming is an idea. What's the difference between an algorithm and an idea?

Generally speaking, we will talk about algorithms and data structures together, because they are closely related, and algorithms are often a rigorous summary of problem-solving schemes based on specific data structures.

For example, sorting on the basis of an unordered array, what does the data structure refer to here? Obviously arrays, and the algorithm is what is called sorting. As for the sorting algorithm, you can consider using a simple bubble sort or the more efficient quicksort method, etc. to solve the problem.

Yes, you should also feel that the algorithm is a simple experience summary and routine. So what is thought? Compared with algorithms, ideas are more about guiding you and me to solve problems.

For example, when solving a complex problem, we can simplify the problem first, solve simple problems first, and then solve difficult problems, then this is a kind of thinking that guides problem solving. In addition, the divide and conquer we often say is also a simple idea, of course, it is often mentioned in such as merge sort or recursive algorithm.

The dynamic programming is such a guiding principle of our solution to the problem: you need to use the good results already calculated to derive your calculations, namely the large scale of the problem is a result of the operation of small-scale problems come .

To summarize: algorithm is a summary of the experience, but it is thought to guide us to solve the problem . Since dynamic programming is an idea, it is actually a relatively abstract concept, and it is difficult to relate it to practical problems. Therefore, it is very important to figure out what kind of problems can be solved using dynamic programming.

Second, the characteristics of dynamic programming problems

As an optimization problem-solving method in operations research, dynamic programming has been widely used in algorithmic problems. Next, let's take a look at some of the characteristics of the moving-return problem.

2.1 Optimal solution problem

Unless your problem is as simple as finding the largest value in an array, for which you can sort the array and then take the elements at the beginning or end of the array, or if you find it troublesome, you can also directly Traverse to get the most value. Otherwise, you'll have to consider using dynamic programming to solve this problem. Questions like this usually ask you to find the largest subarray, find the longest increasing subarray, find the longest increasing subsequence, or find the longest common substring, subsequence, etc.

If we encounter the problem of finding the maximum value, we can use the following routines to solve the problem:

  • Prioritize the possibility of using a greedy algorithm;
  • Then there is violent recursion to exhaustively, for the case of small data size;
  • If the above two are not suitable, then choose dynamic programming.

It can be seen that the core problem of solving dynamic programming is actually exhaustively . Of course, the dynamic programming problem will not be so simple. We also need to consider whether the problem to be solved has overlapping sub-problems, optimal sub-structures and other characteristics.

Now that the characteristics of the dynamic programming algorithm are clear, let's take a look at which problems are suitable for solving problems with dynamic programming.

1. Product maximum subarray

Given an integer array numbers, find the contiguous subarray with the largest product in the array (the subarray contains at least one number), and return the product of the subarrays.


示例1:
输入: [2,7,-2,4]
输出: 14
解释: 子数组 [2,7] 有最大乘积 14。


示例2:
输入: [-5,0,3,-1]
输出: 3
解释: 结果不能为 15, 因为 [-5,3,-1] 不是子数组,是子序列。

First of all, it is obvious that this question contains a "most" word, and the probability of using dynamic programming is very high. The purpose of this problem is to find the largest continuous interval from the array and ensure that the product of this interval is the largest. Since each continuous interval can be divided into two smaller continuous intervals, and the result of the large continuous interval is the product of the two small continuous intervals, this problem is still to solve the maximum value that satisfies the conditions, and the problem can also be decomposed, and It belongs to the problem of finding the maximum value. At the same time, this problem is similar to finding the largest consecutive subsequence and comparison, the only difference is that you need to consider the sign of the sign in this problem, and the others are the same.

Corresponding implementation code:

class Solution {
public:
    int maxProduct(vector<int>& nums) {
        if(nums.empty()) return 0;

        int curMax = nums[0];
        int curMin = nums[0];
        int maxPro = nums[0];
        for(int i=1; i<nums.size(); i++){
            int temp = curMax;    // 因为curMax在下一行可能会被更新,所以保存下来
            curMax = max(max(curMax*nums[i], nums[i]), curMin*nums[i]);
            curMin = min(min(curMin*nums[i], nums[i]), temp*nums[i]);
            maxPro = max(curMax, maxPro);
        }
        return maxPro;
    }
};

2. Longest Palindromic Substring

Problem: Given a string s, find the longest palindromic substring in s. You can assume that s has a maximum length of 1000.

示例1:
输入: "babad"
输出: "bab"


示例2:
输入: "cbbd"
输出: "bb"

[Palindrome] is a string with the same forward reading and reverse reading, such as "level" or "noon", etc. is a palindrome. This problem still contains a "most" word, and since the longest palindrome substring solved must contain a shorter palindrome substring, we can still use dynamic programming to solve this problem.

Corresponding implementation code:

class Solution {
        public boolean isPalindrome(String s, int b, int e){//判断s[b...e]是否为回文字符串
        int i = b, j = e;
        while(i <= j){
            if(s.charAt(i) != s.charAt(j)) return false;
            ++i;
            --j;
        }
        return true;
    }
    public String longestPalindrome(String s) {
        if(s.length() <=1){
            return s;
        }
        int l = 1, j = 0, ll = 1;
        for(int i = 1; i < s.length(); ++i){
             //下面这个if语句就是用来维持循环不变式,即ll恒表示:以第i个字符为尾的最长回文子串的长度
             if(i - 1 - ll >= 0 && s.charAt(i) == s.charAt(i-1-ll)) ll += 2;
             else{
                 while(true){//重新确定以i为边界,最长的回文字串长度。确认范围为从ll+1到1
                     if(ll == 0||isPalindrome(s,i-ll,i)){
                         ++ll;
                         break;
                     }
                     --ll;
                 }
             }
             if(ll > l){//更新最长回文子串信息
                l = ll;
                j = i;
            }
        }
        return s.substring(j-l+1, j+1);//返回从j-l+1到j长度为l的子串
    }
}

3. Longest Ascending Subsequence

Problem: Given an unordered array of integers, find the length of the longest ascending subsequence in it. There may be multiple combinations of longest ascending subsequences, you just need to output the corresponding length.

示例:
输入: [10,9,2,5,3,7,66,18]
输出: 4
解释: 最长的上升子序列是 [2,3,7,66],它的长度是 4。

This problem is still an optimal solution problem. Suppose we require an ascending self-sequence in a string of length 5. We only need to know how long the longest ascending subsequence of a string of length 4 is. number to determine the final result.
Corresponding implementation code:

class Solution {
    public int lengthOfLIS(int[] nums) {
        if(nums.length == 0) return 0;
        int[] dp = new int[nums.length];
        int res = 0;
        Arrays.fill(dp, 1);
        for(int i = 0; i < nums.length; i++) {
            for(int j = 0; j < i; j++) {
                if(nums[j] < nums[i]) dp[i] = Math.max(dp[i], dp[j] + 1);
            }
            res = Math.max(res, dp[i]);
        }
        return res;
    }
}

2.2 Seek feasibility

If there is such a question, let you judge whether there is a path whose sum is x (if found, it is True; if not found, it is naturally False), or let you judge whether a path that meets a certain condition can be found, Then these kinds of problems can be summarized as feasibility problems, and can be solved using dynamic programming.

1. The problem of zero exchange

Question: Give you k coins of denomination, the denominations are c1, c2 ... ck, the number of each coin is unlimited, and then give a total amount, ask you how many coins you need at least to make up this amount, if it is impossible to make up , the algorithm returns -1.

示例1:
输入: c1=1, c2=2, c3=5, c4=7, amount = 15
输出: 3
解释: 11 = 7 + 7 + 1。


示例2:
输入: c1=3, amount =7
输出: -1
解释: 3怎么也凑不到7这个值。

This problem is obvious, if it is impossible to make up the amount we need (that is, the amount), the final algorithm needs to return -1, otherwise output the possible number of coins. This is a typical dynamic programming problem seeking feasibility.

For sample code:

class Solution {
    public int coinChange(int[] coins, int amount) {
        if(coins.length == 0)
            return -1;
        //声明一个amount+1长度的数组dp,代表各个价值的钱包,第0个钱包可以容纳的总价值为0,其它全部初始化为无穷大
        //dp[j]代表当钱包的总价值为j时,所需要的最少硬币的个数
        int[] dp = new int[amount+1];
        Arrays.fill(dp,1,dp.length,Integer.MAX_VALUE);
        for (int coin : coins) {
            for (int j = coin; j <= amount; j++) {
                if(dp[j-coin] != Integer.MAX_VALUE) {
                    dp[j] = Math.min(dp[j], dp[j-coin]+1);
                }
            }
        }
        if(dp[amount] != Integer.MAX_VALUE)
            return dp[amount];
        return -1;
    }
}

2. String Interleaving Composition Problem

Problem: Given three strings s1, s2, s3, verify that s3 is composed of s1 and s2 interleaved.

示例1:
输入: s1="aabcc",s2 ="dbbca",s3="aadbbcbcac"
输出: true
解释: 可以交错组成。


示例2:
输入: s1="aabcc",s2="dbbca",s3="aadbbbaccc"
输出: false
解释:无法交错组成。

This problem is a little more complicated, but we can still use the perspective of sub-problems to first find out whether a substring of a certain length in s1 is composed of substrings of s2 and s3 interleaved until the length of the entire s1 is solved. You can also see into a maximal problem with subproblems.
Corresponding sample code:

class Solution {
    public boolean isInterleave(String s1, String s2, String s3) {
        int length = s3.length();
        // 特殊情况处理
        if(s1.isEmpty() && s2.isEmpty() && s3.isEmpty()) return true;
        if(s1.isEmpty()) return s2.equals(s3);
        if(s2.isEmpty()) return s1.equals(s3);
        if(s1.length() + s2.length() != length) return false;
 
        int[][] dp = new int[s2.length()+1][s1.length()+1];
        // 边界赋值
        for(int i = 1;i < s1.length()+1;i++){
            if(s1.substring(0,i).equals(s3.substring(0,i))){
                dp[0][i] = 1;
            }
        }
        for(int i = 1;i < s2.length()+1;i++){
            if(s2.substring(0,i).equals(s3.substring(0,i))){
                dp[i][0] = 1;
            }
        }
        
        for(int i = 2;i <= length;i++){
            // 遍历 i 的所有组成(边界除外)
            for(int j = 1;j < i;j++){
                // 防止越界
                if(s1.length() >= j && i-j <= s2.length()){
                    if(s1.charAt(j-1) == s3.charAt(i-1) && dp[i-j][j-1] == 1){
                        dp[i-j][j] = 1;
                    }
                }
                // 防止越界
                if(s2.length() >= j && i-j <= s1.length()){
                    if(s2.charAt(j-1) == s3.charAt(i-1) && dp[j-1][i-j] == 1){
                        dp[j][i-j] = 1;
                    }
                }
            }
        }
        return dp[s2.length()][s1.length()]==1;
    }
}

2.3 Find the total

In addition to finding the maximum value and feasibility, finding the total number of solutions is also a common type of dynamic programming problem. For example, given a data structure and constraints, you can calculate all possible paths of a scheme, then this kind of problem belongs to the problem of finding the total number of schemes.

1. Coin Combination Problem

Question: British pound coins are 1p, 2p, 5p, 10p, 20p, 50p, £1 (100p), and £2 (200p). For example, we can make £2 in the following way: 1×£1 + 1×50p + 2×20p + 1×5p + 1×2p + 3×1p. The question is how many ways are there to form n pounds? Note that there can be no repetition, for example, 1 pound + 2 50P and 50P + 50P + 1 pound are the same.

示例1:
输入: 2
输出: 73682 

The essence of this problem is to find combinations that satisfy the conditions, but there is no need to find specific values or combinations here, just calculate the number of combinations.

public class Main {
    public static void main(String[] args) throws Exception {
        
        Scanner sc = new Scanner(System.in);
        while (sc.hasNext()) {
            
            int n = sc.nextInt();
            int coin[] = { 1, 5, 10, 20, 50, 100 };
            
            // dp[i][j]表示用前i种硬币凑成j元的组合数
            long[][] dp = new long[7][n + 1];
            
            for (int i = 1; i <= n; i++) {
                dp[0][i] = 0; // 用0种硬币凑成i元的组合数为0
            }
            
            for (int i = 0; i <= 6; i++) {
                dp[i][0] = 1; // 用i种硬币凑成0元的组合数为1,所有硬币均为0个即可
            }
            
            for (int i = 1; i <= 6; i++) {
                
                for (int j = 1; j <= n; j++) {
                    
                    dp[i][j] = 0;
                    for (int k = 0; k <= j / coin[i - 1]; k++) {
                        
                        dp[i][j] += dp[i - 1][j - k * coin[i - 1]];
                    }
                }
            }
            
            System.out.print(dp[6][n]);
        }
        sc.close();
    }
}

2. Path planning problem

Problem: A robot is located in the upper left corner of an mxn grid. The robot can only move down or to the right one step at a time. How many paths are there in the robot trying to reach the bottom right corner of the grid?

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


示例1:
输入: 3 3
输出: 6

This problem is still a problem of finding the number of combinations that satisfy the condition, but the combination here becomes a combination of paths. We can solve all paths in a smaller grid first, and then solve more combinations in a larger grid. This is not fundamentally different from the problem of coin combinations.

There is a law or phenomenon that needs to be emphasized here, that is, the dynamic programming problem of finding the total number of programs generally refers to finding all the specific forms of "one" program. If it is to find the specific form of "all" schemes, then this is definitely not a dynamic programming problem, but uses traditional recursion to traverse the specific form of all schemes.

Why do you say that? Because you need to enumerate all cases, in most cases there are no overlapping subproblems to optimize for you. Even if there were, you would only have a simple speedup of traversal using the memo. But essentially, this type of problem is not a dynamic programming problem.

Corresponding sample code:

package com.qst.Tesst;

import java.util.Scanner;

public class Test12 {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        while (scanner.hasNext()) {
            int x = scanner.nextInt();
            int y = scanner.nextInt();

            //设置路径
            long[][] path = new long[x + 1][y + 1];
            //设置领导数量
            int n = scanner.nextInt();

            //领导位置
            for (int i = 0; i < n; i++) {
                int a = scanner.nextInt();
                int b = scanner.nextInt();
                path[a][b] = -1;
            }

            for (int i = 0; i <= x; i++) {
                path[i][0] = 1;
            }
            for (int j = 0; j <= y; j++) {
                path[0][j] = 1;
            }

            for (int i = 1; i <= x; i++) {
                for (int j = 1; j <= y; j++) {
                    if (path[i][j] == -1) {
                        path[i][j] = 0;
                    } else {
                        path[i][j] = path[i - 1][j] + path[i][j - 1];
                    }

                }

            }
            System.out.println(path[x][y]);
        }
    }
}

3. How to identify dynamic programming problems

From what I said above, if you encounter the problem of finding the maximum value, finding the feasibility, or finding the total number of solutions, then this problem is almost inseparable, and you can basically be sure that it needs to be solved using dynamic programming. . However, there are some individual cases to be aware of:

3.1 Data is not sortable

Suppose we have an unordered sequence and want to find the sum of the two largest numbers in the sequence. Many beginners have just learned dynamic programming and will be so mad that they want to use dynamic programming to solve the optimization problem. In fact, this problem can not be solved simply by doing a sorting or a traversal. For this kind of problem, we should first consider whether we can simplify the problem by sorting. If not, it is most likely a dynamic programming problem.

minimum k number

Problem: Input an array of integers arr and find the smallest k numbers in it. For example, enter 8 numbers 4, 5, 1, 6, 2, 7, 3, 8, and the smallest 4 numbers are 1, 2, 3, 4.

示例1:
输入:arr = [3,2,1], k = 2
输出:[1,2] 或者 [2,1]


示例2:
输入:arr = [0,1,2,1], k = 1
输出:[0]

We found that although this problem is also to find the "most" value, it can be solved only by sorting, so we should use sorting, heap and other algorithms or data structures to solve it, not dynamic programming.

Corresponding sample code:

public class Solution {
    public ArrayList<Integer> GetLeastNumbers_Solution(int [] input, int k) {
                int t;
        boolean flag;
        ArrayList result = new ArrayList();
        if(k>input.length){
            return result;
        }
        for(int i =0;i<input.length;i++){
            flag = true;
            for(int j = 0; j < input.length-i;j++)
                if(j<input.length-i-1){
                    if(input[j] > input[j+1]) {
                        t = input[j];
                        input[j] = input[j+1];
                        input[j+1] = t;
                        flag = false;
                    }
                }
            if(flag)break;
        }
        for(int i = 0; i < k;i++){
            result.add(input[i]);
        }
        return  result;
    }
}

3.2 Data is not exchangeable

There is also a class of problems that can be classified into the categories of problems we have summarized, but there are no overlapping sub-problems required by dynamic programming (such as the classic eight queens problem), so such problems cannot be solved by dynamic programming.

full array

Question: Given a sequence with no repeating numbers, return all possible permutations of it.


示例:
输入: [1,2,3]
输出:
[
  [1,2,3],
  [1,3,2],
  [2,1,3],
  [2,3,1],
  [3,1,2],
  [3,2,1]
]

Although this problem is a combination, it has no overlapping sub-problems, and there is no optimization requirement, so it can be handled by the backtracking method.

Corresponding sample code:

public class Main {
    public static void main(String[] args) {
        perm(new int[]{1,2,3},new Stack<>());
    }
    public static void perm(int[] array, Stack<Integer> stack) {
        if(array.length <= 0) {
            //进入了叶子节点,输出栈中内容
            System.out.println(stack);
        } else {
            for (int i = 0; i < array.length; i++) {
                //tmepArray是一个临时数组,用于就是Ri
                //eg:1,2,3的全排列,先取出1,那么这时tempArray中就是2,3
                int[] tempArray = new int[array.length-1];
                System.arraycopy(array,0,tempArray,0,i);
                System.arraycopy(array,i+1,tempArray,i,array.length-i-1);
                stack.push(array[i]);
                perm(tempArray,stack);
                stack.pop();
            }
        }
    }
}

To sum up, which problems can be used dynamic programming, usually the following situations can be solved by dynamic programming:

  • Find the optimal solution problem (maximum and minimum);
  • Seek Feasibility (True or False);
  • Find the total number of programs;
  • The data structure is not sortable (Unsortable);
  • Algorithms are not non-swappable.

If an interview question exhibits these characteristics, then 90% of the time you can say that it is a dynamic question. In addition, you also need to consider whether the problem contains overlapping subproblems and optimal substructures. On this basis, you can 99% assert whether it is a dynamic regression problem, and you can also find a general solution to the problem.


xiangzhihong
5.9k 声望15.3k 粉丝

著有《React Native移动开发实战》1,2,3、《Kotlin入门与实战》《Weex跨平台开发实战》、《Flutter跨平台开发与实战》1,2和《Android应用开发实战》