If you haven't heard of Graphs, or have very little knowledge in this subject. I strongly advise you to read my posts:

Tarjan's Algorithm is a classic solution in competitive programming used to find strongly connected components in a directed graph. A strongly connected component is a group of nodes where you can travel from any node to any other node in the group and find a way back. You can kind of see a strongly connected component as an island of nodes in a graph that forms a loop.

This algorithm is extremely similar to the DFS algorithm, I personally love the recursive implementation of this algorithm that works as follows:

Initialize a "state" vector, this will replace the "visited" vector on the classic DFS algorithm, it works very similarly, the big difference is that the visited vector is made out of booleans since you only have two states for a node: visited, and not visited. However, now with our state vector, we have 3 states for a node: 0, 1, and 2, where 0 represents not visited, 1 represents visited, and 2 represents completed.

For each node that is

**not completed**, send the tarjan function through that node.On every Tarjan function call, this procedure is going to mark the current node as visited/1, visit all adjacent nodes to the node, and if you find that one of them has a state of 1, it means that a loop exists, in which case you must end the recursion and output that a loop exists in the graph, if the adjacent node has a state of 2, it means that it has been completed before and you can simply ignore it. if an adjacent node has a state of 0 then you must send the Tarjan algorithm through that unvisited node, repeating the process until there are no more unvisited adjacent nodes. When that happens, you mark the node as completed.

This sounds a bit confusing with text, let's visualize the algorithm in action.

In this first animation, we can see how the algorithm successfully traverses the graph without encountering a cycle, this means that this graph is a valid DAG (Directed Acyclic Graph). Now let's change the graph just a little bit, let's flip the direction of the edge connecting nodes 2 and 6 and run the algorithm again.

There are several ways to implement this algorithm, below I showcase my personal choice, which uses an adjacency list to represent the graph, recursion, and it's the most simple in my opinion.

`// Data structure to represent the graph. Each node is represented by an integer, // and graph[node] is a vector of the nodes to which node has an outgoing edge.vector<vector<int>> graph;// The state of each node in the graph during the traversal:// 0 = not visited, 1 = visited, 2 = completed.vector<int> state;bool tarjan(int node){ state[node] = 1; // Mark the node as visited for(auto adj : graph[node]){ // Iterate through all nodes adjacent to the current node. if(state[adj] == 2) continue; // Skip nodes that are already completed. if(state[adj] == 1) return true; // A cycle is detected if an adjacent node is visited but not yet finished. if(tarjan(adj)) return true; // Recur for adjacent nodes, and return true if any of those recursions detect a cycle. } state[node] = 2; // Mark the node as finished. return false; // No cycle was detected from this node.}int main() { int n, m; cin >> n >> m; // Read the number of nodes and edges. graph.resize(n+1); // Resize the graph to have n+1 elements (nodes are numbered from 1 to n). state.resize(n+1, 0); // Resize the state vector and initialize all states to 0 (not yet visited). for(int i = 0; i < m; i++){ // Read the edges of the graph from standard input. int a, b; cin >> a >> b; graph[a].push_back(b); // Add an edge from node a to node b. } bool cycleFound = false; for(int node = 1; node <= n; node++){ // Check each node for cycles. if(state[node] != 2){ // Skip nodes that are already finished. if(tarjan(node)) cycleFound = true; // If a cycle is detected from this node, set cycleFound to true. } } // Output whether or not a cycle was detected. cout << (cycleFound ? "There is a cycle in this graph" : "valid DAG graph :D");}`

In competitive programming, Tarjan's Algorithm is essential for solving problems related to network connectivity and finding cycles in graphs, more concisely, this algorithm is mainly used to check if a graph is a valid DAG graph (Directed Acyclic Graph) this is a directed graph that does not contain loops. Being able to identify strongly connected components efficiently is often the key to optimizing your solutions and excelling in competitions.

Enough talk for now, let's understand this better by solving two very cool competitive programming challenges using this algorithm.

There is a city represented by a rectangular grid of N x M squares (1 <= N, M <= 1000), unfortunately for this city, there is a gang of criminals that wants to take control of the streets and are not planning on leaving, thankfully, the local police have developed an amazing trap that can cover an entire square of the city grid, if any gang member lands on one of those squares it is guaranteed that he will be captured. The police chief of this city has asked for your help in developing an algorithm that outputs the minimum amount of traps needed to eventually catch the entire gang. To do so, he has valuable information for you. After countless hours of analyzing each individual square of the city, they have figured out that the gang has a fixed direction to go after for each square.

The matrix above is an example of the information given to you, it means that for square {0,0} it is guaranteed that if a criminal lands on that square, he is going to head to the South, square {1,0} next.

S = South

N = North

E = East

W = West

It's guaranteed that the information will always be valid, you can be certain that no square is going to point outside the city.

For the Example shown above the answer should be 4.

Above is one correct layout for the traps, I encourage you to pretend there is a criminal in each cell and follow the "path" for each one in order to visualize how they eventually end up in one of the traps. There are several correct layouts for this example, however, the minimum amount needed is 4. Remember that you don't need to output the configuration of the traps, just the **minimum amount.**

This problem looks very intimidating and just by looking at the problem you might think that the solution is very complex. However, that is not the case, the key to solving this problem is to look at the example map as an explicit graph first.

Once you grab a piece of paper and draw the explicit graph for the provided example you can clearly see how we have a directed graph and the answer is simply the amount of strongly connected components, or "islands" in the graph. Since we have possible cycles we have to use Tarjan's algorithm.

`// Defining arrays to store the city grid and visited status of each squarechar map[1005][1005]; // Stores the direction in which the gang will move from each squareint v[1005][1005]; // Visited status: 0 (not visited), 1 (visited), 2 (fully processed)int res = 0; // Result variable to store the total number of traps required// Tarjan's algorithm function to process each square in a depth-first mannervoid tarjan(int i, int j){ v[i][j] = 1; // Marking the current square as visited int adji, adjj; // Variables to hold coordinates of the adjacent square // Determining the coordinates of the adjacent square based on the direction if(map[i][j] == 'N'){adji = i-1; adjj = j;} if(map[i][j] == 'S'){adji = i+1; adjj = j;} if(map[i][j] == 'E'){adji = i; adjj = j+1;} if(map[i][j] == 'W'){adji = i; adjj = j-1;} // If the adjacent square is visited but not processed, increment the trap count if(v[adji][adjj] == 1)res++; // If the adjacent square is unvisited, recursively apply Tarjan's algorithm else if(v[adji][adjj] == 0)tarjan(adji, adjj); v[i][j] = 2; // Marking the current square as fully processed}// Main functionint main(){ int n, m; // Variables to hold the dimensions of the city grid cin >> n >> m; // Reading the dimensions from the input // Reading the city grid from the input for(int i=0; i<n; i++) for(int j=0; j<m; j++) cin >> map[i][j]; // Processing each square in the city grid for(int i=0; i<n; i++){ for(int j=0; j<m; j++){ // If the square is unvisited, apply Tarjan's algorithm if(v[i][j] == 0)tarjan(i, j); } } cout << res; // Outputting the total number of traps required return 0; // Returning 0 to indicate successful execution}`

**Time Complexity:**

**Grid Reading**: Reading the grid has a time complexity of (O(N * M)), where (N) is the number of rows and (M) is the number of columns.**Tarjan's Algorithm**: Tarjan's algorithm here processes each square in the grid exactly once. Specifically, each square is marked visited once and fully processed once. The recursive depth-first nature of the algorithm ensures that no square is visited more than once. This results in a time complexity of (O(N * M)) for the traversal.

Considering both the grid reading and Tarjan's algorithm, the overall time complexity remains (O(N * M)).

**Space Complexity:**

**map Array**: The space used to store the direction in each grid square is (O(N * M)).**v Array**: The space used to keep track of the visited status for each grid square is also (O(N * M)).**Recursive Stack**: In the worst case, the depth of the recursive calls for the`tarjan`

function can be (O(N * M)) if all squares are connected in a linear sequence. This is rare, but for complexity analysis, we consider worst-case scenarios.

Adding up all the above, the total space complexity is (O(N * M) + O(N * M) + O(N * M)) which simplifies to (O(N * M)).

In conclusion, both the time and space complexities of the provided code are (O(N*M)).

Mexicos City metro is one of the largest in the world, unfortunately, the wagon that you just boarded has a faulty door system. The wagon has P doors numbered from 1 to P, and D of those doors are faulty. Each of the faulty doors is linked to a specified group of doors which it affects. When you try to close a faulty door, the group for that faulty door opens (this group can even contain the door that you just tried to close). Of course, normal doors close without affecting anything.

You are given Q queries, each query is going to specify which doors are initially open and you must output whether or not it's possible to close all doors with "YES" or "NO".

for the following input:

`3 2 2 // P, D, Q (amount of doors, amount of faulty doors, amount of queries)2 1 1 // door 2 opens 1 door, door 11 1 2 // door 1 opens 1 door, door 21 3 // query, 1 door open, door 32 1 2 // query, 2 open doors 1 and 2`

The output should be:

`YESNO`

In the example we have P=3 (there are 3 doors), D=2 (2 doors are defective), and Q=2 (you will be asked 2 questions). When you close door 2, a door will open, which is number 1. When you close door 1, a door will open, which is number 2. In the first question, door 3 is open and the others are closed. Therefore, you only have to close door 3, since this one is not defective, no other doors will open and all doors can remain closed (the answer to that question is YES). In the second question, doors 1 and 2 are open and it is not possible to close one without opening the other (the answer to that question is NO).

1 <= P <=100000

The sum of all Di will not be greater than 1000000.

The sum of all Qi will not be greater than 1000000.

The Index of the doors will always be between 1 and P inclusive.

Once again we can see a great example of a quite intimidating problem, however, a great key point to mention is the fact that you are not asked for the minimum amount of actions (closing doors), or to recreate the "path" of actions you had to do in order to close all doors, instead, you are only asked if it's possible to close them all given an initial configuration.

By observing and thinking about the problem, you can realize that no matter how many faulty doors open functional ones, it will always be possible to close all functional doors at the end. Because of this, you only have to look at faulty doors.

Faulty doors can always be closed as long as you don't enter a cycle. for Example:

See how in example 1 you can eventually close the open door 5, however, in example 2 eventually you fall into a cycle after trying to close door 5 making it impossible to close all doors. Example 3 does not contain any initially open doors, I added the case to emphasize that the cycles are not a problem as long as no open doors lead to one.

After analyzing this, we can rephrase the problem as:

*Given a Directed Graph, output "NO" if an open door node leads to a cycle. else output "YES".*

Immediately we must think of Tarjan's algorithm. My first solution involved sending Tarjan's algorithm from each open node of each query, however, given that the amount of queries is very large, this solution will give us a TLE (Time Limit Exceded) verdict.

We can optimize the solution by running Tarjan's algorithm only once and in the case a cycle is found we can mark all the nodes that reached that cycle as "leads to cycle". The way we do this is by having a flipped graph like this:

We send a simple DFS algorithm from the node in which the cycle was found, marking all visited nodes by the DFS as nodes that lead to a cycle in a vector. This allows us to answer queries very simply by iterating through the open nodes and asking in constant time O(1) if they lead to a cycle.

`#include<bits/stdc++.h>using namespace std;// Declare global variables for the number of doors, faulty doors, and queriesint p, d, q;// Define a graph for door relations and its flipped versionvector<vector<int>>GRAPH;vector<vector<int>>FlippedGraph;// Vector to store the status of a node during tarjan's algorithmvector<int> status; // 0 not visited, 1 visited, 2 ended.// Vector to mark nodes that have been visited during DFSvector<int>visited(1e5+5,0);// Vector to mark nodes leading to a cyclevector<bool>leadsToCycle;// Depth First Search functionvoid dfs(int node){ leadsToCycle[node] = true; visited[node] = true; for(auto adj:FlippedGraph[node])if(!visited[adj])dfs(adj);}// Tarjan's algorithm to detect cyclesvoid tarjan(int node){ if(status[node] == 2)return; if(status[node] == 1){ dfs(node); }else{ status[node] = 1; for(auto adj:GRAPH[node])tarjan(adj); } status[node] = 2;}int main(){ // Input number of doors, faulty doors, and queries cin >> p >> d >> q; // Resize the graphs and vectors based on the number of doors GRAPH.resize(p+1); FlippedGraph.resize(p+1); status.resize(p+1); leadsToCycle.resize(p+1, 0); // Input relationships of faulty doors while(d--){ int node, size; cin >> node >> size; while(size--){ int adj; cin >> adj; GRAPH[node].push_back(adj); } } // Construct the flipped graph for relations for(int node = 1; node <= p; node++) for(auto adj:GRAPH[node])FlippedGraph[adj].push_back(node); // Check each door using tarjan's algorithm to find cycles for(int node = 1; node <=p; node++){ if(status[node] == 0){ tarjan(node); } } // Answer the queries while(q--){ int n; cin >> n; bool possible = true; while(n--){ int x; cin >> x; if(leadsToCycle[x])possible = false; } cout << (possible?"S":"N") << "\n"; // Output "S" for yes, "N" for no } return 0;}`

Reading the number of doors, faulty doors, and queries takes constant time, O(1).

Inputting relationships of faulty doors: This operation is bounded by the number of faulty doors and the number of affected doors. In the worst case, every door affects every other door, which would take O(P^2) time. But typically, this is a lot less than that in practice.

Constructing the flipped graph: This is a nested loop, iterating through each door and its associated doors. Again, this can go up to O(P^2) in the worst case.

Tarjan's algorithm for cycle detection: In essence, Tarjan's SCC (Strongly Connected Components) algorithm runs in linear time with respect to the number of nodes and edges, O(P + E), where E is the number of edges (or door-door relations in our context).

Answering the queries: For each query, in the worst case, we check every door. This takes O(P*Q) time for all queries.

Combining the above, our total time complexity is roughly O(P^2 + P + E + PQ).

**2. Space Complexity:**

Storing the

`GRAPH`

and`FlippedGraph`

: Since both of these graphs store relationships between doors, they can take up to O(P^2) space in the worst case.Auxiliary vectors (

`status`

,`visited`

,`leadsToCycle`

): All these vectors have a length of P, giving O(P) space for each, so O(3P) in total.Temporary variables in the main function and recursive calls stack space are also considered, but they won't exceed the above complexities.

Therefore, our total space complexity is roughly O(P^2 + 3P).

**In Summary:** Our algorithm is efficient enough for moderate values of P, Q, and E. It's designed to solve the problem by determining if we can close all doors without causing an endless loop of doors opening and closing.

As we wrap up our journey with Tarjan's algorithm, it's essential to remember its unique ability to find strongly connected components in directed graphs efficiently. Its depth-first search approach, combined with the use of low-link values, makes it an invaluable tool in a competitive programmer's toolkit. Whether you're tackling complex graph problems or exploring data structures, having Tarjan's algorithm under your belt can be a game-changer. I hope this was a good overview and the topics were well understood. Remember that there is a lot more to learn about graphs and we definitely didn't cover everything related to Tarjan's algorithm. Let me know your thoughts in the comment section below. Stay tuned for more and happy coding!

]]>Hello everyone! 😄 Welcome back to my blog! Today, we're diving into the fascinating world of greedy algorithms in competitive programming. If you've ever wondered how to spot a problem screaming for a greedy solution or how to tackle those classic challenges that have left many scratching their heads, you're in the right place! Buckle up as we embark on this enlightening journey, breaking down these problems with detailed illustrations and explanations.

A greedy algorithm builds a solution step-by-step, always selecting the option that seems best at that particular moment. This means it makes each choice without considering the broader context or reconsidering previous choices. Because of this inherent 'short-sightedness', greedy algorithms can be incredibly efficient but they might not always yield the correct result. It's intriguing to note that a single problem might present various paths for a greedy solution, yet typically only one of these paths is the true solution. Moreover, even if we stumble upon an approach that seems to work perfectly, proving its correctness across all scenarios can be a challenge, given the algorithm's fundamental greedy nature.

Starting with greedy algorithms can make you wonder:

*How do I know when to use a greedy approach?* And *How can I be sure my solution is right?*

It might feel tricky at first. But here's some good news: There are many classic problems with clear greedy solutions. By studying these problems and understanding how they're solved, you'll slowly get better at spotting when to use a greedy method and how to apply it to new challenges.

Imagine that you have an infinite supply of coins with values = {1, 2, 5, 10, 50, 100, 200} Now, someone gives you a number, let's call it N. The challenge? Figure out the fewest coins you'd need to make the exact amount of N.

Sounds fun, right? This problem is a classic one, and many find it a good starting point. The trick? Always choose the biggest coin that's not more than N. Once you've picked it, take it away from N, and keep going until you reach 0. Now, you might wonder, does this method always work? Well, for the coins that match our everyday money, the answer is yes!

Here's why our greedy coin-picking method works:

**Using Bigger Coins**: With coins like 1, 5, 10, 50, and 100, you'll never need more than one of each to get the best solution. Why? Well, if you tried to use two of the same coin (like two 5s), you could simply swap them out for a bigger coin (a 10) and end up with fewer coins.**A Limit on Some Coins**: For coins like 2 and 20, you won't need more than two of them. Three 2s can be swapped out for a 5 and a 1. Similarly, three 20s can be changed for a 50 and a 10. So, you see, there's a better way!**Avoiding Inefficient Mixes**: You also won't need combinations like 2 + 2 + 1 or 20 + 20 + 10 because you can just use a 5 or a 50 coin instead.**Best Way with Bigger Coins**: For any bigger coin, you can't make its value using only smaller coins without needing more of them. For instance, the closest you can get to 100 using smaller coins is 99 (with coins 50, 20, 20, 5, 2, and 2). So, the greedy method of always picking the biggest coin really is the best way.

This example shows that it can be difficult to argue that a greedy algorithm works, even if the algorithm itself is simple.

I want to reiterate the fact that this solution always works for coins 1,2,5,10,50, and 100 that follow our monetary system. HOWEVER, in a general case where the set of coins can contain random values like coins = {1,3,4} and N = 6, this algorithm is going to produce the solution 4 + 1 + 1 which is incorrect, since the most optimal solution is 3 + 3. For such a problem with random coin values, we would have to implement a totally different solution using DP (Dynamic Programming)

`#include <bits/stdc++.h>using namespace std;int main() { int n; cin >> n; int coins[7] = {1,2,5,10,50,100,200}; int current = 6, coinsUsed = 0; while(n){ if(coins[current] > n){current--; continue;} n -= coins[current]; coinsUsed++; } cout << coinsUsed;}`

The inner operations of the loop are O(1). The loop itself is O(n) in the worst case (but keep in mind that for many values of `n`

, it will run far fewer iterations than `n`

). Still, the overall time complexity is O(n).

Here is another very easy problem to warm up.

Steph wants to improve her knowledge of algorithms over winter break. She has a total of X (1 X 10^4 ) minutes to dedicate to learning algorithms. There are N (1 N 100) algorithms, and each one of them requires ai (1 ai 100) minutes to learn. Find the maximum number of algorithms she can learn.

N = 6, X = 15

a = {4, 3, 8, 4, 7, 3}

Result = 4

This problem is very nice since you can trust your intuition and decide to always try to do the tasks that require less time because logically it leaves more time available for the next tasks therefore maximizing the number of tasks done.

`#include <bits/stdc++.h>using namespace std;int main() { int n, x; cin >> n >> x; vector<int>arr(n); for(auto &e:arr)cin >> e; sort(arr.begin(), arr.end()); int sum = 0, res = 0; for(auto e:arr){ if(sum + e > x)break; sum += e; res++; } cout << res;}`

The most time-consuming operation is sorting, which has a time complexity of O(n log n). The other operations are linear (O(n)) or constant (O(1)).

So, in simple terms, the time complexity of the code is **O(n log n)**.

Picture this: A cruise trip has gone horribly wrong, and passengers are in dire need of rescue. As part of the rescue team, your challenge is to determine the fewest number of life-saving boats needed to save everyone. However, there's a catch. Each boat can hold a maximum of two people and has a weight capacity of X (where 1 X 30,000).

Here's how the input is structured:

The first line provides an integer, X, indicating the boat's maximum weight capacity.

The next line offers an integer, N (where 1 N 50,000), representing the number of passengers awaiting rescue.

This is followed by N integers specifying the weight of each individual passenger. Note that each weight, Wi, will satisfy 1 Wi X.

For X=100 and N=4 with weights:

W = {100, 60, 60, 40}

The solution is 3 boats. The most efficient arrangement is:

{100} - Only this boat can carry a person weighing 100. {60, 40} - Together, these two don't exceed the boat's limit. {60} - The remaining passenger.

Remember, in real-life scenarios, efficient problem-solving can make a world of difference!

To solve this problem we need to be very observant, I initially thought of iterating the weights, and for each Wi try to find the pair that would take you closest to the limit X as possible without exceeding. With this idea in mind, I made a key observation to solve this problem by looking at the persons with the greatest weight. I noticed how they were often too heavy to make any pair at all, taking a whole boat to themselves and if they could form a pair it would always be the most convenient to pair them with the lightest person. This is of course very greedy and I wasn't sure if it would work but the concept appeared to give the correct result for the example test cases.

The solution approach that I decided to take was to sort the weights and use two pointers, one at the beginning pointing to the lightest person and one at the end pointing at the person with the most weight. While the pointers are separated, we try to pair the lightest with the heaviest, if it's not possible we simply send the heaviest person alone. Here is my code implementation for better understanding:

`int x, n; cin >> x >> n;vector<int>weights(n);for(auto &e:weights)cin >> e;sort(weights.begin(), weights.end()); // IMPORTANTint boatsUsed = 0, lightest = 0, heaviest = n-1;while(lightest <= heaviest){ if(lightest == heaviest){ // necesary to avoid summing twice for the following if boatsUsed++; break; } if(weights[heaviest] + weights[lightest] > x){ // case for when it's imposible to form a pair boatsUsed++; heaviest--; }else{ // ideal case for when it's possible to form a pair. boatsUsed++; lightest++; heaviest--; }}cout << boatsUsed;`

Notice how we don't have to actually save the pairs or modify the array for the algorithm to work.

The code consists of:

Reading input:

*O*(*n*)Sorting weights:

*O*(*n*log*n*)A while loop processing each passenger:

*O*(*n*) worst case scenario for when all elements are too heavy and need a boat for each passenger.

The dominant factor is the sorting step. Hence, the overall time complexity is *O*(*n*log*n*).

Now let's convince ourselves that our greedy solution works by asking ourselves the following questions:

**Why start with the heaviest?**If the heaviest person doesn't fit with the lightest, they won't fit with anyone else, because all other people are heavier than the lightest. So in this case, the heaviest person takes a boat alone.**Why pair the heaviest with the lightest?**By trying to pair the heaviest and the lightest, we aim to use the remaining space on the boat after the heaviest person efficiently. If they fit together, that's the most people we can fit on that boat (because it's only allowed two). If they don't, we already know the heaviest person should go alone.

As you can tell once again, it's hard to prove that the solution is correct before submitting it but for this particular problem this solution actually outputs the most optimal and correct answer for all cases.

Many problems that involve efficient usage of time can often be solved with a greedy approach, a great example of the very classic "Scheduling" problem.

There are N events (1 <= N <= 1e6). Each event is described by its starting and ending times (0 <= S < E <= 1e9). You want to attend the maximum amount of events possible, you can only attend one at a time, and if you choose to attend an event you have to attend the entire event from beginning to end. Traveling between events is instantaneous.

What is the maximum number of events you can assist?

In the case above you can assist events {2-4} {5-6} {7-10} which it's the maximum amount possible, if you try to assist event {1-9} you are unable to attend any other.

In example 2 there are 3 combinations that give the maximum result of 2, you can assist events {1-3} {6-8}, {1-3}{3-10}, or {2-5}{6-8}.

In this last example, we have more events and several solutions that have a maximum result of 4. An example of an optimal solution is: {1-4}{4-5}{5-8}{8-9}.

When I was first tasked with this problem, I immediately thought of analyzing the example cases by drawing the timeline in order to understand the relationship between the input and the correct result. By drawing the events on paper the result became easily apparent on sight. Physically drawing the events is an excellent idea for this problem in particular because when trying to draw them you are going to question if it's convenient to have them in a special order. Having the events ordered means that you can iterate the events and worry about comparing only two at a time.

In this first approach, we want to sort all the events by starting point. This means always selecting the next possible event that begins as early as possible. Let's visualize all of the examples after applying this sort.

In the animation above you can see the algorithm in action for example 2. Observe how the first event is always chosen and from there, for each event you ask if it's overlapping with the last chosen event, if it is overlapping, you ignore it and continue. else you mark it as chosen. For example 2, this algorithm prints the correct result of 2, However, before concluding that this algorithm works you should first test it again with the other test examples.

Amazingly this algorithm outputs 4 which is the correct result for this case as well.

Let's try to simulate the algorithm one last time for example 1.

This example is a very simple base case and it's clear how this algorithm breaks and no longer outputs the correct result. Looking at this example is enough to discard this sorting idea.

Once algorithm 1 was proven to be incorrect I cleared my mind and decided to think about all the previous greedy problems, remembering the problem "Studying Algorithms", As we saw previously, the solution to that problem was to always pick the tasks that required the less time because it logically allowed more time to complete other tasks.

With that idea in mind, I decided to try the previous algorithm for all example test cases but with the events sorted by duration.

As seen in the animation above this approach gives the correct result for example 1

In Example 2 however, the result is clearly incorrect, outputing 3 instead of 2. By visualizing this case in action it's very clear that this idea is not going to work, since, unlike the "learning algorithms" problem there are starting and ending points that must be taken into account. Another thing that you can look at is the fact that if you have a "group" of events and then you have a second separated group, you must make sure they don't mix during the processing. Example:

With the illustration above you can see that the logically correct thing do to is process the events in the blue group and in a separate manner process the events that are in the red group, however, this algorithm mixes them all up.

The correct greedy approach for solving this problem is to sort the events by ending times, the logic behind this idea is that the sooner an event ends the more opportunity you have to test with more events that come after.

Let's visualize this approach in action for all the test cases.

As you can see from all of the animations above, this code outputs the correct result for all test cases.

`struct ev{ int start, end; bool operator < (const ev &b)const{return end < b.end;} // sort by end};bool notOverlap(ev a, ev b){ return (b.start >= a.end && b.end > a.end) || (b.end <= a.start && b.start < a.start);}int main() { int n; cin >> n; vector<ev> events(n); for(auto &e:events)cin >> e.start >> e.end; sort(events.begin(), events.end()); if(events.size() == 1){cout << 1; return 0;} ev lastValid = events[0]; int res = 1; for(int i=1;i<n; i++){ if(notOverlap(lastValid, events[i])){ lastValid = events[i]; res++; } } cout << res;}`

Notice how the events are nicely represented by a custom-made struct called "ev" that has the starting and ending times represented by integers. Notice how in that same struct we define an operator that is needed to easily sort the events by end time using the sort() procedure. The rest is really straightforward, we iterate the events array and if the last valid event and the current event do not overlap, then we set the new last valid event and increment the answer.

The code consists of:

Reading input:

*O*(*n*)Sorting the events:

*O*(*n*log*n*)A for loop that processes each event:

*O(n)*

The dominant factor is the sorting step. Hence, the overall time complexity is *O*(*n*log*n*).

You have *N (1 <= N <= 1000)* coins in a row, each showing either "1" or "0". In a single move, you can choose any *K (1 <= K <= 1000)* consecutive coins and flip them to their opposite side (e.g., "0" becomes "1" and vice versa).

Your task: Determine the minimum number of moves required to make all coins display "1". If it's impossible to achieve this, output "-1".

Input:

`5 21 0 1 0 1`

Output:

`2`

In this example there are 5 coins and K = 2, this means that you can pick any 2 consecutive coins and perform a flip, Note that you have to pick exactly 2 elements not less. for this specific initial coin layout, you can perform a flip in the range [1, 2] (zero-indexed) leaving the coins as: `1 1 0 0 1`

and then do a flip in the range [2, 3] resulting in `1 1 1 1 1`

, in only 2 moves, which is the minimum amount possible.

When I first came across this problem I looked at three important points.

The fact that N and K could have a worst case of 1000 which is a relatively small limit.

The fact that you were asked to figure out a minimum distance to state "11111..."

The statement specifies to print -1 if there is no solution.

All of the points above are very classic observations for problems that can be solved using a BFS/DFS approach which I immediately tried to implement without thinking of another more simple approach.

If you have never heard of BFS or DFS I strongly advise you to read my BFS and DFS Beginners Overview in order to understand the solution approach for this problem. However, as we'll see later on, this solution gives an MLE (Memory Limit Exeded) verdict so feel free to skip this section.

The way I managed to implement this solution was to visualize the way to represent a node, in this case, I figured that I could represent a coin configuration 1 0 1 0 1 as a string with no spaces "10101". With this, you can create an Adjacency list but with an unordered_map<string, int>. You can also represent the desired "final" node as string "11111". Lastly, the BFS algorithm logic remains the same, in which for a node, you visit the adjacent nodes and mark them as visited to avoid repeating, and by doing this you can test all possible Paths (simulate the flips) in an efficient manner, for example:

