Introduction to Algorithm Study Notes Chapter 4 Divide and Conquer Strategy

In the divide-and-conquer strategy, we solve a problem recursively, and the following three steps are applied in each level of recursion:
1. Decomposition. Divide the problem into some sub-problems, the form of the sub-problems is the same as the original problem, but on a smaller scale.
2. Resolve. Solve the sub-problems recursively. If the size of the sub-problems is small enough, stop the recursion and solve directly.
3. Merge. Combine the solutions of the sub-problems into the solution of the original problem.

When the sub-problem is large enough to be solved recursively, we call it a recursive case. When the sub-problem becomes small enough that recursion is no longer needed, we say that recursion has bottomed out and entered the basic situation.

Recursive expressions can have many forms. A recursive algorithm can divide the problem into sub-problems of varying sizes, such as the division of 1/3 and 2/3. And the size of the sub-problem does not have to be a fixed proportion of the original problem.

In practical applications, we will ignore some technical details of recursive declaration and solution, such as calling MERGE-SORT on n elements. When n is an odd number, the scales of the two sub-problems are n/2 rounded up and down, respectively Integer, not exactly n/2. Boundary conditions are another type of details that are usually overlooked. When n is small enough, the algorithm running time is θ(1), but we do not change the recursive formula that n is small enough to find T(n), because these changes will not exceed one Constant factor, so the growth order of the function will not change.

