Dynamic Programming (DP) was developed by Richard Bellman in the 1950s and has found applications in numerous fields, from aerospace engineering to economics.

 

DP is an algorithm technique that is closely related to the divide and conquer approach. However, while the divide and conquer approach is essentially recursive, and so "top down", dynamic programming works "bottom up".

 

This method is an optimisation approach that transforms a complex problem into a sequence of simpler problems. It creates an array of related but simpler sub-problems, and then, it computes the solution to the big complicated problem by using the solution to the easier sub-problems which are stored in the array. In this way, the efficiency of the CPU can be enhanced.

 

DP solutions have a polynomial complexity which assures a much faster running time than other techniques like backtracking, brute-force etc. For example, the following simple recursive solution (top down) for Fibonacci Numbers gets exponential time complexity.

int fib (int n) {
    if (n <= 1)
    	return n;
    return fib(n - 1) + fib(n - 2);
}

The time complexity reduces to linear in the below-optimised code (bottom up).

std::vector<int> f{0, 1};
for (int i = 2; i <= n; ++i) {
	f.emplace_back(f[i - 1] + f[i - 2]);
}
return f[n];

DP works by storing the result of sub-problems so that when their solutions are required, they are at hand and we do not need to recalculate them. The technique of storing the results of already solved sub-problems is called memoisation. By saving the values in the array, we save time for computations of sub-problems we have already come across.

'UoL > Algorithms' 카테고리의 다른 글

Manacher's Algorithm  (0) 2021.09.12
Window Sliding Technique  (0) 2021.09.11
Hopcroft-Karp algorithm (호프크로프트-카프)  (0) 2021.09.05
Breadth-first, Depth-first searches (BFS/DFS)  (0) 2021.09.03
Recursion vs. Iteration for Fibonacci  (0) 2021.08.25

The Manacher's algorithm is the problem of finding the longest substring which is a palindrome.

The time complexity of the algorithm is O(n).

 

The basic idea of the algorithm is to find the symmetric string on both sides of the centre. For example, abcdcba and madamisimadam. If the length of the string is even, a special character should be added as follows:

  • S: abba
  • S´: |a|b|b|a|

The length of the right string is 9, 2 * length(S) + 1, which is odd.

 

Let P be an array of S´:

| a | b | b | a |

Each position of the array can be the centre to calculate the length of the palindromic string of the positions.

Depending on the position of the centre, the palindromic lengths are stored as below:

Index 0 1 2 3 4 5 6 7 8
| a | b | b | a |
Len 0 3 0 3 5 3 0 3 0

In the array, the centre of the longest palindromic string is at index 4.

 

Pseudocode