In the image above you can see that for the initial node 10101, you have 3 adjacent nodes which are the resulting states after performing the flips in ranges: [0,2], [1,3], [2,4]. the initial node always has a distance of 0 flips but for this particular problem, I decided to set it to 1 and then subtract 1 to the answer at the end, the reason being that we need 0 to represent not visited, traditionaly we used -1, but since we are working with a sting map we can't use that. The adjacent nodes are going to have a distance of the parent + 1, this way, if we get state 01001 later on we can trim that search since we know it was visited in a more optimal manner before.

`unordered_map<string, int> dist; // Adjacency Listint main() { std::cin.tie(nullptr); std::ios_base::sync_with_stdio(false); int n, m; cin >> n >> m; string initial = ""; // representing the coins as a string with no spaces for(int i=0; i<n; i++){ char x; cin >> x; initial += x; } queue<string> BFS; BFS.push(initial); // starting BFS dist[initial] = 1; // starting from 1 because we need 0 to represent not visited while(BFS.size()){ string c = BFS.front(); // getting current node BFS.pop(); for(int i=0; i<=n-m; i++){ // create all possible adjacent nodes string adj = ""; for(int j=0; j<i; j++)adj += c[j]; for(int j=i; j<i+m; j++)adj += c[j] == '1'?'0':'1'; for(int j=i+m; j<n; j++)adj += c[j]; // add node to BFS if not visited. if(dist[adj] == 0){BFS.push(adj); dist[adj] = dist[c] + 1;} } } string endNode(n, '1'); // create ending node if(dist[endNode] == 0)cout << -1; // no answer case else cout << dist[endNode]-1; // print the minimum distance to the ending node return 0;}`

Unfortunately, this solution is not the most optimal possible and gives a MLE verdict. Let's see why:

You're flipping `K`

consecutive coins in a sequence of `N`

coins, where (N = 1000). Let's consider the worst-case scenario for the number of combinations, i.e., the number of unique sequences you could generate by flipping any `K`

consecutive coins.

For a given starting position

`i`

, there's only one unique sequence after flipping`K`

consecutive coins.You can start flipping from position

`i = 0`

to`i = N-K`

. This means there are (N-K+1) possible starting positions for flipping.So, for each starting position, you get a new sequence. In the worst case, every single one of these could be unique, leading to (N-K+1) unique sequences.

However, note that this is just the number of unique sequences obtained from a single flip. When you consider multiple flips, the number of unique sequences can increase dramatically as you can flip any possible segment of `K`

coins in any of these sequences.

So, you can think of it like this:

From the initial state, you have (N-K+1) sequences. From each of those, you can have up to (N-K+1) more sequences and this can keep going.

Now, not every sequence will be unique. Some flips will revert a previous flip, and others might lead to a sequence you've already seen. But even considering that, it's clear that the potential number of combinations can explode quickly, especially as the number of flips (and hence the depth of the BFS) increases.

This doesn't even get us to an exact number, but you can already see how the combinations can quickly grow into the thousands or even millions, which can easily exceed memory limits. Thankfully, this solution is enough to get a partial result, but we'll see in the next section how to get a 100% score.

In order to spot this solution you need to think about the fact that you want all zeros to become 1, and once a value becomes 1 you don't want to modify it under any condition.

With this idea in mind, you can simply iterate the coin array, and if you find a 0 you HAVE to flip the range starting inclusively from that position to that position + k, repeating the process until you finish iterating the array or until you are forced to perform an impossible flip, this means that there are not enough coins for a flip to happen, for example, 1110. Notice how you would get a segmentation fault error.

Basically, this approach works because once we perform a flip we never look back and we will never under any condition flip a previously flipped 0 coin again meaning that it's guaranteed to be the most optimal answer possible.

Here is my code implementation:

`#include <bits/stdc++.h>using namespace std;int main() { int n, m, res = 0; cin >> n >> m; vector<int>arr(n); for(auto &e:arr)cin >> e; for(int i=0; i<=n-m; i++){ if(arr[i] == 0){ res++; for(int j=i; j < i+m; j++)arr[j] = arr[j] == 0?1:0; } } bool valid = true; for(auto e:arr)if(e == 0)valid = false; cout << (valid?res:-1); return 0; }`

Notice how the handling for the impossible case is very easy to check, we just iterate the array after performing the algorithm, and if there is any 0 left, it's imposible and we should output -1, else we just print the res variable that counted the amount of flips.

Reading the input: O(n) - Because you're reading 'n' numbers into the arr vector.

Main Loop: O(nm) - The outer loop runs 'n' times, and for each of those, the inner loop runs 'm' times (in the worst case). So, together, it's like multiplying 'n' and 'm'.

Checking the result: O(n) - You're going through the arr vector again to see if all coins show "1".

Combine all these, and the time complexity is O(n+nm+n), which simplifies to O(nm).

For the Space complexity, we have the **Vector**: *O*(*n*) - The `arr`

vector stores 'n' numbers and **Other Variables**: These just take up constant space. No matter how big 'n' or 'm' get, the space used by variables like `n`

, `m`

, `res`

, and `valid`

doesn't grow.

So, the space complexity is *O*(*n*). Giving us an AC verdict with a much simpler solution. 🥳

You have an endless row of coins, all initially showing heads. And you are given ** N** inclusive range intervals. Write a program that outputs the fewest number of coins you need to flip to ensure there's at least one tails in each of the M intervals provided to you. (heads is represented by '0' and tails by '1')

input:

`31 62 91 4`

output:

`1`

In this example you are given 3 inclusive intervals, by drawing them, the solution becomes clear, you only need to flip 1 coin, which is the minimum amount of flips for a valid solution, in the image I flipped coin 4, however, flipping the coin 3 or 2 are also valid answers. For this problem, you don't need to print the positions of the flipped coins, only the amount.

1 <= N <= 100,000

1 <= i <= j <= 1,000,000 in which i represents the starting index and j represents the ending index to the inclusive interval.

Amazingly this problem has an almost exact solution to The Scheduling Problem in which we sort all intervals by end-time.

The algorithm would work as follows:

Sort the intervals by end time

Keep track of the position of the last 1/flip (initially there is none so it can be initialized with -1)

Iterate the Intervals and for each one ask if the starting point is less than the position of the last 1/flip, if true it means that the last one/flip is still within the current interval and there is no need to make any flip. If false, it means that are forced to make a flip, we choose to follow a greedy strategy by doing the flip at the end position of that interval because it is the most efficient place to target the following intervals.

`struct range{ int i, j; bool operator < (const range &b)const{ return j < b.j; }};int main() { int n; cin >> n; vector<range> arr(n); for(auto &e:arr)cin >> e.i >> e.j; sort(arr.begin(), arr.end()); int lastFlip = -1, res = 0; for(auto e:arr){ if(e.i > lastFlip){ res++; lastFlip = e.j; } } cout << res; return 0;}`

As you can see the solution is incredibly simple, yet it gives us the correct output every time in the most efficient way possible.

The code consists of:

Reading input:

*O*(*n*)Sorting the intervals:

*O*(*n*log*n*)A for loop that processes each interval:

*O(n)*

The dominant factor is the sorting step. Hence, the overall time complexity is *O*(*n*log*n*).

For the space complexity, we only have to store the M intervals in an array, resulting in a space complexity of O(n).

We've journeyed through the exciting world of greedy algorithms together, learning how to spot problems that call for a greedy solution, and breaking down some classic examples. Our adventure into the code, along with its time and space analysis, hopefully gave you a clearer understanding and maybe sparked your curiosity a bit.

With each problem, we saw how a simple idea can lead to solutions that are both smart and efficient. We also noticed how the right approach can help find the best solutions, and how different coding choices can affect how fast our code runs and how much memory it uses.

Feel free to share this post with others who might be interested in diving into greedy algorithms, and dont hesitate to drop your thoughts or questions in the comments below. Your interactions make this community a fun and enriching place to learn. Until next time, happy coding! 💻

]]>Hey there, fellow coders! Today, we're going to navigate through the exciting world of dynamic programming, tackling a captivating competitive programming problem along the way. We'll be stepping into the shoes of game strategists, playing a turn-based game over an array where every decision counts.

Imagine this: you're playing a game where you and your opponent take turns removing elements from either end of an array. The challenge? Both of you are top-notch strategists aiming to maximize your own sums. Sounds intriguing, right?

So, let's roll up our sleeves, dive deep into the strategy of optimal play, and discover how we can wield the power of dynamic programming to solve this challenge. Ready to unlock a new level of your programming skills? Let's get started!

There is a very popular game that's creating quite a buzz. This two-player game revolves around a linearly arranged set of N coins, each represented by a number indicating its value. The crux of the game? To amass the maximum possible wealth. On each turn, a player can choose to take either the coin at the beginning or the end of the line. This back-and-forth continues until the last coin has been claimed.

In a bold move, you've invited your math teacher - a formidable opponent known for his strategic prowess - to a round of this game. Confident in his abilities, he's even let you take the first turn. Your mission, should you choose to accept, is to devise a program that, given an array of coins, predicts the maximum wealth you can amass. The catch? Both you and your teacher are playing optimally.

Your program will be receiving two lines of input:

The first line will contain a single integer N, where 1 <= N <= 1000. This number represents the size of the line of coins.

The second line will contain N space-separated integers. Each of these integers represents the value of a coin in the line and will be between 1 and 1,000,000,000 (1E9), inclusive.

Your program should output a single line containing one integer, representing the maximum amount of money you can win by playing this game optimally. Remember, you're making the first move and both you and your opponent are expected to play in the most strategic manner.

Now that we have understood the problem, let's see an example to bring our understanding to life.

**Input:**

`51 3 5 2 9`

**Output:**

`15`

**Explanation:**

In this game, we start with a line of 5 coins with the values 1, 3, 5, 2, and 9, in that order. We take the first turn, and both we and our opponent play optimally.

First, we take the coin with the value 9 (since it's the highest value), leaving the line as 1, 3, 5, 2.

Our opponent, playing optimally, would take 2 (leaving 1, 3, 5 as our options).

On our turn, we take the coin with value 5, leaving the line as 1, 3.

Our opponent now takes the coin with value 3, leaving only 1 in the line.

Finally, we take the last coin, which is 1.

So, in total, we have taken coins of values 9, 5, and 1, summing up to 15, which is the maximum amount of money we can win in this scenario. Hence, the output is 15.

Upon the first encounter, this problem might seem like a cakewalk. The seemingly straightforward approach would be to opt for the largest available value at each turn and repeat the same strategy for our opponent. Essentially, we're simulating the entire game by iterating through the array in a specific order. With the time and space complexity of O(N), this approach is undoubtedly efficient.

The viability of this strategy shines through in our initial example and even holds up when tested on an example such as:

`41 2 3 4`

However, as the good competitive programmers that we are, we should question more if this solution is correct by trying to create an example in which this solution does not work.

`420 30 2 2`

If we follow the greedy approach:

We first pick 20, leaving the line as 30, 2, 2.

Then our opponent picks 30, leaving the line as 2, 2.

We pick 2, leaving the line as 2.

Lastly, our opponent picks the final 2.

Here, we've ended up with coins worth 20 + 2 = 22.

However, if we played optimally, considering our opponent also plays optimally:

We first pick 2 from the end, leaving the line as 20, 30, 2.

Our opponent, playing optimally, picks 20, leaving the line as 30, 2.

We pick 30, leaving the line as 2.

Lastly, our opponent picks the final 2.

So, playing optimally, we would end up with coins worth 2 + 30 = 32, which is the correct and maximum achievable sum considering both players are playing optimally.

This example clearly demonstrates that the greedy approach doesn't always result in the best outcome, emphasizing the importance of a strategy that considers future moves and the optimal plays of the opponent.

It might be tempting to dissect the shortcomings of our initial greedy strategy until we're utterly convinced of its flaws. But really, that single example where it fell short is quite enough to nudge us away from it, encouraging us to think creatively and explore other strategies.

One such strategy that often comes to mind during these times is the Brute Force approach. If you're new to this concept, it's actually quite simple. All it means is that we try out every possible game scenario. Although this usually gives us the right answer, it can be a bit slow. But no worries - for now, our main aim is to find a solution that works, no matter how slow. We can always speed things up later to avoid hitting any time limits.

Remember, problem-solving isn't always about finding the quickest solution, but about finding a solution that works. Then, we can always improve and refine it!

Picture a maze where you're standing at the entrance, looking to navigate through different routes to find the exit. That's kind of like what we're trying to do here - explore all possible 'paths' of the game, using what's called a recursive approach.

Imagine each decision we make as branching off into a tree-like structure of different outcomes (See the image).

Notice how quickly our 'decision tree' expands. But no worries, we'll tackle this size issue later.

Let's break down this decision tree, starting from the first node. Each node represents a snapshot of our game, defined as: {LeftIndex, RightIndex, Sum1, Sum2}. Here, 'Sum1' and 'Sum2' are the total values of coins you and your opponent have respectively, starting at 0. The 'LeftIndex' and 'RightIndex' mark the inclusive range of coins left to play with. At the start of the game, this range covers the whole array, hence why it starts at 0 and ends at 3.

Each node offers two decisions:

Grab the coin from the left of the array, which means adding the value of the coin at 'LeftIndex' to your total (or your opponent's, depending on whose turn it is), and then moving the 'LeftIndex' one step to the right (essentially 'removing' that coin from the game).

Grab the coin from the right, which means adding the coin at 'RightIndex' to the current player's total, and then moving the 'RightIndex' one step to the left.

Notice that when both indices are equal there is no need to create a new node, and we can directly add the value at the index to however the player was supposed to play on the next move.

Hopefully, by knowing this you can better see how the Tree represents all possible ways a game can unfold.

Initially, I thought of simply finding the path that maximized your sum, however, by looking at the tree, I realized that if I do that, the code is going to output 50, which is an incorrect output because of the fact that we are not considering that the opponent is guaranteed to play in the most optimal way possible.

This adds another layer of complexity, but no worries, it's manageable.

A key question here is: *"Can we predict the opponent's best move from any given node?"* If we could, we'd simply choose the opposite option, aiming to minimize the opponent's total. Let's visualize it to get a better understanding:

Imagine you're at the purple node, deciding whether to go left or right. If we knew that the best move for the opponent would lead us to the pink node, we'd instead move to the blue node - after all, when your opponent isn't doing well, you are.

Unfortunately for us, there is no way for us to know the optimal play for the opponent until we have explored the full path. realizing that the only way for us to know the optimal value is by going down the tree is key because it allows us to change our perspective. Instead of trying to solve the problem from the root, we are going to start from the leaves.

Starting from the leaves makes a ton of sense because it's the only point in time when we know the best decision with certainty.

In the animation above, notice how the pink nodes represent states in which is your turn, and the purple nodes represent states for the opponent. At the beginning, we start at the root node and explore to the right and then to the left for each node, we continue exploring until we reach a base case that marks when a leaf is met. The base case is very simple, it's just when the indexes are equal.

Observe how in the animation, it can be shown up until the point in which you have the information to fill the first two leaves 1,1 and 0,0, and now the parent 0,1 state must be filled.

We're trying to figure out what value the pink node should have. At first, we might want to generalize and think that the value for all nodes should represent the highest possible score we can get from that range of coins.

But when we look at the values of the two nodes beneath the pink node (its children), we only really know which option is better for us. In this situation, if we take a step to the left, the other player (the opponent) will have a chance to earn more than if we take a step to the right. So, we want to choose the option that is worse for the opponent. However, knowing this doesn't tell us what our total score will be for that range of coins. We need to think a bit harder about this problem and consider it from different angles.

What we can do for the value of each node is, instead of saving the maximum earning possible from a range, we save the maximum advantage possible. Let's look more into this idea.

Notice how the leaves stay the same since it's still the correct value with this new logic. But now let's try to fill the value for the pink node.

`val = max(-leftChild + arr[l], -rightChild + arr[r])`

Let's break down the line above with the help of the image below.

The left child contains the maximum advantage it can be obtained from the range [1,1], which in this case, it's 30, trivial because it's the case in which the indexes are the same. For the right child, it's the same leaf case, now with a value of 20. Now, for the pink node, we know for sure that both of the children are going to represent the maximum advantage but for the opponent, this is important, because it means that it's not going to be your advantage. This is the reason we make both children nodes negative when calculating the pink value, and of course, we must not forget to sum the value that made the left and right states happen in the first place. In this example, if we choose to go to the left child state, we would end up with a maximum advantage of -30 + 20 = -10. which is totally correct. and if we choose to go to the right, we would end up with a maximum advantage of -20 + 30 = 10. Of course, we want to maximize this value always. which means that the final value for the pink node is going to be 10.

`val = max(-leftChild + arr[l], -rightChild + arr[r])`

Using this logic, let's fill the entire tree to see how it would end up looking.

Feel free to take any node and "manually" check if the value is the maximum advantage you are going to have if you start from that node. If we take the root node [0, 3] for example, we can see we end up with an advantage of 10. Let's see why:

We first pick 2 from the end, leaving the line as 20, 30, 2.

Our opponent, playing optimally, picks 20, leaving the line as 30, 2.

We pick 30, leaving the line as 2.

Lastly, our opponent picks the final 2.

So, playing optimally, we would end up with coins worth 2 + 30 = 32, and the opponent ends up with 20 + 2 = 22.

If we subtract 32 - 22 = 10 we see that the value is indeed correct.

Excellent!, we got the difficult part out of the way, now, knowing the amount by which we beat the opponent (or lose if it's negative) we can calculate our sum using some math.

Let's break down the image above.

We start by saying the total amount of coins (T) is made up of your coins (x) and the opponent's coins (y).

Next, we say your total coins (x) is made up of your opponent's coins (y) plus the extra coins you managed to snag (d). (d) = the maximum advantage by which you beat the opponent. (or lose if it's negative)

Now, we substitute the second equation (x = y + d) into the first (T = x + y). This is like saying the total coins are the same as your opponent's coins, the extra coins you gathered, plus your opponent's coins again. Sounds a bit repetitive, right? But it's crucial for our math to work!

Simplifying the above, we find that the total (T) is equal to twice your opponent's coins (2y) plus the coins you snagged extra (d). This is like saying the total coins are the same as two times your opponent's coins plus your extra coins.

Rearranging a bit, we subtract your extra coins (d) from the total (T), getting T - d = 2y. This tells us that if we take away your extra coins from the total, we're left with twice the amount of your opponent's coins.

Finally, to find your opponent's coins (y), we divide both sides of the equation by 2, which gives us y = (T - d) / 2. This is like saying your opponent's coins are half of the total coins minus your extra coins.

We now know the opponent sum, because of this finding yours is simple, it's just x = y + d

And that's it! We've used algebra to solve our problem. It's like a smart way of splitting coins between you and your opponent!

As a recap, what we're doing in our solution is exploring every possible move in the game. We do this using a recursive approach, which allows us to create a tree-like map of all potential outcomes. Each node (or possible outcome) tells us the maximum advantage, or benefit, a player can get from a certain range of coins. We define this as:

{LeftIndex, RightIndex, MaximumAdvantage}.

Once we know the maximum advantage possible for the entire range of coins (let's call it 'd'), we can use this in our formula to calculate the final result:

`((T-d)/2) + d`

Below you can find my fully coded solution in C++

`#include <bits/stdc++.h>using namespace std;int n, arr[1005]; int node(int l, int r){ if(l == r)return arr[l]; int left = node(l+1, r); int right = node(l, r-1); int MaximumAdvantage = max(-left+arr[l], -right+arr[r]); return MaximumAdvantage;}int main() { cin >> n; int total = 0; for(int i=0; i<n; i++){cin >> arr[i]; total += arr[i];} int advantage = node(0,n-1); // [inclusive range] int result = ((total - advantage) / 2) + advantage; cout << result;}`

As you can see, the code is amazingly simple and short for such a complex appearing problem, there is no need for an actual tree structure implementation with actual nodes and pointers. This is a beautiful example of recursion simplicity.

As we saw previously, this solution explores all possible ways the game can unfold:

Notice how for each coin, we have two options - to pick the coin on the left or the right. And for each of these options, we again have two options for the next coin, and so on. Because of this, the number of possibilities we have to check grows exponentially, doubling with each decision, making the time complexity O(2^n).

Given that N = 1000, this code can perform up to 1.07150860718626732094842504906e+301 operations, which of course, it's waaaaaay above our usual 400,000,000 operations limit for most online judges.

This is, in fact, one of the most inefficient time complexities there are. With this solution, the maximum N our solution can process within the limits is approximately N = 30. any value greater than 30 will result in a TLE verdict. 😥

What if I told you that we can drastically reduce the time it takes to solve this problem? Sounds exciting, right? The solution lies in a clever technique called Dynamic Programming (or DP, for short).

Think about this: while we're exploring all those different ways the game can unfold, we end up checking the same scenarios multiple times. (see image)

That's a lot of wasted effort! Dynamic Programming helps us avoid this by remembering the results of these already-solved scenarios. We call this "memoization".

So, let's use DP to make our program much, much faster. Instead of recalculating results we already have, we can just look them up in our "DP". This way, we save loads of time and avoid repeating work we've already done. We're about to supercharge our program, enabling it to handle more coins and perform its calculations way faster!

Let's see how to implement DP to our coded solution:

`#include <bits/stdc++.h>using namespace std;int n, arr[1005];vector<vector<int>> DP(1005, vector<int>(1005, -1));int node(int l, int r){ if(l == r)return arr[l]; if(DP[l][r] != -1)return DP[l][r]; int left = node(l+1, r); int right = node(l, r-1); return DP[l][r] = max(-left+arr[l], -right+arr[r]);}int main() { cin >> n; int total = 0; for(int i=0; i<n; i++){cin >> arr[i]; total += arr[i];} int advantage = node(0,n-1); // [inclusive range] int result = ((total - advantage) / 2) + advantage; cout << result;}`

Did you spot the new parts? Let's break them down:

We've got a DP 'notebook' now. It's like a big grid (or a 2D matrix) where we'll note down results so we can refer to them later. It's sized N * N because that covers all possible combinations of our coin pile indices. And see how it's filled with -1s to start with? That's because 0 could be a valid result, so we need something different to show that we haven't calculated a result yet.

We now check our 'notebook' before doing any calculations. If we've already got a result for a particular set of indices, we can just use that instead of doing all the work again. It's like saying, "Hey, we've seen this before! No need to do the same work again."

Last but not least, once we've done the calculation for a particular set of indices, we jot it down in our 'notebook' for later. It's like saying, "This could come in handy later. Better note it down."

And there you have it! The same game, the same process, but now supercharged to be much faster. This shows the beauty of Dynamic Programming - the same logic, but a whole lot more efficient!

Now that we've given our code a dynamic programming boost, you might be curious about how much faster it is and how much more space it needs. Let's look into that:

**How Fast? (Time Complexity):**Before, our code was taking a long time because it was checking every possible coin pick over and over again, which is why the time complexity was O(2^n). But now, thanks to our DP 'notebook', we don't do any calculations more than once. For each possible start and end of the pile (given by the indices), we calculate the result only once and then just look it up whenever needed. So, we've got 'n' possible start points and 'n' possible end points, making it n times n or 'n squared'. Therefore, our new time complexity is O(n^2), which is a whole lot faster!**How Much Space? (Space Complexity):**We're using a bit more memory in this new code. That's because of our DP 'notebook', the 2D matrix. It's a grid with 'n' rows and 'n' columns, so it has n times n, or 'n squared' spots to store results. So, the space complexity is O(n^2).

In simple terms, we've traded a bit more space to make our program run much faster. And the best part? Now we can solve the game for up to 1,000 coins (where 'n' equals 1,000) without running into any issues since 1,000*1,000 < 400,000,000.

We've reached the end of this deep dive into Dynamic Programming (DP) and how it can supercharge your problem-solving abilities, even in a seemingly complex game like this. And remember, this is not something you'll perfect overnight, it's a skill that takes time and practice to master. So if it doesn't click right away, don't be hard on yourself. This stuff isn't easy, but I hope the walkthrough and explanations provided some clarity and a good foundation to build on.

Remember, we've only just scratched the surface of what DP can do. There's a whole world of interesting problems and applications waiting for you to discover, and while we covered some key concepts and methods here, there's always more to learn.

I hope you've enjoyed this journey as much as I have, and that you've gained some valuable insights and knowledge along the way. I would love to hear your thoughts and any questions you might have. Let me know in the comments!

Thanks for sticking with me till the end, and keep an eye out for more problem-solving adventures! Until next time,

]]>Hello everyone, in this post we'll be mastering the 2D prefix sum array and 2D segment Tree while we solve a very interesting competitive programming problem to better understand these structures and their capabilities.

You are given an n x n grid representing the map of a forest. Each square is either empty or contains a tree. The upper-left square has coordinates (1,1)(1,1), and the lower-right square has coordinates (n,n).

Your task is to process K queries of the form: how many trees are inside a given rectangle zone in the forest?

In the first line, you are given two integers, N and Q, where N represents the size of the forest, and Q, the number of Queries you need to answer. Then you will receive N x N characters representing the forest. '.' represents an empty space and '*' a tree.

Lastly, you will get Q lines in the form of x1, y1, x2, and y2 which correspond to the corners of a rectangle zone.

For each query output the amount of trees inside the rectangular zone.

1 n 10000

1 q 200,000

1 y1 y2 n

1 x1 x2 n

When first tasked with this problem, the clearest solution was to iterate through every square in the rectangular zone while adding the values into a counter variable for every query. Such a solution can be implemented like this:

`#include<iostream>using namespace std;typedef long long ll;char map[1005][1005];ll query(int x1, int y1, int x2, int y2){ ll res = 0; for(int i = x1; i<=x2; i++){ for(int j = y1; j<=y2; j++){ if(map[i][j] == '*')res++; } } return res;}int main(){ int n, k; cin >> n >> k; for(int i=0; i<n; i++)for(int j=0; j<n; j++)cin >> map[i][j]; while (k--){ int x1, y1, x2, y2; cin >> x1 >> y1 >> x2 >> y2; x1--; y1--; x2--; y2--; cout << query(x1, y1, x2, y2) << "\n"; } return 0;}`

Reading the input: The code reads the values of

`n`

and`k`

, which takes constant time. Then it reads the`n x n`

characters of the map, resulting in a time complexity of O(n^2).Query Calculation: For each query, the code iterates over the rectangular region defined by

`x1`

,`y1`

,`x2`

, and`y2`

. Since in a worst-case scenario you are prompted with x1 = 1, y1 = 1, x2 = n, y2 = n. This means that the entire map will be traversed resulting in a time complexity of O(n^2).

since we have to answer 200,000 queries and each of them cost 10,000^2 that means that in a worst-case scenario, we have to compute 20,000,000,000,000 operations which exceeds the 400,000,000 limit resulting in a TLE (Time Limit Exceded) verdict.

Let's improve our solution by implementing a technic called Prefix sum Array, the way it works is that we create a helper array that stores the sum of the elements of the original array up to a certain index, basically, the value at index i is going to represent the sum of all elements from index 0 to index i in the original array. Let's see an example:

Why is this useful? well, notice how the last index contains the sum of all elements from [0, 8] of the original array. Let's say you want to find the sum of all the elements between [2, 4] positions in the original array. With a prefix sum array, you can compute the sum in constant time (very fast) by subtracting two elements from the prefix sum array:

Sum = PrefixSum[5] - PrefixSum[2] = 12 - 5 = 7.

Notice how the prefix sum array has an extra zero at the beginning, this is simply to make the implementation of the queries more straightforward and avoid an index out-of-range error.

Knowing this we can implement a prefix sum array for each row in our forest map and of course, we must change the characters to their corresponding values, 1 or 0. Here is my implementation:

`#include<iostream>using namespace std;typedef long long ll;char map[1005][1005];ll mapRowPrefix[1005][1005];ll query(int x1, int y1, int x2, int y2){ ll res = 0; for(int i = x1; i<=x2; i++) res += mapRowPrefix[i][y2+1] - mapRowPrefix[i][y1]; return res;}int main(){ int n, q; cin >> n >> q; for(int i=0; i<n; i++){ for(int j=0; j<n; j++){ cin >> map[i][j]; mapRowPrefix[i][j+1] = mapRowPrefix[i][j] + (map[i][j] == '*'?1:0); } } while (q--){ int x1, y1, x2, y2; cin >> x1 >> y1 >> x2 >> y2; x1--; y1--; x2--; y2--; cout << query(x1, y1, x2, y2) << "\n"; } return 0;}`

- See how we are constructing the row sum prefix array at the same time that we are saving the input, this means that the time complexity for constructing the mapRowPrefix remains the same in O(n^2), however, things change with the query function, since now we only iterate the rows and we no longer have to iterate the columns, thanks to our prefix row sum array that allows us to know the result in constant O(1) time for each row.

We still have to iterate through all the rows, which in a worst-case scenario it's N for each query. This means that the overall code has a time complexity of O(N^2 + (N * Q)). If we do the math this results in approximately 2,100,000,000 operations. We can clearly see an improvement compared to our previous solution, unfortunately, this still exceeds our 400,000,000 limit. 😥

In order to solve this problem we need to make it even more optimal by using a 2D prefix sum Matrix and by doing this removing the need to iterate through the rows.

Since you're familiar with the concept of a prefix sum array, understanding a 2D prefix sum matrix should be straightforward. It's basically the extension of the prefix sum array concept into two dimensions.

Given a 2D grid (or matrix), the 2D prefix sum matrix is another 2D grid of the same size where each cell (i,j) represents the sum of all the values in the rectangle from the top-left cell (0,0) to the current cell (i,j) in the original matrix. This can be clearly understood with the help of an example:

Calculating the Prefix Sum Matrix values is very simple, notice how once again we added an extra zero at the beginning of each row and each column, this once again just makes the implementation easier since we don't have to worry about creating a special case for those values and we don't worry about index out of range errors.

Excluding those extra zeros, filling the rest of the prefix sum Matrix values just evaluates to summing the value on the square above, the value on the square on the left, and the original value. Then subtract the value on the adjacent top left corner like this:

`prefix2D[i][j] = prefix2D[i-1][j] + prefix2D[i][j-1] - prefix2D[i-1][j-1] + map[i][j];`

By using this 2D prefix SumMatrix we can answer each query in constant time O(1) with the following operation:

`sum = prefix2D[x2 + 1][y2 + 1] - prefix2D[x1][y2 + 1] - prefix2D[x2 + 1][y1] + prefix2D[x1][y1]`

Knowing this allows us to solve the problem in a very efficient way, here is my code implementation:

`#include<iostream>using namespace std;typedef long long ll;char map[1005][1005];ll prefix2D[1005][1005];int main(){ int n, k; cin >> n >> k; for(int i=1; i<=n; i++){ for(int j=1; j<=n; j++){ cin >> map[i][j]; prefix2D[i][j] = prefix2D[i-1][j] + prefix2D[i][j-1] - prefix2D[i-1][j-1] + (map[i][j] == '*'?1:0); } } while (k--){ int x1, y1, x2, y2; cin >> x1 >> y1 >> x2 >> y2; x1--; y1--; x2--; y2--; cout << (prefix2D[x2 + 1][y2 + 1] - prefix2D[x1][y2 + 1] - prefix2D[x2 + 1][y1] + prefix2D[x1][y1]) << "\n"; } return 0;}`

Constructing the

`map`

and`prefix2D`

arrays: O(n^2). You're performing`n`

operations (for each row), each of which involves`n`

operations (for each column). This remains the same as the previous solutions.Querying

`k`

ranges: O(k). You're performing`k`

operations, each of which is a constant time operation thanks to the 2D prefix Matrix.

Therefore, the overall time complexity of the program is O(n^2 + k) which evaluates to approximately 100,200,000 operations, this is well within our 400,000,000 limit that is set by most online judges.

This code is efficient and will get AC (Accepted) verdict. 🥳😁

Let's make this problem more interesting by adding updates. In this version of the previous problem, you are going to process K actions that can either type 1 or 2.

Change the state of a square, this means that if the square has a tree you can cut it down making the square empty, or if the square is empty you can plant a tree.

Query, the same thing as before, you are given the coordinates of a rectangle zone and you must output how many trees are completely inside the zone.

In the first line, you are given the integers N and K, the size of the forest and K actions, then there are N * N characters representing the map. Finally, you are given K actions, the format of each action can either be: "1 x y" or "2 x1 y1 x2 y2".

For each action of type 1 you don't need to print anything, just output the answer for each query of type 2.

1 n 10,000

1 q 200,000

1 y,x n

1 y1 y2 n

1 x1 x2 n

Up until this point, you can likely implement the most simple solution that involves iterating all of the squares inside the rectangle zone query and summing the values just like we did in the previous solution, however, now we must now add the Update functionality, which for this solution is super easy since we just have to add:

`void update(int x, int y){ map[x][y] = map[x][y] == '*'?'.':'*';}`

If you are not familiar with this syntaxis, don't worry, it's called a ternary operator and does exactly the same as the code cell below but in a single line 😎.

`void update(int x, int y){ if (map[x][y] == '*') { map[x][y] = '.'; } else { map[x][y] = '*'; }}`

This update solution is super efficient since it works on O(1) constant time. However, as we saw before, the query function has a time complexity of O(N^2).

As the competitive programmers that we are, we must assume the worst case that it's when absolutely all actions are queries and the queries ask for the sum of the entire map.

This means that the time complexity of this solution is going to be O(n^2 + (K * N^2)) which of course, is going to give a TLE verdict even though the updates happen in O(1) constant time.

Previously we saw how this was the best solution since each query was computed in O(1) constant time. Let's see how we can adjust this structure to support updates.

`void update(int x, int y, int n){ map[x][y] = map[x][y] == '*' ? '.':'*'; for(int i=x; i<=n; i++){ for(int j=y; j<=n; j++){ prefix2D[i][j] = prefix2D[i-1][j] + prefix2D[i][j-1] - prefix2D[i-1][j-1] + (map[i][j] == '*'?1:0); } }}`

As you can see we update the map in the correct position and then we recalculate the matrix from that coordinate to the end.

This answer might look great since each query is still answered in O(1) constant time, however, if we analyze the time complexity of the update we can realize that the worst-case scenario would be if the update happens in coordinate {1, 1} because that would mean that the entire prefix2D Matrix is going to be recalculated.

The overall time complexity of this solution would be O(n^2 + (K * N^2)) since in the worst-case scenario all actions are updates that happen on the coordinate {1,1}.

This is going to give us a TLE verdict, and interestingly enough this solution has a time complexity equal to the iterative solution. 🙃😆

Before diving into the solution to this particular problem make absolutely sure you have read my Beginner's Segment Tree Introduction since this post assumes you totally understand how this technic works and how it's implemented.

Basically, a segment tree allows us to know the sum of all elements in a range, it can also be used to know the minimum or maximum value in a range, and allows us to make updates, both of these operations are done in a very efficient O(log(N)) way.

NOTE: in computer science, we work with log in base 2.

Above you can visualize the segment tree in action. on an array of the size shown it might not look very efficient, but when the array becomes very large it becomes clear how the segment tree makes our queries super efficient in log(N).

Above you can see the update in action, this operation is also very efficient having a time complexity of log(N).

Knowing this structure, we can solve the problem at hand by doing a segment tree for each row in a very similar way that we solved the first problem that didn't involve updates with a prefix sum array for each row.

I want to take this chance to show you a slightly different Implementation of the segment tree to the one that I showed in my Beginner's Segment Tree Introduction. The logic and theory remain completely the same, but now the implementation is nicely packed in a struct. This is necessary since we are working with several segment trees.

`#include<iostream>#include<vector>#include<cmath>typedef long long ll;using namespace std;struct segTree{ int n; vector<ll>st; segTree(int s){ n = 2 * pow(2, ceil(log2(s))) - 1; st.resize(n, 0); } void update(int i, int newV){ i += n/2; st[i] += newV; while (i){ i = (i-1)/2; st[i] = st[i*2+1] + st[i*2+2]; } } ll query(int ql, int qr, int l, int r, int i){ if(l >= qr || r <= ql)return 0; if(l >= ql && r <= qr)return st[i]; int mid = (l+r)/2; return query(ql, qr, l, mid, i*2+1) + query(ql, qr, mid, r, i*2+2); } ll query(int ql, int qr){ return query(ql, qr, 0, n/2+1, 0); } // void print(){for(auto e:st)cout << e << " "; cout << endl;}};int main(){ int n, k; cin >> n >> k; vector<segTree> mapSTrees(n, segTree(n)); char map[n][n]; for(int i=0; i<n; i++){ for(int j=0; j<n; j++){ cin >> map[i][j]; mapSTrees[i].update(j, map[i][j] == '*'?1:0); } // rowST.print(); } while (k--){ int action; cin >> action; if(action == 1){ int x, y; cin >> x >> y; x--, y--; if(map[x][y] == '*'){ mapSTrees[x].update(y, -1); map[x][y] = '.'; }else { mapSTrees[x].update(y, 1); map[x][y] = '*'; } }else{ int x1, y1, x2, y2; cin >> x1 >> y1 >> x2 >> y2; x1--,y1--,x2--,y2--; ll res = 0; for(int row=x1; row <= x2; row++){ res += mapSTrees[row].query(y1,y2+1); } cout << res << "\n"; } } return 0;}`

Notice how everything is the same as in my post except for two minor changes.

Getting the size of the segment tree is now done in a single line with the help of the following formula

`: 2 * pow(2, ceil(log2(n))) - 1;`

There are two query methods, one of them is responsible for actually calculating the answer with recursion and 5 parameters, and the other is just the public interface that only takes two arguments. This is simply to make the usage of the function easy for the user.

The building of each segment tree (performed in the segTree constructor) has a time complexity of O(n), where n is the size of the input array (or the row/column length of the map, in this case). This operation happens n times (once per row), leading to an overall time complexity of O(n^2).

The 'update' operation, where an element of the array is modified and the segment tree is subsequently updated, has a time complexity of O(log n). This operation might be performed n times within the 'k' operations, hence the worst-case time complexity would be O(k log n).

The 'query' operation, where a subarray sum is calculated, also has a time complexity of O(log n). In the worst-case scenario, this operation might be performed for all rows of the map within the 'k' operations, leading to an overall worst-case time complexity of O(nk log n).

Combining all of these operations, the overall time complexity in the worst-case scenario is O(n^2 + nk log n), which is about 28,100,000,000 operations. This solution finally shows improvement but it's still not efficient enough to avoid the TLE verdict.

In a very similar way to the first problem in which we used a 2D prefix sum Matrix to avoid iterating over the rows, we need to apply that same optimization concept to this problem by using a 2D Segment Tree like this:

This is where having a nicely packed segment tree struct is extremely useful. In order to implement a 2D version we are going to create another additional structure that uses the 1D previously created version.

`struct segTree2D{ int n; vector<segTree> st; segTree2D(int s){ n = 2 * pow(2, ceil(log2(s)))-1; st.resize(n, segTree(s)); } void update(int x, int y, int newV){ x += n/2; st[x].update(y, newV); while (x){ x = (x-1)/2; st[x].update(y, newV); } } ll query(int ql, int qr, int qt, int qb, int t, int b, int i){ if(t >= qb || b <= qt)return 0; if(t >= qt && b <= qb)return st[i].query(ql, qr); int mid = (t+b)/2; return query(ql, qr, qt, qb, t, mid, i*2+1) + query(ql, qr, qt, qb, mid, b, i*2+2); } ll query(int ql, int qr, int qt, int qb){ return query(ql, qr, qt, qb, 0, n/2+1, 0); }};`

Notice how this 2D segment Tree has all the same methods that the 1D version, we have the initialization:

`vector<segTree> st;segTree2D(int s){ n = 2 * pow(2, ceil(log2(s)))-1; st.resize(n, segTree(s));}`

Notice how instead of the array of ints, we now have an array of segment trees, also, when we resize the vector we initialize the 1D segment trees at the same time `st.resize(n, segTree(s));`

The query works exactly the same but only over the rows, I like to manage the two pointers as top and bottom, and when the range is completely inside the range we access that particular segment tree and we perform the query for Left and Right.

Lastly, the update, in this case, is very simple because it's a **sum** segment tree, we just access the corresponding segment tree to the x row, we update the value in the y position on that array, and then we return until we reach the root at the same time that we update the value on the y axis of each segment tree along the way. However, keep in mind that min or max segment tree versions are much different.

For the

`update`

operation, the time complexity is O(log n). In the worst-case scenario, the update operation affects elements all the way up the segment tree, which has a height of log n.For the

`query`

operation, the time complexity is O(log(n)^2). This is because the query operation may need to traverse the tree in two dimensions: once for the row and once for the column. In the worst-case scenario, this traversal happens over a height of log n in each dimension, resulting in a total time complexity of O(log(n)^2).

These complexities hold true assuming that the underlying 1D segment tree (`segTree`

) operations (`update`

and `query`

) have time complexities of O(log n).

The overall Time complexity of this algorithm is O(N^2 + (q * (log(N)^2))) which results in approximately 100,000,000 + (200,000 * 196) = 139,200,000 operations which is well within our limit.

`#include<iostream>#include<vector>#include<cmath>typedef long long ll;using namespace std;struct segTree{ int n; vector<ll>st; segTree(int s){ n = 2 * pow(2, ceil(log2(s)))-1; st.resize(n, 0); } void update(int i, int newV){ i += n/2; st[i] += newV; while (i){ i = (i-1)/2; st[i] = st[i*2+1] + st[i*2+2]; } } ll query(int ql, int qr, int l, int r, int i){ if(l >= qr || r <= ql)return 0; if(l >= ql && r <= qr)return st[i]; int mid = (l+r)/2; return query(ql, qr, l, mid, i*2+1) + query(ql, qr, mid, r, i*2+2); } ll query(int ql, int qr){ return query(ql, qr, 0, n/2+1, 0); }};struct segTree2D{ int n; vector<segTree> st; segTree2D(int s){ n = 2 * pow(2, ceil(log2(s)))-1; st.resize(n, segTree(s)); } void update(int x, int y, int newV){ x += n/2; st[x].update(y, newV); while (x){ x = (x-1)/2; st[x].update(y, newV); } } ll query(int ql, int qr, int qt, int qb, int t, int b, int i){ if(t >= qb || b <= qt)return 0; if(t >= qt && b <= qb)return st[i].query(ql, qr); int mid = (t+b)/2; return query(ql, qr, qt, qb, t, mid, i*2+1) + query(ql, qr, qt, qb, mid, b, i*2+2); } ll query(int ql, int qr, int qt, int qb){ return query(ql, qr, qt, qb, 0, n/2+1, 0); }};int main(){ cin.tie(nullptr); ios_base::sync_with_stdio(false); int n, k; cin >> n >> k; segTree2D SegTreeMap(n); char map[n][n]; for(int i=0; i<n; i++){ for(int j=0; j<n; j++){ cin >> map[i][j]; SegTreeMap.update(i, j, map[i][j] == '*'?1:0); } } while (k--){ int action; cin >> action; if(action == 1){ int x, y; cin >> x >> y; x--, y--; if(map[x][y] == '*'){ SegTreeMap.update(x, y, -1); map[x][y] = '.'; }else { SegTreeMap.update(x, y, 1); map[x][y] = '*'; } }else{ int x1, y1, x2, y2; cin >> x1 >> y1 >> x2 >> y2; x1--,y1--,x2--,y2--; cout << SegTreeMap.query(y1, y2+1, x1, x2+1) << "\n"; } } return 0;}`

Competitive programming is vast and filled with seemingly endless complexities. Yet, as we explored these two problems, we realized that even the most intricate challenges have a variety of viable solutions, each with its own efficiency and uniqueness.

Iterative methods, prefix sum arrays, 2D prefix sum matrices, segment trees, and even their 2D counterparts - all these approaches offer unique insights into problem-solving, and I hope this post made you better understand the capabilities of each technic.

Remember, there is no one-size-fits-all solution in programming. The 'best' solution depends heavily on the problem at hand, the constraints we're working within, and, not least, your own creativity and understanding. Continue to explore, to learn, and to challenge yourself - every problem you encounter is an opportunity to grow.

I invite you to delve deeper, to question, and to play with the various strategies we've discussed. They're merely tools in your programming arsenal, and their real power is realized in their application.

Thank you for joining me on this journey. Keep the spirit of learning alive, and I hope to see you in future posts.

Let me know your thoughts and suggestions in the comment section below and wish you happy coding.

]]>Hello everyone, in this post we will look in detail at the solution to two similar, very classic DP problems: 'Roads' and 'Organizing Tiles', as well as their implementation in C++, altho the solution can be implemented in the language of your choice.