Investing in stocks can only be bought and sold once, in order to maximize the return, that is, buy at the lowest price and sell at the highest price, but the lowest price may appear after the highest price
:! [Insert picture description here](https://img- blog.csdnimg.cn/20210320124004302.png?x-oss- process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3R1czAwt_FF, when
you can sell at the highest price, or you think you can sell at the highest price, or at the lowest price you think You can maximize the revenue. If this strategy is effective, it is very simple to determine the maximum revenue: look for the highest price and the lowest price, and then start from the highest price to the left to find the lowest price on the left, and start from the lowest price to the right to find the right The highest price of, and then take the two pairs of prices with the largest difference, but the following figure is a counter-example: the
Insert picture description here
above problem can be solved simply by violence, try every possible combination of buying and selling dates, as long as the selling date is on the buying date after the can, a total of n-day n(n-1)/2kinds of date combination, and the processing time spent for each date is at least constant, therefore, this method of operating time Ω (n²).

We can examine daily price changes. The price change on the i-th day is defined as the price difference between the i-th day and the i-1th day. Then the problem becomes to find the largest non-empty continuous sub-array, called this sub-array Array is the largest sub-array: The
Insert picture description here
largest sub-array problem is meaningful only when the array contains negative numbers. If all array elements are non-negative, the largest sub-array is the sum of the entire array.

Consider using divide-and-conquer technology to solve the largest sub-array problem. Using divide-and-conquer technology means that we have to divide the sub-array into two sub-arrays of the same size as possible, that is, find the center position mid of the sub-array, and then consider solving the two sub-arrays A[ low… mid] and A[mid + 1… high], the position of any continuous sub-array A[i… j] of A[low… high] must be one of the following three situations:
1. Fully located in the sub-array In A[low… mid], at this time low <= i <= j <= mid.
2. It is completely located in the sub-array A[mid + 1… high], at this time mid <i <= j <= high.
3. Cross the midpoint, at this time low <= i <= mid <= j <= high.

In fact, a largest sub-array of A[low… high] must be completely located in A[low… mid], completely located in A[mid + 1… high], and the largest of all sub-arrays across the midpoint, we It is possible to recursively solve the largest sub-arrays that are completely located on both sides, so all the remaining work is to find the largest sub-array that spans the midpoint, and then choose the largest sum in these three cases:
Insert picture description here
find the largest sub-array that spans the midpoint:

FIND-MAX-CROSSING-SUBARRAY(A, low, mid, high)
	left-sum = -∞
	sum = 0
	for i = mid down to low
		sum = sum + A[i]
		if sum > left-sum
			left-sum = sum
			max-left = i
	right-sum = -∞
	sum = 0
	for j = mid + 1 to high
		sum = sum + A[j]
		if sum > right-sum
			right-sum = sum
			max-right = j
	return (max-left, max-right, left-sum + right-sum)

The above process takes θ(n) time.

With the above pseudocode of linear time, you can write the following pseudocode of the divide and conquer algorithm for solving the largest subarray problem:

FIND-MAXIMUM-SUBARRAY(A, low, high)
	if high == low    // base case: only one element
		return (low, high, A[low])
	else 
		mid = floor((low + high) / 2)    // 向下取整
		(left-low, left-high, left-sum) = FIND-MAXIMUM-SUBARRAY(A, low, mid)
		(right-low, right-high, right-sum) = FIND-MAXIMUM-SUBARRAY(A, mid + 1, high)
		(cross-low, cross-high, cross-sum) = FIND-MAX-CROSSING-SUBARRAY(A, low, mid, high)
		if left-sum >= right-sum and left-sum >= cross-sum
			return (left-low, left-high, left-sum)
		elseif right-sum >= left-sum and right-sum >= cross-sum
			return (right-low, right-high, right-sum)
		else 
			return (cross-low, cross-high, cross-sum)

The time complexity of this solution is θ(nlgn).

C++ implementation of the above process:

#include <iostream>
#include <vector>
using namespace std;

int findMaxCrossSubarray(const vector<int> &nums, size_t start, size_t mid, size_t end) {
    
    
	int leftSum = 0;
	int sum = 0;
	for (size_t i = mid + 1; i > start; --i) {
    
    
		sum += nums[i - 1];
		if (sum > leftSum) {
    
    
			leftSum = sum;
		}
	}

	int rightSum = 0;
	sum = 0;
	for (size_t i = mid + 1; i <= end; ++i) {
    
    
		sum += nums[i];
		if (sum > rightSum) {
    
    
			rightSum = sum;
		}
	}

	return leftSum + rightSum;
}

int findMaximumSubarray(const vector<int>& nums, size_t start, size_t end) {
    
    
	if (start == end) {
    
    
		return nums[start];
	}

	size_t mid = (start + end) >> 1;
	int leftMax = findMaximumSubarray(nums, start, mid);
	int rightMax = findMaximumSubarray(nums, mid + 1, end);
	int crossMax = findMaxCrossSubarray(nums, start, mid, end);

	return max(leftMax, max(rightMax, crossMax));
}

int main() {
    
    
	vector<int> ivec = {
    
     13,-3,-25,20,-3,-16,-23,18,20,-7,12,-5,-22,15,-4,7 };
	cout << findMaximumSubarray(ivec, 0, ivec.size() - 1);
}

Find the largest sub-array non-recursively and linearly, and the time complexity is O(n):

#include <iostream>
#include <vector>
#include <limits>
using namespace std;

int findMaximumSubarray(const vector<int>& nums, size_t start, size_t end) {
    
    
	int sumMax = numeric_limits<int>::min();
	int curSum = 0;
	for (size_t i = 0; i < nums.size(); ++i) {
    
    
		curSum += nums[i];
		if (curSum > sumMax) {
    
    
			sumMax = curSum;
		}

		if (curSum < 0) {
    
    
			curSum = 0;
		}
	}
	return sumMax;
}

int main() {
    
    
	vector<int> ivec = {
    
     13,-3,-25,20,-3,-16,-23,18,20,-7,12,-5,-22,15,-4,7 };
	cout << findMaximumSubarray(ivec, 0, ivec.size() - 1);
}

Matrix multiplication pseudo code:

SQUARE-MATRIX-MULTIPLY(A, B)
	n = A.rows
	let C be a new nXn matrix
	for i = 1 to n
		for j = 1 to n
			cij = 0
				for k = 1 to n
					cij = cij + aik * bkj
	return C

Matrix multiplication does not necessarily take Ω(n³) time. Even the natural definition of matrix multiplication requires so many scalar multiplications. There is Strassen's matrix multiplication recursive algorithm, which has a time complexity of O(n 2.81 ).

For the sake of simplicity, assume that the matrix is ​​nXn, where n is a power of 2, and then divide the nXn matrix into 4 sub-matrices of n/2Xn/2. The properties of a matrix operation are as follows: the
Insert picture description here
following formula is equivalent to the above formula:
Insert picture description here
according to the above The process can write pseudo code:

SQUARE-MATRIX-MULTIPLY-RECURSIVE(A, B)
	n = A.rows
	let C be a new nXn matrix
	if n == 1
		c11 = a11 * b11
	else
		C11 = SQUARE-MATRIX-MULTIPLY-RECURSIVE(A11, B11) + SQUARE-MATRIX-MULTIPLY-RECURSIVE(A12, B21)
		C12 = SQUARE-MATRIX-MULTIPLY-RECURSIVE(A11, B12) + SQUARE-MATRIX-MULTIPLY-RECURSIVE(A12, B22)
		C21 = SQUARE-MATRIX-MULTIPLY-RECURSIVE(A21, B11) + SQUARE-MATRIX-MULTIPLY-RECURSIVE(A22, B21)
		C22 = SQUARE-MATRIX-MULTIPLY-RECURSIVE(A21, B12) + SQUARE-MATRIX-MULTIPLY-RECURSIVE(A22, B22)
	return C

The above code conceals a subtle but important detail of how to decompose the matrix. If we really create 12 new n/2Xn/2 matrices, it will take θ(n²) time to copy the matrix elements. In fact, we can pass the subscript To specify a sub-matrix.

In the above process, the fifth line takes θ(1) time to calculate the decomposition matrix, and then SQUARE-MATRIX-MULTIPLY-RECURSIVE is called eight times. Each call completes the multiplication of two n/2Xn/2 matrices, so the total time spent is 8T (N/2). In this process, it takes time θ(n²) to call the matrix addition 4 times, so the total time of the recursive case is:
Insert picture description here
if the matrix decomposition is realized by copying the elements, the required cost is θ(n²), The total running time will be increased by a constant factor, and T(n) will remain unchanged. Therefore, the running time formula is as follows:
Insert picture description here
This method T(n)=θ(n³), so the simple divide-and-conquer algorithm is not better than the direct method.

In the formula of T(n), θ(n²) actually omits the constant coefficients before n², because the θ symbol already contains all constant coefficients, but the 8 in formula 8T(n/2) cannot be omitted, because 8 represents a recursive tree Each node has several child nodes, which determines how many items each layer of the tree contributes to the sum. If 8 is omitted, the recursive tree becomes a linear structure.

The core idea of ​​the Strassen algorithm is to make the recursive tree a little less lush, that is, only recursively perform n/2Xn/2 matrix multiplications seven times instead of eight. The cost of reducing one matrix multiplication may be an additional number of n/2Xn/ 2 Matrix addition, but only constant times. The algorithm includes the following steps. Steps 2 to 4 will explain the specific steps later:
1. Decompose the matrix according to the index. The method is the same as the ordinary recursive method, and it takes θ(1) time.
2. Create 10 n/2Xn/2 matrices, each matrix saves the sum or difference of the two sub-matrices created in step 1, and it takes time θ(n²).
3. Using the submatrix created in step 1 and the 10 matrices created in step 2, recursively calculate 7 matrix products, and the result of each matrix product is n/2Xn/2.
4. Calculate C11, C12, C21, and C22 of the result matrix C based on the result of the matrix product in step 3.

Steps 1, 2, and 4 spend a total of θ(n²) time, and step 3 requires 7 times n/2Xn/2 matrix multiplications, so the running time T(n) of Strassen algorithm is obtained:
Insert picture description here
T(n)=θ(n) lg7 ), lg7 is between 2.80 and 2.81.

