Day 13 - Taking a seat

Advent of Code 2015

Adrien Foucart

2026-04-21

[Back to index]

Puzzle: https://adventofcode.com/2015/day/13

My solution: https://codeberg.org/adfoucart/aocode15/src/branch/main/src/day13.c

For most of the puzzles so far, I’ve focused more on the “learning how to C” side of things and less on the puzzle-solving part, but this may need to change here. This looks very similar to day 9: we have a bunch of nodes (this time, people sitting at a table), and connections between those node have a certain cost (the “happiness gains” or “losses”). We have to find the best seating arrangement, a.k.a. the circular graph with the most total happiness. The input looks like this:

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.

Thinking time

My first thought is that the “two-ways” definition of the gains and losses doesn’t really matter here. We can reduce those statements to net gains or losses per pair, as if Alice sits next to Bob, Bob necessarily sits next to Alice. So the first step would be to go from the above to:

Alice-Bob: +137
Alice-Carol: -141
Alice-David: +44
Bob-Carol: +53
Bob-David: -70
Carol-David: +96

Since the graph is circular, we can place any person as the “first” seat. Let’s put Alice there. Now how do we choose where to put the others?

        Alice
    /           \
   
    ?           ?
   
    \           /
          ?

If I put Bob and Carol, I have so far a net happiness of -4. If I put Bob and David, +181. Carol and David, -97. So Bob and David seem better:

        Alice
    /           \
   
   Bob          David
   
    \           /
          ?

That leaves us with Carol at the end, meaning that we add a Bob-Carol and a David-Carol connection, worth +53 and +96.

        Alice
    /           \
   
   Bob          David
   
    \           /
        Carol

That’s the optimal seating for this example, but I don’t think the reasoning I used here is good enough to guarantee the result when we have more guests. In the real puzzle input, I have eight guests.

How many distinct arrangements can I have with eight guests?

There are 8! permutations, but many of those are equals: all “rotations” (ABCDEFGH is the same as BCDEFGHA, etc.) and all “inversions” (ABCDEFGH is the same as HGFEDCBA). So if I’m not mistaken, I should have 7!/2 real permutations: all permutations with A as an arbitrary start, divided by 2 to remove the reverse (even when we start with A: ABCDEFGH is the same as AHGFEDCB). 7! is just 5040, so that would mean 2520 things to test. That’s not bad, as long as we can properly generate those distinct solutions to test.

Coding time: parsing

I’ll first need to parse the input. From previous puzzles, I have a function freadall to read the whole file, and a function parse_strings to split a string into a str_list_t, which is a simple table of strings, using a separating character. Usually \n, which it will be here. Then I need to find the pairs, the happiness gain/loss, and to put both directions of the pairings together (Alice-Bob with Bob-Alice). I don’t really need the full names, I can use the first letter (that holds in the true input), so I actually need the first character of each line and the first character after the last space. As for the number, it’s the integer value of what’s between the third and fourth spaces, with a minus sign if the letter before the third space is e. That should do it, the rest of the string may be ignored.

How to best store those values? I think a graph-like structure, similar to day 9, would be suitable. From each node, we can store the “bidirectional” happiness factor with all potential neighbours. Finding the best arrangement would then become almost identical, I think, to finding the best way through all the nodes. The only difference is that we also have to take into account the last node wrapping into the first. And that we are searching for maximum happiness instead of minimum distance, of course.

Let’s start with the parsing. I think we can use strchr and strrchr to find the spaces that we need. I’m not sure yet exactly what the parsing function will return, so we’ll first just print things out to check that we retrieve the right info, starting with the “node names”.

void parse_input(char* fname){
    char* content = freadall(fname);
    str_list_t* list = parse_strings(content, '\n');

    for (int i = 0; i < list->n; i++){
        char* s = list->str_list[i];
        if (strlen(s) == 0) continue;
        char from = s[0];
        char* last_space = strrchr(s, ' ');
        char to = *(last_space+1);
        printf("%c -> %c\n", from, to);
    }
}

This gives me, on the test input:

A -> B
A -> C
A -> D
B -> A
B -> C
B -> D
C -> A
C -> B
C -> D
D -> A
D -> B
D -> C

Good. Now the happiness gain/lost. I want to find the third and fourth space, copy what’s in between to a new null-terminated string, and use atoi to convert it to an integer, checking if the word before ends with an e while we’re there to flip the sign if necessary:

int find_happiness_score(char* s){
    // find third and fourth space
    char *third = s;
    char *fourth;
    for (int i=0; i < 3; i++){
        third = strchr(third, ' ')+1;
    }
    fourth = strchr(third, ' ');

    // copy what's in between, with added null character
    char i_string[fourth-third+1];
    memcpy(i_string, third, fourth-third);
    i_string[fourth-third] = '\0';
    
    // convert to int
    int i = atoi(i_string);

    // check sign
    if (*(third-2) == 'e') i *= -1;
    
    return i;
}

And change parse_input:

// ...
        int h = find_happiness_score(s);
        printf("%c -> %c [%d]\n", from, to, h);
// ...

So that now we have:

A -> B [54]
A -> C [-79]
A -> D [-2]
B -> A [83]
B -> C [-7]
B -> D [-63]
C -> A [-62]
C -> B [60]
C -> D [55]
D -> A [46]
D -> B [-7]
D -> C [41]

Which seems correct. Obviously I’m just assuming here that the strings are always well formatted as in the example, but in a puzzle context I think that’s fair.

Copy/pasting time: nodes