In this problem we need to imagine a rectangular table of size N x 2, this table needs to be completely covered with tiles of size 2 x 1 in such a way that all tiles are completely inside the table and do not overlap, the image below shows all possible valid coverings for a table where N = 4.

you are given T test cases and for each case a single line: N. Output the number of valid ways for covering a grid of size 2 x N.

The first line contains T, the following T lines contain a single integer: N.

for each test case output the answer, since the answer can be very large make sure to apply MOD 1,000,000.

0 <= N <= 1,000,00

1 <= T <= 10,000

The first big observation for this problem that we should make is to notice that the height of the table is set to 2 always no matter N, this means that we must only be concerned with the length. Knowing this, a great idea is to manually test and solve the problem as a human for n = 1, n = 2, n = 3, and n = 4. This process can be easily done by drawing all the valid ways we can cover the grid for each N on a notebook.

By doing these drawings, you can clearly see that it's impossible for a valid solution to contain something like this:

At first sight of the examples, it appears to not have any sort of pattern with the shapes, by observing the coverings for longer we can realize that the shapes themselves don't provide any information in order to predict another N. Because of this we should only pay attention to the number of coverings possible instead of the shapes.

Keeping the idea that the only thing that varies is the length, I started to wonder how can I know the solution for position 5 (1st indexed) without using the drawing method. To answer this question I began by only wondering the amount of ways we can fill that specific index.

Here is where the interesting solution starts to show. Notice how we have two ways of covering that index. Logically, if we want to know the ways to fill the entire table we should add the possibilities of the remaining space of the first way, with the possibilities of the remaining space of the second. This is looking like a recursive solution, and like most recursive functions we need some base conditions to end it. This case is very easy since we know the answer for N = 0, N = 1, and N = 2;

Below you can find my implementation of the recursive function that solves for N.

`typedef long long ll;ll solve(int n){ if(n == 0)return 0; if(n == 1)return 1; if(n == 2)return 2; return (solve(n-1) + solve(n-2)) % MOD;}`

Once we code the line `solve(n-1) + solve(n-2);`

it becomes obvious that this code is calculating the Fibonacci sequence. Notice how we are using long long, this is important since the answer can become large, thankfully the problem asks us to apply MOD.

The slight problem with this implementation is that for every N query, the recursion will be redone until one of the base conditions is fulfilled, this is very inefficient. however, we can use DP (Dynamic Programming) to compute the solution for N so that if it at some moment N gets repeated we can simply return the pre-computed answer.

`typedef long long ll;ll DP[1e6 + 5], MOD = 1e6;ll solve(int n){ if(n == 0)return 0; if(n == 1)return 1; if(n == 2)return 2; if(DP[n]) return DP[n]; return DP[n] = (solve(n-1) + solve(n-2))%MOD;}`

in the code cell above you can see the DP container added, now in our solve function, if a solution for N is already computed in the DP we return the value, else, we compute the solution and save it to the DP.

`return DP[n] = solve(n-1) + solve(n-2);`

this is a fancy way of assigning the answer to the DP and returning the value in a single line.

since the solution to this problem is simply to calculate the Fibonacci sequence, there is no need to use recursion, and you can do something iterative like this:

`typedef long long ll;vector<ll>fib = {0,1,2};ll MOD = 1e6;int main(){ ll n; cin >> n; for(ll i = 2; i <= 1e6 + 5; i++){ fib.push_back((fib[i]+fib[i-1])%MOD); } cout << fib[n]%MOD; return 0;}`

Time Complexity: The time complexity of this algorithm is O(n), where

`n`

is the input to the function. This is because each function call to solve for a particular value of`n`

is made only once due to memoization. Once`solve(n)`

is calculated, its value is stored in the DP array. Therefore, the total number of operations is linear with respect to the input size, leading to a time complexity of O(n).Space Complexity: The space complexity of this algorithm is also O(n), where

`n`

is the input to the function. This is because an array of size`n`

(specifically n + 5, but we ignore constants in Big O notation) is created to store the results of the function for each possible value of n from 0 to n. Thus, the amount of memory used scales linearly with the size of the input, leading to a space complexity of O(n).

Time Complexity: The time complexity of this code is O(n), where

`n`

is the maximum number for which you want to compute the Fibonacci number. This is because there is a loop that iterates`n`

times, and within each iteration, you perform a constant amount of work: retrieving two elements from the vector, adding them, taking the modulus, and appending the result to the vector. All of these operations are assumed to take constant time. Thus, the total amount of work done is proportional to`n`

, leading to a time complexity of O(n).Space Complexity: The space complexity of the code is also O(n). This is due to the

`fib`

vector, which stores`n`

elements. Since the amount of space used by the vector scales linearly with`n`

, the space complexity is O(n).

Imagine that there is a city represented by a rectangular grid of size H * W, where H is the height of the grid and W is the width. (See picture)

Jhon lives in the left top corner of block {0, 0}. you need to help him figure out the number of ways he can reach his parent's house which is located in the bottom right corner of the grid, by only moving to either right or down.

Note: Jhon moves through the "roads" which are represented by the edge of the block, for better understanding look at the picture below in which W = 1 and H = 1;

The first and only line contains integers W and H, representing the height of the grid,

Your code must output a single integer, the number of distinct ways for Jhon to reach his parent's house.

- 1 <= W, H <= 30

As we usually do when solving a problem, it's a good idea to solve the problem as a human for a couple of examples with the help of a notebook and make drawings of all paths to better understand the relationship between input and output.

When doing this manual process we can figure out a first brute-force approach of trying all possible paths. This can be done with a recursive function solve(int x, int y) that tries the two different paths solve(int x + 1, y) + solve(int x, int y + 1);

Let's see how such a solution can be implemented.

`#include <bits/stdc++.h>using namespace std;typedef long long ll;int w, h;ll solve(int x, int y){ if(x > w || y > h)return 0; if(x == w && y == h)return 1; return solve(x+1, y) + solve(x, y+1);}int main() { cin >> w >> h; cout << solve(0,0); return 0;}`

as you can see the solve solution only uses three lines of code, one condition to see if we have reached the bottom right corner (therefore a path exists), and we also check if it's inside the bounds of the map.

Unfortunately, because this code tries all different paths possible, it means that the Time Complexity In the worst case is O(2^(w+h)) because the function `solve`

will be called for every possible pair `(x, y)`

where `x`

is in the range `[0, w]`

and `y`

is in the range `[0, h]`

. However, due to the nature of the recursive calls (moving right or down), many of these calls will be made multiple times for the same pair of coordinates, leading to a large number of redundant operations. This, of course, is exponential. Given that W and H can be up to 30 this means that approximately 2^60 operations are going to be computed, which of course exceed the 400,000,000 operations that are normally the limit for most online judges. This solution is going to receive a TLE (Time Limit Exceded) verdict. However, because there are repeated solve(x, y) calls, it means that we can apply DP.

`#include <bits/stdc++.h>using namespace std;typedef long long ll;ll w, h, DP[31][31];ll solve(int x, int y){ if(x > w || y > h)return 0; if(x == w && y == h)return 1; if(DP[x][y])return DP[x][y]; return DP[x][y] = solve(x+1, y) + solve(x, y+1);}int main() { cin >> w >> h; cout << solve(0,0); return 0;}`

by doing this very slight code addition we are Reducing Redundant Calculations, In the previous version of the code, the same calculations were performed many times due to overlapping sub-problems. For example, both `solve(1, 2)`

and `solve(2, 1)`

would end up recursively calculating the result for `solve(2, 2)`

, among other overlapping calculations. This redundancy leads to an exponential time complexity, which is highly inefficient. However, with DP, the result of each unique call to `solve(x, y)`

is stored in the `DP[x][y]`

array after it's calculated for the first time. Then, if `solve(x, y)`

is called again, the function will first check if `DP[x][y]`

is non-zero, and if so, it will immediately return this stored result instead of re-calculating it. By applying this technic we managed to reduce the 2^60 operation to just 60 🤯.

Altho the previous solution is already enough to receive an AC verdict (accepted) it is not the only correct implementation to get it. I made this very cool observation when printing out the DP from the previous solution:

`35 15 5 1 20 10 4 1 10 6 3 1 4 3 2 1 1 1 1 0`

First of all, notice how the orientation of the table does not matter. Then, if we look closely the value clearly represents the number of ways for reaching a certain point if we start from the end which happens to always be the sum of the ways we can reach the cell below, with the ways we can reach the cell in the right. This makes a lot of sense, since if we can reach point A in 20 ways, and point B in 5, and we want to know the number of ways we can reach point C, it's trivial that it's the sum of 20 + 5.

By knowing this, we can initialize a DP matrix in which all the values in the first row and the first column are set to 1, this is going to be our base case since as we can confirm in our image there is always 1 way only to reach any of the points of the first row and the first column. (see image)

Then we can fill the rest of the table by iterating square by square and making the value of the point equal to the ways we can reach the point above, with the ways we can reach the point on the left. Lastly, we can just access the point {H, W}.

Here is my code implementation:

`#include<iostream>using namespace std;typedef long long ll;int main(){ ll H, W; cin >> H >> W; ll DP[H+1][W+1]; for(int i=0; i<=W; i++)DP[0][i] = (ll)1; // filling the first row for(int i=0; i<=H; i++)DP[i][0] = (ll)1; // filling the first column for(int i=1; i<=H; i++){ for(int j=1; j<=W; j++){ DP[i][j] = DP[i-1][j] + DP[i][j-1]; // filling DP } } cout << DP[H][W]; return 0;}`

**Time Complexity:**The primary operations in this algorithm are done in the

`solve`

function, where it calculates the number of paths from (0,0) to (w,h).The number of distinct problems to be solved is

`w*h`

, where`w`

is the width and`h`

is the height of the 2D grid. This is because we need to compute the number of paths for each cell from (0,0).The time complexity of each problem is O(1). This is because the function

`solve`

either returns the stored result in DP[x][y] (if it has been computed before), or it computes the result as the sum of`solve(x+1, y)`

and`solve(x, y+1)`

. Both of these are O(1) operations because they are simple mathematical operations or array accesses.

Therefore, the overall time complexity of this algorithm is O(w*h).

**Space Complexity:**The space complexity is also O(w

*h) because of the 2D DP array used to store the intermediate results. Each cell in the grid has a corresponding cell in the DP array, thus leading to w*h cells in the DP array.The 2D array

`DP`

is of size [31][31], but the actual space used will be w*h where`w`

and`h`

are the width and height of the 2D grid. This is the space needed to store the number of paths for each cell.

**Time Complexity:**The time complexity of this program is determined by the double for-loop that iterates over all the cells in the grid to calculate and fill in the DP array.

The outer loop runs H+1 times (from 0 to H) and for each iteration, the inner loop runs W+1 times (from 0 to W).

Inside the inner loop, you're performing a constant amount of work (accessing elements in the DP array and adding them together).

Given this, the time complexity is O(H*W).

**Space Complexity:**The space complexity of this program is determined by the size of the DP array.

The DP array is of size (H+1)x(W+1). You need to store a long long integer for each cell in the grid, which gives a space complexity of O(H*W).

Besides the DP array, the rest of the variables occupy constant space that does not grow with the size of the input.

Therefore, the overall space complexity is O(H*W).

We've explored two dynamic programming (DP) problems in-depth - 'Organizing Tiles' and 'Roads'. Both were tackled with recursive and iterative solutions, helping us appreciate the versatility of DP. By analyzing time and space complexity, we've learned about performance trade-offs in different scenarios.

Dynamic Programming is a powerful technique, breaking down problems into manageable sub-problems. It's essential in competitive programming and beyond. Remember, mastering DP needs practice and intuitive understanding. Identify overlapping subproblems, understand them, and choose between recursion or iteration as per the constraints.

Keep practicing and learning. Every problem solved takes you closer to mastery. The learning journey is as rewarding as the destination itself, enriching you with invaluable skills.

Thank you for joining this DP journey. I hope it's been insightful and encourages you to solve more DP challenges. Until next time, happy coding and stay curious!

]]>https://blog.garybricks.com/bfs-and-dfs-beginners-overview-in-c

Imagine a parallel universe where our beloved characters, Mario and Luigi, find themselves in a challenging situation. They are trapped within an intricate maze filled with rocks and spaces ablaze with fire. Navigating through these hazards is no easy task - the brothers cannot move through the fire, and the rocks form impregnable barriers.

Luckily, this world has an interesting mechanism - buttons distributed randomly within the maze. When either of the brothers steps on one of these buttons, something miraculous happens: every fire within the maze is instantly extinguished. This only happens as long as the brother is on top of the button, the moment he moves out of the button, all fires reappear.

The brothers are aware that somewhere within this maze lies an elusive exit. They are desperate to find a path to this escape route but two pressing questions loom in their minds: "Is it even feasible for both of us to reach this exit together?" and "If so, what is the quickest time in which we can both safely reach the exit?"

Here's where you come in. Your mission is to assist Mario and Luigi in finding answers to these questions. Keep in mind the following rules of movement:

For each second that ticks by, only one brother can make a move. This means they must strategize and choose their steps wisely.

Mario and Luigi can only move to the four adjacent spaces (up, down, left, right) in the maze, provided that the spaces are free from fire or rocks.

Lastly, don't worry about them colliding - both brothers are able to occupy the same space within the maze.

This is a tale of brotherhood, strategy, and survival. Are you ready to guide Mario and Luigi toward their escape?