In step 2 of the Strassen algorithm, the following 10 matrices are created: The
Insert picture description here
Insert picture description here
above steps calculate 10 additions and subtractions of n/2Xn/2 matrices, which takes θ(n²) time.

In step 3, recursively calculate the multiplication of the n/2Xn/2 matrix 7 times: in the
Insert picture description here
above formula, only the multiplication in the middle column needs to be actually calculated, and the right column just illustrates the relationship between these products and the sub-matrix created in step 2.

Step 4 Perform addition and subtraction operations on the matrix created in Step 3:
Insert picture description here
Expand the right side of the above formula:
Insert picture description here
and C12 is equal to:
Insert picture description here
C21 is equal to:
Insert picture description here
C22 is equal to:
Insert picture description here
Substitution method to solve the recursive formula:
1. Guess the form of the solution.
2. Use mathematical induction to find the constants in the solution and prove that the solution is correct.

The substitution method can be used to find the upper bound of the following recursive formula:
Insert picture description here
we guess the solution is O(nlgn), and the substitution method requires proof that if the constant c>0 is appropriately selected, there can be T(n)<=cnlgn. First, suppose this upper bound is for all The integer m<n is all true, especially for m=⌊n/2⌋, there is T(⌊n/2⌋)<=c⌊n/2⌋lg(⌊n/2⌋), which is substituted into the recursive formula, Get:
Insert picture description here
Among them, as long as c>=1, the last step will be established. The mathematical induction method requires proof that the solution is also valid under the boundary conditions. Assume that T(1)=1 is the only boundary condition (initial condition) of the recursive formula. For n=1, the boundary condition T(n)<=cnlgn derives T (1)<=c1lg1=0, which contradicts T(1)=1. At this time, n can be extended, taking n=2 as the boundary condition, and T(2)=4 and T(3)= can be calculated from the recursive formula 5. At this time, the inductive proof can be completed: for a certain constant c>=1, T(n)<=cnlgn, the method is to choose a large enough c to satisfy T(2)<=c2lg2 and T(3)<=c3lg3 .