Longest_Palindrome(string S) {
        string S' = S with a bogus character (eg. '|') inserted between each character (including outer boundaries)
        array PalindromeRadii = [0,...,0]
        // The radius of the longest palindrome centered on each place in S'
        // note: length(S') = length(PalindromeRadii) = 2 × length(S) + 1
        
        Center = 0
        Radius = 0
        
        while Center < length(S') 
        {
            // At the start of the loop, Radius is already set to a lower-bound for the longest radius.
            // In the first iteration, Radius is 0, but it can be higher.
            
            // Determine the longest palindrome starting at Center-Radius and going to Center+Radius
            while Center-(Radius+1) >= 0 and Center+(Radius+1) < length(S') and S'[Center-(Radius+1)] = S'[Center+(Radius+1)] {
                Radius = Radius+1
            }             
         
            // Save the radius of the longest palindrome in the array
            PalindromeRadii[Center] = Radius
            
            // Below, Center is incremented.
            // If any precomputed values can be reused, they are.
            // Also, Radius may be set to a value greater than 0
            
            OldCenter = Center
            OldRadius = Radius
            Center = Center+1
            // Radius' default value will be 0, if we reach the end of the following loop. 
            Radius = 0 
            while Center <= OldCenter + OldRadius {
                // Because Center lies inside the old palindrome and every character inside
                // a palindrome has a "mirrored" character reflected across its center, we
                // can use the data that was precomputed for the Center's mirrored point. 
                MirroredCenter = OldCenter - (Center - OldCenter)
                MaxMirroredRadius = OldCenter + OldRadius - Center
                if PalindromeRadii[MirroredCenter] < MaxMirroredRadius {
                    PalindromeRadii[Center] = PalindromeRadii[MirroredCenter]
                    Center = Center+1
                }   
                else if PalindromeRadii[MirroredCenter] > MaxMirroredRadius {
                    PalindromeRadii[Center] = MaxMirroredRadius
                    Center = Center+1
                }   
                else { // PalindromeRadii[MirroredCenter] = MaxMirroredRadius
                    Radius = MaxMirroredRadius
                    break  // exit while loop early
                }   
            }      
        }
        
        longest_palindrome_in_S' = 2*max(PalindromeRadii)+1
        longest_palindrome_in_S = (longest_palindrome_in_S'-1)/2
        return longest_palindrome_in_S 
    }

Code

int longest_palindrome(string s)
{
	string s2 = "";
	for (int i = 0; i < s.length(); i++)
	{
		s2 += '|';
		s2 += s[i];
	}
	s2 += '|';

	int centre = 0, radius = 0;
	int oldCentre, oldRadius;
	int mirrorCentre, maxMirroredRadius;

	vector<int> palindromeNum;
	palindromeNum.resize(s2.length());

	while (centre < s2.length())
	{
		while ((centre - (radius + 1) >= 0) && (centre + (radius + 1) < s2.length()) &&
			s2[centre - (radius + 1)] == s2[centre + (radius + 1)])
		{
			radius++;
		}

		palindromeNum[centre] = radius;
		oldCentre = centre;
		oldRadius = radius;
		centre++;
		radius = 0;

		while (centre <= (oldCentre + oldRadius))
		{
			mirrorCentre = oldCentre - (centre - oldCentre);
			maxMirroredRadius = oldCentre + oldRadius - centre;
			if (palindromeNum[mirrorCentre] < maxMirroredRadius)
			{
				palindromeNum[centre] = palindromeNum[mirrorCentre];
				centre++;
			}
			else if (palindromeNum[mirrorCentre] > maxMirroredRadius)
			{
				palindromeNum[centre] = maxMirroredRadius;
				centre++;
			}
			else
			{
				radius = maxMirroredRadius;
				break;
			}
		}
	}
	return palindromeNum[max_element(palindromeNum.begin(), palindromeNum.end()) - palindromeNum.begin()];
}

The window sliding algorithm is used to convert two nested loops (bruth force) into a single loop (linear), which means it enables us to reduce the time complexity from O(n²) to O(n).

 

The following is an example of converting the bruth force approach into the window sliding technique.

 

Given an array A = { 1, 3, 5, 6, 8, 10 }, find the maximum sum of 3 consecutive elements.

 

Bruth Force

int maxSum = 0;

for (int i = 0; i < A.size() - 3; i++)
{
	int sum = 0;
    for (int j = 0; j < 3 + 1; j++)
    {
    	sum += A[i + j];
    }
    maxSum = max(sum, maxSum);
}

return maxSum;

 

Window Sliding

int maxSum = 0;
int panes = 0;
int windowSum = 0;
for (int i = 0; i < sizeA + 1; i++)
{
  if (panes == 3)
  {
    maxSum = max(maxSum, windowSum);
    windowSum -= A[i - 3];
    panes--;
  }

  windowSum += A[i];
  panes++;
}

'UoL > Algorithms' 카테고리의 다른 글

Dynamic Programming (DP)  (0) 2021.10.17
Manacher's Algorithm  (0) 2021.09.12
Hopcroft-Karp algorithm (호프크로프트-카프)  (0) 2021.09.05
Breadth-first, Depth-first searches (BFS/DFS)  (0) 2021.09.03
Recursion vs. Iteration for Fibonacci  (0) 2021.08.25

+ Recent posts