The first line of input will consist of a single integer, N, representing the dimensions of the maze. The maze itself is represented by a character matrix of size N x N, with specific characters symbolizing different elements within the maze. Here's what each character represents:

'.' Empty space

'*' Fire

'L' Luigi

'M' Mario

'S' Exit point

'#' Rock (forming an impenetrable wall)

For a clearer understanding, let's look at a few examples:

`-------- EXAMPLE1 ---------6#######S.B.##****##L.M.##B...#######output: 8-------- EXAMPLE2 ---------7########S.*LB##...**##....B##....*##...*M########output: 13---------------------------`

- The size of the maze, N, is bounded as follows: 5 N 50.

Your code should generate a single-line output. Depending on the maze configuration and the feasibility of the problem, it can be one of two possible statements:

"Not possible for both Brothers to reach the exit" - This output should be returned when it is impossible for both Mario and Luigi to arrive at the exit given the current maze conditions.

"The minimum time for both brothers to reach the exit is: X" - This statement should be the output when there exists a path for both brothers to reach the exit. Here, 'X' should be replaced with an integer representing the minimum time required for both Mario and Luigi to reach the exit.

We'll begin our problem-solving journey by dissecting the relationship between the provided input and output examples.

In our first example, it's apparent that the exit is entirely encircled by fire. As a consequence, Luigi is compelled to rush to the closest button, thereby allowing Mario to activate the button adjacent to the exit. This course of action then provides Luigi with a clear path to the exit, which he can take before Mario. Two crucial insights were gleaned from this analysis:

If the exit is enveloped by fire, both characters can only reach the exit if a button is located within the fiery perimeter.

To determine the shortest possible path in this instance, we must evaluate both potential routes. This entails testing scenarios in which Mario reaches the first button prior to Luigi and vice versa.

Let's now turn our attention to the second example. Notably, a button exists within the fiery exit zone, suggesting that a viable solution may be at hand. However, Mario is entirely ensnared, immobilized by the fire. Thankfully, Luigi is free to move and can reach a button. Upon pressing this button, Luigi frees Mario, who can then head to the button within the exit zone. This subsequently paves the way for Luigi to arrive at the exit, followed closely by Mario.

Based on the insights drawn from these examples, I began formulating a general solution. My initial idea involved testing all possible moves for Mario. This would involve him attempting to reach all buttons and the exit. Then, for each of these scenarios, we would repeat the process for Luigi. This essentially amounts to exploring all possible routes and then identifying the most efficient one.

Retaining our initial thought:

*"Explore all feasible movements for Mario and Luigi to identify the most efficient path."*

My first instinct was to employ Breadth-First Search (BFS) independently for Mario, tracing all accessible buttons. For each of these options, Luigi should also execute a BFS to reach all buttons located within the fire zone surrounding the exit, as well as the exit itself. Following each of Luigi's paths, Mario should again perform a BFS towards the exit. We then repeat all these steps, this time initiating with Luigi. Quite a whirlwind of thoughts, right? 🤯😵

This solution, although exhaustive, appears immensely complex and potentially riddled with challenges during implementation. Primarily, it involves launching multiple BFS procedures, each accompanied by unique conditions.

The intricacy of this implementation mainly stems from the fact that we're dealing with Mario and Luigi separately. This prompts us to ask, "Could we conduct a single BFS that examines all potential routes, treating Mario and Luigi as a unified entity?"

And that, dear readers, is indeed the golden key to unlocking this problem.

Assuming you're familiar with BFS and have taken a glance at my BFS beginners overview, implementing a solution for this problem would be fairly straightforward if it only concerned **one** character. The completed code for such a scenario might look like this:

`// ( Scenario with Mario only )#include<iostream>#include<queue>using namespace std;int X[4] = {1,0,-1,0}; // helper directions for X (right, up, left, down)int Y[4] = {0,1,0,-1}; // helper directions for Y (right, up, left, down)int dist[60][60], n;char MAP[60][60];struct node{int x, y;}; // node represented by coordinatesbool valid(int x, int y){ // checks if the coordinate is within the map, is not fire and not rock, and has not been visited return x >= 0 && x < n && y >= 0 && y < n && dist[x][y] == -1 && MAP[x][y] != '*' && MAP[x][y] != '#';}int main(){ int mx, my, ex, ey; // mario {x, y} and exit {x, y} cin >> n; // --------- getting input --------- // for(int i=0; i<n; i++){ for(int j=0; j<n; j++){ dist[i][j] = -1; // initializing the Dist for that node to not visited cin >> MAP[i][j]; if(MAP[i][j] == 'S'){ex = i, ey = j;} // exit found if(MAP[i][j] == 'M'){mx = i, my = j;} // mario found } } queue<node> BFS; BFS.push({mx, my}); // starting BFS dist[mx][my] = 0; // starting node distance set to 0 while (BFS.size()){ node current_node = BFS.front(); BFS.pop(); // visiting all 4 adjacent coords for (int d = 0; d<4; d++){ int newX = current_node.x + X[d]; int newY = current_node.y + Y[d]; if(valid(newX, newY)){ BFS.push({newX, newY}); dist[newX][newY] = dist[current_node.x][current_node.y] + 1; } } } if(dist[ex][ey] == -1)cout << "imposible to reach the exit"; else cout << "the minimum time for reaching the exit: " << dist[ex][ey]; return 0;}`

The code shown above should look very familiar, it's the very classic implementation for a BFS on an implicit Graph, shown in numerous examples in my: BFS beginners overview.

So how might we modify this single-character set up to accommodate two characters? Our initial question is: "What would a node comprising two characters look like?" Here's a possible idea:

`// Before (Only one character)struct node{int x, int y};// After (Mario and Luigi)struct character{int x, int y};struct Node{ character Mario; character Luigi;};`

This structure appears plausible. However, referring to the x and y coordinates of Mario and Luigi in the later parts of our code using `currentNode.Mario.x`

and `currentNode.Luigi.x`

might be a tad cumbersome. Let's streamline this with a simpler approach:

`struct Node{int Mx, My, Lx, Ly};`

In this structure, we're tracking Mario with the first pair of integers and Luigi with the second pair. This approach leads to more straightforward and cleaner code.

To begin, let's accommodate the new node in the `dist`

matrix which keeps track of the minimum distance to a node and also indicates if it hasn't been visited yet.

`int dist[60][60] // before, mario only {x, y}int dist[60][60][60][60] // after, both brothers {Mx, My, Lx, Ly}`

this is a very simple change that often confuses many programmers at the beginning (me included) because you get used to seeing the distance the same way you see the map, which for this example is 2D. however, remember that in reality `dist`

is made specifically for whatever the node is, and must be adjusted. on a side note, you are not obligated to use a matrix like I'm doing here, you can also use an unordered map for example, and give an "id" to the nodes. However, this matrix approach simplifies matters for this case. It's important to note that this solution is feasible because N is at most 50. The required space for this matrix is 60^4, which is suitable for this problem but may pose issues for larger constraints.

Earlier, we initialized the 'dist' matrix at the same time as taking our input, but given the change, don't forget to initialize the matrix separately like so:

`void initDist(){ for(int i=0; i<60; i++){ for(int j=0; j<60; j++){ for(int k=0; k<60; k++){ for(int l=0; l<60; l++)dist[i][j][k][l] = -1; } } }}`

Before we revise the actual BFS, let's update the input-receiving part to consider Luigi:

`int mx, my, lx, ly, ex, ey; // mario {x, y}, Luigi {x, y}, and exit {x, y}cin >> n;// --------- getting input --------- //for(int i=0; i<n; i++){ for(int j=0; j<n; j++){ cin >> MAP[i][j]; if(MAP[i][j] == 'S'){ex = i, ey = j;} // exit found if(MAP[i][j] == 'M'){mx = i, my = j;} // Mario found if(MAP[i][j] == 'L'){lx = i, ly = j;} // Luigi found }}`

Next, we can rectify the initial part of the BFS algorithm that involves adding the starting node to the queue and setting its distance to 0:

`initDist();queue<node> BFS;BFS.push({mx, my, lx, ly}); // starting BFSdist[mx][my][lx][ly] = 0; // starting node distance set to 0`

Let's then refine the part of the algorithm that handles visiting all nodes adjacent to the current one:

`// Before:node current_node = BFS.front();BFS.pop();// visiting all 4 adjacent coordsfor (int d = 0; d<4; d++){ int newX = current_node.x + X[d]; int newY = current_node.y + Y[d]; if(valid(newX, newY)){ BFS.push({newX, newY}); dist[newX][newY] = dist[current_node.x][current_node.y] + 1; }}`

`// Afternode current_node = BFS.front();BFS.pop();// visiting all 4 adjacent coordinates for Mario onlyfor (int d = 0; d<4; d++){ int newMX = current_node.Mx + X[d]; int newMY = current_node.My + Y[d]; int newLX = current_node.Lx; int newLY = current_node.Ly; if(valid(newMX, newMY, newLX, newLY)){ BFS.push({newMX, newMY, newLX, newLY}); dist[newMX][newMY][newLX][newLY] = dist[current_node.Mx][current_node.My][current_node.Lx][current_node.Ly] + 1; }}// visiting all 4 adjacent coordinates for Luigi onlyfor (int d = 0; d<4; d++){ int newMX = current_node.Mx; int newMY = current_node.My; int newLX = current_node.Lx + X[d]; int newLY = current_node.Ly + Y[d]; if(valid(newMX, newMY, newLX, newLY)){ BFS.push({newMX, newMY, newLX, newLY}); dist[newMX][newMY][newLX][newLY] = dist[current_node.Mx][current_node.My][current_node.Lx][current_node.Ly] + 1; }}`

Where we once checked the 4 adjacent coordinates to the current one and pushed the BFS if it was valid, now however, we must do that independently for Mario but considering that Luigi is part of the node, and his "new" position, is the same, and once the 4 possible changes for Mario have been made, we repeat the process but for Luigi.

We're nearly done! The last adjustment we need is to modify the 'valid' function to consider coordinates for both brothers. This entails checking that both coordinates are within the map and legal. Given the exact coordinates for both Mario and Luigi, we can easily add the condition for the scenario in which one of them is standing on a button, thereby allowing passage through fire. Here is my implementation:

`bool valid(int Mx, int My, int Lx, int Ly){ // checks if both coordinates are within the map and the node has not been visited. if(Mx >= 0 && My >= 0 && Mx < n && My < n && dist[Mx][My][Lx][Ly] == -1){ if(MAP[Mx][My] == '#' || MAP[Lx][Ly] == '#')return false; // Rock Wall always illegal for either one if(MAP[Mx][My] == '*' && MAP[Lx][Ly] == 'B')return true; // If Mario on fire and Luigi on button if(MAP[Lx][Ly] == '*' && MAP[Mx][My] == 'B')return true; // if Luigi on fire and Mario on button // if the code didn't enter the previous conditions, it means that no buttons are pressed if(MAP[Mx][My] == '*' || MAP[Lx][Ly] == '*')return false; // fire illegal // else it means that this node is valid :D return true; } return false;}`

To retrieve the actual answer, we must access the desired final node. For a single character, this would be implemented as follows:

`if(dist[ex][ey] == -1)cout << "imposible to reach the exit";else cout << "the minimum time for reaching the exit: " << dist[ex][ey];`

However, we now need to revise this to work with the new 4D node distance matrix. This can be perplexing, given that we only have a single exit represented with two numbers: ex, ey, and we require 4 values for accessing the 4D matrix. But don't panic, the solution is quite straightforward:

`if(dist[ex][ey][ex][ey] == -1)cout << "imposible to reach the exit";else cout << "the minimum time for reaching the exit: " << dist[ex][ey][ex][ey];`

Remember, the first pair of values represents Mario's position, and the second pair signifies Luigi's location. We are interested in the distance for the node where both characters reach the same exit.

Let's examine the time and space complexity for this algorithm:

Time Complexity: The main factor affecting time complexity in this algorithm is the breadth-first search (BFS) which in the worst case, visits all nodes in the graph. As the graph in this case is essentially a 4-dimensional grid of size 60 (considering both Mario and Luigi's x, y positions), the time complexity can be considered as O(N^4) where N is the size of the graph (here, N = 60). This is because BFS in the worst case explores all possible combinations of Mario and Luigi's positions.

Space Complexity: The space complexity is largely driven by the 4-dimensional distance matrix of size 60, 'dist'. As each element of the matrix can be occupied, the space complexity is O(N^4), the same as the time complexity. This is because we need to store the distance for each possible combination of positions for Mario and Luigi.

It's important to note that this solution takes advantage of the fact that N is small (60), as both the time and space complexity are polynomial, making the algorithm impractical for larger inputs.

The first time I submitted this implemented solution, I got a 90% TLE verdict, which forced me to think of a way of optimizing this solution, This compelled me to consider optimizing the solution. Initially, my focus was on streamlining the code while maintaining the underlying logic. Unfortunately, this approach didn't yield the desired improvement. Then I stumbled upon a realization. Consider two nodes, one with coordinates {Mx = 5, My = 5, Lx = 1, Ly = 4} and the other with {Mx = 1, My = 4, Lx = 5, Ly = 5}. Interestingly, the minimum distance from either of these nodes to the exit would be identical. Refer to the image below for a clearer understanding.

This observation led me to understand that these nodes are effectively identical for our purpose, yet the current code treats them as separate entities. If the code has marked the first node as visited, upon encountering the second node, it erroneously treats it as unvisited. To fix this, we can simply add one additional check to our `valid`

function like so:

`if(Mx >= 0 && My >= 0 && Mx < n && My < n && dist[Mx][My][Lx][Ly] == -1 && dist[Lx][Ly][Mx][My] == -1)`

The remedy for this issue is a simple augmentation to our 'valid' function, adding an extra check for the node where Mario and Luigi have swapped positions. By making this minor adjustment, we effectively halve the number of possibilities, significantly improving the solution's efficiency.

`#include<iostream>#include<queue>using namespace std;int X[4] = {1,0,-1,0}; // helper directions for X (right, up, left, down)int Y[4] = {0,1,0,-1}; // helper directions for Y (right, up, left, down)int dist[60][60][60][60], n;char MAP[60][60];struct node{int Mx, My, Lx, Ly;}; // node represented by coordinatesbool valid(int Mx, int My, int Lx, int Ly){ // checks if both coordinates are within the map and the node has not been visited. if(Mx >= 0 && My >= 0 && Mx < n && My < n && dist[Mx][My][Lx][Ly] == -1 && dist[Lx][Ly][Mx][My] == -1){ if(MAP[Mx][My] == '#' || MAP[Lx][Ly] == '#')return false; // Rock Wall always illegal for either one if(MAP[Mx][My] == '*' && MAP[Lx][Ly] == 'B')return true; // If Mario on fire and Luigi on button if(MAP[Lx][Ly] == '*' && MAP[Mx][My] == 'B')return true; // if Luigi on fire and Mario on button // if the code didn't enter the previous conditions, it means that no buttons are pressed if(MAP[Mx][My] == '*' || MAP[Lx][Ly] == '*')return false; // fire illegal // else it means that you are ok :D return true; } return false;}void initDist(){ // Initialized the Dist matrix to -1 for(int i=0; i<60; i++){ for(int j=0; j<60; j++){ for(int k=0; k<60; k++){ for(int l=0; l<60; l++)dist[i][j][k][l] = -1; } } }}int main(){ int mx, my, lx, ly, ex, ey; // mario {x, y}, Luigi {x, y}, and exit {x, y} cin >> n; // --------- getting input --------- // for(int i=0; i<n; i++){ for(int j=0; j<n; j++){ cin >> MAP[i][j]; if(MAP[i][j] == 'S'){ex = i, ey = j;} // exit found if(MAP[i][j] == 'M'){mx = i, my = j;} // Mario found if(MAP[i][j] == 'L'){lx = i, ly = j;} // Lario found } } initDist(); queue<node> BFS; BFS.push({mx, my, lx, ly}); // starting BFS dist[mx][my][lx][ly] = 0; // starting node distance set to 0 while (BFS.size()){ node current_node = BFS.front(); BFS.pop(); // visiting all 4 adjacent coordinates for Mario only for (int d = 0; d<4; d++){ int newMX = current_node.Mx + X[d]; int newMY = current_node.My + Y[d]; int newLX = current_node.Lx; int newLY = current_node.Ly; if(valid(newMX, newMY, newLX, newLY)){ BFS.push({newMX, newMY, newLX, newLY}); dist[newMX][newMY][newLX][newLY] = dist[current_node.Mx][current_node.My][current_node.Lx][current_node.Ly] + 1; } } // visiting all 4 adjacent coordinates for Luigi only for (int d = 0; d<4; d++){ int newMX = current_node.Mx; int newMY = current_node.My; int newLX = current_node.Lx + X[d]; int newLY = current_node.Ly + Y[d]; if(valid(newMX, newMY, newLX, newLY)){ BFS.push({newMX, newMY, newLX, newLY}); dist[newMX][newMY][newLX][newLY] = dist[current_node.Mx][current_node.My][current_node.Lx][current_node.Ly] + 1; } } } if(dist[ex][ey][ex][ey] == -1)cout << "imposible to reach the exit"; else cout << "the minimum time for reaching the exit: " << dist[ex][ey][ex][ey]; return 0;}`

In wrapping up, we've navigated through the intricate details of a 4D Breadth-First Search algorithm for a captivating problem. We've traveled from understanding the problem, implementing the solution, encountering setbacks, and eventually triumphing by innovating an optimized solution.

Before I sign off, I would like to remind you that the process of learning and improving is an iterative one. If your first solution doesn't work or isn't efficient enough, don't be disheartened. Every attempt, every setback, and every small victory serves as a stepping stone toward becoming a better problem solver.

I trust this walkthrough has offered you valuable insights and has further sharpened your skills in tackling complex algorithmic problems. I hope to see you take these lessons forward into your next challenge.

Stay tuned for more competitive programming problem breakdowns and insights. Happy coding!

]]>A k-Tree is a unique tree structure where each node has exactly k children nodes, extending infinitely. The tree is weighted, such that each of the k edges stemming from a node carries a weight value ranging from 1 to k. In the illustration below, we have an example of a k-tree where k equals 3.

Please note that in this particular image, the tree is depicted to a depth of 3. However, remember that a k-tree is infinite, and this pattern of branching continues endlessly from each child node.

Now, let's consider the following problem:

Given three parameters **K, N, and D**, write a program that answers the following question:

*'How many paths are there starting from the root node, such that the total sum of the edge weights in a path equals N, and the path includes at least one edge of weight D or more?'*

A single line containing three integers: N, K, and D.

- 1 <= N, K <= 100

- 1 <= D <= K

A single integer: the answer to the problem. Given that the answer can be quite large, ensure to output your answer modulo 10^9 + 7.

`Example 1:Input: 3 3 2 Output: 3Example 2: Input: 3 3 3 Output: 1Example 3: Input: 4 5 2 Output: 7`

Here is an image for the first example:

When tackling a problem, it's often best to start by examining an example and trying to solve it logically, as a human would. In this case, it's helpful to sketch out a tree for the first example to understand the relationship between the input and output. The first and most apparent solution was to model the tree structure and attempt to traverse all possible paths.

Before diving into implementing this approach, let's pause and consider potential challenges. The first thing I noticed is that the tree's "nodes" are irrelevant - they carry no intrinsic value. We are only interested in the edges, and the order in which we traverse the tree doesn't matter. Moreover, the tree is infinite, which could lead to complications during implementation.

With these insights, I decided to reassess the situation, returning to the drawing board. But this time, instead of focusing on the tree, I would only consider the edge values.

For the first example, I wrote down the variable values. This reminded me of three key points:

The sum of the path should be

**exactly**NAll Edges used must be

**less or equal to K**A path is valid only if there is an edge with a weight greater than or equal to D.

Taking these points into account, we can rephrase the problem as:

*"How many ways can you form a sum equal to N using numbers less than or equal to K, with at least one number being greater than or equal to D?"*

This greatly simplifies the problem since we have effectively removed the "tree" aspect from it. Now, we can solve the problem using only numbers and we can still maintain the same basic approach of testing all possible number combinations.

For problems involving combinations, a recursive function often comes in handy. However, it's crucial to define base cases and divide the problem into manageable components. In this scenario, we'll start with:

*"How many ways can you form a sum equal to N using numbers less than or equal to K, with at least one number being greater than or equal to D?"*

Since the Tree is infinite, we need a condition to stop the current path when the sum exceeds N. This is important as further exploration of this path won't yield a valid solution (given that there are no negative edges).

We need to acknowledge when we've successfully formed a sum equal to N. This involves incrementing our answer count and halting the current path exploration.

With these two considerations, let's lay out the following pseudo code:

`N = 3, K = 3RESULT = 0function solve(sum): if sum > N: return if sum == N: {RESULT += 1; return} for each number from 1 to k: solve(sum + number)`

now let's consider the second part of the problem:

*"How many ways can you form a sum equal to N using numbers less than or equal to K, with at least one number being greater than or equal to D?"*

We're interested in whether there's at least one number greater than or equal to D. To capture this, we add a boolean parameter to our function,

`valid`

, which becomes true if a number meets this condition.Additionally, we need to modify the base case that increments the result. Now, we should only increment if

`valid`

equals true.

Combining both problem parts, we can arrive at our final pseudo code:

`N = 3, K = 3, D = 2RESULT = 0function solve(sum, valid): if sum > N: return if sum == N and valid: {RESULT += 1; return} for each number from 1 to k: solve(sum + number, number >= D or already valid)solve(0, false)print(RESULT)`

I highly recommend trying to implement the solution in your preferred programming language. Below is my implementation in C++:

`#include <bits/stdc++.h>using namespace std;int N, K, D, RES = 0;void solve(int sum, bool valid){ if(sum > N)return; if(sum == N && valid){RES++; return;} for(int i=1; i<=K; i++)solve(sum + i, i >= D || valid);}int main() { cin >> N >> K >> D; solve(0, false); cout << RES; return 0;}`

The worst-case time complexity is exponential, O(K^N), as the function `solve`

can potentially make K recursive calls at each depth level, with the maximum depth level being N. However, the actual time complexity will likely be significantly less than O(K^N) due to the early return condition (if(sum > N)return;) which prunes many branches of the recursion tree where the sum has already exceeded N.

The space complexity is O(N), which represents the maximum depth of the recursive call stack. Each recursive call adds a new level to the stack.

However, keep in mind that the time complexity for this solution is not desirable given that N and K can be up to 100. This could result in up to 1.e+200 operations, which is far beyond what can be executed without encountering a Time Limit Exceeded (TLE) verdict.

Whenever I create a recursive solution that tests for all possibilities and it needs optimization, the first thing I check is the possibility of repeated function calls, or what I call "repeated states".

In this specific solution, the state is represented by the parameters of the function {sum, valid}. Both values are critical to defining the state.

An easy method to identify repeated states is to add a print statement in our function like this:

`void solve(int sum, bool valid){ cout << "{ " << sum << " , " << valid << " }\n";`

By running the code with input: 4, 5, 2, it becomes evident that state { 6, 1 } is repeated eight times.

If repeated states exist, it indicates that Dynamic Programming (DP) can be applied to optimize the code. DP involves computing the solution for a state, storing it, and reusing it whenever the state is encountered again. This approach saves significant computation time.

Here's the C++ implementation utilizing DP:

`#include <bits/stdc++.h>using namespace std;int N, K, D;vector<vector<int>> DP(105,vector<int>(2, -1));int solve(int sum, bool valid){ if(sum > N)return 0; if(sum == N && valid)return 1; if(DP[sum][valid] != -1) return DP[sum][valid]; int resultForThisState = 0; for(int i=1; i<=K; i++){ resultForThisState += solve(sum + i, i >= D || valid); } return DP[sum][valid] = resultForThisState;}int main() { cin >> N >> K >> D; cout << solve(0, false); return 0;}`

This code is similar to the original recursive solution with the addition of a DP approach. Let's look at the new elements in detail.

`vector<vector<int>> DP(105,vector<int>(2, -1));`

This line of code initializes our DP storage. It's a 2D matrix of size 105x2. We use 105 as the size because, although N can only reach 100, it's a good practice to leave a little margin for error. The second dimension is 2, as `valid`

is a boolean variable and can only have two values (true or false).

Note: The entire matrix is initialized with -1. This is critical because 0 can be a valid computed answer.

`if(DP[sum][valid] != -1) return DP[sum][valid];`

This line checks if there's a precomputed result for this state. If there is, it returns that value, saving us from unnecessary computation.

if none of the base case conditions are met, we store the result for the current state, which is the sum of all of its children's results.

`return DP[sum][valid] = resultForThisState;`

This line stores the result of the current state in the DP table and returns that value. It's a concise way of accomplishing two tasks at once.

This solution using Dynamic Programming has considerably improved the time and space complexity compared to the previous recursive solution without memoization.

Time complexity: The time complexity of the function is O(NK), since the function solve is called for all sums from 0 to N (N+1 states), and for each sum, there are K possibilities to consider. But due to memoization, each state {sum, valid} is computed only once, giving us a time complexity of O(NK).

Space complexity: The space complexity of the program is O(N) due to the DP array, which has a size of 105x2. The recursion stack in this case doesn't contribute significantly to the space complexity because once a state is computed, it is stored and doesn't need to be computed again. The 2D DP array size is constant (105x2) and does not depend on the inputs N, K, or D. Therefore, the space complexity is O(1). However, considering that the size of the DP array is proportional to N, it could also be considered as O(N) in terms of input size.

So, we can say that the time complexity is O(N*K) and the space complexity is O(N) (or O(1) if considering the fixed size of the DP array).

This is incredibly efficient because N and K can be up to 100, this means that at most, this code runs 10,000 operations. 😎🥳

Two small details we left out in our coded solution were: *"Given that the answer can be quite large, ensure to output your answer modulo 10^9 + 7."*

However, this can be added very simply by using the C++ long long data type for large numbers and adding the Modulo operation every time the solve() function gets used.

`#include <bits/stdc++.h>using namespace std;typedef long long ll;int N, K, D;ll MOD = 1e9 + 7vector<vector<ll>> DP(105,vector<ll>(2, -1));ll solve(ll sum, bool valid){ if(sum > N)return 0; if(sum == N && valid)return 1; if(DP[sum][valid] != -1) return DP[sum][valid]; ll resultForThisState = 0; for(ll i=1; i<=K; i++){ resultForThisState += solve(sum + i, i >= D || valid) % MOD; } return DP[sum][valid] = resultForThisState % MOD;}int main() { cin >> N >> K >> D; cout << solve(0, false) % MOD; return 0;}`

In conclusion, solving a competitive programming problem requires an understanding of the problem statement, the ability to break down the problem, and knowing the right approach to use. The k-Tree problem provided a clear illustration of these steps, taking us from an initial analysis and solution that wasn't efficient, to a significantly more efficient solution using Dynamic Programming.

Don't be disheartened if you don't immediately see how to solve a problem. The beauty of competitive programming is in the journey and the learning process. With practice and a determination to understand the concepts, you will continually improve. Always remember to keep refining your skills, keep learning, and most importantly, enjoy the journey!

We hope you found this article useful and educational. Stay tuned for more competitive programming problem breakdowns and insights. Happy coding!

]]>Hello everyone 👋, In today's post I wanted to talk about the famous Dijkstra algorithm, we are going to see what it is, what it's used for, how it works, and the implementation of the algorithm in c++ altho the idea can be applied in any language.

Before we continue I highly recommend you read the following resources:

The Dijkstra algorithm is a graph search algorithm that solves the single-source shortest path problem for a graph with non-negative edge weights, producing a shortest path tree, this means that we can know the **shortest** distance from the origin node to the rest of the nodes in a graph. It is very similar to the BFS algorithm, however, the BFS algorithm does not give the shortest distance in a **weighted** graph.

Dijkstra's algorithm is a popular choice for finding the shortest path in a graph, and it has many practical applications. Some examples of where Dijkstra's algorithm is used include:

Navigation: Dijkstra's algorithm can be used to find the shortest route between two locations on a map, taking into account factors such as distance, time, and traffic.

Network routing: In computer networking, Dijkstra's algorithm can be used to find the shortest path between two nodes in a network, such as routers or servers.

Traffic management: Dijkstra's algorithm can be used to optimize traffic flow by finding the shortest routes for vehicles to take, based on factors such as distance, time, and traffic.

Resource allocation: Dijkstra's algorithm can be used to find the most efficient allocation of resources, such as in a supply chain or manufacturing process.

Artificial intelligence: In artificial intelligence and machine learning, Dijkstra's algorithm can be used to find the shortest path in a graph representing a problem space, such as in pathfinding or planning.

These are just some examples, in my case I use Dijkstra for competitive programming problems, we'll look at an example later in this post.

The Dijkstra algorithm works by starting from a specified node and repeatedly expanding to the closest adjacent node until it reaches the destination node, very similar to the BFS algorithm however the Dijkstra algorithm uses a priority queue instead of a queue, this is necessary since the adjacent node with the less cost has the highest priority. At each step, the algorithm removes the node at the front of the priority queue and updates the distances of its adjacent nodes based on the distance from the starting node to the current node.