But the substitution method has no universal method to guess the correct recursive solution. Guessing the solution requires experience and occasionally creativity. You can borrow a recursive tree to make a good guess. If the recursive formula is similar to the recursive formula you have seen, it is reasonable to guess a similar solution, such as the following recursive formula:
Insert picture description here
Compared with the above example, it is only +17. When n is large, it is close to n Half, so guess T(n)=O(nlgn), this guess is correct.

Another good guessing method is to prove the loose upper and lower bounds of the recursive formula first, and then reduce the uncertainty range. As in the above example, you can start from the lower bound Ω(n), because the recursive formula contains the term n. We can prove an upper bound O(n²), and then gradually lower the upper bound and raise the lower bound until it converges to the asymptotically compact bound θ(nlgn).

Sometimes the asymptotic bound is guessed, but the inductive proof fails. At this time, modify the guess and subtract a low-order term from it. The mathematical proof can often proceed smoothly, such as the following recursive formula:
Insert picture description here
we guess the solution is T(n)=O (N), and try to prove that for a proper constant c, T(n)<=cn is true, substitute the guess into the recursive formula, and get:
Insert picture description here
But this does not mean that for any c, T(n)<cn , We may guess a larger bound, such as T(n)=O(n²), although the result can be inferred, the original T(n)=O(n) is correct.

We make a new guess: T(n)<=cn-d, d is a constant ≥0, and now there is:
Insert picture description here
as long as d≥1, this formula holds, then as before, choose a large enough c to deal with the boundary condition.

You may find that the idea of ​​subtracting a low-order term is counterintuitive. After all, if the upper bound proves to be unsuccessful, the guess should be increased rather than decreased, but a looser bound is not necessarily easier to prove, because in order to prove a weaker For the upper bound, the same weaker bound must be used in the inductive proof.

In the above example, we may have the following wrong proof:
Insert picture description here
because c is a constant, the error is that we have not proved a form that is strictly consistent with the induction hypothesis, that is, T(n)≤cn.

The rest is too difficult to learn, maybe later

Guess you like

Origin blog.csdn.net/tus00000/article/details/114990966