-
Book Overview & Buying
-
Table Of Contents
-
Feedback & Rating
C++ Data Structures and Algorithm Design Principles
By :
Let's begin by considering the base case and recurrence relation for this problem. Unlike some of the other examples we have discussed in this chapter, this particular problem has just one base case – the point at which the destination has been reached. The intermediate states are also quite simple: given a location at index i that has a distance limit of x, we can travel to any location between indices i + 1 and i + x (inclusive). For example, let's consider the following two cities:
Let's say we wanted to calculate the number of ways to reach the city at index 3. Because we can reach city 3 from both city 1 and city 2, the number of ways to reach city 3 is equivalent to the sum of the number of ways to reach city 1 and the number of ways to reach city 2. This recurrence is quite similar to the Fibonacci series, except that the number of previous states from which the current state's substructure is formed is variable according to the values of distance.
So, let's say we have the following four cities:
[1]: distance = 5
[2]: distance = 3
[3]: distance = 1
[4]: distance = 2
From this, we want to calculate the number of ways to travel to city 5. To do this, we can formulate the substructure as follows:
Cities reachable from index [1] -> { 2 3 4 5 6 }
Cities reachable from index [2] -> { 3 4 5 }
Cities reachable from index [3] -> { 4 }
Cities reachable from index [4] -> { 5 6 }
We can now invert this logic to find the cities from which we can travel through to reach a given location:
Cities that connect to index [1] -> START
Cities that connect to index [2] -> { 1 }
Cities that connect to index [3] -> { 1 2 }
Cities that connect to index [4] -> { 1 2 3 }
Cities that connect to index [5] -> { 1 2 }
Taking this a step further, we can now devise an outline of the state logic:
Ways to reach City 1 = 1 (START)
Ways to reach City 2 = 1
1 " 2
Ways to reach City 3 = 2
1 " 2 " 3
1 " 3
Ways to reach City 4 = 4
1 " 2 " 3 " 4
1 " 2 " 4
1 " 3 " 4
1 " 4
Ways to reach City 5 = 6
1 " 2 " 3 " 4 " 5
1 " 2 " 4 " 5
1 " 2 " 5
1 " 3 " 4 " 5
1 " 4 " 5
1 " 5
Thus, we can define the recurrence as follows:
F(1) = 1 (We have reached the destination)
In other words, the number of ways to reach a given location is equal to the sum of the number of ways to reach each location that connects to it. Using this logic, a recursive function for solving this problem might look like this:
F(n) -> number of ways to reach n'th location
F(i) =
if i = N:
return 1
Otherwise:
result = 0
for j = 1 to distance[i]:
result = result + F(i + j)
return result
Now that we have a functional definition of the problem's states, let's begin implementing it in code.
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
typedef long long LL;
const LL MOD = 1000000007;
Handling the input and output in this problem can be implemented very simply:
int main()
{
int n;
cin >> n;
vector<int> distance(n);
for(int i = 0; i < n; i++)
{
cin >> distance[i];
}
LL result = TravelItinerary(n, distance);
cout << result << endl;
return 0;
}
LL TravelItinerary(int n, vector<int> distance)
{
...
}
DP -> Array of size N + 1
DP[0] = 1 (There is one way to reach the starting location)
for i = 0 to N-1:
for j = 1 to distance[i]:
DP[i + j] += DP[i]
return DP[N]
vector<LL> DP(n + 1, 0);
DP[0] = 1;
reverse(distance.begin(), distance.end());
DP[0] = 1;
for(int i = 1; i <= n; i++)
{
int dist = distance[i-1];
for(int j = 1; j <= dist; j++)
{
DP[i] = (DP[i] + DP[i – j]) % MOD;
}
}
return DP[n];
This is certainly a viable solution to the problem that will be completely satisfactory in the vast majority of cases. However, since dynamic programming is first and foremost an optimization technique, we should still ask ourselves if a better approach exists.
Handling the Extra Credit Test Case
As both n and the maximum distance value increase, even the preceding algorithm will eventually prove to be rather inefficient. If n = 10000000 and the distance values can vary between 1 and 10000, then the inner for loop would have to perform nearly 100000000000 iterations in the worst case. Thankfully, there is a very simple technique that will allow us to completely remove the inner loop, which means we will have to perform exactly n iterations for any input.
To handle this reduction, we will create a prefix sum array, which will allow us to calculate the range sums we previously handled by the inner loop in constant time. If you are unfamiliar with this technique, the basic concept is as follows:
Take a look at the following example:
0 1 2 3 4
A = { 3 1 10 2 5 }
0 1 2 3 4 5
sums = { 0 3 4 14 16 21 }
range(1, 3) = A[1] + A[2] + A[3]
= 1 + 10 + 2
= 13
sums[4] – sums[1] = 13
range(3, 4) = A[3] + A[4]
= 2 + 5
= 7
sums[5] – sums[3] = 7
LL TravelItinerary(int n, vector<int> distance)
{
vector<LL> DP(n + 1, 0);
vector<LL> sums(n + 2, 0);
DP[0] = sums[1] = 1;
reverse(distance.begin(), distance.end());
for(int i = 1; i <= n; i++)
{
int dist = distance[i-1];
LL sum = sums[i] – sums[i – dist];
DP[i] = (DP[i] + sum) % MOD;
sums[i + 1] = (sums[i] + DP[i]) % MOD;
}
return DP[n];
}
return (DP[n] < 0) ? DP[n] + MOD : DP[n];
With these slight modifications, we now have an elegant and efficient solution to the problem that can handle massive input arrays in a fraction of a second!
vector<string> types =
{
"BRUTE FORCE",
"MEMOIZATION",
"TABULATION"
};
const int UNKNOWN = INT_MAX;
void GetTime(clock_t &timer, string type)
{
timer = clock() - timer;
cout << "TIME TAKEN USING " << type << ": " << fixed << setprecision(5) << (float)timer / CLOCKS_PER_SEC << " SECONDS" << endl;
timer = clock();
}
int LCS_Memoization(string A, string B, int i, int j, vector<vector<int>> &memo)
{
……
}
// Base case — LCS is always zero for empty strings
if(i == 0 || j == 0)
{
return 0;
}
// Have we found a result for the prefixes of the two strings?
if(memo[i - 1][j - 1] != UNKNOWN)
{
// If so, return it
return memo[i - 1][j - 1];
}
// Are the last characters of A's prefix and B's prefix equal?
if(A[i-1] == B[j-1])
{
// LCS for this state is equal to 1 plus the LCS of the prefixes of A and B, both reduced by one character
memo[i-1][j-1] = 1 + LCS_Memoization(A, B, i-1, j-1, memo);
// Return the cached result
return memo[i-1][j-1];
}
// If the last characters are not equal, LCS for this state is equal to the maximum LCS of A's prefix reduced by one character and B's prefix, and B's prefix reduced by one character and A's prefix
memo[i-1][j-1] = max(LCS_Memoization(A, B, i-1, j, memo),
LCS_Memoization(A, B, i, j-1, memo));
return memo[i-1][j-1];
int main()
{
string A, B;
cin >> A >> B;
int tests = 2;
clock_t timer = clock();
for(int i = 0; i < tests; i++)
{
int LCS;
switch(i)
{
case 0:
{
LCS = LCS_BruteForce(A, B, 0, 0, {});
#if DEBUG
PrintSubsequences(A, B);
#endif
break;
}
case 1:
{
vector<vector<int>> memo(A.size(), vector<int>(B.size(), UNKNOWN));
LCS = LCS_Memoization(A, B, A.size(), B.size(), memo);
break;
}
}
cout << "Length of the longest common subsequence of " << A << " and " << B << " is: " << LCS << ends;
GetTime(timer, types[i]);
cout << endl;
}
return 0;
}
SIZE = 3
ABC________ ABC_______
SIZE = 4
ABC_B______ ABCB______
ABC_B______ ABC___B___
ABC_B______ ABC______B
ABC___B____ ABC______B
ABC____E___ ABC____E__
ABC______B_ ABC___B___
ABC______B_ ABC______B
ABC_______A ABC_____A_
SIZE = 5
ABCAB______ ABC_____AB
ABC_B_B____ ABCB_____B
ABC_B__E___ ABCB___E__
ABC_B____B_ ABCB__B___
ABC_B____B_ ABCB_____B
ABC_B_____A ABCB____A_
ABC_B_B____ ABC___B__B
ABC_B__E___ ABC___BE__
ABC_B____B_ ABC___B__B
ABC_B_____A ABC___B_A_
ABC___BE___ ABC___BE__
ABC____E_B_ ABC____E_B
ABC____E__A ABC____EA_
ABC_____FB_ ABC__FB___
ABC______BA ABC___B_A_
SIZE = 6
ABC_B_BE___ ABCB__BE__
ABC_B__E_B_ ABCB___E_B
ABC_B__E__A ABCB___EA_
ABC_B___FB_ ABCB_FB___
ABC_B____BA ABCB__B_A_
ABC_B__E_B_ ABC___BE_B
ABC_B__E__A ABC___BEA_
ABC___BE_B_ ABC___BE_B
ABC___BE__A ABC___BEA_
ABC____EFB_ ABC_EFB___
ABC_____FBA ABC__FB_A_
SIZE = 7
ABC_B_BE_B_ ABCB__BE_B
ABC_B_BE__A ABCB__BEA_
ABC_B__EFB_ ABCBEFB___
ABC_B___FBA ABCB_FB_A_
ABC____EFBA ABC_EFB_A_
SIZE = 8
ABC_B__EFBA ABCBEFB_A_
Length of the longest common subsequence of ABCABDBEFBA and ABCBEFBEAB is: 8
TIME TAKEN USING BRUTE FORCE: 0.00242 SECONDS
Length of the longest common subsequence of ABCABDBEFBA and ABCBEFBEAB is: 8
TIME TAKEN USING MEMOIZATION: 0.00003 SECONDS
Length of the longest common subsequence of ABCABDBEFBA and ABCBEFBEAB is: 8
TIME TAKEN USING BRUTE FORCE: 0.00055 SECONDS
Length of the longest common subsequence of ABCABDBEFBA and ABCBEFBEAB is: 8
TIME TAKEN USING MEMOIZATION: 0.00002 SECONDS
Length of the longest common subsequence of ABZCYDABAZADAEA and YABAZADBBEAAECYACAZ is: 10
TIME TAKEN USING BRUTE FORCE: 8.47842 SECONDS
Length of the longest common subsequence of ABZCYDABAZADAEA and YABAZADBBEAAECYACAZ is: 10
TIME TAKEN USING MEMOIZATION: 0.00008 SECONDS
The actual values for the time taken will vary depending on your system. Please note the difference in the values.
As we can clearly see, the gains in performance provided by memoization are quite significant!
As we did previously, we will add a new function, LCS_Tabulation(), to the same code file that contains our brute-force and memoized solutions.
string LCS_Tabulation(string A, string B)
{
……
}
vector<vector<int>> DP(A.size() + 1, vector<int>(B.size() + 1));
for(int i = 0; i <= A.size(); i++)
{
for(int j = 0; j <= B.size(); j++)
{
……
}
}
if(i == 0 || j == 0)
{
DP[i][j] = 0;
}
else if(A[i-1] == B[j-1])
{
DP[i][j] = DP[i-1][j-1] + 1;
}
else
{
DP[i][j] = max(DP[i-1][j], DP[i][j-1]);
}
string LCS_Tabulation(string A, string B)
{
vector<vector<int>> DP(A.size() + 1, vector<int>(B.size() + 1));
for(int i = 0; i <= A.size(); i++)
{
for(int j = 0; j <= B.size(); j++)
{
if(i == 0 || j == 0)
{
DP[i][j] = 0;
}
else if(A[i-1] == B[j-1])
{
DP[i][j] = DP[i-1][j-1] + 1;
}
else
{
DP[i][j] = max(DP[i-1][j], DP[i][j-1]);
}
}
}
int length = DP[A.size()][B.size()];
……
}
At this point, we have discussed several ways to find the length of the longest common subsequence, but what if we also want to output its actual characters? Of course, our brute-force solution does this, but very inefficiently; however, using the results contained in the preceding DP table, we can use backtracking to reconstruct the LCS quite easily. Let's highlight the path we would need to follow in the table to accomplish this:

By collecting the characters associated with each column in the path where the value increases, we get the LCS ABCBEFBA.
if i = 0 or j = 0:
Return an empty string
If the characters at the end of A's prefix and B's prefix are equal:
Return the LCS of the next smaller prefix of both A and B, plus the equal character
Otherwise:
If the value of DP(i - 1, j) is greater than the value of DP(i, j - 1):
– Return the LCS of A's next smaller prefix with B's current prefix
– Otherwise:
Return the LCS of B's next smaller prefix with A's current prefix
In C++, this can be coded as follows:
string ReconstructLCS(vector<vector<int>> &DP, string &A, string &B, int i, int j)
{
if(i == 0 || j == 0)
{
return "";
}
if(A[i-1] == B[j-1])
{
return ReconstructLCS(DP, A, B, i-1, j-1) + A[i-1];
}
else if(DP[i-1][j] > DP[i][j-1])
{
return ReconstructLCS(DP, A, B, i-1, j);
}
else
{
return ReconstructLCS(DP, A, B, i, j-1);
}
}
string LCS_Tabulation(string A, string B)
{
……
string lcs = ReconstructLCS(DP, A, B, A.size(), B.size());
return lcs;
}
int main()
{
string A, B;
cin >> A >> B;
int tests = 3;
clock_t timer = clock();
for(int i = 0; i < tests; i++)
{
int LCS;
switch(i)
{
……
case 2:
{
string lcs = LCS_Tabulation(A, B);
LCS = lcs.size();
cout << "The longest common subsequence of " << A << " and " << B << " is: " << lcs << endl;
break;
}
}
cout << "Length of the longest common subsequence of " << A << " and " << B << " is: " << LCS << endl;
GetTime(timer, types[i]);
}
return 0;
}
Length of the longest common subsequence of ABCABDBEFBA and ABCBEFBEAB is: 8
TIME TAKEN USING BRUTE FORCE: 0.00060 SECONDS
Length of the longest common subsequence of ABCABDBEFBA and ABCBEFBEAB is: 8
TIME TAKEN USING MEMOIZATION: 0.00005 SECONDS
The longest common subsequence of ABCABDBEFBA and ABCBEFBEAB is: ABCBEFBA
Length of the longest common subsequence of ABCABDBEFBA and ABCBEFBEAB is: 8
TIME TAKEN USING TABULATION: 0.00009 SECONDS
The actual values for the time taken will vary depending on your system. Please note the difference in the values.
Now, we have looked at another detailed example of how the same logic can be applied to the same problem using different techniques and the corresponding effect this has on the execution time of the algorithm.
The first question to ask ourselves is: what constitutes a single state in this problem?
Base case --> Empty set:
Given that our options are to either append a note to a previous subset or leave it as-is, we could restate the logic as follows:
For a given note in the melody, the count of subsets of size | n | containing the note is equal to the total count of all subsets of size | n - 1 | that did not contain the note.
So, each state can be expressed in two dimensions:
In pseudocode, the logic could be expressed as follows:
for i = 1 to length of melody (inclusive):
for each subset previously found:
DP(i, subset) = DP(i, subset) + DP(i - 1, subset)
DP(i, subset ∪ melody[i - 1]) = DP(i, subset ∪ melody[i - 1]) + DP(i - 1, subset)
So, the primary question now is, how can we represent these states?
Remember that for an n-element collection, there are a total of 2n subsets comprising it — for example, a set of 4 elements can be divided into a total of 24 (or 16) subsets:
S = { A, B, C, D }
{ } —> { _ _ _ _ }
{ A } —> { # _ _ _ }
{ B } —> { _ # _ _ }
{ C } —> { _ _ #_ }
{ D } —> { _ _ _ # }
{ A, B } —> { # # _ _ }
{ A, C } —> { # _ #_ }
{ A, D } —> { # _ _ # }
{ B, C } —> { _ # #_ }
{ B, D } —> { _ # _ # }
{ C, D } —> { _ _ # # }
{ A, B, C } —> { # # # _ }
{ A, B, D } —> { # # _ # }
{ A, C, D } —> { # _ # # }
{ B, C, D } —> { _ # # # }
{ A, B, C, D } —> { # # # # }
If we iterate from 0 to (24 - 1) inclusive in binary, we get the following numbers:
0 —> 0000 —> { _ _ _ _ }
1 —> 0001 —> { # _ _ _ }
2 —> 0010 —> { _ # _ _ }
3 —> 0011 —> { # # _ _ }
4 —> 0100 —> { _ _ # _ }
5 —> 0101 —> { # _ # _ }
6 —> 0110 —> { _ # # _ }
7 —> 0111 —> { # # # _ }
8 —> 1000 —> { _ _ _ # }
9 —> 1001 —> { # _ _ # }
10 —> 1010 —> { _ # _ # }
11 —> 1011 —> { # # _ # }
12 —> 1100 —> { _ _ # # }
13 —> 1101 —> { # _ # # }
14 —> 1110 —> { _ # # # }
15 —> 1111 —> { # # # # }
As we can see, the digits of each binary number from 0 to 2n correspond exactly to the indices of one possible subset of n elements. Since there are 12 notes in the scale, this means there is a total of 212 (or 4,096) possible subsets of notes. By mapping each note in the scale to a power of 2, we can use bitwise arithmetic to represent the subsets encountered across each state.
The following are the steps to solve this activity:
#include <iostream>
#include <vector>
#include <string>
#include <map>
using namespace std;
int main()
{
int melodyLength;
int setLength;
cin >> melodyLength;
vector<string> melody(melodyLength);
for(int i = 0; i < melodyLength; i++)
{
cin >> melody[i];
}
cin >> setLength;
vector<string> set(setLength);
for(int i = 0; i < setLength; i++)
{
cin >> set[i];
}
……
}
vector<int> ConvertNotes(vector<string> notes)
{
map<string, int> M =
{
{ "A", 0 },
{ "A#", 1 },
{ "Bb", 1 },
{ "B", 2 },
{ "Cb", 2 },
{ "B#", 3 },
{ "C", 3 },
{ "C#", 4 },
{ "Db", 4 },
{ "D", 5 },
{ "D#", 6 },
{ "Eb", 6 },
{ "E", 7 },
{ "Fb", 7 },
{ "E#", 8 },
{ "F", 8 },
{ "F#", 9 },
{ "Gb", 9 },
{ "G", 10 },
{ "G#", 11 },
{ "Ab", 11 }
};
vector<int> converted;
for(auto note : notes)
{
// Map to powers of 2
converted.push_back(1 << M[note]);
}
return converted;
}
int CountMelodicPermutations(vector<int> melody, vector<int> set)
{
……
}
unsigned int target = 0;
for(auto note : set)
{
target |= note;
}
C = 3
F# = 9
A = 0
converted = { 23, 29, 20 } = { 8, 512, 1 }
target = (8 | 512 | 1) = 521
0000001000
+ 0000000001
+ 1000000000
= 1000001001
vector<vector<int>> DP(melody.size() + 1, vector<int>(4096, 0));
Base case:
DP(0, 0) —> 1
Recurrence:
DP(i, subset) —> DP(i, subset) + DP(i - 1, subset)
DP(i, subset ∪ note[i-1]) —> DP(i, subset ∪ note[i]) + DP(i - 1, subset)
Here, i ranges from 1 to the length of the melody. We can write the preceding logic in C++ like this:
// Base case —> empty set
DP[0][0] = 1;
for(int i = 1; i <= melody.size(); i++)
{
for(unsigned int subset = 0; subset < 4096; subset++)
{
// Keep results for previous values of i
DP[i][subset] += DP[i-1][subset];
// Add results for union of subset with melody[i-1]
DP[i][subset | melody[i-1]] += DP[i-1][subset];
}
}
// Solution
return DP[melody.size()][target];
int count = CountMelodicPermutations(ConvertNotes(melody), ConvertNotes(set));
cout << count << endl;
Change the font size
Change margin width
Change background colour