To implement Dijkstra's algorithm, we need to keep track of the following information for each node:

The distance from the starting node to the current node.

The previous node in the path from the starting node to the current node.

Initially, the distance of the starting node is set to 0, and the distances of all other nodes are set to infinity. As the algorithm progresses, it updates the distances of the neighboring nodes based on the distance from the starting node to the current node.

Here is an outline of the steps:

Set the distance of the starting node to 0 and the distance of all other nodes to infinity.

Add the starting node to the priority queue

While the priority queue is not empty:

Remove the node with the highest priority (lowest distance) from the priority queue, (The Top).

Check if the current node has a cost greater than what it already has, if this is true, it means that the cost is not being improved and we can

`continue`

.Update the distances of its neighboring nodes based on the distance from the current node to the neighboring node.

Save that the neighboring node comes from the current node in the

`BACK`

array, this is going to help us to recreate the path.Add the neighboring nodes to the priority queue.

Once the priority queue is empty, we can reconstruct the shortest path from the starting node to the destination node by following the previous nodes back from the destination node to the starting node using the

`BACK`

.

This algorithm is more efficient and can handle larger graphs in comparison to other algorithms like the "Bellman-Ford". However, the Dijkstra algorithm requires that there are no negative weight edges.

In order to better understand the algorithm I made the following animation:

the input for the example graph shown above would be:

`8 110 3 30 2 81 3 11 6 12 5 82 4 73 7 44 7 24 6 35 7 86 7 71`

The last number is the starting node.

Below you can find the implementation of the algorithm in c++, remember that there are several ways to code this algorithm and I'm just showing the version I use. you can find the explanation below the code cell.

`#include<iostream>#include<vector>#include<queue>#include<algorithm>using namespace std;typedef long long ll;struct node{ int node; ll cost; bool operator < (const struct node &b) const{ if(cost != b.cost) return cost > b.cost; if(node != b.node) return node > b.node; return 0; }};const ll INF = 1E18;int main(){ int n, m; cin>>n>>m; vector<vector<node>> adj(n); vector<int> BACK(n, -1); for(int i = 0; i < m; i++){ int u, v; ll w; cin>>u>>v>>w; adj[u].push_back({v, w}); adj[v].push_back({u, w}); } vector<ll> dist(n, INF); int source; cin >> source; dist[source] = 0; priority_queue<node> dijkstra; dijkstra.push({source, 0}); while(!dijkstra.empty()){ node c = dijkstra.top(); dijkstra.pop(); if(c.cost > dist[c.node]) continue; for(node adj: adj[c.node]){ if(c.cost + adj.cost >= dist[adj.node])continue; dist[adj.node] = c.cost + adj.cost; BACK[adj.node] = c.node; dijkstra.push({adj.node,c.cost + adj.cost}); } } for(int i = 0; i < n; i++){ cout<<i<<": cost:"<<dist[i]<<" path:"; vector<int> path; int current = i; while (current != -1) { path.push_back(current); current = BACK[current]; } reverse(path.begin(), path.end()); for(auto e:path)cout << e << " ==> "; cout << endl; } return 0;}`

First, we define our node struct, which is going to have two properties, the name or in this case number of the node, and the cost or weight. Notice that we also define the "<" operator, this is necessary and very important for the priority queue to know how to order the nodes by cost. Then we input the graph itself in the exact same way as we do with a BFS algorithm and we set the source distance to 0. Notice how we are going to keep track of the distances in the `dist`

vector. Additionally, we also define the Back vector, with -1 as the default value.

Now we define our Priority Queue which I like to call "Dijkstra" and we push the origin node while it's not empty, we are going to perform the following actions:

remove the top of the priority queue. Check if the node has a cost greater than the already saved distance for that node in the `dist`

vector, in which case you continue.

`if(c.cost > dist[c.node]) continue;`

This is going to help to not come back from where you came from. Else, we are going to iterate through the adjacent nodes to that node and once again check if the distance from the current node plus the neighboring node improves the cost. If it is greater, that means worse so continue.

`if(c.cost + adj.cost >= dist[adj.node])continue;`

else we update the distance to that adjacent node, the back, and push that adjacent node with the updated cost.

`dist[adj.node] = c.cost + adj.cost;BACK[adj.node] = c.node;dijkstra.push({adj.node,c.cost + adj.cost});`

Once the Dijkstra is empty, we can prove that it worked by showing the minimum cost from the origin to the rest of the nodes, AND the path that it took.

`for(int i = 0; i < n; i++){ cout<<i<<": cost:"<<dist[i]<<" path:"; vector<int> path; int current = i; while (current != -1) { path.push_back(current); current = BACK[current]; } reverse(path.begin(), path.end()); for(auto e:path)cout << e << " ==> "; cout << endl;}`

The process to recover the path is exactly the same as with the BFS.

Now you can try to run the full code with the example input and you should get the following output:

`0: cost:4 path:1 ==> 3 ==> 0 ==> 1: cost:0 path:1 ==> 2: cost:11 path:1 ==> 6 ==> 4 ==> 2 ==> 3: cost:1 path:1 ==> 3 ==> 4: cost:4 path:1 ==> 6 ==> 4 ==> 5: cost:13 path:1 ==> 3 ==> 7 ==> 5 ==> 6: cost:1 path:1 ==> 6 ==> 7: cost:5 path:1 ==> 3 ==> 7 ==>`

In conclusion, the Dijkstra algorithm is a powerful tool for finding the shortest path between two nodes in a graph. Whether you are a beginner or an experienced programmer, understanding how the Dijkstra algorithm works is an important skill to have in your toolkit. Whether you are navigating a map, routing network traffic, or optimizing the allocation of resources, the Dijkstra algorithm can help you find the most efficient solution to a wide range of problems. So if you want to learn more about the Dijkstra algorithm and how to implement it in your own projects, be sure to check out the resources and code samples provided in this post.

remember that this is a skill that takes a lifetime to master so don't feel frustrated if you don't get it right away because this isn't easy, but I hope some of the guidelines we saw today were helpful and the basic foundations on this topic were well understood, remember, there is still a lot to learn and we definitively didn't cover everything on Dijkstra, but I hope this was a good beginner overview and that you managed to grasp the basic ideas.

Let me know in the comments what you thought about this post and let me know what you will like to see next. Stay tuned!

]]>Hello Everyone! 👋, welcome back, in this post I want to show you one of my favorite trivial stack problems and some variations, I will explain the problem, go through the most simple but inefficient answer, and then the correct and brilliant solution using stacks.

Given an Array of positive integers, for each array element output the nearest smaller element, i.e., the first smaller element that comes after the element in the array, if no element exists, output -1.

N (the size of the array) 1 <= N <= 10^6

Ai (Element in the array) 1 <= Ai <= 10^6

`INPUT:63 2 4 5 1 6OUTPUT:2 1 1 1 -1 -1`

`INPUT:33 1 2OUTPUT:1 -1 -1`

If you understood the problem then it's very likely that you already thought of this solution, the algorithm is very simple, for each element simply iterate from that position to the end of the array in the search for a smallest element, and if found, immediately output that element, finish the search, and continue to the next item. If not found output -1.

Based on the explanation above, I highly recommend that you try to code the solution for yourself, and then compare it to the code implementation below.

`#include <iostream>#include <vector>using namespace std;int main() { int n; cin >> n; vector<int>arr(n); for(auto &e:arr)cin >> e; // getting input for(int i=0; i<n; i++){ int current = arr[i]; bool found = false; for(int j=i; j<n; j++){ if(arr[j] < current){cout << arr[j] << " "; found=true; break;} } if(!found)cout << -1 << " "; } return 0;}`

When I first made this solution, I thought: "Hey! this is a great simple solution let's submit this code!", but that's because I didn't had a great understanding of complexity and efficiency at the time, and I got a TLE verdict 😔.

If you have never heard of time and space complexity and Big-O notation, I highly suggest that you read my post: Efficiency and Big-O Notation Overview

This code has a time complexity of ** O(n^2)** and a space complexity of

`n`

. The space complexity is determined by the use of the `vector`

container, which has a size of `n`

.in the worst case, ** n** can be 1,000,000, with this algorithm the amount of operations is going to be approximately 1,000,000,000,000 in a worst-case scenario which is why we get a Time Limit Exceeded (TLE) verdict.

Now that we understand why the iterative solution is not the right choice, let's think of a different solution, this one involves a stack. If you have never heard about a "stack" I highly recommend you to read my "Stacks and Queues a Beginners Overview" post and come back here.

We can iterate the array backward from right to left, and for each element perform the following algorithm:

If our helper stack is empty, output -1, add the element to the stack, and continue to the next element.

else, check the element that is on the top of the stack, and if it's smaller than the current element, output the top of the stack and add the current element to the stack, don't delete the top!

if the top of the stack is not smaller, remove the top of the stack until either steps 1 or 2 are fulfilled.

If you are struggling to understand this solution, check out the animation below of the algorithm in action with the first input example.

I highly encourage you to code the algorithm as described above in the language of your choice. here is my implementation in c++:

`#include <iostream>#include <vector>#include <stack>#include <algorithm>using namespace std;int main(){ int n; cin >> n; vector<int>arr(n); for(auto &e:arr)cin >> e; stack<int>S; vector<int>result; for(int i=n-1; i>=0; i--){ while(true){ if(S.size() == 0){result.push_back(-1); S.push(arr[i]); break;} if(S.top() < arr[i]){ result.push_back(S.top()); S.push(arr[i]); break; }else{ S.pop(); } } } reverse(result.begin(), result.end()); for(auto &e:result)cout << e << " "; return 0;}`

The efficiency of this algorithm depends on the total number of stack operations. if the current item is larger than the top element of the stack it gets directly added to the stack in constant time ** O(1)**. However, if it's not, we might have to remove several elements of the stack which has a worst-case of

Up until this moment, the problem consists of finding the minimum nearest element to the right, however, you can also find the Maximum item by just changing a single character. before: `if(S.top() < arr[i])`

After: `if(S.top() > arr[i])`

. Another common variation is to find the minimum nearest element to the **left**. To do that simply iterate the array normally from left to right and don't reverse the `result`

array.

`int main(){ int n; cin >> n; vector<int>arr(n); for(auto &e:arr)cin >> e; stack<int>S; vector<int>result; for(int i=0; i<n; i++){ while(true){ if(S.size() == 0){result.push_back(-1); S.push(arr[i]); break;} if(S.top() < arr[i]){ result.push_back(S.top()); S.push(arr[i]); break; }else{ S.pop(); } } } for(auto &e:result)cout << e << " "; return 0;}`

You've reached the end of this post, hope you enjoyed the solution for this classical competitive programming problem and learned something new. Here are some related posts:

https://blog.garybricks.com/stacks-and-queues-a-beginners-overview

https://blog.garybricks.com/efficiency-and-big-o-notation-overview-with-python-examples

https://blog.garybricks.com/programmers-guide-to-solving-computational-problems

Let me know in the comments what you thought about this post and let me know what you will like to see next. See you in the next post, stay tuned!

]]>Hello everyone! 👋, In this Post, we'll dive deeper into Graphs in programming, we'll take a look at the most common algorithms for solving competitive programming problems using Graphs, DFS, and BFS, and how to implement them in c++ while we solve some interesting problems.

If you have no idea what a graph is and how to represent one check out my previous post: Graphs Introduction For Beginners

Depth-first search is a straightforward graph traversal technique. The idea is that the algorithm begins at a specified node and from there proceeds to visit all nodes that are reachable from the current node. This algorithm always follows a single path in the graph as long as it finds new **unvisited** nodes. If the algorithm has no new unvisited nodes it returns to previous nodes and begins to explore in other directions. With DFS, each node is visited only once.

Let's see how a DFS algorithm would process the following graph:

In this case, we are starting from node 1, and the algorithm proceeds to visit the neighbor node 2, the process repeats for nodes 3 and 4 until node 5 where there are no longer new **unvisited** nodes, that's the moment our algorithm returns to node 4 and chooses another route, in this case, node 6. Once the algorithm reaches node 6 there are no longer new unvisited nodes so the search terminates. The time complexity of DFS is `O(n+m)`

where **n** is the number of nodes and **m** is the number of edges.

DFS can be very easy and convenient to implement using recursion, the main idea is that you define a function that receives the current node and marks it as visited, then checks for all of the adjacent nodes to that node and sends the same function with those nodes as long as they are not visited.

`#include <iostream>#include <vector>using namespace std;int n, k;vector<vector<int>> GRAPH;vector<bool> visited;void DFS(int current) { visited[current] = 1; // marking the active node as visited cout << "currently on:" << current << endl; for (auto node : GRAPH[current]) // iterate through all of it's neighboors if (!visited[node]) DFS(node); // send DFS if it's a new node}int main() { cin >> n >> k; GRAPH.resize(n + 1); visited.resize(n + 1); for (int i = 0; i < k; i++) { int a, b; cin >> a >> b; GRAPH[a].push_back(b); GRAPH[b].push_back(a); } DFS(1); // starting algorithm from node 1 return 0;}`

In the code cell above you can see the implementation using an adjacency list to represent the graph and a simple vector of booleans to keep track of the visited nodes. In this case, we are sending the DFS algorithm starting from node 1 but you can try and make the algorithm start from a different node to see what the output is.

Breadth-first search is also a traversal algorithm that is very commonly used for searching a node, finding the shortest path in a graph, and for simulations. BFS visits all the nodes but in increasing order based on their distance from the starting node, because of this, BFS can help us calculate the distance from the starting node to all other nodes.

Let's see what a BFS algorithm would look like on our example graph.

Just like a DFS, BFS starts from a specified node, and from there visits **ALL** of the nodes that are exactly 1 node away, once all nodes of that level are visited, the algorithm continues with the second level and this process continues until all levels are visited.

The time complexity of BFS is `O(n+m)`

exactly the same as DFS.

The implementation of a BFS algorithm is not as simple as with the DFS but in this section, we'll see the most typical method that is based on a queue of nodes. The algorithm works as follows: First, we define a queue of nodes, I normally like to name it "BFS", and we push the origin (starting node). Here is the interesting part: while the queue is not empty, we are going to save the front of the queue and remove it no matter what, then we check all of the adjacent nodes to that saved node and push them to the queue but **only if they are not visited**, this process is going to repeat until the queue is empty.

If you don't know what a queue is, check out this section on my post about stacks and queues: https://blog.garybricks.com/stacks-and-queues-a-beginners-overview#heading-what-is-a-queue just understand the main idea and come back here since we are going to use c++ handy implementation.

`#include <iostream>#include <queue>#include <vector>using namespace std;int main() { int n, k; cin >> n >> k; vector<vector<int>> GRAPH(n + 1); vector<int> distance(n + 1, -1); for (int i = 0; i < k; i++) { int a, b; cin >> a >> b; GRAPH[a].push_back(b); GRAPH[b].push_back(a); } queue<int> BFS; BFS.push(1); distance[1] = 0; while (!BFS.empty()) { int current = BFS.front(); BFS.pop(); cout << "currently on:" << current << endl; for (auto node : GRAPH[current]) if (distance[node] == -1) { distance[node] = distance[current] + 1; BFS.push(node); } } for (int i = 1; i <= n; i++) cout << "Node:" << i << " is " << distance[i] << " nodes away from node 1\n"; return 0;}`

In the code cell above you can see my implementation in c++ using an adjacency list to represent the graph, notice how instead of using a vector of bools to see if a node has been visited we use a vector of ints to save the "distance" to node 1. For this case, I decided to initialize that distance vector with -1, "-1" is going to represent a node that has not been visited. Just like DFS, try to change the origin to see what the output is.

In this section, we'll see some competitive programming problems I've faced and how they were solved using a DFS or BFS algorithm, at the same time we dive a little deeper into the topic and learn new things, read the problems and try to solve them by yourself, don't worry, the solutions are at the end of this section.

Given a Matrix of characters representing a maze, where "*" represents a wall, "E" represents the entry to the maze, and "X" the exit, print whether is possible or not to reach "X" starting from "E" through the empty spaces. Examples:

Given a Matrix of characters of size **n * m** in which **"w"** represents water and **"L"** represents land, output how many puddles of water are on the map and the size of the biggest one.

A puddle is considered valid if it's **completely** surrounded by land, this means that any body of water touching the edges of the map does not count as a puddle.

Any body of water is considered connected to another if it's orthogonally or diagonally adjacent to it. See the examples below:

Given a Matrix of characters of size n*m in which "." represents water, "#" land, "*" contaminated water, and "$" represents an oil plant that has a spill that every day spreads to an orthogonally adjacent square of water. Output what the Matrix will look like after k days. Example:

It's 2020 and covid is all over the place, as a responsible human you are trying to stay as furthest away from other people.

Given a Matrix of characters of size n*m in which "#" represents a wall, "." an empty space, and "G" a person. find the empty square in which you can be the furthest from everyone. Example:

Output the coordinates of the safest place, for the examples above this would be the answer:

`3 24 4`

You are given a chess board of size n*n, the coordinates of an enemy queen, the coordinates of your king, and an exit coordinate, your job is to output whether or not the king can reach the exit coordinate without getting in check and following the king movement rules. If it's possible, output the coordinates the king took to reach the exit, and if it's not possible, output -1;

IMPORTANT:

For this problem, the enemy queen will never move.

If it's possible to reach the exit coordinate you have to output the

**shortest**path the king took, if there are several answers output any of them.

This is a very classical problem, however, the tricky part of this problem was to figure out how to represent the graph in order to apply the DFS algorithm.

`#include <iostream>#include <vector>using namespace std;struct node { int i, j;};int n, m;vector<string> MAZE;vector<vector<bool>> visited;node E, X;void getInput() { string row; cin >> n >> m; getline(cin, row); for (int i = 0; i < n; i++) { getline(cin, row); MAZE.push_back(row); }}void findStartEnd() { for (int i = 0; i < n; i++) for (int j = 0; j < m; j++) { if (MAZE[i][j] == 'e') E = {i, j}; if (MAZE[i][j] == 'x') X = {i, j}; }}bool valid(int i, int j) { if (i >= 0 && j >= 0 && i < n && j < m && MAZE[i][j] != '*' && !visited[i][j]) return true; return false;}void DFS(node c) { visited[c.i][c.j] = true; if (valid(c.i + 1, c.j)) DFS({c.i + 1, c.j}); if (valid(c.i - 1, c.j)) DFS({c.i - 1, c.j}); if (valid(c.i, c.j + 1)) DFS({c.i, c.j + 1}); if (valid(c.i, c.j - 1)) DFS({c.i, c.j - 1});}int main() { getInput(); findStartEnd(); visited.resize(n, vector<bool>(m, 0)); DFS(E); if (visited[X.i][X.j]) cout << "There is a solution!"; else cout << "IT'S IMPOSSIBLE TO ESCAPE"; return 0;}`

This is the solution I came up with, it's a bit longer than what we are used to, but don't worry, I carefully separated every part in order to make it as readable as possible. Notice how when working with this type of problem is very comfortable to have your graph and visited matrix global. You might be wondering where our Graph is, we'll get to that in a second, first, let's get the input using our `getInput()`

function. You might already have noticed the problem does not give you a graph as we have seen above, this is where you must think and realize that you can represent the nodes as the coordinates in the `MAZE`

matrix, that's why I defined a node structure that simply saves an **x** and **y** coordinates. Then, we call our `findStartEnd()`

function, this simply iterates through the `MAZE`

and finds the starting and ending nodes. Now we initialize our visited matrix, and finally, we send our DFS starting from the start node. The DFS works as follows: first, we mark the current node as visited as we normally do, here's the interesting part: notice how we don't have our adjacency matrix, that is because it's not necessary, we literally check the adjacent positions in the Matrix! to do that I defined a `valid()`

function that simply checks if it's a valid node for the DFS to go, that function takes care of the walls, if it's visited, and for out-of-bounds cases, this is a perfect example of an implicit graph.

The tricky part of this problem was to figure out if a puddle is valid, and its size. Below you can find the code with the explanation.

`#include <iostream>#include <vector>using namespace std;int n, m, numPuddles = 0, BiggestPuddle = 0;vector<vector<char>> MAP;vector<vector<bool>> visited;vector<int> X = {1, -1, 0, 0, 1, -1, 1, -1};vector<int> Y = {0, 0, 1, -1, 1, -1, -1, 1};void init() { cin >> n >> m; MAP.resize(n, vector<char>(m)); visited.resize(n, vector<bool>(m)); for (auto &row : MAP) for (auto &e : row) cin >> e;}bool valid(int i, int j) { if (i >= 0 && j >= 0 && i < n && j < m && !visited[i][j] && MAP[i][j] == 'W') return true; return false;}void DFS(int i, int j, int &size, bool &puddle) { visited[i][j] = true; size++; if (i == 0 || j == 0 || i == n - 1 || j == m - 1) puddle = false; for (int d = 0; d < 8; d++) { if (valid(i + X[d], j + Y[d])) DFS(i + X[d], j + Y[d], size, puddle); }}int main() { init(); for (int i = 0; i < n; i++) { for (int j = 0; j < m; j++) if (MAP[i][j] == 'W' && !visited[i][j]) { int size = 0; bool isPuddle = true; DFS(i, j, size, isPuddle); if (isPuddle) { BiggestPuddle = max(BiggestPuddle, size); numPuddles++; } } } cout << "There are: " << numPuddles << " puddles in the map, the largest one has a size of: " << BiggestPuddle; return 0;}`

To solve this problem, we iterate over the map of chars and if we find water we are going to assume it's a valid puddle and send the `dfs`

on that specific coordinate, notice how we have two more parameters: `size`

and `isPuddle`

which are always passed by reference: `&`

, this is important since we want to be able to modify the variables. `DFS`

is not very different from the others, we mark the current position as visited and send the `DFS`

to the eight possible adjacent coordinates, the only difference is that we always increment the `size`

variable and if we reach an edge of the Map, we are going to say that the puddle is not valid. Once the `DFS`

is done, we check if it's a valid puddle, if true, we increment the number of puddles found, and we check if it's bigger than the biggest puddle.

Just by looking at the example, you can tell that a BFS algorithm behaves exactly the same, the tricky part was how to stop the BFS at the **k** day. Below you can find my code with the explanation.

`#include <iostream>#include <queue>#include <vector>using namespace std;struct node { int i, j, level; };int N, M, days;vector<vector<char>> MAP;int X[4] = {0,1,0,-1};int Y[4] = {1,0,-1,0};node source;void get_input() { cin >> N >> M >> days; MAP.resize(N, vector<char>(M)); for (int i = 0; i < N; i++) { for (int j = 0; j < M; j++) { cin >> MAP[i][j]; if (MAP[i][j] == '$') source = {i, j, 0}; } }}void print() { for (auto e : MAP) { for (auto c : e) cout << c; cout << "\n"; }}bool valid(int i, int j){ return i >= 0 && j >= 0 && i < N && j < M && MAP[i][j] == '.';}int main() { get_input(); queue<node> BFS; BFS.push(source); while (!BFS.empty()) { node e = BFS.front(); BFS.pop(); if (e.level >= days) break; for(int i=0; i<4; i++){ if(valid(e.i + X[i], e.j + Y[i])){ MAP[e.i + X[i]][e.j + Y[i]] = '*'; BFS.push({e.i+X[i], e.j+Y[i], e.level+1}); } } } print(); return 0;}`

Just like the previous problems, we represented a node as the coordinates on the matrix, and notice how this node has an extra **level** variable, this is going to help us know how far we are from the origin which is going to help us stop the BFS.

The algorithm works as follows:

while the BFS is not empty get the current node and remove it

check if the node has a distance greater or equal to k. if so, end the algorithm.

for each valid adjacent node, modify that node to become contaminated and add it to the BFS with a level increment.

Once the BFS finishes, print the final Map. This problem cannot be solved with a DFS due to the land that can ruin the simulation and give an incorrect result.

One of the first ideas most people have is to send a BFS from every single empty space and get the distance to the **closest** person and by doing that, find the best square, however, this idea is extremely inefficient. Another idea can be to send a BFS from every single person and get the distance to every single empty square and then fuse them together on a final matrix that contains all of the minimum distances, and lastly just search for the biggest value.

This is the correct idea, however, creating a distance matrix for each person can bring a lot of implementation and memory problems, the best and easiest solution involves using just a single BFS and distance matrix, it turns out that is totally possible and easy to send a BFS algorithm from several origins. Below you can see my solution.

`#include <iostream>#include <queue>#include <vector>using namespace std;struct node {int i, j;};int N, M;vector<vector<char>> MAP;vector<vector<int>> V; // Distance Matrixint X[4] = {0,1,0,-1};int Y[4] = {1,0,-1,0};vector<node> people;void get_input() { // get Map, initialize the visited matrix, find the people cin >> N >> M; MAP.resize(N, vector<char>(M)); V.resize(N, vector<int>(M, -1)); // -1 represents not visited for (int i = 0; i < N; i++) { for (int j = 0; j < M; j++) { cin >> MAP[i][j]; if (MAP[i][j] == 'G') people.push_back({i, j}); } }}// checks if the node is inside bounds, is an empty square, and is not visitedbool valid(int i, int j){ return i >= 0 && j >= 0 && i < N && j < M && MAP[i][j] == '.' && V[i][j]==-1;}int main() { get_input();// instead of adding a single source, we add all people. queue<node> BFS; for(auto e:people){BFS.push(e); V[e.i][e.j] = 0;} while (!BFS.empty()) { node e = BFS.front(); BFS.pop(); for(int i=0; i<4; i++){ if(valid(e.i + X[i], e.j + Y[i])){ V[e.i + X[i]][e.j + Y[i]] = V[e.i][e.j] + 1; BFS.push({e.i+X[i], e.j+Y[i]}); } } } node result; // final search for the maximum value int maxi = -1; for(int i=0; i<N; i++){ for(int j=0; j<M; j++)if(V[i][j] > maxi){ maxi = V[i][j]; result = {i, j}; } } cout << result.i << " " << result.j; return 0;}`

This is a very standard BFS the big difference is how we define our visited matrix, for a DFS algorithm we normally use booleans but for a BFS we can actually save ints that represent the distance, and by doing that we can know the minimum distance from the origin to ANY other square which is going to be really helpful. And lastly, instead of sending the BFS from a single origin, we initialize the BFS with all of the nodes representing people, after that the algorithm remains the same.

Once the BFS is done, we are going to have our perfect visited matrix with all of the correct distances and we just need to find the maximum value.

This problem was very similar to the escape maze problem, however, the problem was that if there was a solution, you had to output the shortest route the king took, because of this, a **BFS** algorithm was the right choice. Below you can see my implementation with the explanation.

`#include <algorithm>#include <iostream>#include <queue>#include <vector>using namespace std;struct node { int x, y;};vector<int> X = {1, -1, 0, 0, 1, -1, 1, -1}; // directionsvector<int> Y = {0, 0, 1, -1, 1, -1, -1, 1}; // directionsint n, qx, qy, kx, ky, ex, ey;vector<vector<bool>> M; // 0 = empty, 1 = checkvector<vector<int>> V; // distance MAPvector<vector<node>> BACK; // Helper for recreating pathvector<node> path; // final pathvoid initBoard() { M.resize(n, vector<bool>(n, 0)); V.resize(n, vector<int>(n, -1)); BACK.resize(n, vector<node>(n, {-1, -1})); for (int i = 0; i < n; i++) { M[qx][i] = 1; M[i][qy] = 1; } int x = qx, y = qy; while (x >= 0 && y >= 0) { M[x][y] = 1; x--, y--; } x = qx, y = qy; while (x >= 0 && y < n) { M[x][y] = 1; x--, y++; } x = qx, y = qy; while (x < n && y >= 0) { M[x][y] = 1; x++, y--; } x = qx, y = qy; while (x < n && y < n) { M[x][y] = 1; x++, y++; }}// checks if it's not visited, not in check and inside the board.bool valid(int x, int y) { return x >= 0 && y >= 0 && x < n && y < n && !M[x][y] && V[x][y] == -1;}int main() { cin >> n >> qx >> qy >> kx >> ky >> ex >> ey; qx--, qy--, kx--, ky--, ex--, ey--; initBoard(); queue<node> BFS; BFS.push({kx, ky}); V[kx][ky] = 0; while (BFS.size()) { node current = BFS.front(); BFS.pop(); for (int i = 0; i < 8; i++) { if (valid(current.x + (X[i]), current.y + (Y[i]))) { V[current.x + (X[i])][current.y + (Y[i])] = V[current.x][current.y] + 1; BACK[current.x + (X[i])][current.y + (Y[i])] = current; BFS.push({current.x + (X[i]), current.y + (Y[i])}); } } } if (V[ex][ey] == -1) { // checking if the escape square was not visited cout << -1; return 0; } node current = {ex, ey}; while (!(current.x == -1 && current.y == -1)) { path.push_back(current); current = BACK[current.x][current.y]; } reverse(path.begin(), path.end()); cout << "-------------\n"; for (int i = 1; i < path.size(); i++) cout << path[i].x + 1 << " " << path[i].y + 1 << " => "; return 0;}`

