【C++】Dynamic programming

Reference blog: Detailed explanation of dynamic programming

1. What is dynamic programming?

Dynamic programming (English: Dynamic programming, referred to as DP) is a method used in mathematics, management science, computer science, economics and bioinformatics to solve complex problems by decomposing the original problem into relatively simple sub-problems. Methods. Dynamic programming is often suitable for problems with overlapping subproblems and optimal substructure properties.

1.1 Overlapping subproblems and optimal substructures

1.1.1 Overlapping subproblems

Overlapping subproblems refer to the situation where the same subproblems are used multiple times during the problem solving process, that is, at different stages of solving the problem, the subproblems that need to be solved may be the same. Overlapping subproblems are one of the foundations of dynamic programming algorithm design. Using the overlap of subproblems can reduce repeated calculations and improve algorithm efficiency.

1.1.2 Optimal substructure

Optimal substructure means that the optimal solution to the problem can be constructed from the optimal solutions to the subproblems. That is, the optimal solution to the problem contains the optimal solutions to the sub-problems. In layman's terms, the optimal solution to a large problem can be derived from the optimal solution to a small problem. This is one of the key properties of dynamic programming.

1.1.3 Examples

For example, suppose there is a sequence containing n elements, and you need to find the longest increasing subsequence (LIS, Longest Increasing Subsequence). This problem has optimal substructure properties. If the LIS of a sequence is known, then if an element is added to the end, there are two cases:

  1. If the element is greater than the last element of the current LIS, then the LIS of the new sequence is the current LIS plus this element, and the length is the LIS length of the original sequence plus 1;
  2. If the element is less than or equal to the last element of the current LIS, the current LIS will not be affected, and the LIS of the new sequence is still the LIS of the original sequence.

Therefore, the optimal solution to the problem can be constructed from the optimal solutions to the known subproblems.

1.2 The core idea of ​​dynamic programming

The core idea of ​​dynamic programming is to split sub-problems, remember the past, and reduce repeated calculations.

Let’s take a look at a popular example on the Internet:

A : "1+1+1+1+1+1+1+1 =?"
A : "上面等式的值是多少"
B : 计算 "8"
A : 在上面等式的左边写上 "1+" 呢?
A : "此时等式的值为多少"
B : 很快得出答案 "9"
A : "你怎么这么快就知道答案了"
A : "只要在8的基础上加1就行了"
A : "所以你不用重新计算,因为你记住了第一个等式的值为8!动态规划算法也可以说是 '记住求过的解来节省时间'"

1.3 Jump from frog to dynamic programming

Leetcode original question: A frog can jump up 1 step or 2 steps at a time. Find out how many ways the frog can jump up a 10-level staircase.

1.3.1 Problem-solving ideas

Basic idea: Dynamic programming gradually determines the solution to the larger problem from the solution of the smaller problem based on the overlapping nature. It is from the direction of f(1) to f(10) and pushes upward to solve the problem, so it is called bottom-up. Upward solution.

What does it mean? We push up from the first step. Assume that the number of jumps to the nth step is defined as f(n):

  1. When there is only one step, there is only one way to jump, that is, f(1) = 1;
  2. When there are only 2 steps, there are two ways to jump. The first is to skip two levels directly. The second is to skip one level first and then another level. That is, f(2) = 2;
  3. When there are 3 steps, there are also two ways to jump. The first is to jump two steps directly from the first step. The second is to jump one step from step 2. That is, f(3) = f(1) + f(2);
  4. If you want to jump to the 4th step, either jump to the 3rd step first, and then jump up the 1st step; or jump to the 2nd step first, and then go up 2 steps at a time. That is, f(4) = f(2) + f(3);

At this point, we can get the formula:

f(1) = 1;
f(2) = 2;
f(3) = f(1) + f(2);
f(4) = f(2) + f(3);

f(10) = f(8) + f(9);
即f(n) = f(n - 2) + f(n - 1)。

At this point, let’s take a look at how the typical characteristics of dynamic programming are displayed in this question:

  1. Optimal substructure: f(n-1) and f(n-2) are called the optimal substructure of f(n).
  2. Overlapping sub-problems: For example, f(10)= f(9)+f(8), f(9) = f(8) + f(7), f(8) is an overlapping sub-problem.
  3. State transition equation: f(n)= f(n-1)+f(n-2) is called the state transition equation.
  4. Boundary: f(1) = 1, f(2) = 2 is the boundary.

1.3.2 Code

The code idea is as follows:
Insert image description here

Code:

class Solution {
    
    
public:
    int numWays(int n) {
    
    
        int dp[101] = {
    
    0};
        int mod = 1000000007;

        dp[0] = 1;
        dp[1] = 1;
        dp[2] = 2;
        for (int i = 3; i <= n; i++) {
    
    
            dp[i] = dp[i - 1] + dp[i - 2];
            dp[i] %= mod;
        }

        return dp[n] % mod;
    }
};

The space complexity of this method is O(n). However, if you look carefully at the picture above, you can find that f(n) only depends on the first two numbers, so only two variables a and b are needed to store it, which can meet the needs. , so the space complexity is O(1).
Insert image description here

Code:

class Solution {
    
    
public:
    int numWays(int n) {
    
    
        if (n < 2) {
    
    
            return 1;
        }
        if (n == 2) {
    
    
            return 2;
        }
        int a = 1;
        int b = 2;
        int temp = 0;
        for (int i = 3; i <= n; i++) {
    
    
            temp = (a + b)% 1000000007;
            a = b;
            b = temp;
        }
        return temp;
    }
};

2. Dynamic programming problem-solving routines

2.1 Core idea

The core idea of ​​dynamic programming is to split sub-problems, remember the past, and reduce repeated calculations. And dynamic programming is generally bottom-up, so here, based on the frog jumping problem, I summarize the ideas for dynamic programming:

  1. exhaustive analysis
  2. determine boundaries
  3. Find the rules and determine the optimal substructure
  4. Write the state transition equation

2.1.1 Case-by-case analysis

  1. exhaustive analysis
  • When the number of steps is 1, there is a jumping method, f(1) =1
  • When there are only 2 steps, there are two ways to jump. The first is to jump two steps directly, and the second is to jump one step first and then another step. That is, f(2) = 2;
  • When the steps are 3 steps, if you want to jump to the 3rd step, you can either jump to the 2nd step first, and then jump up the 1st step, or jump to the 1st step first, and then go up 2 steps at a time. So f(3) = f(2) + f(1) =3
  • When the steps are 4, if you want to jump to the 3rd step, you can either jump to the 3rd step first, and then jump 1 step up, or jump to the 2nd step first, and then go up 2 steps at a time. So f(4) = f(3) + f(2) =5
  • When the steps are 5…
  1. determine boundaries

Through exhaustive analysis, we found that when the number of steps is 1 or 2, we can clearly know the frog jumping method. f(1) =1, f(2) = 2, when the step n>=3, the rule f(3) = f(2) + f(1) =3 has been shown, so f(1) =1 , f(2) = 2 is the boundary for the frog to jump.

  1. Find patterns and determine the optimal substructure

When n>=3, the rule f(n) = f(n-1) + f(n-2) has been shown. Therefore, f(n-1) and f(n-2) are called f(n) the optimal substructure. What is the optimal substructure? There is such an explanation:

A dynamic programming problem is actually a recursion problem. Assume that the current decision result is f(n), then the optimal substructure is to make f(nk) optimal. The property of the optimal substructure is that the transition to the state of n is optimal and has nothing to do with subsequent decisions. , that is, a property that allows subsequent decisions to safely use the previous local optimal solution.

  1. Through the previous three steps, exhaustive analysis, determination of boundaries and optimal substructure, we can derive the state transition equation:

Insert image description here

3. Example questions

3.1 Increasing subsequence

Insert image description here

3.1.1. Exhaustive analysis:

  1. When nums is only 10, the longest subsequence [10] has a length of 1.
  2. When nums is added to 9, the longest subsequence [10] or [9], length 1.
  3. When nums is added to 2, the longest subsequence is [10] or [9] or [2], with length 1.
  4. When nums is added to 5, the longest subsequence [2, 5], length 2.
  5. When nums is added to 3, the longest subsequence is [2, 5] or [2, 3], with length 2.
  6. When nums is added to 7, the longest subsequence is [2, 5, 7] or [2, 3, 7], with length 3.
  7. When another element 18 is added to nums, the longest increasing subsequence is [2,5,7,101] or [2,3,7,101] or [2,5,7,18] or [2,3,7,18] , the length is 4.

3.1.2 Determine boundaries

For each element of the nums array, when we have not started traversing and searching, their initial longest subsequence is their own length of 1.

3.1.3 Find patterns and determine the optimal substructure

Through the above analysis, we can find a rule:

For the auto-increasing subsequence ending in nums[i], just find the subsequence ending with nums[j] smaller than nums[i] and add nums[i]. Obviously, a variety of new subsequences may be formed. We choose the longest one, which is the longest increasing subsequence.

Get the optimal substructure:

The longest increasing subsequence (nums[i]) = max(the longest increasing subsequence (nums[j])) + nums[i]; 0<= j < i, nums[j] < nums[i];

3.1.4 Write the state transition equation

We set up the dp array to store the length of the longest subsequence ending with the elements of the nums array, initialize it to 1, and obtain the state transition equation from the optimal substructure:

dp[i] = max(dp[j]) + 1; 0<= j < i, nums[j] < nums[i];

3.1.5 Code

class Solution {
    
    
public:
    int lengthOfLIS(vector<int>& nums) {
    
    
        vector<int> dp(nums.size(), 1);
        int ans = 1;
        for (int i = 1; i < nums.size(); i++) {
    
    
            for (int j = 0; j < i; j++) {
    
    
                if (nums[j] < nums[i]) {
    
    
                    dp[i] = max(dp[i], dp[j] + 1);
                }
            }
            ans = max(ans, dp[i]);
        }
        return ans;
    }
};

3.2 The longest string chain

Insert image description here
Insert image description here

This question is very similar to the previous question, but note that this question does not require that the word order of the word chain must be in the order of the original word array.

3.2.1 Exhaustive analysis

Note here that because this question does not require that the word order of the word chain must be in the order of the original word array, our analysis cannot be exhaustive in the order of the original word array. According to the characteristics of the word chain, we should start from the shortest word in the original word array and analyze exhaustively towards the longest word.

Example 1:

  1. When words only have "a", the longest word chain ["a"] has a length of 1.
  2. When words are added to "b", the longest word chain is ["a"] or ["b"], with a length of 1.
  3. When words are added to "ba", the longest word chain is ["a", "ba"] or ["b", "ba"]], with a length of 2.
  4. When words are added to "bca", the longest word chain is ["a", "ba", "bca"] or ["b", "ba", "bca"], with a length of 3.
  5. When words are added to "bda", the longest word chain is ["a", "ba", "bda"] or ["b", "ba", "bda"], with a length of 3.
  6. When words are added to "bdca", the longest word chain ["a", "ba", "bca", "bdca"] or ["b", "ba", "bca", "bdca"] or [" a", "ba", "bda", "bdca"] or ["b", "ba", "bda", "bdca"], length 4.

3.2.2 Determine boundaries

For each word in the original word array, before we start traversing and searching, their longest word chain is themselves, with a length of 1.

3.2.3 Find patterns and determine the optimal substructure

For each words[i], if their predecessor words[j] exists in the original array, then one of their word chains is the word chain of their predecessor words[j] plus their own words[i]. The longest subchain is the longest one among them.

Get the optimal substructure

The longest subchain (words[i]) = max(words[j]) + words[i]; words[j] is the predecessor of words[i]

3.2.4 Write the state transition equation

In order to ensure that when traversing the original array, we traverse from the shortest word in the original word array to the longest word, we need to sort the original array first.

We set up the dp array to store the length of the longest sub-chain of each word in the words array, and obtain the state transition equation.

dp[i] = max(dp[j]) + 1; 0 <= j < i, dp[j] is the predecessor of dp[i]

In this equation, in order to find the predecessor dp[j] of dp[i], we need to reduce words[i] one letter at a time to get all its predecessors, and then traverse the previous part of the original word array words[i]. Determine whether this predecessor exists before proceeding. This is of course cumbersome, and as the words get longer, the time required skyrockets. So, is there any way we can simplify this process?

Yes, use a hash table to store the longest sub-chain length of each word, using the word itself as the key value. In this way, we directly use all the predecessors of words[i] to access the hash table, and we can simultaneously complete the two tasks of determining whether this predecessor exists and operating on it.

Rewrite the state transition equation using a hash table.

dp[words[i]] = max(dp[word]) + 1; word is all the predecessors of words[i]

3.2.5 Code

class Solution {
    
    
public:
    int longestStrChain(vector<string>& words) {
    
    
        unordered_map<string, int> cnt;
        sort(words.begin(), words.end(), [](const string a, const string b) {
    
    
            return a.size() < b.size();
        });
        int res = 0;
        for (string word : words) {
    
    
            cnt[word] = 1;
            for (int i = 0; i < word.size(); i++) {
    
    
                string prev = word.substr(0, i) + word.substr(i + 1, word.size());
                if (cnt[prev]) {
    
    
                    cnt[word] = max(cnt[prev] + 1, cnt[word]);
                }
            }
            res = max(cnt[word], res);
        }
        return res;
    }
};

Guess you like

Origin blog.csdn.net/m0_63852285/article/details/130541116