Dynamic Programming in 5 easy steps - Examples - Two Robots
This time we will solve a HackerRank problem, rated as a medium in difficulty. It’s advised for you to go through a similar, but in my opinion easier problem described by me previously.
The problem statement is as follows:
You have a warehouse with M containers filled with an infinite number of candies. The containers are arranged in a single row, equally spaced to be 1 meter apart. You also have 2 robots that can pick up 1 piece of candy and transport it between any two containers.
The robots take instructions in the form of queries consisting of two integers, M_{a} and M_{b}, respectively. To execute a query, a robot travels to container M_{a}, picks up 1 candy, transports it to container M_{b}, and then stops at M_{b} until it receives another query.
Calculate the minimum total distance the robots must travel to execute N queries in order.
Note: You choose which robot executes each query.
Reminder - the 5 easy steps are:
define subproblems, count number of subproblems
guess (part of solution), count number of choices
relate subproblem solutions, compute time per subproblem
recurse + memoize OR build DP table bottom-up check subproblems acyclic/topological order, time = time per subproblem · number of subproblems
solve original problem: = a subproblem OR by combining subproblem solutions ⇒ extra time
Step 1. Let’s try to figure out what our subproblems are. At a given point in solving the problem, what information do we want to have? First obvious thing: index of queries. Next obvious thing: we want to optimize over total distance made by robots, therefore our DP function will return the minimum total distance made by robots at some point in our problem. Having that, we also probably will need positions of two robots. The situation looks like this:
Two robots sketch.
Step 2. Now, we basically have two choices, A: proceed with the first robot or B: with the second robot. When A - we add the distance of moving the r1 from it’s last place M_{b(r1)} to M_{a(INDEX)}, otherwise B: we add the distance of moving the r2 from it’s last place M_{b(r2)} to M_{a(INDEX)}. Additionally, in both cases we have to add the cost of moving M_{a(INDEX) }to M_{b(INDEX).} Step 3. Writing the DP function is now straightforward:
DP(r1, r2, index) = min from: A: M_{a(INDEX) - }M_{b(r1) }+ M_{b(INDEX) }- M_{a(INDEX) }+ DP(INDEX, r2) B: M_{a(INDEX) - }M_{b(r2) }+ M_{b(INDEX) }- M_{a(INDEX) }+ DP(r1, INDEX) However, having the index in DP function is redundant. We can decrease the time complexity of our solution by removing it - index is always the next item after max(r1, r2). Thus, DP function now looks like:
The number of subproblems equals to M*M, because that’s how many states which in the robots can be arranged. Time of computing one subproblem is constant, therefore the final complexity of this solution is O(M^{2}). Step 4. So is it finite algorithm? Yes, because the graph is traversed through two increasing values, r1 and r2. Step 5. The solution is in DP(-1, -1), assuming -1 denotes that the robot wasn’t used yet and the cost of moving it to M_{x} is 0 (because it starts at this location).
using System;
using System.Collections.Generic;
namespace Algos
{
public class Solver
{
private readonly int _n;
private readonly List<Tuple<int, int>> _queries;
private int[][] _memo;
// ReSharper disable once UnusedParameter.Local
public Solver(int m, int n, List<Tuple<int, int>> queries)
{
_n = n;
_queries = queries;
}
public int SolveCase()
{
_memo = new int[_n + 1][];
for (int i = 0; i < _n + 1; i++)
_memo[i] = new int[_n + 1];
for (int i = _n; i >= 0; i--)
{
for (int j = _n; j >= 0; j--)
{
_memo[i][j] = -1;
}
}
return Dp(-1, -1);
}
private int Dp(int r1, int r2)
{
if (r1 + 1 == _n || r2 + 1 == _n)
return 0;
if (_memo[r1 + 1][r2 + 1] != -1)
return _memo[r1 + 1][r2 + 1];
var i = Math.Max(r1, r2) + 1;
//r1 stays in place
var r2Cover = 0;
if (r2 != -1)
r2Cover = Math.Abs(_queries[r2].Item2 - _queries[i].Item1);
var d1 = Dp(r1, i);
//r2 stays in place
var r1Cover = 0;
if (r1 != -1)
r1Cover = Math.Abs(_queries[r1].Item2 - _queries[i].Item1);
var d2 = Dp(i, r2);
var queryCover = Math.Abs(_queries[i].Item1 - _queries[i].Item2);
var min = Math.Min(r2Cover + d1 + queryCover, r1Cover + d2 + queryCover);
_memo[r1 + 1][r2 + 1] = min;
return min;
}
}
class Solution
{
static void Main(string[] args)
{
//CaseSub0();
//Case0();
//Case1();
//Case2();
//RandomBigCase();
//return;
var T = int.Parse(Console.ReadLine());
for (var i = 0; i < T; i++)
{
ProcessCase();
}
}
private static void ProcessCase()
{
var @case = Console.ReadLine().Split(new[] { " " }, StringSplitOptions.RemoveEmptyEntries);
var M = int.Parse(@case[0]);
var N = int.Parse(@case[1]);
var queries = new List<Tuple<int, int>>(N);
for (int i = 0; i < N; i++)
{
var query = Console.ReadLine().Split(new[] { " " }, StringSplitOptions.RemoveEmptyEntries);
queries.Add(Tuple.Create(int.Parse(query[0]), int.Parse(query[1])));
}
var solution = new Solver(M, N, queries).SolveCase();
Console.WriteLine(solution);
}
private static void CaseSub0()
{
var M = 2;
var N = 1;
var queries = new List<Tuple<int, int>> ();
queries.Add(Tuple.Create(1, 2));
var solution = new Solver(M, N, queries).SolveCase();
Console.WriteLine("Casesub0 " + solution);
}
private static void Case0()
{
var M = 5;
var N = 4;
var queries = new List<Tuple<int, int>>();
queries.Add(Tuple.Create(1, 5));
queries.Add(Tuple.Create(3, 2));
queries.Add(Tuple.Create(4, 1));
queries.Add(Tuple.Create(2, 4));
var solution = new Solver(M, N, queries).SolveCase();
Console.WriteLine("Case0 " + solution);
}
private static void Case1()
{
var M = 4;
var N = 2;
var queries = new List<Tuple<int, int>> ();
queries.Add(Tuple.Create(1, 2));
queries.Add(Tuple.Create(4, 3));
var solution = new Solver(M, N, queries).SolveCase();
Console.WriteLine("Case1 " + solution);
}
private static void Case2()
{
var M = 10;
var N = 3;
var queries = new List<Tuple<int, int>> ();
queries.Add(Tuple.Create(2, 4));
queries.Add(Tuple.Create(5, 4));
queries.Add(Tuple.Create(9, 8));
var solution = new Solver(M, N, queries).SolveCase();
Console.WriteLine("Case2 " + solution);
}
private static void RandomBigCase()
{
var M = 1000;
var N = 1000;
var queries = new List<Tuple<int, int>> ();
var r = new Random(666);
while (queries.Count != N)
{
var from = r.Next(1, M + 1);
var to = r.Next(1, M + 1);
var t = Tuple.Create(from, to);
if (!queries.Contains(t))
queries.Add(t);
}
var solution = new Solver(M, N, queries).SolveCase();
Console.WriteLine("RandomBigCase " + solution);
}
}
}
Bob is a very successful guy. He is auto scaling his service by automatically adding hosts when the CPU increases, and he is removing them when CPU goes down. Dear Bob, there is a trap waiting for you around the corner.
Monitoring services is crucial, if you care about the application uptime. There are hundreds if not thousands parameters which you can (and should) monitor, related to CPU, network, hosts, application and so on. What are they? What are the non-obvious choices?
It seems that most people know the importance of software design patterns, best practices or continuous integration. While those subjects are important, there is one more equally essential term, which yields only one relevant result link on the first Google page. Meet Operational Excellence.