Just like every problem until now, we need to represent our graph, in this case, we can create a Matrix as the chessboard, and the values of the matrix can be either 0 or 1 where 1 is going to represent a square under check. We also create our distance matrix which is going to be helpful.

In order to "reconstruct" the path we are going to need an additional BACK matrix this is a brilliant way to know the square a square comes from. And lastly, we have our directions array that is going to help later on in pointing to the adjacent squares.

First, we initialize our Graph "M" with the `initBoard()`

function which means marking every queen attacking square as "check".

Then we send our BFS on the king square and for each valid adjacent square we update the distance on the Visited Matrix "V", we also update our BACK matrix and send the BFS.

`if (valid(current.x + (X[i]), current.y + (Y[i]))) {// the adjacent node is going to have a distance of the current node +1V[current.x + (X[i])][current.y + (Y[i])] = V[current.x][current.y] + 1;// the adjacent node is going to come from the current nodeBACK[current.x + (X[i])][current.y + (Y[i])] = current;BFS.push({current.x + (X[i]), current.y + (Y[i])});}`

Once the BFS finishes. we check if the escape square was not visited, if this is the case, we output -1 else we can reconstruct the path by starting from the escape square and going back until it's no longer possible.

` node current = {ex, ey}; while (!(current.x == -1 && current.y == -1)) { path.push_back(current); current = BACK[current.x][current.y]; } reverse(path.begin(), path.end());`

Notice how we created a final path vector of nodes, and we push_back the current position, this is going to give us the final route backward, that's why we have to use the reverse() method.

You've reached the end of this lesson on BFS and DFS, remember that this is a skill that takes a lifetime to master so don't feel frustrated if you don't get it right away because this isn't easy, but I hope some of the guidelines we saw today were helpful and the basic foundations on this topic were well understood, remember, there is still a lot to learn and we definitively didn't cover everything on Graphs, but I hope this was a good beginner overview and that you managed to grasp the basic ideas.

Let me know in the comments what you thought about this post and let me know what you will like to see next. Stay tuned!

]]>Hello everyone! 👋, In this Post, we will take a beginner's dive into Graphs data structure in programming, we'll take a look at what are they, what are they for, the different types of graphs, the differences between graphs and trees, and how to implement a Graph structure using c++,

In Computer Science a graph is a non-linear data structure that can be used to represent complex relationships between objects. Graphs are made up of a finite number of nodes or vertices and the edges that connect them. The vertices are sometimes also referred to as nodes and the edges are lines or arcs that connect any two nodes in the graph. In this post, we will follow the node and edge convention.

A graph can be a possible choice every time there are relations between objects for example in social networking sites where a user can be represented with a node and connections between them are represented with edges, another good example can be found on google maps where a location can be represented as the node, and the path can be represented with the edges, then we can implement an algorithm to find the shortest route between two nodes. Graphs can also be used for solving puzzles with only one solution like mazes.

Null Graph - There are no edges connecting the Nodes.

Trivial Graph - only has one single node and it's the smallest graph possible.

Undirected Graph - Edges don't have any direction, if a node

**A**connects a node**B**the opposite is true.Directed Graph - All edges have a specified direction, the normal convention is that node

**A**connects the node**B**in that order.Connected Graph - Each one of the nodes can visit any other node in the Graph.

Disconnected Graph - If at least one node is not reachable from another node is considered a disconnected Graph.

Complete Graph - Every single node is connected to each other node.

Cycle Graph - A graph where all of the nodes form one perfect cycle.

Cyclic Graph - at least 1 cycle is formed within the Graph.

Bipartite Graph - A graph in which the nodes can be divided into two sets such that the nodes in each set do not contain any edge between them.

Weighted Graph - A graph where all of its edges have a specified value, for example, if each node represents a city, the edges can represent roads with the time it takes to reach another city.

Here are some of the most common and basic operations for Graphs:

- Insertion of Nodes/Edges
- Deletion of Nodes/Edges
- Searching - Uses Algorithms like BFS
- Traversal - Visit all the nodes in the graph using algorithms like DFS

If you have worked with trees before, you might get a little confused about the differences, but in simple terms, just know that trees are just more restricted types of graphs. Every tree is always a graph, but not every graph is a tree. The same is true with Linked Lists and Heaps.

You might be wondering how can a graph be represented with code. Well, there are 2 simple ways to store a graph, using an Adjacency Matrix or an Adjacency List, we'll see both of them in action.

In this method, the graph is stored in the form of a 2D matrix where rows and columns denote the nodes and each entry in the matrix represents either 1(connected) or 0(disconnected), if the graph you are trying to represent is a weighted graph, then you can put the weight instead.

In the image above, you can see how that specific graph would be represented using an adjacency matrix, the node 1 is connected to nodes 2 and 5, that's why on row 1 columns 2 and 5 are 1 which means connected, this process repeats for every single node.

This method is very similar to the 2D matrix because we are going to create a collection of N empty lists, and for each node, we push_back() the adjacent nodes like this:

If you want to use this method to represent a weighted graph then you would have to save pairs in each element of the array, the node, and the weight.

there are several factors to consider when choosing what method to use. If for example, the amount of nodes **N** is very large, then you only have the adjacency list method which in most cases has way less memory usage, however, the Matrix method can perform `O(1)`

constant operation for adding a node as well as for removing a connection which the adjacency list does not. However, if we try to consult the adjacent nodes to a specific node the List method is better since it doesn't have unnecessary nodes stored in it, and on top of that, you can be more creative by ordering the list or changing the List for a priority queue, set, and more.

The problem normally specifies how the input is given, but the standard is as follows:In the first line **N** and **K** where **N** is the number of nodes in the graph and **K** is the number of edges, then we are given **K** lines, and for each line **A** and **B** representing that the node **A** connects node **B**, the problem normally specifies if it's directed or undirected and if it's a weighted graph, if it is, the third number for each line is added representing the weight of the edge.

The code below uses a 2D vector of booleans because, for this particular example, the graph is Undirected and has no weight.

`#include <iostream>#include <vector>using namespace std;int main() { int n, k; cin >> n >> k; // +1 because the graph nodes start from 1 not 0 vector<vector<bool>> GRAPH(n + 1, vector<bool>(n + 1)); for (int i = 0; i < k; i++) { int nodeA, nodeB; cin >> nodeA >> nodeB; GRAPH[nodeA][nodeB] = 1; GRAPH[nodeB][nodeA] = 1; } for (auto row : GRAPH) { for (auto col : row) cout << col << " "; cout << endl; } return 0;}`

Remember that if we wanted to represent a weighted graph, we receive an extra parameter for every edge, normally at the end, with the weight of that connection, and the only thing we would have to do is change the **bool** for **int** on our matrix.

If the problem specifies that the graph is directed, we would have to simply remove: `GRAPH[nodeB][nodeA] = 1;`

and that's it!

In the code cell below you can see the implementation. Notice how it's very similar to the Matrix method.

`#include <iostream>#include <vector>using namespace std;int main() { int n, k; cin >> n >> k; // +1 because the graph nodes start from 1 not 0 vector<vector<int>> GRAPH(n + 1); for (int i = 0; i < k; i++) { int nodeA, nodeB; cin >> nodeA >> nodeB; GRAPH[nodeA].push_back(nodeB); GRAPH[nodeB].push_back(nodeA); } for (int node = 1; node <= n; node++) { cout << node << " : "; for (auto adjacent : GRAPH[node]) cout << adjacent << ", "; cout << endl; } return 0;}`

I personally like this method more because we are saving a lot of memory and it's more readable and easy to understand in my opinion. Remember that if we wanted to add weight to this graph, instead of doing: `.push_back(nodeB);`

we can: `.push_back({nodeB, weight});`

and change the vector type from int, to pair.

You've reached the end of this lesson on Graphs, remember that this is a skill that takes a lifetime to master so don't feel frustrated if you don't get it right away because this isn't easy, but I really hope some of the guidelines we saw today were helpful and the basic foundations on this topic were well understood, remember, there is still a lot to learn and we definitively didn't cover everything on Graphs, but I hope this was a good beginner overview and that you managed to grasp the basic ideas.

Let me know in the comments what you thought about this post and let me know what you will like to see next. In the next posts, we are going to dive a little bit more into this topic and we will see the common algorithms that are applied to Graphs, like DFS and BFS, and how we can use them to solve some basic competitive programming problems. Stay tuned!

]]>Welcome Back! In this post, we will take a beginner's dive into segment trees in c++, we will look at what are they. what are they for? and we will solve some basic competitive programming problems together.

Before we begin, I highly suggest that you check out my post about Efficiency And Big O Notation altho it is not required 😉.

Segment Tree is one of the most used data structures in competitive programming, to understand why they are such a big deal, let's think of the following problem:

Let's say we have an array of N elements, like the one shown below:

And we need to perform two types of operations. The first operation will be an `update(i, v)`

, this function will take the index of the element we want to change, and the new value, and replace it.

The second operation is going to calculate to sum in a segment a.k.a range in the array from [ L to R) Note that in the request for the sum we take the left border [ L inclusive, and the right border R ) exclusive. In this post, we will follow this inclusive exclusive standard for all segments. Here are some examples of the `sum(l, r)`

in action:

A Segment tree is a data structure that will allow us to perform both of the operations in **log(n)** time complexity, we will look more in detail at how this works, but before we continue, let's see how we could solve this problem only using iteration.

Let's think of the most basic solution that does not involve a segment tree first.In the code cell below, we define a vector globally for commodity reasons, and we receive the input **N**, then we resize the vector to the given size and store the data from the terminal.

`#include <bits/stdc++.h>#include <vector>using namespace std;int n;vector<int> arr;int main() { cin >> n; arr.resize(n); for (auto &e : arr) cin >> e; return 0;}`

`sum()`

ProcedureAs you can see from the code cell below, this method is very simple, we simply iterate the array using a for loop, starting from **a** to **b-1**, notice how we don't have to subtract 1 to **b** because we use "<" instead of "<=", and sum all the values to the `result`

variable.

`int sum(int a, int b) { int result = 0; for (int i = a; i < b; i++) result += arr[i]; return result;}`

`update()`

ProcedureThis function is the easiest to implement because we only need the following line of code:

`void update(int index, int new_value) { arr[index] = new_value; }`

The `Update`

method does not return anything, that's why we use `void()`

, and we don't need to pass the array as a parameter because it's defined globally.

Feel free to copy the code locally on your machine and test it out!

`int main() { cin >> n; arr.resize(n); for (auto &e : arr) cin >> e; cout << sum(0, 7) << endl; cout << sum(0, 1) << endl; cout << sum(1, 6) << endl; update(0, 10); cout << sum(0, 7) << endl; cout << sum(0, 1) << endl; cout << sum(1, 6) << endl; return 0;}`

Expected output:

`29 5 20 34 10 20`

After coding the iterative solution, you must be thinking: "Hey!, this was easy to code, why don't we just use this approach?", and yes! that solution is simple and easy, but is it efficient? for example what happens if you have the following limits?:

- (1N10^5, 1M10^5) where N is the number of elements, and M is the number of operations that are going to be performed.

`Update()`

Time ComplexitySurprisingly, the `update`

procedure takes `O(1)`

constant time, which means it's super efficient!

`Sum()`

Time ComplexityHowever, this procedure has a worst-case scenario of `O(N)`

this is the case if we ask for a range from 0 to N, the problem is that there can be up to 10^5 of this type of function calls

`sum(0, 1E5)sum(0, 1E5)sum(0, 1E5)sum(0, 1E5)sum(0, 1E5)... 10^5 more times`

and for every call, we have to iterate the entire array even tho the array stays the same. Because of this, this procedure has a complexity of `O(N*M)`

and because in this case N and M can be the same worst-case value, we say that this solution has a complexity of `O(N)`

which is not efficient at all!

Starting from the previous example array, let's see what a segment tree would look like for this particular array.

This is a binary tree, in the leaves of which there are elements of the original array, and each internal node contains the sum of the numbers in its children.

notice how we added a 0 at the end of the original array because we need to create a Binary Tree, and for the tree to be created perfectly we need the length of the array to be a power of two. If the length of the array is not a power of two, you can extend the array with a neutral value, in this case, a 0, notice how the length of the array will increase no more than twice, so the asymptotic time complexity of the operations will not change.

Now let's see how the operations will look on this tree.

`Update()`

OperationWhen an element of the array changes, what we need to do is traverse the tree until we reach the corresponding leave of the tree, then update the value, and recalculate all the values higher up the tree from the modified leaf. When performing such an operation, we need to recalculate one node on each layer of the tree.

In the animation shown above, you can see the `Update(i, v)`

in action.

`Sum()`

OperationNow let's see what the `Sum()`

operation would look like on our segment tree. Notice how we already have all the values that we need to be stored in the nodes of our tree. In this case, the values are the sum of the segments in the original array.Observe how the root already has the answer for a query from 0 to 8 which would be a perfect query, however, what happens if we have a nonperfect query like [2, 7)?

The algorithm will be a recursive traversal of the tree that will be interrupted by two cases.

- The segment corresponding to the current node is outside of the query, if this happens it means that all the children are outside of the desired query and we can stop the recursion.
- The segment corresponding to the current node is completely inside of the query, this means that all the children are inside of the range and we need to sum the value of the current node to our result and stop the recursion.

If the current node segment is partially inside the range query, then we simply continue traversing its children until one of both break cases happens.

Altho we haven't touched any of the code for this solution, you might already be thinking about if this solution is actually better than the iterative one, after all, it seems like there are a lot of more elements in our structure, and in the sum operation example, it might seem like there was more traversing and was slower than the other one, but is this really the case?

if the array size **N** is a power of 2, then we have exactly **n-1** internal nodes, summing up to **2n-1** total nodes. But not always do we have n as the power of 2, so we basically need the smallest power of 2 which is greater than n. For good measure, it's normally said that a Segment Tree has a space complexity of `O(4n)`

which is manageable. If you want to dig a bit more about this topic I'll recommend you to check out this link

`Update()`

Time ComplexityWhen performing the update operation, we need to recalculate one node on each layer of the tree. We have only `logn`

layers, so the operation time will be `O(logn)`

.

`Sum()`

Time ComplexityWhen performing the `Sum()`

operation, we don't need to visit all the elements of the tree, thus the general asymptotic time of this procedure will be `O(logn)`

, way more efficient compared to the iterative solution.

Segment Trees are useful whenever you're frequently working with ranges of numerical data. We can use a segment tree if the function *f* is associative and the answer of an interval [l,r] can be derived from the data array.Here are some common examples:

- Find the sum of all values in a range
- Find the smallest value in a range
- Find the Max value in a rage
- Find the Product of all values in a Range (Multiplication)
- Bitwise operations
`|`

`&`

`^`

In this section, we will look at how to implement the solution for the original sum problem using a genius Segment tree Array representation starting from the code template below.

`#include <iostream>#include <vector>using namespace std;int main() { int n; cin >> n; vector<int> arr(n); for (auto &e : arr) cin >> e; return 0;}`

As you can see from the code cell below, we initialize two global variables, the segment tree size, and the base size.

`int seg_tree_size, base_size;vector<long long> segment_tree;`

We also create our segment_tree that, as we mentioned earlier, it's going to be represented with an array, in this case, **long long** is the data type we use because we are going to be managing sums.

`void init(const vector<int>& a) { int arr_size = a.size(); base_size = 1; while (base_size < arr_size) base_size *= 2; seg_tree_size = base_size * 2 - 1; segment_tree.resize(seg_tree_size, 0); // neutral value;`

In the code cell above, you can see how we use a while loop, to find the smallest power of 2 which is greater than n in order to have a perfect binary tree. Now it's time to fill the tree leaves with the values of the original array.

` for (int i = seg_tree_size / 2, j = 0; i < seg_tree_size && j < arr_size; i++, j++) { segment_tree[i] = a[j]; }`

Notice how we are representing the Tree as an array where each node has its index, and the furthest left leaf is always going to be the segment tree size divided by two.

Now that we filled all the leaves it's time to fill the rest of the tree

` for (int i = seg_tree_size / 2 - 1; i >= 0; i--) { segment_tree[i] = segment_tree[i * 2 + 1] + segment_tree[i * 2 + 2]; }}`

Notice how we start from the element in the `(seg_tree_size / 2 - 1)`

position in this case the node at index 6, and thanks to the perfect binary tree that we have, we can easily check for both of its children with `i * 2 + 1`

and `i * 2 + 2`

, In this particular case, we are making the node value to be the sum of both its children, this is normally what is adjusted for other associative properties.

`update()`

ProcedureIn case you forgot, this function is going to receive an index and a value, and update our array in the index to the new value. Thanks to our Array representation method we don't have to traverse the tree until we reach the desired node, instead, we just directly access the element with `i += st_size / 2`

`void update(int i, int v) { i += seg_tree_size / 2; segment_tree[i] = v; while (i > 0) { i = (i - 1) / 2; segment_tree[i] = segment_tree[i * 2 + 1] + segment_tree[i * 2 + 2]; }}`

Remember that once the value is updated, all the parent nodes to that leave have to be updated.

`sum()`

ProcedureThis recursive function shall take the query that is, [L and R), a helper left and right "searching" range and the index to the current node in the tree.

`long long sum(int L, int R, int sl = 0, int sr = base_size, int i = 0) { if (sl >= R || sr <= L) return 0; // Outside of range if (sl >= L && sr <= R) return segment_tree[i]; // Inside of range int mid = (sl + sr) / 2; return sum(L, R, sl, mid, i * 2 + 1) + sum(L, R, mid, sr, i * 2 + 2);}`

Let's see what is happening here, in the first iteration we try to "search" the entire array, that's why `sl`

is set to 0, and `sr`

is set to the `base_size`

, and we start on the root node, a.k.a the node at index 0.

The function will check if the searching range is outside of the query, if it is, we return 0, if instead, the searching range is completely inside of the query, we return the value of the segment tree node, else, it means that we are partially inside of the query, and because this is a binary tree, we can simply trim the search range in 2 with `int mid = (sl + sr) / 2;`

and send the recursive function again to BOTH children.

`#include <iostream>#include <vector>using namespace std;int seg_tree_size, base_size;vector<long long> segment_tree;void init(const vector<int>& a) { int arr_size = a.size(); base_size = 1; while (base_size < arr_size) base_size *= 2; seg_tree_size = base_size * 2 - 1; segment_tree.resize(seg_tree_size, 0); // valor neutro; for (int i = seg_tree_size / 2, j = 0; i < seg_tree_size && j < arr_size; i++, j++) { segment_tree[i] = a[j]; } for (int i = seg_tree_size / 2 - 1; i >= 0; i--) { segment_tree[i] = segment_tree[i * 2 + 1] + segment_tree[i * 2 + 2]; }}void update(int i, int v) { i += seg_tree_size / 2; segment_tree[i] = v; while (i > 0) { i = (i - 1) / 2; segment_tree[i] = segment_tree[i * 2 + 1] + segment_tree[i * 2 + 2]; }}long long sum(int L, int R, int sl = 0, int sr = base_size, int i = 0) { if (sl >= R || sr <= L) return 0; // Outside of range if (sl >= L && sr <= R) return segment_tree[i]; // Inside of range int mid = (sl + sr) / 2; return sum(L, R, sl, mid, i * 2 + 1) + sum(L, R, mid, sr, i * 2 + 2);}int main() { int n; cin >> n; vector<int> arr(n); for (auto& e : arr) cin >> e; init(arr); // <- DON'T FORGET TO CREATE THE SEGMENT TREE! cout << sum(0, 7) << endl; cout << sum(0, 1) << endl; cout << sum(1, 6) << endl; update(0, 10); cout << sum(0, 7) << endl; cout << sum(0, 1) << endl; cout << sum(1, 6) << endl; return 0;}`

Expected output:

`29 5 20 34 10 20`

You've reached the end of this lesson on the Segment tree data structure, remember that this is a skill that takes a lifetime to master so don't feel frustrated if you don't get it right away because this isn't easy, but I really hope some of the guidelines we saw today were helpful and the basic foundations on this topic were well understood, remember, there is still a lot to learn and we definitively didn't cover everything on Segment Trees, but I hope this was a good beginner overview and that you grasped the concepts and feel more confident on your programming journey.

Let me know in the comments what you thought about this post and let me know what you will like to see next. See you in the next post, stay tuned!

]]>Hello everyone, welcome back!, today I wanted to give a beginners overview of stacks and queues in Python, we will take a good look at what are they? what are they for? and we will look through some examples together.

The first time I ever heard about stacks, I couldn't help but imagine a stack of Legos or a stack of boxes, and in programming, a stack is a linear list-based data structure that behaves very similar to a stack of boxes in real life.

The main idea is that you can keep putting elements on top, and you have easy quick access to remove or look at the top element. And just like a stack of boxes in real life, if you want access to the bottom element you can't get to it easily, you will need to take the top box over and over again until you reach the box that you want.

In computer science, a stack is an abstract data type that serves as a collection of elements, with two main principal operations:

- Push, which adds an element to the collection, and
Pop, which removes the most recently added element that was not yet removed.

https://en.wikipedia.org/wiki/Stack_(abstract_data_type)

Stacks can be extremely useful and efficient when you only care about the most recent elements or the order in which you see and save elements matters.

For example, if you made a news page, you'll need to access the more recent elements first and more quickly but you may want to show all of the elements when the user scrolls down.

When talking about stacks, it's important to know some specific terminology. When you add an element to a stack, the operation is called "Push" instead of "Insert", and when you take out an element off the stack the operation is called "Pop" instead of "remove", and remember that we always push and pop at the top of the stack. L.I.F.O(Last In, First Out) as the image below illustrates.

Hopefully, you can see that because all you need to do is work with the top of the stack, both operations should take constant time O(1) which can be a really good reason to implement a stack.

Because a stack it's a pretty abstract concept you can actually implement it in two different ways, with an array, or with a Linked list.

A stack has some things in common with an array in the sense that we have a collection of elements and an order to them. One difference with an array is that if we wanted, we could actually access an element in the middle of the array or at the beginning of the array which wouldn't actually be a Stack. Remember that with a stack, we can only access one end, the top.

So how do we implement a stack using an array? well, one way we could look at it is if we rotate an array sideways, we could use it as the container for the stack, then, we could restrict the ways we can interact with this container so that we get the behavior we expect from a Stack, in practical terms, we are going to create a Stack class that has a `push`

and `pop`

methods.

`class Stack: # defining stack def __init__(self): # initializing list self.items = []myStack = Stack() # creating stack`

From the code above you can see our `Stack`

class and in the `__init__`

method we initialize our list that will contain all the items of our stack.

`push`

methodThe behavior of this method is very straightforward, simply push a new element into the top of the stack. Python makes this task especially easy thanks to the built-in `append`

method that adds a new element to the end of a list which in this case it's going to be considered the head of our stack. The `append`

method also takes care of the size of the array, so we won't ever have a stack overflow problem.

`class Stack: # defining stack def __init__(self): # initializing list self.items = [] def push(self, element): # push method self.items.append(element) # append method to add an element to the end of the list.myStack = Stack() # creating stackprint(myStack.items) # [ ]myStack.push("element1")print(myStack.items) # ["element1"]myStack.push("element 2") print(myStack.items) # ["element1", "element2"] # the head will be considered the end of the list`

If you wish to print the stack to get a better understanding of what the stack looks like, you can use `print(myStack.items)`

.

`pop`

methodAs we mentioned earlier, the purpose of this method is to remove the element at the top of the stack, in this case, the last element of the list. Just as we used the `append`

method for adding elements to our stack, python provides us with a built-in `pop`

method that removes the last element of an array, precisely what we need.

`class Stack: def __init__(self): self.items = [] def push(self, element): self.items.append(element) def pop(self): self.items.pop()myStack = Stack() # creating stackprint(myStack.items) # [ ]myStack.push("element1") print(myStack.items) # ["element1"]myStack.push("element2") print(myStack.items) # ["element1", "element2"]myStack.pop()print(myStack.items) # ["element1"]myStack.pop()print(myStack.items) # []myStack.pop() # IndexError: pop from empty listprint(myStack.items)`

The pop method takes no arguments, we only pass the `self`

attribute and using `self.items.pop()`

we can easily achieve the desired behavior, but there's a problem, If we call the pop method on an empty array, we are going to get the following error:

IndexError: pop from empty list.

To prevent this error from happening, we are going to check the size of the array first, and if it's not empty, run the `pop`

method like we already have. To check the size of the array we can use the built-in `len()`

method.

`class Stack: def __init__(self): self.items = [] def push(self, element): self.items.append(element) def pop(self): if len(self.items) > 0: # checking if not empty self.items.pop()myStack = Stack() # creating stackprint(myStack.items) # [ ]myStack.push("element1")print(myStack.items) # ["element1"]myStack.push("element2")print(myStack.items) # ["element1", "element2"]myStack.pop()print(myStack.items) # ["element1"]myStack.pop()print(myStack.items) # []myStack.pop()print(myStack.items) # []`

Lastly, we want our pop() method to return the "popped" element and we are done!

`class Stack: def __init__(self): # initializing stack self.items = [] def push(self, element): self.items.append(element) # adding new element def pop(self): if len(self.items) > 0: # checking if not empty return self.items.pop() # removing and returning the headmyStack = Stack() # creating stackprint(myStack.items) # []myStack.push("element1")print(myStack.items) # ["element1"]myStack.push("element2")print(myStack.items) # ["element1", "element2"]print(myStack.pop()) # element2print(myStack.pop()) # element1print(myStack.pop()) # None`

Previously, we looked at how to implement a stack using an array. While that approach works great in python because the append and pop method takes O(1) constant time, in other languages where there is no such efficiency when working with arrays it might be a good idea to implement a stack using a linked list.

If this is the first time you've heard about a linked list, check out my post titled A Beginners Overview of Linked Lists in Python where I explain what they are, what they are used for, and how to implement one using python. After you've read that article come back and continue from here.

As you can see from the code below, we initialize our Stack class with the `__init__`

method, and we add two attributes: the `head`

which by default it's going to be None since there are no elements in our linked list yet, and we set the `num_elements`

variable to zero for the same reason.

`class Stack: def __init__(self): self.head = None self.num_elements = 0`

Because we are going to be implementing a linked list we are going to need a `Node`

class that it's going to be an "element" with a value property and a pointer to the next node in the linked list.

`class Node: def __init__(self, value): self.value = value self.next = None`

`Push`

MethodNext, we will be adding the `push`

method to the Stack class. As we mentioned earlier, the purpose of this method is to remove the element at the top of the stack, in this case, the top of the stack is going to be the **head **of the linked list.

`class Node: def __init__(self, value): self.value = value self.next = Noneclass Stack: def __init__(self): self.head = None # No items in the stack, so head should be None self.num_elements = 0 def push(self, value): new_node = Node(value) if self.head is None: self.head = new_node else: new_node.next = self.head self.head = new_node self.num_elements += 1`

From the code above, you can see that once the push method is called, we create a new element "Node" with the value passed and we check if the linked list is empty with `if self.head is None:`

if that is the case, we make the head equal to the newly created node. If not, that means that the linked list is not empty, and the `next`

node is now the old head, and the new head will be the `new_node`

, lastly, we increment the number of elements by 1.

`Pop`

MethodFirst, this method needs to check if the stack is empty, then get the value of the head, which is the top of the stack, and store it in a local variable, then change the head to the next node, and in doing so, removing the top of the stack, finally subtract 1 to the number of elements variable in the stack and return the "popped" value.

`class Node: def __init__(self, value): self.value = value self.next = Noneclass Stack: def __init__(self): self.head = None self.num_elements = 0 def push(self, value): new_node = Node(value) if self.head is None: self.head = new_node else: new_node.next = self.head self.head = new_node self.num_elements += 1 def pop(self): if self.num_elements == 0: return None else: value = self.head.value self.head = self.head.next self.num_elements -= 1 return value`

We are done! now it's time to test it out and see if it works as expected

`myStack = Stack()myStack.push("e1")myStack.push("e2")myStack.push("e3")myStack.push("e4")print(myStack.num_elements) # 4print(myStack.pop()) # e4print(myStack.num_elements) # 3print(myStack.pop()) # e3`

In this section, you are going to apply what you learned about stacks with a real-world problem. We will be using stacks to make sure the parentheses are balanced in mathematical expressions such as: ((32+8)(5/2))/(2+6).

Take a string as an input and return True if its parentheses are balanced or False if it is not.

For this problem, you can choose to implement the stack with an array or a Linked list, it's completely up to you.

`myStack = Stack()equation = input()`

`def solve(equation): for char in equation:`

`def solve(equation): for char in equation: if char == "(": myStack.push(char)`

As you can see from the code above, if there is an opening parenthesis, we can add that character to the stack.

If we detect a closing character, we must check if the stack is empty, if it is, we will immediately know that the equation is unbalanced and if is not empty then we remove an element from the stack with the pop method.

`def solve(equation): for char in equation: if char == "(": myStack.push(char) elif char == ")": if myStack.num_elements > 0: myStack.pop() else: return False`

