Here is Part 13 of a long series on Advent Of Code. You can find the previous part here.
For this new post, we are going to solve the problem from the 13th December 2015, named "Knights of the Dinner Table". I will propose the solution in C++, but the reasoning can be applied to other languages.
Part 1
The Problem
The full version of this problem can be found directly on the Advent of Code website, I will only describe the essence of the problem here:
For the holiday feast, you need to make the table arrangement, and decide to go for the one which will be optimal to avoid awkward conversations. For that, you have a list of everyone invited and the amount their happiness would increase or decrease if they were to find themselves sitting next to each other person. You also have a circular table that will be just big enough to fit everyone comfortably, and so each person will have exactly two neighbors.
For example, with the list below :
Alice would gain 54 happiness units by sitting next to Bob.
Alice would lose 79 happiness units by sitting next to Carol.
Alice would lose 2 happiness units by sitting next to David.
Bob would gain 83 happiness units by sitting next to Alice.
Bob would lose 7 happiness units by sitting next to Carol.
Bob would lose 63 happiness units by sitting next to David.
Carol would lose 62 happiness units by sitting next to Alice.
Carol would gain 60 happiness units by sitting next to Bob.
Carol would gain 55 happiness units by sitting next to David.
David would gain 46 happiness units by sitting next to Alice.
David would lose 7 happiness units by sitting next to Bob.
David would gain 41 happiness units by sitting next to Carol.
You should find that the arrangement below has the optimal change in happiness equal to 330
:
+41 +46
+55 David -2
Carol Alice
+60 Bob +54
-7 +83
And we need to do that for a much bigger table… Let’s go !
Solution
We are going to split the solution in two parts: the extraction of the useful data in the attendees list, and the algorithm to find the optimal change in happiness. Without further ado, let’s go to the first part.
Data Extraction
Before solving anything, we need to extract the useful data from the list of attendees. So first let’s identify them! In the statement below, there is a lot of useful information:
Alice would gain 54 happiness units by sitting next to Bob.
There is Alice, the person who is the main subject of this relationship, and Bob, the neighbor who will impact Alice's happiness. The other information is obviously the amount of happiness 54 and also, the keyword gain which indicates an increase in happiness. If it wasn’t a gain, but a lose keyword, then, it would be a decrease of happiness.
That is all the information that one line can give us. In fact, the other information needed will be the list of all the attendees. And now that we know what we need, let’s write the code for it.
First, we can extract the happiness, only by getting the first number in the instruction
using Happiness = int;
Happiness extractHappinessFrom (const std::string& instruction)
{
std::regex word_regex("[0-9]+");
auto words_begin = std::sregex_iterator(std::begin(instruction),
std::end(instruction), word_regex);
auto words_end = std::sregex_iterator();
for (std::sregex_iterator i = words_begin; i != words_end; ++i)
{
std::smatch match = *i;
return static_cast<Happiness>(atoi(match.str().c_str()));
}
assert(false);
return 0;
}
Then, we can extract a relationship between the two persons by extracting the happiness and getting the first and the last words of the relations list:
std::tuple<PersonName, PersonName, Happiness> extractInformationFrom
(const std::string& instruction)
{
const auto firstSpaceCharacter = std::find_if(std::begin(instruction),
std::end(instruction), [](const auto& c){ return c == ' '; });
const auto lastSpaceCharacter = std::find_if(std::rbegin(instruction),
std::rend(instruction), [](const auto& c){ return c == ' '; });
PersonName fromPersonName (std::begin(instruction), firstSpaceCharacter);
PersonName toPersonName (lastSpaceCharacter.base()++, std::rbegin(instruction).base());
toPersonName.pop_back();
auto happiness = extractHappinessFrom (std::string(instruction));
if(instruction.find("lose") != std::string_view::npos)
{
happiness *= -1;
}
return {fromPersonName, toPersonName, happiness};
}
During this step, we can also know if this relation means an increase or a decrease of happiness.
So now, we have all our data ! \o/
All we need to do it, is store them in some Relationship
object. Moreover, we can also find the list of attendees by storing their names like:
std::vector<Person> persons;
foreachLineIn(fileContent, [&persons](const std::string& line)
{
const auto elements = extractInformationFrom (line);
persons.emplace_back(std::get<0>(elements));
});
auto last = std::unique(persons.begin(), persons.end());
persons.erase(last, persons.end());
So now that we have all our data, we can go to the algorithm part!
The Algorithm
First of all, let’s describe what this algorithm must do. It must be able to find all the combination of attendees around the table, calculate the total amount of happiness on a table, and find the optimal happiness.
There may be other solutions to find all the possible combination of attendees around the table, but we will go with the recursion ! And it looks like that:
using Happiness = int;
using PersonsRefs = std::vector<std::reference_wrapper<Person>>;
using Relationships = std::vector<Relationship>;
Happiness gerateTablePosition (PersonsRefs& personsPlaced,
PersonsRefs& personsToPlace, const Relationships& relationships)
{
if(personsToPlace.empty())
{
return calculateHappiness(personsPlaced, relationships);
}
Happiness optimalHappiness{0};
for(const auto& personToPlace : personsToPlace)
{
auto newPersonsPlaced = personsPlaced;
auto newPersonsToPlace = personsToPlace;
newPersonsPlaced.emplace_back(personToPlace);
newPersonsToPlace.erase(
std::remove_if(
std::begin(newPersonsToPlace),
std::end(newPersonsToPlace),
[&personToPlace](const auto& person)
{
return personToPlace.get() == person.get();
}),
std::end(newPersonsToPlace));
const auto optimalHappinessSoFar = gerateTablePosition
(newPersonsPlaced, newPersonsToPlace, relationships);
optimalHappiness = std::max(optimalHappiness, optimalHappinessSoFar);
}
return optimalHappiness;
}
Don’t worry, I am going to explain that chuck of code.
This function takes several parameters: the list of persons we have already placed, the list of persons we yet have to place, and the list of relationships (only necessary to calculate the amount of happiness). And there is more! The vector containing the list of persons we have already placed, also represents the places of the person on the table!
Indeed, each person placed in this vector has as neighbors the persons placed before and after them, at the index minus one, and plus one. And for the persons placed at the edges of the vector, we loop back of the other edge.
Note that I could have used a Ring Buffer to represent this instead of a vector, but I have only thought about it now. I’ll probably update it later.
Now, let’s go through this algorithm.
First, we check if we have placed everybody. If this is the case, it’s good! We can calculate the amount of happiness of the table.
If this is not the case, then, we have to place the other persons. To do that, for each person we haven’t placed on the table, we created a new table with them in it and rerun the function again. So if it’s the last person to place, the next call will calculate the happiness of the table, if not, it will place every other person left and run this function again until the table is full. And for each person placed at the table, we check if the amount of happiness found is the greatest we had so far, and store it, if so.
Once we have finished iterating, we return the optimal amount of happiness found.
We have two other things to do: call the recursive function, and calculate the amount of happiness of a table.
Happiness getMostOptimalHappiness
(std::vector<Person>& persons, const std::vector<Relationship>& relationships)
{
std::vector<std::reference_wrapper<Person>> personsToPlaceRefs, personsPlacedRefs;
std::copy(persons.begin(), persons.end(), std::back_inserter(personsToPlaceRefs));
personsToPlaceRefs.pop_back();
personsPlacedRefs.emplace_back(persons.back());
return gerateTablePosition (personsPlacedRefs, personsToPlaceRefs, relationships);
}
Here is the function calling the recursive function. All it does, is to create the persons to place and place the first person on the table.
So let’s go to the most interesting part, the calculation of the happiness amount:
using Happiness = int;
using PersonsRefs = std::vector<std::reference_wrapper<Person>>;
Happiness calculateHappiness(const PersonsRefs& personsPlaced,
const std::vector<Relationship>& relationships)
{
Happiness happiness{0};
for(auto index = size_t{0}; index < personsPlaced.size(); ++index)
{
const auto neighborLeftIndex = (index == 0) ? (personsPlaced.size() - 1) : (index - 1);
const auto neighborRightIndex = (index == (personsPlaced.size() - 1)) ? 0 : (index + 1);
const auto leftRelationShip = std::find_if(std::begin(relationships),
std::end(relationships),
[&personsPlaced, neighborLeftIndex, index](const auto& relationship)
{
return relationship.isRelationBetween(personsPlaced[index].get(),
personsPlaced[neighborLeftIndex].get());
});
happiness += leftRelationShip->getHappiness();
const auto rightRelationShip = std::find_if(std::begin(relationships),
std::end(relationships),
[&personsPlaced, neighborRightIndex, index](const auto& relationship)
{
return relationship.isRelationBetween(personsPlaced[index],
personsPlaced[neighborRightIndex]);
});
happiness += rightRelationShip->getHappiness();
}
return happiness;
}
Now, all the persons have a seat at the table, we can calculate the amount of happiness of the table. To do so, we calculate the amount of happiness for each person. And the amount of happiness of each person is described by their relation to their neighbors which is at an index 1 on either side. We only have to be careful for the persons at the edge of the vector, to make sure we don’t go out of bounds of the vector.
And voilà! We have an algorithm which is able to find the optimal amount of happiness on any circular table.
Part 2
Problem
We forget to add ourselves in the seating arrangement.
So we need to do it again, with ourselves at the table, knowing that we will add 0 amount of happiness no matter where we are seated at the table.
Solution
There are several solutions for this problem, and I went with the laziest one, by not modifying the code, but only the input. Indeed, by adding a person named Myself in the input file and a relation to each other persons at the table, I only add to run the program again to find the solution. This was a quick solution since there weren't a lot of persons. If that wasn’t the case, I would have modified the code to add those relationships in the code itself.
Conclusion
You can note that the solutions, written in this post, don’t include all the sources to make running programs, but only the interesting part of the sources to solve this problem. If you want to see the programs from end to end, you can go on my GitHub account, explore the full solution, add comments or ask questions if you want to, on the platform you read this article, it will also help me improve the quality of my articles.
Here is the list of elements that we have used, I can’t encourage you enough to look at their definitions:
Thanks for reading, hope you liked it!
And until next part, have fun learning and growing.