Let’s grab all the nodes and connections stuff from day 9. The only thing I’ve changed is that connection_t will have a score instead of a dist, and that the node names will be chars instead of char*s.

typedef struct connection_t connection_t;
typedef struct node_t node_t;

struct connection_t {
    node_t* dest;
    int score;
};

connection_t* connection_create(node_t* dest, int score){
    connection_t* connection = malloc(sizeof(connection_t));
    connection->dest = dest;
    connection->score = score;
}

void connection_destroy(connection_t* connection){
    free(connection);
    // don't delete node!
}

struct node_t {
    char name;
    connection_t** connections;
    int n_connections;
    int capacity;
};

node_t* node_create(char name){
    node_t* node = malloc(sizeof(node_t));
    node->name = name;
    node->capacity = 8;
    node->n_connections = 0;
    node->connections = malloc(sizeof(connection_t*)*node->capacity);
    return node;
}

void node_destroy(node_t* node){
    for (int i=0; i < node->n_connections; i++)
        connection_destroy(node->connections[i]);
    free(node->connections);
    free(node);
}

void node_add_connection(node_t* node, connection_t* conn){
    if (node->n_connections == node->capacity){
        node->capacity *= 2;
        node->connections = realloc(node->connections, sizeof(connection_t*)*node->capacity);
    }

    node->connections[node->n_connections] = conn;
    node->n_connections++;
}

typedef struct {
    node_t** nodes;
    int n_nodes;
    int capacity;
} list_node_t;

list_node_t* list_node_create(int initial_capacity){
    list_node_t* list = malloc(sizeof(list_node_t));
    list->capacity = initial_capacity;
    list->n_nodes = 0;
    list->nodes = malloc(sizeof(node_t*)*initial_capacity);
}

void list_node_destroy(list_node_t* list){
    free(list->nodes); // don't free the nodes?
    free(list);
}

void list_node_add(list_node_t* list, node_t* node){
    if (list->n_nodes == list->capacity){
        list->capacity *= 2;
        list->nodes = realloc(list->nodes, sizeof(node_t*)*list->capacity);
    }

    list->nodes[list->n_nodes] = node;
    list->n_nodes++;
}

node_t* list_node_find(list_node_t* list, char name){
    for (int i = 0; i < list->n_nodes; i++){
        if (list->nodes[i]->name == name)
            return list->nodes[i];
    }
    return NULL;
}

void list_node_print(list_node_t* list){
    for (int i=0; i < list->n_nodes; i++){
        node_t* node = list->nodes[i];
        printf("%c:\n", node->name);
        for (int j=0; j < node->n_connections; j++){
            printf("\t-> %c (%d)\n", 
                node->connections[j]->dest->name, 
                node->connections[j]->score);
        }
    }
}

Coding time: making the graph

Modifying parse_input to start building the graph as we encounter node. Every time, we check if it was already there, and we create the connections.

void parse_input(char* fname){
    char* content = freadall(fname);
    str_list_t* list = parse_strings(content, '\n');
    list_node_t* nodes = list_node_create(ceil(sqrt(list->n)));

    for (int i = 0; i < list->n; i++){
        char* s = list->str_list[i];
        if (strlen(s) == 0) continue;
        char from = s[0];
        char* last_space = strrchr(s, ' ');
        char to = *(last_space+1);
        int h = find_happiness_score(s);

        node_t* node_from = list_node_find(nodes, from);
        if (node_from == NULL){
            node_from = node_create(from);
            list_node_add(nodes, node_from);
        }
        node_t* node_to = list_node_find(nodes, to);
        if (node_to == NULL){
            node_to = node_create(to);
            list_node_add(nodes, node_to);
        }
        node_add_connection(node_from, connection_create(node_to, h));
        node_add_connection(node_to, connection_create(node_from, h));
        // printf("%c -> %c [%d]\n", from, to, h);
    }
    list_node_print(nodes);
}

When we print the graph, we get:

A:
        -> B (54)
        -> C (-79)
        -> D (-2)
        -> B (83)
        -> C (-62)
        -> D (46)
// ...

I forgot that if a connection to the same node was already there (added from the other direction), we should add the score and not put it again. An alternative would be to discard the “simplification” I had in mind and only keep track of unidirectional scores. Computing the total score of a link would still be reasonably easy, but I think I still prefer the bidirectional idea. I think it will make the hardest part of the code easier. I may, of course, be very wrong about that.

I think the best option is to change the add_connection code to only create a new connection if necessary.

void node_add_connection(node_t* node, node_t* dest, int score){
    // check if there is already a connection to that node
    connection_t* existing = NULL;
    for (int i = 0; i < node->n_connections; i++){
        if (node->connections[i]->dest->name == dest->name){
            existing = node->connections[i];
            // update existing connection
            existing->score += score;
            return;
        }
    }
    // otherwise, create new connection
    if (node->n_connections == node->capacity){
        node->capacity *= 2;
        node->connections = realloc(node->connections, sizeof(connection_t*)*node->capacity);
    }

    node->connections[node->n_connections] = connection_create(dest, score);
    node->n_connections++;
}

Which changes the parse_input only slightly:

// ...
        node_add_connection(node_from, node_to, h);
        node_add_connection(node_to, node_from, h);
// ...

And now my graph is correct:

A:
        -> B (137)
        -> C (-141)
        -> D (44)
B:
        -> A (137)
        -> C (53)
        -> D (-70)
C:
        -> A (-141)
        -> B (53)
        -> D (96)
D:
        -> A (44)
        -> B (-70)
        -> C (96)

Seems like a good place for a break. It’s lunchtime anyway.