At this point, if the program finishes iterating over the equation it means that the equation is balanced or that there are leftover elements in the stack if this is the case the equation is unbalanced.

`def solve(equation): for char in equation: if char == "(": myStack.push(char) elif char == ")": if myStack.num_elements > 0: myStack.pop() else: return False if myStack.num_elements > 0: return False else: return True`

We are done! now it's time to test it out and see if it works as expected.

`((3^2 + 8)*(5/2))/(2+6)) False((3^2 + 8)*(5/2))/(2+6) True`

Just like with Stacks, queues have a very descriptive name and behave very similar to a queue in real life, you can imagine a queue of people that are waiting to get their hands on the best ice cream in town. The way this works is that the person who is at the front of the queue is the first one to receive the ice cream and leave. People can always join at the back of the queue but can only receive the ice cream and leave at the front of the queue. This is called a First In, First out structure, remember that a Stack is a Last In, First out structure very similar but kind of the opposite.

When working with queues it's important to know some queue-related terminology like:

- Head - First (oldest) element added to the queue
- Tail - Last (Newest) element added to the queue
- Enqueue - add an element to the back/tail of the queue
- Deque - A double-ended queue and pronounced "Deck"
- Dequeue - Remove the element at the front of the queue

In this section, we'll look at one way to implement a `Queue`

with an array by creating a Stack class that has the following methods:

- Push() - inserts an element at the back/tail of the queue.
- Pop() - removes an element from the front of the queue.
- Front() - returns the first element/Head of the queue.
- Back() - returns the tail of the queue.
- Size() - returns the number of elements in the queue
- Empty() - returns boolean
`True`

if the queue is empty.

`Queue`

class`class Queue: def __init__(self): self.items = [] self.size = 0`

As you can see from the code above, the initialization of our `Queue`

class is basically the same as we did with the `Stack`

, we have an items array and a variable to keep track of the size of the `Queue`

.

`push()`

method`class Queue: def __init__(self): self.items = [] self.size = 0 def push(self, value): self.items.append(value) self.size += 1`

The `push`

method it's the same as the one with the Stack, we are going to handle the end of the array as the tail of the `Queue`

, and with just 2 lines of code, we already added the Enqueue behavior.

`pop()`

method`class Queue: def __init__(self): self.arr = [] self.size = 0 def push(self, value): self.arr.append(value) self.size += 1 def pop(self): if self.size > 0: self.items.pop(0) self.size -= 1`

The pop method may look very similar to the Stack method, BUT remember that the stack took out the last element of the array, and with a queue, we must take the first element of the array aka the front/head, that's why instead of doing `pop()`

we do `pop(0)`

where 0 it's the index of the array.

`front()`

methodThis method simply returns the element that's at the Front/Head of the Queue, this method should not remove the element.

`class Queue: def __init__(self): self.items = [] self.size = 0 def push(self, value): self.items.append(value) self.size += 1 def pop(self): if self.size > 0: self.items.pop(0) self.size -= 1 def front(self): return self.items[0]`

`back()`

methodThis method behaves very similarly to the `front()`

method but instead of returning the front, this method should return the last element.

`class Queue: def __init__(self): self.items = [] self.size = 0 def push(self, value): self.items.append(value) self.size += 1 def pop(self): if self.size > 0: self.items.pop(0) self.size -= 1 def front(self): return self.items[0] def back(self): if self.size > 0: return self.items[self.size - 1] # -1 because indexing starts at 0`

As you can see from the code above, the back method first checks that the Queue is not empty, this is important because if we don't check, we are going to get an index out of range error.

`empty()`

method`class Queue: def __init__(self): self.items = [] self.size = 0 def push(self, value): self.items.append(value) self.size += 1 def pop(self): if self.size > 0: self.items.pop(0) self.size -= 1 def front(self): return self.items[0] def back(self): if self.size > 0: return self.items[self.size - 1] def empty(self): if self.size > 0: return False else: return True`

`myQueue = Queue()myQueue.push(1)myQueue.push(2)myQueue.push(3)myQueue.push(4)print(myQueue.items)print(myQueue.size)print(myQueue.front())print(myQueue.back())myQueue.pop()myQueue.pop()print(myQueue.items)print(myQueue.size)print(myQueue.front())print(myQueue.back())`

By now, you may be noticing a pattern. Earlier, we implemented a stack using an array and a linked list. Here, we're doing the same thing with queues.

`Queue`

classin the cell below, you can see the Queue class initialization with the head and tail set to None and the num_elements variable set to cero.

`class Queue: def __init__(self): self.head = None self.tail = None self.num_elements = 0`

`Node`

classThis is exactly the same code we did previously.

`class Node: def __init__(self, value): self.value = value self.next = None`

`push()`

methodIn the method from the cell below, we create a new node with the passed value, and we check if the linked list is empty with `if self.head == None:`

, if it is, we make the head of our linked list to be the newly created node, as well as the tail. if the linked list is not empty, we simply set the next node of the tail to the new node and we shift the tail to make sure it's always at the end. Lastly, we increment the number of elements in the linked list.

`class Queue: def __init__(self): self.head = None self.tail = None self.num_elements = 0 def push(self, value): new_node = Node(value) if self.head == None: self.head = new_node self.tail = self.head else: self.tail.next = new_node self.tail = self.tail.next self.num_elements += 1`

`pop()`

methodThe first step is to check if the linked list is not empty with `if self.num_elements > 0:`

and if so, we shift the head to the next node removing it from the list, lastly, we subtract 1 to the `num_elements`

counter.

` def pop(self): if self.num_elements > 0: self.head = self.head.next self.num_elements -= 1`

`front()`

method.This method is very easy thanks to the way we keep track of the head on the linked list.

` def front(self): if self.num_elements > 0: return self.head.value`

`back()`

method.Likewise, creating this method is a breeze thanks to the tail attribute of the linked list.

` def back(self): if self.num_elements > 0: return self.tail.value`

`size()`

method` def size(self): return self.num_elements`

`print()`

methodIn the first method, we could simply print the array, but here, we need to traverse the linked list and print each of the elements' values.

` def print_items(self): current = self.head while current: print(current.value, "=>", end=" ") # prints in a single line current = current.next print("") # just to add the end line at the end`

`myQueue = Queue()myQueue.push(1)myQueue.push(2)myQueue.push(3)myQueue.push(4)myQueue.print_items()print(myQueue.size())print(myQueue.front())print(myQueue.back())myQueue.pop()myQueue.pop()myQueue.print_items()print(myQueue.size())print(myQueue.front())print(myQueue.back())`

Output:

`1 => 2 => 3 => 4 => 4143 => 4 =>234`

You've reached the end of this lesson on Stacks and Queues, I really hope you liked it and learned something new today, if you have any questions or suggestions, feel free to comment in the section below, remember, there is still a lot to learn and we definitively didn't cover everything on this topic, especially on the applications of the queue like in a BFS algorithm for example, but that's a topic for another day. See you in the next post, stay tuned! 👋

]]>Hello everyone, welcome back! today, I wanted to talk about the importance of efficiency with data structure and algorithms and how to use Big-O notation to measure the Time and Space complexity of an algorithm.

In my last post, we talked about how to solve computational problems with python but we didn't dive into whether our solution was efficient or not. That's what we will be looking at in this section.

When we refer to the efficiency of a program, we don't only look at the time it takes to run, but at the space required in the computer's memory as well. Often, there will be a trade-off between the two, where you can design a program that runs faster by selecting a data structure that takes up more spaceor vice versa.

In order to quantify the time and space an algorithm takes, let's first understand what an algorithm is.

## Algorithm

An algorithm is a series of well-defined steps for solving a problem. Usually, an algorithm takes some kind of input (such as a list) and then produces the desired output (such as a reversed list).

For any problem, there is likely more than one algorithm that can solve the problem, but there are some algorithms that are more efficient than others. However, computers are so fast! that in some problems, we can't tell the difference, so how can we know what algorithm is more efficient than another? and if we can't tell the difference, why try to make our code efficient?

Well, those are some great questions, and in some cases it's true, one version of a program may take 5 times longer than another, but they both still run so quickly that it has no real impact. However, in other cases, a very small change can make the difference between a program that takes milliseconds to run and a program that takes hours!.

Sometimes, you will hear programmers say:

"This algorithm is better than that algorithm"

But how can we be more specific than that? How do we quantify efficiency?.

Let's take a look at some code examples.

`def add200(n): for i in range(2): n += 100 return n`

`def add200_2(n): for i in range(100): n += 2 return n`

Both of the functions above have no real-world use, they are just dummy functions that will help us understand efficiency. Both of them add 200 to whatever the input(n) is. Take a good look at them and tell me, which one is more efficient?

The answer is the `add200`

function. Why?

Although both functions output the exact same result, the `add200_2`

function makes too many iterations, while the `add200()`

function only iterates twice.

With the example above, what we basically did was to estimate which code had more lines to run, let's take a look again at both functions

`def add200(n): for i in range(2): n += 100 return ndef add200_2(n): for i in range(100): n += 2 return n`

The first function has a total of 4 lines but because of the for loop that gets called twice, there is a total of 5 lines (the for loop line doesn't get counted).

Now, let's take a look at the second function. the total of lines is 4 but the for loop gets called 100 times! so running this code will involve running 103 lines of code!.

Counting lines it's not a perfect way of quantifying the efficiency of a program but it's an easy way for us to **approximate** the difference in efficiency between two solutions.

In both examples above, no matter what number we passed as an argument, the number of lines executed will remain the same.

Here's a new code example:

`def print_to_num(n): for i in range(n+1): # +1 because python counts from 0 print(i)print_to_num(10)`

The code above prints all numbers from 0 to N, meaning the bigger the input N is, the number of lines executed will increase, and that means a longer time to run.

The highlighted idea it's that:

As the input to an algorithm increases, the time required to run the algorithm

mayalso increase.

This **may** happen in some cases, in the last example it does increase the time, but in the first two, it does not.

Let's keep working with the last function and let's try several function calls examples to see the number of lines each one gets to run.

`print_to_num(2) # 4 linesprint_to_num(3) # 5 linesprint_to_num(4) # 6 linesprint_to_num(5) # 7 lines`

As you can see, when N goes up by 1 the number of lines will also go up by 1. We can also say that the number of lines executed increases by a **proportional** amount. This type of relationship is called a **Linear relationship** if we graph the relationship we can see why it's called that.

The x-axis represents the input size, in this case, a number, and the y-axis represents the number of operations that will be performed in this case we're thinking of an "operation" as a line of Python code which is not the most accurate but will do for now.

Let's take a look at another function example where the operations increase at a **none** constant rate.

`def print_square(n): for i in range(1, n+1): for j in range(1, n+1): print("*", end=" ") # end=" " for printing all "*" in a single line separated by two white spaces print("") # this is used to mimic an "enter"print_square(5)`

This code prints a square made out of "*" that it's exactly N x N in size

`output: * * * * ** * * * ** * * * ** * * * ** * * * *`

Notice that this function has a **nested** loop, that means, a loop inside a loop, and take a good look at the fact that both loops have a linear rate of increase but they are **nested** that makes the rate of increase **quadratic** that means that when the input goes up by a certain amount, the number of operations goes up by the **square** of that amount.

`print_square(1) # 1 lineprint_square(2) # 4 linesprint_square(3) # 9 lines`

Let's add the quadratic rate of increase to our graph:

Our `print_square`

function exhibits a quadratic rate of increase which as you can see is a much faster rate of increase, this means that as we pass larger numbers, the number of operations the computer has to perform shoots up very quickly making the program far less efficientThese are only two examples of the rate of increase but there are many more. Here are the most common:

Note. when people refer to the rate of increase, they will often use the term "order". For example, instead of saying: "This algorithm has a linear rate of increase" they will say: "The order of this algorithm is linear"

Big-O notation is a "simplified analysis of an algorithm's efficiency". Big-O gives us an algorithm's complexity in terms of the input size (N) and it's independent of the machine we run the algorithms on. Big-O can give us the time but also space complexity of an algorithm

There are three types of ways we can look at an algorithm's efficiency.

- Worst-case
- Best-case
- Average-case

Normally, when talking about Big-O notation we will typically look at the worst-case scenario, this doesn't mean the others are not used, but normally we want to know what the worst case is.

Big-O notation ignores constants. Example:let's say you have a function that has a running time of O(5n), in this case, we say that it runs on the order of O(n) because as N gets larger, the 5 no longer matters.

Terms priority.This means that if a function has a part that has an O(1) order but another part of that same function has an order of O(n) we will say that the whole function has an order of O(n). This includes sections that might not run, for example, in an if-else statement.

`O(1) < O(logn) < O(n) < O(nlogn) < O(n2) < O(2n) < O(n!)`

(see graph above)

In this section, we will take a look at example lines of code and see how O(n) is used.

`O(1)`

Take a look at the line below

`print((5*10)/2) # outputs 25`

As you can see, this code simply computes a simple math operation and it's not dependent on any input. This is an example of O(1) order or constant time.

What if we have not one but three simple math operations.

`n = 5 + 2 # O(1)y = 3 * 5 # O(1)x = n + y # O(1)`

Well, in this case, each line of code has an O(1) order, so what we need to do is add all of them like this: `O(1)+O(1)+O(1)`

BUT let's remember the first rule and ignore constants, making the final result be `O(1)`

`O(n)`

Let's try another example:

`for i in range(n): # n * O(1) = O(n) print(i) # O(1)`

The clearest example of Linear time code, it's a simple for loop that iterates from 0 to N. In the code shown above, the print statement has an order of `O(1)`

but because it's inside a loop, we need to multiply `O(1) * N`

which would give us a result of `O(N)`

If, for example, we had both examples above together like this:

`n = 5 + 2 # O(1)y = 3 * 5 # O(1)x = n + y # O(1)for i in range(n): # n * O(1) = O(n) print(i) # O(1)`

We would have the `O(1)`

of the simple math and print lines, and `O(n)`

of the for loop, so, to calculate the total time we need to add both of them BUT let's remember our second rule of terms priority and make `O(N)`

the final result for this code

`O(n)`

The easiest way to get a code to have a Quadratic order it's to simply have a nested loop with both of them iterating from 0 to N.Let's see the code example that we saw earlier:

`def print_square(n): for i in range(1, n+1): for j in range(1, n+1): print("*", end=" ") # end=" " for printing all "*" in a single line separated by two white spaces print("") # this is used to mimic an "enter"print_square(5)`

The first for loop has a time complexity of `O(N)`

, the second for loop also has a time complexity of `O(N)`

, and finally, the print statements have a complexity of `O(1)`

. And hopefully, you can see pretty clearly that the print statement will be executed `N * N`

times, making this whole code have a time complexity of `O(N)`

If, we put all previous codes together like this:

`n = 5 + 2 # O(1)y = 3 * 5 # O(1)x = n + y # O(1)for i in range(n): # n * O(1) = O(n) print(i) # O(1)for i in range(1, n+1): # O(n) * O(n) = O(n) for j in range(1, n+1): print("*", end=" ") print("") # this is used to mimic an "enter"`

we know the first code has a complexity of `O(1)`

the second code has a complexity of `O(n)`

and the third code has a complexity of `O(n)`

and because of our second rule, the complexity of the whole code it's `O(n)`

`O(log n)`

An algorithm with a Logarithmic run time, it's that code that reduces the size of the input data in each step (it doesn't need to look at all values of the input data), a great example of this, is a **Binary Search**. Binary Search is a searching algorithm for finding an element's position in a sorted array.

`def binary_search(arr, element): left = 0 right = len(arr)-1 while(left <= right): mid = (left + right)//2 if arr[mid] == element: return mid if arr[mid] < element: left = mid +1 else: right = mid -1 return -1print(binary_search([1,2,3,4,5,6,7,8,9], 8))`

`(O(n log n))`

An algorithm is said to have a log-linear time complexity when each operation in the input data has a `logarithm`

time complexity. It is commonly seen in sorting algorithms

`codes = set()for phone in phones_arr: codes.add(phone[:4])print('\n'.join(sorted(codes))) # <== O(n log n)`

The code above iterates through an array of phone numbers and adds the first four digits of each phone to a "codes" set(a set doesn't allow duplicates) and finally the code prints the codes but are sorted.

`O(2)`

Algorithms have an exponential time complexity when the growth doubles with each addition to the input data set. This kind of time complexity is usually seen in brute-force algorithms.

`def fibonacci(n): if n <= 1: return n return fibonacci(n-1) + fibonacci(n-2)print(fibonacci(8)) # outputs 21`

A great example of an exponential time algorithm is the recursive calculation of Fibonacci numbers, the code above receives a number and prints the N number in the Fibonacci sequence:`0, 1, 1, 2, 3, 5, 8, 13, 21`

An algorithm where the operational execution complexity increases factorially with the increase in the input size. As you can see from the graph above, this is the most inefficient order that an algorithm can have.

`def factorial(n): if n == 1: return n else: return n * factorial(n-1)print(factorial(4))`

The function above receives a number and uses recursion to print the result of multiplying all numbers from 1 to N`4*3*2*1 = 24`

You've reached the end of this post on complexity and Big-O notation to quantify time efficiency, I really hope some of the concepts we saw today have helped you and hopefully you will be thinking not only about solving a problem but to think weather your code is efficient or not. If you liked this post, make sure to share it with another person that might find this interesting, and let me know in the comments your thoughts, suggestions, and what you will like to see next.

See you in the next post, stay tuned for more!

]]>Hello everyone, welcome back!, today I wanted to give a beginners overview of Linked Lists, we will take a good look at what are they? what are they for? and we will look through some examples together.

A linked list is an extension of a list but it's definitely NOT an array. By definition a linked list is a:

Dynamic data structure where each element (called a node) is made up of two items: the data and a reference (or pointer), which points to the next node. A linked list is a collection of nodes where each node is connected to the next node through a pointer.

Here it's an image to give a better understanding of a linked list

With a linked list, there are still some things that have order but there are no indices. Instead, a linked list is characterized by its `Links`

. Each element has a notion of what the next element is since it's connected to it, but not necessarily how long the list is or where it is in the list.An array's different. There is nothing in one element of the array that says "here's your next element" like a linked list would, instead an array knows what the next element is by what the next `index`

is.

Let's walk through the differences together.

- A data structure consisting of a collection of elements each identified by the array index.
- Supports random access, the programmer can directly access an element in the array using an index
- Elements are stored in contiguous memory locations
- Programmer has to specify the size of the array when declaring the array
- Elements are independent of each other

- A linear collection of data elements whose order is not given by their location in memory.
- Supports sequential access, the programmer has to go sequentially through each element until reaching the searched element
- Elements can be stored anywhere in memory
- There is no need in specifying the length of the linked list
- An element has to point to the next or previous element

When talking about linked lists you will often hear "Node" instead of "Element"

When I was studying linked lists I was already pretty familiar with arrays and felt pretty comfortable using them, and if you are a beginner you are probably feeling the same way and are wondering "Hey, arrays are easy to access, have a set length, the elements are independent of each other, why make my life harder with this new linked list stuff?", if you thought of that, congratulations! you are on your way to becoming a great programmer since you have to always ask yourself the why of things.

The answer to the question "Why use linked lists?" can be a complex one, but for now, the simple and short answer is Efficiency. Turns out, that adding or removing an element from a linked list is so much easier in comparison to an array. And I want to give a quick reminder that we are working with python here.

Let's learn more about linked lists while we work through some examples and start writing some code.

Because of the definition we saw earlier, we are going to approach this, by creating a `Node`

, a node it's going to have, two attributes, the value, and the `next`

attribute that's going to be a pointer pointing to the next node. From the image above, we can see how each node points to the next one until the list it's finished and the last node points to `None`

. this collection of nodes pointing to the next one is going to be the Linked List.

`class Node: def __init__(self, value): self.value = value self.next = None`

this node has a "value" property that can be passed as an argument, and a "next" attribute that points to the next `Node`

, in this case, it's pointing to None by default.

Using our newly created Node class, it's time to create our `Head`

, the "Head" it's basically the first Node.

`head = Node(8)`

Creating a new Node with the value of 8 and saving it in a new head variable.

Like we saw in the previous step, to create a new Node simply code `Node(value)`

, and to link it to the head what we need to code is:

`head.next = Node(4)`

In the code above, `head.next`

had the value of None, but now we are replacing that with a new Node with the value of 4.

Right now, we have a linked list that has a Head with the value of 8 then the next node with the value of 4 like this:

Here's our progress:

`class Node: def __init__(self, value): self.value = value self.next = Nonehead = Node(2)head.next = Node(1)`

To print out the values in the linked list we have to code:

`print(head.value)print(head.next.value)`

`Output:84`

Let's remember that this is what we want to accomplish:

To do this, we need to create three more nodes, and we need to attach each one to the next attribute of the node that comes before it. Notice that we don't have a direct reference to any of the nodes other than the head.

`head.next.next = Node(2)head.next.next.next = Node(5)`

Let's print all the values:

`print(head.value)print(head.next.value)print(head.next.next.value)print(head.next.next.next.value)`

This process isn't very efficient, let's write a function that receives the head and traverses the linked list to print out all of the values.

`def print_linked(head): current = head # current node set to the head while current: # loop that will run as long as current exists print(current.value) # printing the current node value current = current.next # moving to the next nodeprint_linked(head)`

Ahh, so much better, now, no matter how long the linked list is, with `print_linked(head)`

we no longer need to code `print(head.next.next.next.next)`

you get the point.

Up until now, we have to create a variable with the head and pass that to our functions in order to interact with it, but an alternative it's to create a Linked_List class and code methods to it. Let's take a look at some code to understand what I mean.

`# Linked_List class with a default head value of None meaning empty listclass Linked_List: def __init__(self): self.head = None`

Let's add our print function as a method to this class.

` def print_linked(self): current = self.head while current: print(current.value) current = current.next`

In the code above, we made tiny changes to fit in as a method, in this case instead of receiving the head, we receive the `self`

attribute, and instead of setting current to the head, we set it to the `self.head`

, other than that, everything stays the same.

Now, we have our Linked list class with a print method and we have to create the actual object like this:

`my_linked_list = Linked_List()`

and if we wanted to print the values of that linked list, all we have to do is:

`my_linked_list.print_linked()`

If you tested the lines above you should have seen no output since the linked list is completely empty, let's change that by adding a method that allows us to append items into the list.

` def append(self, value): # checking if the linked list is empty if self.head is None: # If so, set the head to a new node with the passed value self.head = Node(value) return # Traverse the linked list until reaching the end of the list current = self.head while current.next: current = current.next # Append the new node with the passed value current.next = Node(value)`

Here's the full code:

`class Node: def __init__(self, value): self.value = value self.next = Noneclass Linked_List: def __init__(self): self.head = None def print_linked(self): current = self.head while current: print(current.value) current = current.next def append(self, value): if self.head is None: self.head = Node(value) return current = self.head while current.next: current = current.next current.next = Node(value)my_linked_list = Linked_List() # this is creating a Linked_List objectmy_linked_list.append(8) # appending 8 to the listmy_linked_list.append(4) # appending 4 to the listmy_linked_list.append(2) # appending 2 to the listmy_linked_list.append(5) # appending 5 to the listmy_linked_list.print_linked() # printing the list`

If you test the code above you shall now see an output of `8 4 2 5`

like the image below

I hope you're getting the hang of this, let's try adding more methods that will make working with linked lists better.

Below you will find the exercises for this section, I highly encourage you to try to code them yourself before watching the solutions that can be found at the end of this section. And I also recommend you try each of the methods individually first before trying to use multiple of them together.

`to_list()`

methodWhen the method is called on the linked list it should convert the linked list back into a normal python list.

`output: [8,4,2,5]`

`prepend()`

methodMethod similar to the `append()`

method but instead of appending at the end of the linked list it will append at the beginning of the list

`my_linked_list.prepend(7)my_linked_list.print_linked()`

`output: 78425`

`search()`

method`print(my_linked_list.search(4))`

will return the Node if there is one with a matching value, if not, it will return a ValueError indicating that the searched value is not found in the list. Because it returns the searched Node you can access its value like this:

`# if there is a node with the value of 4, 4 will be printedprint(my_linked_list.search(4).value)`

`output: 4`

`remove()`

methodMethod traverses the linked list and removes the Node with the passed value

`my_linked_list.remove(4)my_linked_list.print_linked()`

`output: 7825`

`pop()`

methodMethod that removes the first node of the linked list

`my_linked_list.pop()my_linked_list.print_linked()`

`output: 825`

`insert()`

methodThis method receives a position and a value and inserts a new node with the passed value in the passed position

`my_linked_list.insert(10,1)my_linked_list.print_linked()`

`output: 81025`

`size()`

methodThis method shall return the length of the linked list like the method `len()`

would on a normal python list

`print(my_linked_list.size())`

`output: 4`

`reverse()`

methodReversing a linked list is a really common interview problem and I highly recommend that you put extra effort into understanding this method. This method shall, like the name suggests, reverse the linked list nodes order.

`my_linked_list.reverse()my_linked_list.print_linked()`

`output: 52108`

Feel free to test out combinations of the methods and try each one individually and do your best in understanding them. You can also try adding your own methods, and remember that the solutions shown below are not the only correct ways of coding them.

`class Node: def __init__(self, value): self.value = value self.next = Noneclass Linked_List: def __init__(self): self.head = None # ------------------------------------------ # def print_linked(self): current = self.head while current: print(current.value) current = current.next # ------------------------------------------ # def append(self, value): if self.head is None: self.head = Node(value) return current = self.head while current.next: current = current.next current.next = Node(value) # ------------------------------------------ # def to_list(self): out = [] node = self.head while node: out.append(node.value) node = node.next return out # ------------------------------------------ # def prepend(self, value): if self.head is None: self.head = Node(value) return new_head = Node(value) new_head.next = self.head self.head = new_head # ------------------------------------------ # def search(self, value): if self.head is None: return None node = self.head while node: if node.value == value: return node node = node.next raise ValueError("Value not found in the list.") # ------------------------------------------ # def remove(self, value): if self.head is None: return if self.head.value == value: self.head = self.head.next return node = self.head while node.next: if node.next.value == value: node.next = node.next.next return node = node.next raise ValueError("Value not found in the list.") # ------------------------------------------ # def pop(self): if self.head is None: return None node = self.head self.head = self.head.next return node.value # ------------------------------------------ # def insert(self, value, pos): if self.head is None: self.head = Node(value) return if pos == 0: self.prepend(value) return index = 0 node = self.head while node.next and index <= pos: if (pos - 1) == index: new_node = Node(value) new_node.next = node.next node.next = new_node return index += 1 node = node.next else: self.append(value) # ------------------------------------------ # def size(self): size = 0 node = self.head while node: size += 1 node = node.next return size # ------------------------------------------ # def reverse(self): previous = None current = self.head while current: next = current.next current.next = previous previous = current current = next self.head = previous # ------------------------------------------ #my_linked_list = Linked_List() # creating linked list objmy_linked_list.append(8) # appending 8my_linked_list.append(4) # appending 4my_linked_list.append(2) # appending 2my_linked_list.append(5) # appending 5my_linked_list.print_linked() # output: 8 4 2 5print(my_linked_list.to_list()) # output: [8,4,2,5]my_linked_list.prepend(7) # appending 7 to the start of the linked listmy_linked_list.print_linked() # output: 7 8 4 2 5print(my_linked_list.search(4).value) # output: 4my_linked_list.remove(4) # remove the node with value 4 from the listmy_linked_list.print_linked() # output: 7 8 2 5my_linked_list.pop() # remove the first nodemy_linked_list.print_linked() # output: 8 2 5my_linked_list.insert(10, 1) # insert node with value 10 in position 1my_linked_list.print_linked() # output: 8 10 2 5print(my_linked_list.size()) # output: 4my_linked_list.reverse() # reverse the order of the nodesmy_linked_list.print_linked() # output: 5 2 10 8`

Up until this point, we have been working with a type of linked list called: "singly linked list", in this kind of linked list, each node is connected only to the next node in the list.

This connection is typically implemented by setting the `next`

attribute on a node object itself. But there are more types of linked lists.

This type of list is the exact same as the singly linked list with the addition of also having a connection backward.

To implement a Doubly linked list simply change your node to have a previous property like this:

`class DoubleNode: def __init__(self, value): self.value = value self.next = None self.previous = None # new property`

using doubly linked lists can give us advantages that a singly linked list can't, for example, now that we track the tail, we can do things like appending a node to a list's tail in constant time.

`def append(self, value): if self.head is None: self.head = DoubleNode(value) self.tail = self.head return self.tail.next = DoubleNode(value) self.tail.next.previous = self.tail self.tail = self.tail.next return`

Linked circular lists occur when the chain of nodes links to itself somewhere. For example, NodeA -> NodeB -> NodeC -> NodeD -> NodeB is a circular list because NodeD points to NodeB creating a NodeB -> NodeC -> NodeD -> NodeB loop.

A circular linked list in simple words, it's a singly or doubly list where all nodes are connected to form a loop or "circle" meaning there is no NULL at the end, this can be a problem since if we try to iterate through it we will never find the end. We usually want to detect if there's a loop in our linked list to avoid an infinite iteration.

In this section, we'll see a way we can detect loops in a linked list with python. The way we'll do this is by using two pointers called "runners", both of them will traverse through the linked list but one of them it's going to iterate twice as fast, meaning it will advance two nodes at a time.If a loop exists, the fast runner will eventually catch up with the slow runner and both pointers will be pointing at the same node at the same time, if this happens you will know for sure there's a loop, see the image below to understand this technique.

`hasLoop()`

methodI'm confident you can do this on your own, see the image above thoroughly to understand the algorithm you're about to code, and also take another look at the past exercises, you should find everything you need in this post. Make your best effort! and don't worry if you don't get it right away we'll see the solution below.

`hasLoop()`

methodThe first thing we need to do is to define our `hasLoop()`

method:

`def hasLoop(self): # your code goes here`

Next, we'll check if the list is empty, in the case that the condition is True we'll return False meaning there is no loop

` if self.head is None: return False`

Then, we need to create our two pointers as we talked about earlier

` slow = self.head fast = self.head`

Now the fun part! while the fast node and the fast.next node exists, let's advance the slow node by one and the fast by two

` while fast and fast.next: slow = slow.next fast = fast.next.next`

and for each iteration, we need to check if the slow node is the same as the fast one, if that is the case then we just found a loop and the code should return True, if the while loops finishes, it means there was an end to the list and the code should return False.

` if slow == fast: return True return False`

Full code:

` def hasLoop(self): if self.head is None: # check if the linked list is empty return False # return False, meaning there is no loop slow = self.head # create the slow node fast = self.head # create the fast node while fast and fast.next: # while fast and fast.next exist: slow = slow.next # advance the slow node by one fast = fast.next.next # advance the fast node by two if slow == fast: # if the nodes are the same return True # return True, meaning there is a loop # if the loop finishes, that means there was an end to the list # and there was no loop return False`

Let's suppose we have a **nested** linked list, this means that the value of each node in the linked list is another ordered linked list. The image below illustrates this.

In this section, we'll **flatten** this linked list, which means to combine all nested lists into one **ordered** single linked list like this:

`NestedLinkedList`

classThis class should inherit the Linked_List class and inside of the new class let's add our flatten method.

`class NestedLinkedList(Linked_List): def flatten(self): # code goes here`

`merge()`

Helper FunctionThis function will be useful for merging two linked lists and having them into a single **ordered** linked list, this function it's not going to be a `NestedLinkedList`

method and should return an instance of LinkedList.

`def merge(list1, list2): # your code goes here return`

`merge()`

walkthroughNote, this solution expects the passed lists to be ordered.

First, we need to create a new LinkedList object, that will be named `merged`

` merged = Linked_List()`

Next, let's check whether any of the passed lists are None. If list1 is None, immediately return list2, if list2 is None, immediately return list1.

` if list1 is None: return list2 if list2 is None: return list1`

Then, we'll create two traversers, one called list1_node and the second one called list2_node. And using both of them, we'll traverse both lists while they exist.

` list1_node = list1.head list2_node = list2.head while list1_node or list2_node: # pay attention to the OR`

If the first node does not exist, meaning the first list is traversed, append the list2_node to the "result list"(merged) and advance the list2_node to the next node in that list.

` if list1_node is None: # if list1 is traversed merged.append(list2_node.value) # append the second list node to the "result" list2_node = list2_node.next # and advance the second node to the next one`

Repeat the step above but with the list2

` elif list2_node is None: # if list2 is traversed merged.append(list1_node.value) # append the first node to the "result" list1_node = list1_node.next # advance the first node to the next one`

Now we can work in the cases that none of the lists are done traversing. Which would always be the first iteration.

Because we want the output to be an ordered linked list but with both lists merged, we need to check which node has the smallest value and append it.

` # if the first node value is smaller than the second node value elif list1_node.value <= list2_node.value: merged.append(list1_node.value) # append the first node list1_node = list1_node.next # and advance to the next node else: merged.append(list2_node.value) # append the second node list2_node = list2_node.next # advance to the next node`

Finally, after the while loop exits indicating that both lists have been traversed, return the result, (merge) linked list.

` return merged`

Here's the full `merge()`

procedure:

`def merge(list1, list2): merged = Linked_List() if list1 is None: return list2 if list2 is None: return list1 list1_node = list1.head # [5,6,7] list one list2_node = list2.head # [1,2,3] list two while list1_node or list2_node: # 5 - 1 if list1_node is None: # if list1 is traversed # append the second list node to the "result" merged.append(list2_node.value) list2_node = list2_node.next # and advance the second node to the next one elif list2_node is None: # if list2 is traversed # append the first node to the "result" merged.append(list1_node.value) list1_node = list1_node.next # advance the first node to the next one # if the first node value is smaller than the second node value elif list1_node.value <= list2_node.value: merged.append(list1_node.value) # append the first node list1_node = list1_node.next # and advance to the next node else: merged.append(list2_node.value) # append the second node list2_node = list2_node.next # advance to the next node return merged`

`Flatten()`

MethodWith the help of our `merge()`

helper function, there is an easy solution to finish our `Flatten()`

method by using **recursion**.

We won't talk about recursion in detail in this post, but for those that are not familiar with the concept, recursion is basically invoking a function inside itself, here's a code example that returns the factorial of any number.

`def factorial(x): # function definition if x == 1: # break point, important to exit the "loop" return 1 else: return (x*factorial(x-1)) # invoking itselfprint(factorial(4)) # 4 * 3 * 2 * 1 = 24`

As you can see from the code above, the `factorial()`

function it's being invoked inside of itself, that's a recursive function.

Back to our `Flatten()`

method. Let's apply recursion!

`class NestedLinkedList(Linked_List): def flatten(self): return self._flatten(self.head) def _flatten(self, node): # a recursive function # A termination condition if node.next is None: # if there is no next node in the list # merge the last, current node return merge(node.value, None) # _flatten() is calling itself untill a termination condition is achieved # <-- Both arguments are a simple LinkedList each return merge(node.value, self._flatten(node.next))`

`flatten()`

FunctionFirst, let's create three simple, normal,ordered, linked lists.

`''' Create a simple LinkedList'''linked_list = Linked_List()linked_list.append(0)linked_list.append(4)linked_list.append(8)linked_list.print_linked()# 0 => 4 => 8''' Create another simple LinkedList'''second_linked_list = Linked_List()second_linked_list.append(1)second_linked_list.append(2)second_linked_list.append(3)second_linked_list.print_linked()# 1 => 2 => 3''' Create another simple LinkedList'''third_linked_list = Linked_List()third_linked_list.append(6)third_linked_list.append(7)third_linked_list.append(9)third_linked_list.print_linked()# 6 => 7 => 9`

Next, let's create a **nested** linked list object using our class

`nested_linked_list = NestedLinkedList()`

Something really cool, it's that when we defined the `NestedLinkedList`

class, we inherited the `Linked_List`

class, which means that all of the previous methods we wrote for the `Linked_list`

class are going to be available to use in the `NestedLinkedList`

class.

`# Here we inherited the Linked_List classclass NestedLinkedList(Linked_List): # making all of its methods available here too!`

Now, let's append the three simple linked lists into this `nested_linked_list`

using the `append()`

method.

`nested_linked_list.append(linked_list)nested_linked_list.append(second_linked_list)nested_linked_list.append(third_linked_list)`

Lastly, create the flattened list like this:

`flattened = nested_linked_list.flatten()`

and print it out!

`flattened.print_linked()`

You've reached the end of this lesson on Linked Lists, we saw what are they? how do they differentiate from a normal python list? what are some pros and cons? and we also saw code examples of the most common ways to interact with Linked lists.

I really hope some of the guidelines we saw today were helpful and the basic foundations on this topic were well understood, remember, there is still a lot to learn and we definitively didn't cover everything on linked lists, but I hope this was a good beginner overview and that you grasped the concepts and feel more confident on your programming journey.

Let me know in the comments what you thought about this post and let me know what you will like to see next. See you in the next post, stay tuned!

]]>Hello everyone, welcome back! today I want to talk about a very important topic: how to solve problems?. Learning how to solve problems is one of the most important skills you can learn, and improving as a problem solver is a lifelong challenge that cannot be learned through this short lesson, however, I want to show you important tips on how to tackle more complex programming problems, and hopefully, this post will teach you the essentials to problem-solving.

In this post, we will tackle a specific problem, and talk about how I would go about solving it, and the goal of this exercise is not just to solve the problem but to draw general ideas on how to solve any type of programming problem.

Given your birthday and the current date, calculate your age in days. Compensate for leap years. Assume that the birthday and current date are correct (no time travel).

It's often tempting to start writing code early, the problem with this is that we are likely to write the wrong code and get frustrated, it's also likely that you think that you "solved the problem" but in reality, you might end up doing something completely different to what it was asked.

We need to emphasize that we are working on a `computational problem`

, and what all computational problems have in common is that they have inputs and desired outputs, so, a problem is defined by the set of possible inputs (this is usually an infinite set) and the relationships between those inputs and the desired outputs. And a solution is a procedure that can take any input in that set and produces the desired output that satisfies the relationship, in this case, the relationship we want is that the output is the number of days between the birthday and the current date.

So, the first step to understanding a problem is to understand what the possible inputs are.

If we take another look at the problem we realized that it's clearly stated that we're given two dates as inputs.

Given your birthday and the current date, calculate your age in days. Compensate for leap years. Assume that the birthday and current date are correct (no time travel).

We always need to ask ourselves this question, for the moment we know the type of the inputs, but if we take another look at our problem we can find a good clue on what to expect.

Given your birthday and the current date, calculate your age in days. Compensate for leap years. Assume that the birthday and current date are correct (no time travel).

Thanks to that statement we know that the second date needs to be after the first one. Assumptions like this one make life easier for programmers since our code has to work for fewer possible inputs, however, we are going to be good defensive programmers and check if the second date is after the first date in our code. Checking if the requirement is correct is a good practice since other people or even ourselves can make mistakes.

The other assumption we might need to think about is the range of dates. Calendars are very complicated and they've changed over history, so, we are going to require that the dates are valid dates in the Gregorian calendar, which started in October 1582.

For most real-world problems, it's up to you to figure out how to encode the inputs and this is one of the most important decisions that you can make in solving a problem. In this case, the problem template below indicates how the inputs are represented

`# function that returns the dates difference in daysdef daysBetweenDates( year1, month1, day1, year2, month2, day2 ): # You're code goes here`

As we can see there are six parameters to the daysBetweenDates procedure, which means we're going to be passing in six different values to represent those two dates. Some of you might see that there are better ways to pass the date, as an object for example but for the sake of clarity we're going to be passing those six values.

The statement of the question gives us some idea of what the output should be in the calculate your age part.

But it doesn't specify really explicitly what we want the output to be

In this case, after reading the problem, it's clear that the output should be a **number ** representing the days of the difference between date1 and date2 assuming date1 is before date2.

Now that we know what the inputs are and what the outputs are it's time to understand the relationship between the two by working out some examples.

Take a good look at the problems below and try to figure out what the output for each call should be.

`daysBetweenDates(2020,11,7, 2020,11,7)daysBetweenDates(2015,12,7, 2015,12,8)daysBetweenDates(2010,12,8, 2010,12,7)daysBetweenDates(2019,5,15, 2021,5,15)daysBetweenDates(2012,6,29,2013,6,31)`

For problem one, the output should be 0 since both dates are the same. For problem two the output should be 1 since there is only a one-day difference. Output for problem three should be an error since the second date is before the first date. Problem four was hard since, there was a two-year difference and 2020 is a leap year, so, the output should be 731. The last one was tricky since there is no day 31 in June!, so we would like to have an error indicating the invalid input.

At this point, you might be ready to start coding, but if the problem is challenging one you're probably not.

The first step to solving a problem as a human is to look at an example and in this case let's say we wanted to find the difference between the dates (2013,01,24) and (2013,06,29), as a human the first thing that we might do is to look at a calendar and find the first date, then see how many days are left for that month, in this case, that is 7 days, we would probably grab a piece of paper and write that number down. Then we would count how many days has every month until we reach the second date.`7+28+31+30+31+29 = 156`

We now have a starting point and it's time to write down an algorithm that systematizes how we solve it, in this case, we're going to write this as `Pseudocode`

meaning we aren't focusing on coding real python code but the ideas instead.

`# find the days between the dates (2013,01,24) and (2013,06,29)#(year1,month1,day1,year2,month2,day2)days = # days in month1(31) - starting day(24) = 7while month1 < month2: days += # days in current month1 month1 += 1 # moving to the next monthdays += day2 # add the remaining days from the second date month(29)# add the years dayswhile year1 < year2: days += # days in year1`

In this case, I don't think we should, there are several cases that this code does not consider, like:

- input dates in same month
`(2013,06,24 and 2013,06,29) # valid input`

- month2 < month1
`(2012,07,24 and 2013,06,29) # valid input`

- year2 < year1
`(2013,01,24 and 2012,06,29) # invalid input`

- accounting for leap years.

Let's think of a more simple mechanical algorithm. As humans, it's very inefficient to manually count day by day, but not for computers, and I want to emphasize optimization, don't optimize prematurely! focus on solving the problem first.

`days = 0while date1 < date2: date1 += # advance to next day days += 1return days`

This is the most simple approach possible, it's adding 1 day until we reach the second date.

I want to take a look at line three.

`date1 += # advance to next day`

This is clearly the most important part, and because of it, we should start by coding a `nextDay(year,month,day)`

procedure. The function should receive a year, month, and day and return the year, month, and day of the next day.

`def nextDay(year, month, day): """ For simplicity, assume every month has 30 days. """ if day < 30: return year, month, day + 1 elif month < 12: return year, month + 1, 1 else: return year + 1, 1 ,1print(nextDay(2021,3,11)) # expected output: 2021,3,12print(nextDay(2021,3,29)) # expected output: 2021,3,30print(nextDay(2021,3,30)) # expected output: 2021,4,1print(nextDay(2021,11,30)) # expected output: 2021,12,1print(nextDay(2021,12,30)) # expected output: 2022,1,1`

As we can see, the code above accounts for the case that it's the end of the month and for the end of the year, the only problem with it it's that it's assuming all months have 30 days, so, what should we do next?

In this case, we can make the `nextDay`

procedure to work with real months but we can actually do that later and start coding the `daysBetweenDates`

procedure first, the advantage is that we'll be more confident if we're on the right track and most importantly, we'll be closer to having an answer, then we can correct the `nextDay`

procedure since that will be a significantly smaller detail that shouldn't affect our `daysBetweenDates`

procedure.

`days = 0while date1 < date2: date1 += # advance to next day days += 1return days`

with our new `nextDay`

procedure we can now code this right? well, not quite, if you have already tried you realized what the problem is. The comparison between dates.

`while date1 < date2:`

Let's understand exactly what the helper function shall do, in this case, we only need to return a boolean, True if the first date is before the second, and False if it is not. Let's code it!.

`def date1BeforeDate2(year1,month1,day1,year2,month2,day2): """ returns True if the first date is before the second date """ if year1 < year2: return True if month1 < month2: return True if day1 < day2: return True return False`

The solution for this helper function was really easy, we only needed to compare if the year1 was before year2, if that condition it's True, simply return True, then repeat the condition with the months and days, if none of the conditions were True simply return False.

Now that we have our helper function, it's time to try to code the pseudocode again with our newly created function.

Pseudocode:

`days = 0while date1 < date2: date1 += # advance to next day days += 1return days`

DaysBetweenDates procedure:

`def daysBetweenDates(year1, month1, day1, year2, month2, day2): """ returns the difference of two dates in days """ days = 0 while date1BeforeDate2(year1, month1, day1, year2, month2, day2): year1, month1, day1 = nextDay(year1, month1, day1) days += 1 return daysprint(daysBetweenDates(2019, 5, 15, 2021, 5, 15)) # output should have been 731print(daysBetweenDates(2023, 5, 15, 2020, 5, 15)) # invalid inputs`

So much progress! however, if you run the code above you will realize that the output is 720 when it should have been 731, why?. Let's not forget that the `nextDay`

procedure is incorrectly assuming all months have 30 days!!. However, it's very clear that we're on the right track. For the second print, we purposely passed invalid inputs to see what happens, in this case, the code output 0, but that is not really what we want.

Like we talked about before, we are going to follow a good defensive programming practice and check for invalid inputs inside our `daysBetweenDates`

procedure.

Python `assert()`

is perfect for what we want. `assert()`

allows us to perform comparisons. If the expression contained within it is False, an exception will be thrown, specifically an `AssertionError`

.

Example:

`assert(5 < 3) # False, an AssertionError will be shownassert(2 < 4) # True, nothing will happen`

This fits perfectly for what we want, that is, checking if the first date is before the second date. Here is my implementation:

`def daysBetweenDates(year1, month1, day1, year2, month2, day2): """ returns the difference of two dates in days """ assert(date1BeforeDate2(year1, month1, day1, year2, month2, day2)) days = 0 while date1BeforeDate2(year1, month1, day1, year2, month2, day2): year1, month1, day1 = nextDay(year1, month1, day1) days += 1 return days`

Now if we test an invalid case where the second date is before the first date we should see an `AssertionError`

pop up in the terminal like the image below:

Right now, we're 70% of the way, some of the things left to do are to make sure that the `daysBetweenDates`

procedure accounts for leap years and months with the correct amount of days.

The way I'm going to approach this is that I'm going to write a `daysInMonth(year,month)`

procedure that returns the correct number of days of the specified month.

`def daysInMonth(year, month): return # the number of days of the specified month`

First things first, I'm going to do some googling to figure out what months have 31 days.

After a quick search, I found that the months with 31 days are months: 01, 03, 05, 07, 08, 10, and 12. With this information we can now add a condition in our code, like this:

`if month in (1, 3, 5, 7, 8, 10, 12): return 31`

Let's repeat the process with the month of February, normally it has 28 days except on leap years where February has 29. For this first stage, we are going to check if the month is the number two and return 28 without accounting for leap years yet.

`def daysInMonth(year, month): """ Function that receives a month and a year and based on that, it will return the number of days the specific month has. WARNING: This code does not account for leap years yet """ if month in (1, 3, 5, 7, 8, 10, 12): return 31 elif month == 2: return 28 else: return 30`

As you can see from the code above, in the case that none of the conditions are true, it will return the normal 30 days.

Let's go back to our `nextDay`

procedure and change it so that it now uses the correct number of days.

Before:

`if day < 30:`

After:

`if day < daysInMonth(year, month):`

Just as we coded our `daysInMonth`

helper procedure, we should also create a procedure to check if a specified year is a leap year, but before we code, we should understand what a leap year is.

## Leap year

a year, occurring once every four years, that has 366 days including February 29 as an intercalary day. - https://en.wikipedia.org/wiki/Leap_year

If you scroll below the Wikipedia article, you will find an algorithm section that is incredibly useful for programmers.

Here is my implementation of the algorithm:

`def isLeapYear(year): """ Function that receives a year and returns a boolean, True if the year is a leap year and False if it's not """ if not (year % 4 == 0): return False elif not (year % 100 == 0): return True elif not (year % 400 == 0): return False else: return True`

After we are done writing our function it's important to test it to make sure it works, in this case, the code is giving the expected output so now it's time to integrate it in our `daysInMonth`

procedure.

For the moment, the `daysInMonth`

procedure does not account for leap years and it's returning 28 days if the month is February, let's change that with our newly created `isLeapYear`

procedure.

`def daysInMonth(year, month): """ Function that receives a month and a year and based on that, it will return the number of days the specific month has, accounting for leap years. """ if month in (1, 3, 5, 7, 8, 10, 12): return 31 elif month == 2: if isLeapYear(year): return 29 else: return 28 else: return 30`

Now, if the month is February, the code will check if the year is a leap year, if that it's true it will return 29 days, and if it's not it will return the normal 28 days. At this point, it looks like we are done, now it's time to test the full code with several examples.

After testing we realized that everything looks to be working except for the case where the dates are the same.`print(daysBetweenDates(2021, 8, 24, 2021, 8, 24))`

If you run this test case you will see an AssertionError when the output should be 0, to fix this I'm going to simply change the assertion line in the `daysBetweenDates`

procedure.

Before:

`assert(date1BeforeDate2(year1, month1, day1, year2, month2, day2))`

After:

`assert not (date1BeforeDate2(year2, month2, day2, year1, month1, day1))`

Now everything looks to be finished, it's time to do final testing to be confident our code it's working as expected, and in this case, it looks like we can consider our code done!.

Full python code:

`def isLeapYear(year): """ Function that receives a year and returns a boolean, True if the year is a leap year and False if it's not """ if not (year % 4 == 0): return False elif not (year % 100 == 0): return True elif not (year % 400 == 0): return False else: return Truedef daysInMonth(year, month): """ Function that receives a month and a year and based on that, it will return the number of days the specific month has, accounting for leap years. """ if month in (1, 3, 5, 7, 8, 10, 12): return 31 elif month == 2: if isLeapYear(year): return 29 else: return 28 else: return 30def nextDay(year, month, day): """ The function receives a year, month, and day and returns the year, month, and day of the next day. """ if day < daysInMonth(year, month): return year, month, day + 1 elif month < 12: return year, month + 1, 1 else: return year + 1, 1, 1def date1BeforeDate2(year1, month1, day1, year2, month2, day2): """ The function receives two dates and returns a boolean, True if the first date is before the second, and False if it is not. """ if year1 < year2: return True if month1 < month2: return True if day1 < day2: return True return Falsedef daysBetweenDates(year1, month1, day1, year2, month2, day2): """ returns the difference between two dates in days accounting for leap years and no time travel. """ assert not (date1BeforeDate2(year2, month2, day2, year1, month1, day1)) days = 0 while date1BeforeDate2(year1, month1, day1, year2, month2, day2): year1, month1, day1 = nextDay(year1, month1, day1) days += 1 return daysprint(daysBetweenDates(2019, 5, 24, 2020, 5, 24))`

You've reached the end of this lesson on problem-solving for programmers, remember that this is a skill that takes a lifetime to master so don't feel frustrated if you don't get it right away because this isn't easy, but I hope that some of the guidelines I've shared today can help you with any problem you might encounter in the future.

That's all for today guys, I really hope this was helpful, and let me know in the comments any thoughts, suggestions and I will see you in the next post, stay tuned!.

]]>Hello everyone, today's post is going to be a bit shorter than usual but it's a very important topic non the less, I want to talk about adding Fonts to Figma.

Okay, so, when I was making a cover design in Figma I came up with the really common pain of not finding the right font for my project, normally you can find LOT's of fonts to try your design with, but I wasn't finding the font I wanted, `Sans Pro Display`

, I was really surprised this font didn't come included in the really big Figma library, and for the first time ever I had to add a font to Figma, in the end, it wasn't complicated but it took me way longer than it should have and that's why I wanted to share this with you.

The first step is going to find your wanted font, in my case, I simply googled "Sans Pro Display free Font Download" and easily found it here: https://www.cufonfonts.com/font/sf-pro-display, make sure you are downloading from a secure website.

Download your font and you should get a zip file, extract the elements from the zip file and you should get a folder with OpenType Font files like this:

Open your Figma project and if you select your desired text and go to ==> Design ==> Text and search for your newly downloaded Font you will realize that it cannot be found in the library.

The reason why you can't find the font it's because it's not installed in your system yet.To install the font you can simply double click the OpenType Font file and a screen like the one shown below should appear then you simply click on the install button.

This step is SUPER IMPORTANT and I really hoped someone would have told me to RESTART Figma, I lost a lot of time trying to figure out why the font was still not appearing and the solution was really simple, simply close the app and open again.

After restarting the app you should be able to select any text element, go to ==> Design ==> Text and search for the name of your font, Figma will likely auto-complete the search, and now we are ready to start using our new font.

In today's post, we learned how to add any font to Figma, I want to mention that this method is for the Desktop version only, and I really hope this post helps anyone that was looking to expand their font library, here it's the cover design I was working on:

Comment below any thoughts, suggestions, and i will see you in the next post.

]]>Hello everyone, in today's post I wanted to show you how to use the google cloud hosting service. using that service, we can host static web pages easily, free, and securely.

Firebase hosting will provide you impressive good speed without the need for separate CDN and it's FREE, easy to use, and provides the SSL certificate so that your website will be hosted securely with HTTPS.

- Google account for Firebase setup in console
- Domain: you can purchase a domain from any provider
- Firebase-CLI (installation process showed later in this post)

Open Firebase Console ==> Sign in with your Google account ==> Create project ==> Add title ==> Enable Google analyitics for your country.

See the images below for guidance.

Simply execute the following command:

`npm install -g firebase-tools`

or you can use:

`yarn global add firebase-tools`

- Create a new folder on your computer
- Open a new CMD(command prompt) and cd into the newly created folder
- Now execute the command:

`firebase login`

This will redirect you to the sign-in page in the browser, where you just need to simply sign in.

After successfully given access, you should get the below message on the browser

On your command prompt, simply execute the following command:

`firebase init`

After executing this command you should see the following screen:

After pressing enter to the question

are you ready to proceed?

you should select the option:

Hosting: Configure files for Firebase Hosting and (optionally) set up GitHub Action deploys

Use arrows to change selection and select using the `spacebar`

, then press `enter`

.

Once the Hosting option is selected, they'll provide the option to choose an existing project or create a new one. As we have already created a sample static project, you need to just choose the existing option and press `enter`

.

After pressing `enter`

you should see your Firebase project, select it, and press `enter`

.Then it will ask you for a name for the public directory, you can put the same name as your project, and after pressing `enter`

the following question should appear

If your page is a single page like mine you should answer yes.

After we are done with the configuration we need to run the following command:

`firebase serve`

If we followed all of the steps correctly, in the folder created at the beginning we will find another folder and inside an index.html file that will be shown on `localhost:5000`

After watching the screen above without showing problems then we can exit with `Ctrl+C`

We're almost done!, now it's time to delete the index.html file and replace it with our wanted index.html, index.css, images, and icon files.

Back at the CMD terminal we can repeat step 5 and now we should see our website on `localhost:5000`

, it's likely that we see the old file instead of the new one so to fix that make sure to refresh the browser with `Ctrl+Shift+R`

After making sure everything works properly on `localhost:5000`

, we can now exit and proceed with the deploy

`firebase deploy`

As we can see from the image above, after the deploy it's complete, we should get a Hosting URL, if we paste that into our web browser we should see our website.

We are Done!

In the likely case that you don't want your website URL to look like this: https://sample-9f6cf.web.app/ and want to use a custom domain instead, we can do just that from the Project dashboard, just follow the images below.

In my case, I already had a domain from https://domains.google/ and after following the three steps from the image above we should get a register type, host, and value (image below).

Now we need to go to Google Domains ==> Select your domain ==>DNS ==> Manage custom records ==> Create new record and enter the values there ==> Save.See the images below for guidance.

Now we should wait some time, normally it's a few minutes, and after that, we are done hosting our website with firebase!.

I really hope this post has taught you how to host your website with Firebase and if you have any suggestions or ideas, let me know in the comments below, see you in the next post!.

]]>`PCA9685`

driver and thought I shared my experience with anyone interested in this topic.- Raspberry Pi (mine is the 3B+)
- PCA9685 driver
- Servos (the driver supports up to 16)
- 4 female to female jumper cables.
- External 5v power for the driver
- Python3

5v-6v recommended power and you can connect as many servos as you like

Turn on your Raspberry Pi and open a new terminal, then run the following commands:

`sudo apt-get install git build-essential python-devgit clone https://github.com/adafruit/Adafruit_Python_PCA9685.gitcd Adafruit_Python_PCA9685sudo python setup.py installcd examples`

Inside the examples folder, you should find the `simplest.py`

example code, to run it use the command

`python3 simplest.py`

However, if we execute this program we get an error

No such file or directory: '/dev/i2c-1'

To fix this we need to open the raspberry pi software configuration

`sudo raspi-config`

Use the down arrow to select interfacing options and press enter, then select P5 I2C and enable the interface ==> ok ==> finish.

Now, if we execute the `simplest.py`

file we shouldn't get any errors and our servo should start moving, Nice!, however the code it's unnecessarily complex for what I need. I would like to call a function that passes the target degree as a parameter and nothing else, this way the code would be easier to read and with fewer bugs, to accomplish this I'm going to use the Adafruit Servo Kit library for python

https://github.com/adafruit/Adafruit_CircuitPython_ServoKit

To use this library, open a new terminal on your Raspberry Pi and execute the following command:`pip3 install adafruit-circuitpython-servokit`

Here we can see a full example:

`from time import sleepfrom adafruit_servokit import ServoKit# Set channels to the number of servo channels on your kit.# 8 for FeatherWing, 16 for Shield/HAT/Bonnet.kit = ServoKit(channels=8)kit.servo[0].angle = 175sleep(1)kit.servo[0].angle = 45sleep(1)`

We can specify the servo in the`kit.servo[0-15].`

I really hope this post helped you out and feel free to share this with someone that might need it, see you next time!.

]]>