Skip to main content

Challenge #11: Taco crawl optimization [COMPLETED]

Created
Active
Viewed 5k times
55 entries
62

Update on October 23: Huge thanks to everyone who participated in the challenge. The correct routing was [0,7,5,6,2,10,4] for 97 tastiness and 2.8945 tastiness/distance.

43 of the 53 entries provided the correct response (81%). The most common errors were limiting to exactly 8 taco spots or using an algorithm that missed the most optimal route.

Some participants took delight in their inefficient algorithms. Perhaps one day, we'll have a competition for that (although I guess there is no upper bound for inefficiency!)

Congrats to the following users for the correct response:

huseyin tugrul buyukisik, André, LittleBit, Ali Sheikhpour, Serpent7776, Istvan Dupai, ValNik, Abdul Wahab, Nyps, Dr Pelforth, wenbang, Markus Jarderot, Alen Joseph, John Bollinger, Peter Dongan, mpx, Lior, Smylic, Blindleistung, BillBokeey, Rubén Vega, undefined, Jorne Schuurmans, Dmitry, Trashman, gee3107, sceaj, DarthGizka, ssd, ChrisoLosoph, Vincent C., amance, Gary, Grismar, Neil Butcher, YvesScherdin, Emi OB, Mircea, marcguery, Peter Draganov, Andrey Dmitriev, Raghav Mohta, Phil

Update on October 8: The original taco spot "tastiness" values were incorrect. Sorry about that! The correct values are now reflected in the table.

Yesterday it was National Taco Day 🌮 in the United States. In honor of the holiday, we have a taco themed challenge.

Tacos are snack-sized and come in many variations. This makes them a great food for a "crawl", an event in which people would try to visit multiple taco spots.

Taco Crawl image

The Challenge

Given a list of taco spots, determine the most optimal path for the taco crawl.

Important details:

  • There are 10 taco spots, each with a location on a two dimensional space, i.e. (x, y) coordinates.

  • You can only visit up to 8 of them (hunger isn't infinite).

  • Each spot has a "tastiness" score - some of the locations have better tacos than others.

  • The optimal path would return the highest total tastiness per distance traveled.

    • Sum up the tastiness of the visited taco spots (up to 8 spots)

    • Sum up the travel distance. Travel distance is the straight-line distance, calculated by the Euclidean distance formula. For example, to travel from (0,0) to (3,3) the distance is sqrt(32+32)=4.24

    • Divide by the total tastiness by distance traveled to reach those spots. This is the metric we want to optimize for.

  • You start at (0,0) and finish at the last spot. No need to return back to the start.

  • You can only visit each taco spot once

Taco Spot Coordinates Tastiness
1 (2, 8) 7
2 (15, 3) 17
3 (6, 6) 8
4 (20, 15) 30
5 (11, 2) 12
6 (14, 1) 15
7 (7, 3) 5
8 (1, 12) 12
9 (3, 6) 3
10 (14, 13) 18

This is a pass/fail challenge. Everyone who determines the optimal path (and shows their work), will be considered a challenge winner.

At the scale of this challenge, a brute force approach could probably work. That's valid. Other approaches are welcome also.

Key information

  • You have two weeks from the date this challenge is posted to submit your entry.

    • October 8: Challenge goes live

    • October 23: Challenge is complete and we'll announce winners soon after.

  • Your submission should include:

    • The optimal routing you determined to visit up to 8 taco spots, e.g. [3, 2, 1 , 7, 9, 4, 6]

    • The total tastiness achieved

    • The total distance traveled

    • The "tastiness per distance" metric (this is what we want to optimize for)

    • The code you used to solve this challenge

    • [Optional] Anything you learned or any interesting challenges you faced while coding

    Your entry is not permitted to be written by AI. For any feedback on this Challenge, please head over to the Meta post.

55 entries
Sorted by:
79833553
0
#include <iostream>
#include <algorithm>
#include <vector>
#include <array>
#include <span>
#include <optional>
#include <immintrin.h>
#include <emmintrin.h>
#include <climits>

using B = std::byte;
using Arr = std::array<B, 10>;

std::ostream& operator<<(std::ostream& os, const B& b) {
    os << (int)b;
    return os;
}

template <typename T, std::size_t N>
std::ostream& operator<<(std::ostream& os, const std::array<T, N>& arr) {
    os << "[";
    for (std::size_t i = 0; i < N; ++i) {
        os << arr[i];
        if (i != N - 1) {
            os << ", ";
        }
    }
    os << "]";
    return os;
}

template <typename T>
std::ostream& operator<<(std::ostream& os, const std::span<T>& arr) {
    os << "[";
    for (std::size_t i = 0; i < arr.size(); ++i) {
        os << arr[i];
        if (i != arr.size() - 1) {
            os << ", ";
        }
    }
    os << "]";
    return os;
}

// Only sums least significant 8 values
static inline __attribute__((always_inline))
__m128i plus_scan_epi8(__m128i v0)
{
    __m128i shift = _mm_set_epi8(-1, -1, -1, -1, -1, -1, -1, -1, 6, 5, 4, 3, 2, 1, 0, -1);
    __m128i v1 = _mm_shuffle_epi8(v0, shift);
    __m128i v2 = _mm_shuffle_epi8(v1, shift);
    __m128i v3 = _mm_shuffle_epi8(v2, shift);
    __m128i v4 = _mm_shuffle_epi8(v3, shift);
    __m128i v5 = _mm_shuffle_epi8(v4, shift);
    __m128i v6 = _mm_shuffle_epi8(v5, shift);
    __m128i v7 = _mm_shuffle_epi8(v6, shift);
    return v0 + v1 + v2 + v3 + v4 + v5 + v6 + v7;
}

static inline __attribute__((always_inline))
__m128i plus_scan_epi16(__m128i v0)
{
    __m128i shift = _mm_set_epi8(13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0, -1, -1);
    __m128i v1 = _mm_shuffle_epi8(v0, shift);
    __m128i v2 = _mm_shuffle_epi8(v1, shift);
    __m128i v3 = _mm_shuffle_epi8(v2, shift);
    __m128i v4 = _mm_shuffle_epi8(v3, shift);
    __m128i v5 = _mm_shuffle_epi8(v4, shift);
    __m128i v6 = _mm_shuffle_epi8(v5, shift);
    __m128i v7 = _mm_shuffle_epi8(v6, shift);
    return v0 + v1 + v2 + v3 + v4 + v5 + v6 + v7;
}

static inline __attribute__((always_inline))
__m256 plus_scan_ps(__m256 v0)
{
    __m256 zero = _mm256_setzero_ps();
    __m256i shift = _mm256_set_epi32(6, 5, 4, 3, 2, 1, 0, 0);
    __m256 v1 = _mm256_blend_ps(_mm256_permutevar8x32_ps(v0, shift), zero, 0x01);
    __m256 v2 = _mm256_blend_ps(_mm256_permutevar8x32_ps(v1, shift), zero, 0x01);
    __m256 v3 = _mm256_blend_ps(_mm256_permutevar8x32_ps(v2, shift), zero, 0x01);
    __m256 v4 = _mm256_blend_ps(_mm256_permutevar8x32_ps(v3, shift), zero, 0x01);
    __m256 v5 = _mm256_blend_ps(_mm256_permutevar8x32_ps(v4, shift), zero, 0x01);
    __m256 v6 = _mm256_blend_ps(_mm256_permutevar8x32_ps(v5, shift), zero, 0x01);
    __m256 v7 = _mm256_blend_ps(_mm256_permutevar8x32_ps(v6, shift), zero, 0x01);
    return v0 + v1 + v2 + v3 + v4 + v5 + v6 + v7;
}

int main()
{
    Arr bytes = {B{0}, B{1}, B{2}, B{3}, B{4}, B{5}, B{6}, B{7}, B{8}, B{9}};

    std::vector<Arr> permutations;
    permutations.reserve(3628800); // 10!

    do {
        permutations.push_back(bytes);
    } while (std::next_permutation(bytes.begin(), bytes.end()));

    Arr Xs = {B{2}, B{15}, B{6}, B{20}, B{11}, B{14}, B{7}, B{1}, B{3}, B{14}};
    Arr Ys = {B{8}, B{3}, B{6}, B{15}, B{2}, B{1}, B{3}, B{12}, B{6}, B{13}};
    Arr Tastiness = {B{7}, B{17}, B{8}, B{30}, B{12}, B{15}, B{5}, B{12}, B{3}, B{18}};

    __mmask16 mask8 = (1 << 8) - 1;
    __mmask16 mask10 = (1 << 10) - 1;
    __m128i zero = _mm_set1_epi8(0);
    __m256 zerof = _mm256_set1_ps(0.0f);

    __m128i xs = _mm_mask_loadu_epi8(zero, mask10, Xs.data());
    __m128i ys = _mm_mask_loadu_epi8(zero, mask10, Ys.data());
    __m128i tastiness = _mm_mask_loadu_epi8(zero, mask10, Tastiness.data());

    std::optional<size_t> best_n;
    float best_val = 0.0f;
    float best_dist = float(INT_MAX);
    int best_t = INT_MAX;
    int best_len = 0;

    for (size_t n = 0; n < permutations.size(); n++)
    {
        __m128i path = _mm_mask_loadu_epi8(zero, mask8, permutations[n].data());
        __m128i x = _mm_mask_permutexvar_epi8(zero, mask8, path, xs);
        __m128i y = _mm_mask_permutexvar_epi8(zero, mask8, path, ys);
        __m128i t = _mm_mask_permutexvar_epi8(zero, mask8, path, tastiness);
        __m128i shift = _mm_set_epi8(-1, -1, -1, -1, -1, -1, -1, -1, 6, 5, 4, 3, 2, 1, 0, -1);
        __m128i x0 = _mm_shuffle_epi8(x, shift);
        __m128i y0 = _mm_shuffle_epi8(y, shift);
        __m128i dx = _mm_sub_epi8(x, x0);
        __m128i dy = _mm_sub_epi8(y, y0);
        __m128i tt = plus_scan_epi8(t);
        __m128i xx = _mm_cvtepi8_epi16(dx);
        __m128i yy = _mm_cvtepi8_epi16(dy);
        __m128i x2 = _mm_mullo_epi16(xx, xx);
        __m128i y2 = _mm_mullo_epi16(yy, yy);
        __m128i x2y2 = _mm_add_epi16(x2, y2);
        __m256 x2y2f = _mm256_cvtepi32_ps(_mm256_cvtepi16_epi32(x2y2));
        __m256 sqrts = _mm256_sqrt_ps(x2y2f);
        __m256 tdist = plus_scan_ps(sqrts);
        __m256 ttf = _mm256_cvtepi32_ps(_mm256_cvtepi8_epi32(tt));
        __m256 v = _mm256_div_ps(ttf, tdist);
        __m256 bests = _mm256_set1_ps(best_val);
        __mmask8 m = _mm256_cmp_ps_mask(bests, v, _CMP_LT_OS);
        unsigned k = _cvtmask8_u32(m);
        int bits = _mm_popcnt_u32(k);
        __m256 vc = _mm256_mask_compress_ps(zerof, m, v);
        __m256 tdistc = _mm256_mask_compress_ps(zerof, m, tdist);
        __m128i ttc = _mm_mask_compress_epi8(zero, m, tt);
        for (int b = 0; b < bits; b++)
        {
            int len = _bit_scan_forward(k);
            (k) &= ~(1ULL << (len));
            alignas(32) float vals[8]; _mm256_store_ps(vals, vc);
            alignas(32) float tdists[8]; _mm256_store_ps(tdists, tdistc);
            std::byte tts[16]; _mm_storeu_epi8(tts, ttc);
            float val = vals[b];
            best_len = val > best_val ? len : best_len;
            best_n = val > best_val ? n : best_n;
            best_dist = val > best_val ? tdists[b] : best_dist;
            best_t = val > best_val ? int(tts[b]) : best_t;
            best_val = std::max(best_val, val);
        }
    }
    if (best_n)
    {
        Arr path = permutations[best_n.value()];
        for (auto& p : path) p = (std::byte)((int)p+1);
        std::cout << "Best n =  " << best_n.value_or(-1) << "\n"
            << "Best distance = " << best_dist << "\n"
            << "Best tastiness = " << best_t << "\n"
            << 
79804372
0

All code and visuals available in this demo notebook. It should contain the cached visual results if you download and open in an IDE to follow along.

1 - Visualize (2.89, ~10 s)

I started by making a visual of the data, using a scatterplot that encoded position along with tastiness represented by size. My first 'solution' was just a few guesses at what I thought looked like the optimal path. This fed into my intuition for later algorithms.

2 - Brute Force (2.89, 6.83 s)

Since we only dealing with ten nodes, brute forcing a solution, even in Python, only took a few seconds. This confirmed my human intuition, and gave me an upper bound for how good a solution could get.

3 - Greedy Score Chaser (1.74, 76 μs)

The obvious naïve approach to me was choosing maximizing tastiness / distance for the next step. Visualizing this approach immediately surfaced a flaw: the single best choice (#4) is the furthest away from the start, so you end up passing by many other nodes on your way there. This also kept adding nodes until the limit of 8, which isn't necessarily optimal. Both of these learnings fed into the next iteration:

4 - Greedy Insert (2.21, 796 μs)

To address these problems, I modified the algorithm to look at inserting the next step anywhere along the path instead of only at the end, with the hope of picking up nodes that are 'on the way' if it's beneficial to do so. This 'if it's beneficial to do so' condition also allows early stopping if it doesn't make sense to go anywhere else. This looks much better, however we are still strongly biased by this one node so the behavior is more like "rush for #4 and get some stuff along the way", while missing the high tastiness density shops of 2, 5, and 6.

5 - Gravity (2.06, 4.8 ms)

With 'density' in mind, I tried a physics-informed approach where we simulate motion through 2d space being pulled by the 'gravity' of the tastiness of nodes we haven't visited yet. This had the intended effect of pulling the path toward higher density node clumps, but didn't have a stopping condition beyond the 8 node maximum which drastically lowered the score as it got pulled from #4 all the way back to #8. This could be addressed with a secondary trimming routine, but this was already a slow solution and I knew I wanted to try something different.

6 Stable pairs (2.89, 588 ms)

A common problem with greedy solutions is that they end up in awkward states with no good choices. To address this, I first tried to find stable pairs of nodes where the best move from either node was to go to the other. This identified edges that are more likely to belong to an optimal path regardless of the order of visiting. I repeated this process, ignoring already-found matches, until every node was connected at least once (a fully-connected graph probably isn't actually necessary, but I needed some stopping condition so that seemed reasonable). Using this graph of a few dozen edges, I used a breadth-first-search approach of exploring each potential unseen path across this graph, and then keeping only the top N best-scoring paths. Once we've explored the whole graph, we return the best solution found--in this case the right one.

Conclusion

Travelling Salesman is well studied and there are plenty of great approaches out there, but this was a fun exploration of graphically-inspired problem solving with a good dataset for learning--small enough to not be daunting, but challenging enough to be non-trivial.

79799430
0

For Challenge #11 – Taco Crawl Optimization, I’ve implemented a solution that selects up to 8 taco spots out of the 10 given, starting from (0, 0), computing Euclidean travel distance, and maximizing the ratio:

(total tastiness of chosen spots) ÷ (total travel distance)

The algorithm used is a hybrid heuristic combining a nearest-neighbor seed with local swap improvements to efficiently explore high-score routes in reasonable time. I then compare several candidate routes and pick the best by the defined metric.

Key features:

  • Distance is computed using √((x₂−x₁)² + (y₂−y₁)²).

  • Stops list limited to 8 spots.

  • No return trip required.

  • Final output prints selected spot indices in visiting order, total tastiness, total distance, and resulting ratio.

I believe this meets the challenge criteria and demonstrates a clear approach to solving the optimization puzzle.

79798219
3

Code: Brute force, with precalculated distances between taco spots

import pandas as pd
import numpy as np
import itertools

TACO_SPOTS = {
    1:   (2,  8,  7),
    2:  (15,  3, 17),
    3:  ( 6,  6,  8),
    4:  (20, 15, 30),
    5:  (11,  2, 12),
    6:  (14,  1, 15),
    7:  ( 7,  3,  5),
    8:  ( 1, 12, 12),
    9:  ( 3,  6,  3),
    10: (14, 13, 18)
}
df = pd.DataFrame(TACO_SPOTS).T
df.columns = ['x', 'y', 'taste']

names = df.index.to_numpy()
x = df['x'].to_numpy()
y = df['y'].to_numpy()
tasty = df['taste'].to_numpy()

tasty_map = dict(zip(names, tasty))
distance_matrix = np.zeros((len(df), len(df)))
zero_distance_matrix = np.zeros(len(df))
for i in range(len(df)):
  for j in range(len(df)):
    distance_matrix[i, j] = np.sqrt((x[i] - x[j])**2 + (y[i] - y[j])**2)
  zero_distance_matrix[i] = np.sqrt((x[i])**2 + (y[i])**2)

best_combo = None
best_metric = -np.inf
best_distance = 0
best_tastiness = 0

for k in range(1,9):
  for combo in itertools.permutations(names, k):
    dist = sum(distance_matrix[combo[i] - 1][combo[i + 1] - 1] for i in range(len(combo) - 1)) + zero_distance_matrix[combo[0]-1]
    tastiness_sum = sum(tasty_map[i] for i in combo)
    metric = tastiness_sum / dist if dist != 0 else 0
    #print(f"Combo: {combo}, Distance: {dist}, Tastiness: {tastiness_sum}, Metric: {metric}")
    if metric > best_metric:
        best_metric = metric
        best_combo = combo
        best_distance = dist
        best_tastiness = tastiness_sum
  result = {
      'Combination': best_combo,
      'Total Distance': best_distance,
      'Total Tastiness': best_tastiness,
      'Metric': best_metric
  }
print(result)

Output

{'Combination': (np.int64(7), np.int64(5), np.int64(6), np.int64(2), np.int64(10), np.int64(4)), 'Total Distance': np.float64(33.51165531060739), 'Total Tastiness': np.int64(97), 'Metric': np.float64(2.8945153290979557)}

Nice problem, but need to figure out algorithms which can be used to implement if the taco spots increase. Any suggestions would be appreciated. Thanks

79797532
3

[I accidentally used 14,3 as coordinates for spot 6]

  • Optimal routing: [7, 5, 6, 2]
  • total tastiness: 49
  • total distance: 15.901156391649948
  • tastiness per distance: 3.0815368890863177
  • cycles: 1456

[with the correct coordinates]

  • Optimal routing: [0,7,5,6,2,10,4]
  • total tastiness: 97
  • total distance: 33.51165531060739
  • tastiness per distance: 2.8945153290979557
  • cycles: 736

I deployed the B* searching algorithm with a specific lower and upper heuristics for edge sets. My code is about 600 lines. Therefore, I uploaded the code to a Github Gist.

Taco Crawl Code

This is my attempt at trying to come up with an efficient algorithm. I took very much time thinking and had not enough time eventually to check against a brute force solution. In theory it should work, if there is not another bug in the code.

It finds the given solution above almost immediately but takes many cycles to prove it. If the upper estimate heuristic can be improved, it would finish faster. I am afraid, the actual result runs not notably faster than a quick brute force approach.

Asking for a good algorithm to this problem is actually beyond a programming challenge in my opinion. It becomes a mathematical challenge.

I wish, I could give a solid answer about the achieved algorithmic complexity. I think, this problem is less complex than Traveling Salesman because the tastiness-per-distance metric has some specific properties which allows cuts. Without a starting point, the problem would be very easy to optimize (choose the most valuable edge).

N = number of spots, E = number of possible edges between spots, K = number of nodes in the search tree.

79797022
4

best route: [ 7, 5, 6, 2, 10, 4 ]

total taste: 97

distance traveled: 33.512

taste per distance: 2.895

#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <math.h>

//#define READ_FROM_FILE
#define HARD_CODE_DATA

#ifdef READ_FROM_FILE
#define FILE_NAME_TACO_SPOTS_DATA "taco_spots.txt"
#endif // READ_FROM_FILE

#define NUM_TACO_SPOTS 10
#define MAX_TACO_SPOT_VISITS 8

typedef struct {
    int x;
    int y;
} tPoint;

typedef struct {
    tPoint p;   // location
    int t;      // taste points
} tTacoSpot;

#ifdef HARD_CODE_DATA
tTacoSpot tacoSpots[NUM_TACO_SPOTS + 1] = {
    { { 0,  0},  0},
    { { 2,  8},  7},
    { {15,  3}, 17},
    { { 6,  6},  8},
    { {20, 15}, 30},
    { {11,  2}, 12},
    { {14,  1}, 15},
    { { 7,  3},  5},
    { { 1, 12}, 12},
    { { 3,  6},  3},
    { {14, 13}, 18}
};
#else
tTacoSpot tacoSpots[NUM_TACO_SPOTS + 1] = { 0 };
#endif // HARD_CODE_DATA

void
tacoSpotsDataPrint(
    const tTacoSpot tacoSpots[]
) {
    printf("Taco Spot No. | Location | Taste\n");
    printf("--------------|----------|------\n");

    for (int i = 1; i <= NUM_TACO_SPOTS; i++) {
        printf("       %2d     | (%2d, %2d) |   %2d\n", i, tacoSpots[i].p.x, tacoSpots[i].p.y, tacoSpots[i].t);
    }

    printf("\n");
    return;
}

void
tablePrint(
    const char *title,
    const double table[][NUM_TACO_SPOTS + 1]
) {
    printf("%s\n", title);

    for (int i = 0; i <= NUM_TACO_SPOTS; i++) {
        for (int j = 0; j <= NUM_TACO_SPOTS; j++) {
            printf("%8.3f ", table[i][j]);
        }

        printf("\n");
    }

    printf("\n");
    return;
}

#ifdef READ_FROM_FILE
int
tacoSpotsDataReadFromFile(
    void
) {
    FILE *fileTacoSpotsData;
    int returnCode = 0, i = 1;

    if ((returnCode = fopen_s(&fileTacoSpotsData, "./"FILE_NAME_TACO_SPOTS_DATA, "r")) != 0) {
        printf("Error: Failed to read from file %s\n", FILE_NAME_TACO_SPOTS_DATA);
    }
    else {
        while (!feof(fileTacoSpotsData) && i <= NUM_TACO_SPOTS) {
            fscanf_s(fileTacoSpotsData, "%d %d %d", &tacoSpots[i].p.x, &tacoSpots[i].p.y, &tacoSpots[i].t);
            i++;
        }

        fclose(fileTacoSpotsData);
    }

    return(returnCode);
}
#endif // READ_FROM_FILE

void
tableCompute(
    double distanceTable[][NUM_TACO_SPOTS + 1]
) {
    int dx, dy;
    double ds;

    for (int i = 0; i <= NUM_TACO_SPOTS; i++) {
        for (int j = i + 1; j <= NUM_TACO_SPOTS; j++) {
            dx = tacoSpots[j].p.x - tacoSpots[i].p.x;
            dy = tacoSpots[j].p.y - tacoSpots[i].p.y;
            ds = sqrt((double)(dx * dx + dy * dy));
            distanceTable[i][j] = ds;
            distanceTable[j][i] = ds;
        }
    }

    return;
}

void
selectBestRoute(
    bool tacoSpotVisited[],
    int tacoSpotVisitSequence[],
    const double distanceTable[][NUM_TACO_SPOTS + 1],
    int *tasteTotal,
    double *distanceTotal,
    int *tacoSpotCurrent,
    double *tasteMax,
    int *tacoSpotCount
) {
    double tasteTmp = 0.0;
    int tacoSpotTmp = 0;

    if (*tacoSpotCount < MAX_TACO_SPOT_VISITS) {
        for (int i = 1; i <= NUM_TACO_SPOTS; i++) {
            if (tacoSpotVisited[i] == false) {
                tacoSpotVisited[i] = true;
                *tacoSpotCount += 1;
                *distanceTotal += distanceTable[*tacoSpotCurrent][i];
                *tasteTotal += tacoSpots[i].t;
                tacoSpotVisitSequence[*tacoSpotCount] = i;
                tasteTmp = (double)(*tasteTotal) / *distanceTotal;

                if (tasteTmp > *tasteMax) {
                    *tasteMax = tasteTmp;
                    printf("total taste: %3d distance travelled: %8.3f\n", *tasteTotal, *distanceTotal);
                    printf("max taste: %8.3f best path [ %2d", *tasteMax, tacoSpotVisitSequence[1]);

                    for (int j = 2; j <= *tacoSpotCount; j++) {
                        printf(", %2d", tacoSpotVisitSequence[j]);
                    }

                    printf(" ]\n");
                }

                tacoSpotTmp = *tacoSpotCurrent;
                *tacoSpotCurrent = i;
                selectBestRoute(tacoSpotVisited, tacoSpotVisitSequence, distanceTable, tasteTotal, distanceTotal, tacoSpotCurrent, tasteMax, tacoSpotCount);
                *tacoSpotCurrent = tacoSpotTmp;
                *tasteTotal -= tacoSpots[i].t;
                *distanceTotal -= distanceTable[*tacoSpotCurrent][i];
                *tacoSpotCount -= 1;
                tacoSpotVisited[i] = false;
            }
        }
    }

    return;
}

int
main(
    void
) {
    bool tacoSpotVisited[NUM_TACO_SPOTS + 1] = { false };
    int tasteTotal = 0,
        tacoSpotCount = 0,
        tacoSpotCurrent = 0,
        tacoSpotVisitSequence[MAX_TACO_SPOT_VISITS + 1] = { 0 };
    double  tasteMax = 0.0,
        distanceTotal = 0.0,
        distanceTable[NUM_TACO_SPOTS + 1][NUM_TACO_SPOTS + 1] = { 0.0 };

#ifdef READ_FROM_FILE
    int returnCode = tacoSpotsDataReadFromFile();

    if (returnCode != 0) {
        return (returnCode);
    }
#endif // READ_FROM_FILE

    tacoSpotsDataPrint(tacoSpots);
    tableCompute(distanceTable);
    tablePrint("Distance Table", distanceTable);
    selectBestRoute(tacoSpotVisited, tacoSpotVisitSequence, distanceTable, &tasteTotal, &distanceTotal, &tacoSpotCurrent, &tasteMax, &tacoSpotCount);

#ifdef READ_FROM_FILE
    return(returnCode);
#else
    return(0);
#endif // READ_FROM_FILE
}
79795791
2

LabVIEW + Rust - based solutions

I've completed this challenge twice using two different programming languages, with the same results. (Actually, this is my second attempt, because I initially overlooked the "UP to 8 spots" requirement.)

  • The optimal routing (up to 8 taco spots) : [7, 5, 6, 2, 10, 4]

  • The total tastiness achieved : 97

  • The total distance traveled : 33,5117

  • The tastiness per distance : 2,89452

The first solution was implemented in LabVIEW, using a classical brute-force approach (we only need to analyze 2 606 490 routes).

Link to Block Diagram Image.

As a cross-check, reimplemented the solution in Rust, using the distance matrix computed in LabVIEW:

use itertools::Itertools;

// Distance matrix
const D: [[f64; 11]; 11] = [
  [0.00, 8.25, 15.30, 8.49, 25.00, 11.18, 14.04, 7.62, 12.04, 6.71, 19.10],
  [8.25, 0.00, 13.93, 4.47, 19.31, 10.82, 13.89, 7.07, 4.12, 2.24, 13.00],
  [15.30, 13.93, 0.00, 9.49, 13.00, 4.12, 2.24, 8.00, 16.64, 12.37, 10.05],
  [8.49, 4.47, 9.49, 0.00, 16.64, 6.40, 9.43, 3.16, 7.81, 3.00, 10.63],
  [25.00, 19.31, 13.00, 16.64, 0.00, 15.81, 15.23, 17.69, 19.24, 19.24, 6.32],
  [11.18, 10.82, 4.12, 6.40, 15.81, 0.00, 3.16, 4.12, 14.14, 8.94, 11.40],
  [14.04, 13.89, 2.24, 9.43, 15.23, 3.16, 0.00, 7.28, 17.03, 12.08, 12.00],
  [7.62, 7.07, 8.00, 3.16, 17.69, 4.12, 7.28, 0.00, 10.82, 5.00, 12.21],
  [12.04, 4.12, 16.64, 7.81, 19.24, 14.14, 17.03, 10.82, 0.00, 6.32, 13.04],
  [6.71, 2.24, 12.37, 3.00, 19.24, 8.94, 12.08, 5.00, 6.32, 0.00, 13.04],
  [19.10, 13.00, 10.05, 10.63, 6.32, 11.40, 12.00, 12.21, 13.04, 13.04, 0.00],
];

const W: [i32; 11] = [0, 7, 17, 8, 30, 12, 15, 5, 12, 3, 18];

struct GlobalBest {
    ratio: f64,
    distance: f64,
    tastiness_sum: i32,
    order: Vec<usize>,
}

fn path_distance(order: &[usize]) -> f64 {
    let mut dist = D[0][order[0]];
    dist += order.windows(2).map(|w| D[w[0]][w[1]]).sum::<f64>();
    dist
}

fn sum_tastiness(subset: &[usize]) -> i32 {
    subset.iter().map(|&c| W[c]).sum()
}

fn best_permutation(subset: &[usize]) -> (f64, Vec<usize>) {
    subset
        .iter()
        .copied()
        .permutations(subset.len())
        .map(|perm| (path_distance(&perm), perm))
        .min_by(|a, b| a.0.partial_cmp(&b.0).unwrap())
        .unwrap()
}

fn main() {
    let candidates: Vec<usize> = (1..=10).collect();
    let mut global_best: Option<GlobalBest> = None;

    for k in 2..=8 { // UP to 8!
        for subset in candidates.iter().copied().combinations(k) {
            let tsum = sum_tastiness(&subset);
            let (distance, order) = best_permutation(&subset);
            let ratio = tsum as f64 / distance;

            match &mut global_best {
                Some(gb) if ratio > gb.ratio => {
                    *gb = GlobalBest {
                        ratio,
                        distance,
                        tastiness_sum: tsum,
                        order,
                    };
                }
                None => {
                    global_best = Some(GlobalBest {
                        ratio,
                        distance,
                        tastiness_sum: tsum,
                        order,
                    });
                }
                _ => {}
            }
        }
    }

    let gb = global_best.unwrap();

    println!("=== Results ===");
    println!("The optimal routing (up to 8 taco spots) : {:?}", gb.order);
    println!("The total tastiness achieved             : {}", gb.tastiness_sum);
    println!("The total distance traveled              : {:.2}", gb.distance);
    println!("The tastiness per distance               : {:.12}", gb.ratio);
}
79795800
1

Andrey, please note that the challenge says 'up to 8'. This means that routes with fewer than 8 steps are also eligible.

Quite few others here - myself included - have stumbled over this little thing. ;-)

79795806
1

Ah, good catch — "up to 8", I see. I need to think it through a bit more. Thank you! I should read the requirements thoroughly.

79795661
-1

Given Taco spot sequence

spot 3 : 8

spot 2 : 17

spot 1 : 7

spot 7 : 5

spot 9 : 3

spot 4 : 30

spot 6 : 15

spot 8 : 12

total_tastiness : 97

total_distance : 95.47 [using the euclidean path]

current_location (1, 12) 8th Place

Code used

import math

# Taco spot data: {spot_number: ((x, y), tastiness)}
taco_spots = {
    1: ((2, 8), 7),
    2: ((15, 3), 17),
    3: ((6, 6), 8),
    4: ((20, 15), 30),
    5: ((11, 2), 12),
    6: ((14, 1), 15),
    7: ((7, 3), 5),
    8: ((1, 12), 12),
    9: ((3, 6), 3),
    10: ((14, 13), 18)
}

# Given path
given_path = [3, 2, 1, 7, 9, 4, 6, 8]

# Function to calculate Euclidean distance
def cal_distance(p1, p2):
    return math.sqrt((p2[0] - p1[0])**2 + (p2[1] - p1[1])**2)

# Initialize
total_tastiness = 0
total_distance = 0
current_location = (0, 0)

# Traverse the path
for spot in given_path:
    location, tastiness = taco_spots[spot]
    total_tastiness += tastiness
    total_distance += cal_distance(current_location, location)
    current_location = location

# Calculate tastiness per distance
tastiness_per_distance = total_tastiness / total_distance if total_distance > 0 else 0

# Output results
print(f"Total Tastiness: {total_tastiness}"
        f"\nTotal Distance: {total_distance}"
        f"\nTastiness per Distance {tastiness_per_distance}"
        f"\nCurrent Locaiton {current_location}")
79796034
0

Your solution calculates only the scoring details for a single, pre-defined path. The challenge was to write code that finds the path with at most 8 steps and the highest score.

You're still in time for fixing your solution before the deadline comes ... ;-)

79796457
0

Thanks for pointing out, i have updated code and considered the top 50. Here is the updated code


                     Path Last Location  Total Tastiness  Total Distance  Tastiness per Distance
   (9, 3, 5, 6, 2, 4, 10)      (14, 13)              103           40.83                  2.5224
       (1, 3, 7, 5, 6, 2)       (15, 3)               64           25.40                  2.5195
      (7, 6, 5, 2, 10, 4)      (20, 15)               97           38.56                  2.5158
(9, 7, 3, 5, 6, 2, 10, 4)      (20, 15)              108           43.05                  2.5089
                (5, 2, 6)       (14, 1)               44           17.54                  2.5086
import itertools
import math
import pandas as pd

# Taco spot data: {spot_number: ((x, y), tastiness)}
taco_spots = {
    1: ((2, 8), 7),
    2: ((15, 3), 17),
    3: ((6, 6), 8),
    4: ((20, 15), 30),
    5: ((11, 2), 12),
    6: ((14, 1), 15),
    7: ((7, 3), 5),
    8: ((1, 12), 12),
    9: ((3, 6), 3),
    10: ((14, 13), 18)
}

# Function to calculate Euclidean distance
def cal_distance(p1, p2):
    return math.sqrt((p2[0] - p1[0])**2 + (p2[1] - p1[1])**2)

# Store all results
results = []

# Try all permutations of taco spots with lengths from 1 to 8
for r in range(1, 9):
    for path in itertools.permutations(taco_spots.keys(), r):
        total_tastiness = 0
        total_distance = 0
        current_location = (0, 0)
        for spot in path:
            location, tastiness = taco_spots[spot]
            total_tastiness += tastiness
            total_distance += cal_distance(current_location, location)
            current_location = location
        score = total_tastiness / total_distance if total_distance > 0 else 0
        last_location = taco_spots[path[-1]][0]
        results.append({
            "Path": path,
            "Last Location": last_location,
            "Total Tastiness": total_tastiness,
            "Total Distance": round(total_distance, 2),
            "Tastiness per Distance": round(score, 4)
        })

# Convert results to DataFrame and sort by score descending
df = pd.DataFrame(results)
df_sorted = df.sort_values(by="Tastiness per Distance", ascending=False)

# Print the top 50 paths
print(df_sorted.head(50).to_string(index=False))
79798256
0
Author

Your updated results are better but still not getting the optimal solution

79795522
1
The optimal routing is [7,5,6,2,10,4]
Total testiness: 97
Total distance: 33.511655310607
Testiness per distance: 2.894515329098

First created an Excel sheet with distances between each 2 taco points and between (0,0) and each taco point. Then created the following php code with recursive function with excel sheet data as array. There are 1 814 400 variations (10!/2!), so it executes fast enough.

<?php
$bestpath=array();
$totaltestiness=0;
$totaldistance=0;
$testperdist=0;
function nexttaco($n,$path,$testiness,$distance){
  global $bestpath,$totaltestiness,$totaldistance,$testperdist;
  $distances=array(
    array(8.24621125123532,15.2970585407784,8.48528137423857,25,              11.1803398874989,14.0356688476182,7.61577310586391,12.0415945787923,6.70820393249937,19.1049731745428),
    array(0,               13.9283882771841,4.47213595499958,19.313207915828, 10.816653826392, 13.8924439894498,7.07106781186548,4.12310562561766,2.23606797749979,13),
    array(13.9283882771841,0,               9.48683298050514,13,              4.12310562561766,2.23606797749979,8,               16.6433169770932,12.369316876853, 10.0498756211209),
    array(4.47213595499958,9.48683298050514,0,               16.6433169770932,6.40312423743285,9.4339811320566, 3.16227766016838,7.81024967590665,3,               10.6301458127347),
    array(19.313207915828, 13,              16.6433169770932,0,               15.8113883008419,15.2315462117278,17.6918060129541,19.2353840616713,19.2353840616713,6.32455532033676),
    array(10.816653826392, 4.12310562561766,6.40312423743285,15.8113883008419,0,               3.16227766016838,4.12310562561766,14.142135623731, 8.94427190999916,11.4017542509914),
    array(13.8924439894498,2.23606797749979,9.4339811320566, 15.2315462117278,3.16227766016838,0,               7.28010988928052,17.0293863659264,12.0830459735946,12),
    array(7.07106781186548,8,               3.16227766016838,17.6918060129541,4.12310562561766,7.28010988928052,0,               10.816653826392, 5,               12.2065556157337),
    array(4.12310562561766,16.6433169770932,7.81024967590665,19.2353840616713,14.142135623731, 17.0293863659264,10.816653826392, 0,               6.32455532033676,13.0384048104053),
    array(2.23606797749979,12.369316876853, 3,               19.2353840616713,8.94427190999916,12.0830459735946,5,               6.32455532033676,0,               13.0384048104053),
    array(13,              10.0498756211209,10.6301458127347,6.32455532033676,11.4017542509914,12,              12.2065556157337,13.0384048104053,13.0384048104053,0)
  );
  $tastinesses=array(7,17,8,30,12,15,5,12,3,18);
  for($t=1;$t<11;$t++)if(!in_array($t,$path)){
    $path[$n]=$t;
    $tt=$testiness+$tastinesses[$t-1];
    if($n==0)$dist=$distances[0][$t-1];
    else $dist=$distance+$distances[$path[$n-1]][$t-1];
    if($tt/$dist>$testperdist){
      $bestpath=$path;
      $testperdist=$tt/$dist;
      $totaltestiness=$tt;
      $totaldistance=$dist;
    }
    if($n<7)nexttaco($n+1,$path,$tt,$dist);
  }
}
nexttaco(0,array(),0,0);
echo "The optimal routing is ".json_encode($bestpath)."\nTotal testiness: $totaltestiness\nTotal distance: $totaldistance\nTestiness per distance: $testperdist\n";
?>
79795662
1

Total testiness: 97

I didn't know that eating tasty tacos makes people testy, in direct proportion to how much they enjoyed the taste ... ;-)

79795867
1

Peter, in my humble understanding there are 2 606 490 possible routes (10!/2! + 10!/3! + 10!/4! +...). 1 814 400 is for 8 spots only, isn't?

79796023
2

Andrey, when you enumerate the 10!/2! paths of length 8 then you have also enumerated all shorter paths along the way. Hence it is sufficient to do the high score check after each and every step instead of only after step 8; everything else can remain the same.

Since the check involves only a multiplication and a comparison it is essentially free, and the complexity is dominated by the enumeration of the 10!/2! paths of length 8.

79796438
0

Andrey, you are right, there are 2 606 490 divisions and comparisons, but they are still not so many, as the code is executed under 1 second on my old AMD Athlon II X4 635 PC :)

79796444
0

DarthGizka, sorry for my bad English - it is my second foreign language :)

79796498
0

Peter, you do not have to apologise for giving us all a brief second of enjoyment. ;-)

Or, as the old Greeks said: sh*t happens to everyone, even native speakers, but funny sh*t does not happen often enough.

79794917
-2

My code only checks for the full 8 Taco stops, and returns

Path is 82645193 (which is 9,3,7,5,6,2,10,4). Taste 108, distance 38.766364, ratio 2.785920

The code is Visual Studio C++ and executes the search in about 5 seconds.

#include <windows.h>
#include <stdio.h>
#include <math.h>

#pragma warning (disable:4996)
#pragma warning (disable:6031)

HWND hw;
MSG msg;

CHAR prm[40320][8];
int i, j, k, h, mi, r1, r2, mr1, mr2;
BYTE b, bp, dk;
double d, mx;
INT  xd, yd, tt;

struct TACO {
    BYTE x;
    BYTE y;
    BYTE t;
} taco[10];

CHAR route[8];
CHAR ladder[8];

int WINAPI WinMain(_In_ HINSTANCE hInst, _In_opt_ HINSTANCE hPInst, _In_ LPSTR lpCmdLine, _In_ int nShowCmd) {

    WNDCLASS wc;
    memset(&wc, 0, sizeof(WNDCLASS));
    wc.style = 3;
    wc.lpszClassName = L"1";
    wc.lpfnWndProc = DefWindowProc;
    wc.hCursor = LoadCursor(NULL, IDC_ARROW);
    wc.hbrBackground = CreateSolidBrush(0x00ddbbbb);
    ATOM a = RegisterClass(&wc);
    hw = CreateWindow(MAKEINTATOM(a), 0, 0, 440, 72, 450, 450, 0, 0, 0, 0);
    SetWindowTextA(hw, "TACO");
    ShowWindow(hw, 1);

    AllocConsole();
    SetConsoleTitleA("TACO");
    freopen("CONOUT$", "w", stdout);

    printf("---Messages---\n---------------\n\n");

    taco[0] = { 2, 8, 7 };
    taco[1] = { 15, 3, 17 };
    taco[2] = { 6, 6, 8 };
    taco[3] = { 20, 15, 30 };
    taco[4] = { 11, 2, 12 };
    taco[5] = { 14, 1, 15 };
    taco[6] = { 7, 3, 5 };
    taco[7] = { 1, 12, 12 };
    taco[8] = { 3, 6, 3 };
    taco[9] = { 14, 13, 18 };

    mr1 = 99; mr2 = 99; mx = 0; mi = 99999;

    h = 40320;
    for (j = 0; j < 8; j++) {
        for (i = 0; i < 40320; i++)
            prm[i][j] = (i % h) / (h / (j + 1));
        h /= (j + 1);
    }

    for (r1 = 0; r1 < 9; r1++)
        for (r2 = r1 + 1; r2 < 10; r2++) {
            for (k = 0; k < 8; k++) {
                dk = k;
                if (dk >= r1) dk++;
                if (dk >= r2) dk++;
                route[k] = dk;
            }

            tt = 0;
            for (k = 0; k < 8; k++) tt += taco[route[k]].t;

            printf("%d %d : %d\n", r1, r2, tt);

            for (i = 0; i < 40320; i++) {

                for (k = 0; k < 8; k++) {
                    b = 0; bp = prm[i][k];
                    for (j = k + 1; j < 8; j++) if (prm[i][j] <= bp + b) b++;
                    ladder[b + bp] = route[k];
                }

                d = sqrt(pow(taco[ladder[0]].x, 2) + pow(taco[ladder[0]].y, 2));

                for (k = 1; k < 8; k++) {
                    xd = taco[ladder[k]].x - taco[ladder[k - 1]].x;
                    yd = taco[ladder[k]].y - taco[ladder[k - 1]].y;
                    d += sqrt(pow(xd, 2) + pow(yd, 2));
                }

                if ((double)tt / (double)d > mx) {
                    mx = (double)tt / (double)d;
                    mi = i;
                    mr1 = r1;
                    mr2 = r2;
                }

                //printf("%d: %f %f\n", i, d, (double)tt / d); //data for each route

            }//end i

            printf("\n-----\n");

        }//end r1,r2

    BringWindowToTop(hw);

    CHAR dgt[2];
    HDC hdc = GetDC(hw);
    SetBkColor(hdc, 0x00ddbbbb);
    for (i = 0; i < 10; i++) {
        double sqt = 4*sqrt(taco[i].t);
        Ellipse(hdc, taco[i].x * 20 - sqt, taco[i].y * 20 - sqt, taco[i].x * 20 + sqt, taco[i].y * 20 + sqt);
        dgt[0] = 49 + i; dgt[1] = 0;
        if (i == 9) { dgt[0] = '1'; dgt[1] = '0'; }
        TextOutA(hdc, taco[i].x * 20 + sqt/2, taco[i].y * 20 + sqt/2, dgt, 2-(i<9));
    }

    for (k = 0; k < 8; k++) {
        dk = k;
        if (dk >= mr1) dk++;
        if (dk >= mr2) dk++;
        route[k] = dk;
    }

    for (k = 0; k < 8; k++) {
        b = 0; bp = prm[mi][k];
        for (j = k + 1; j < 8; j++) if (prm[mi][j] <= bp + b) b++;
        ladder[b + bp] = route[k];
    }

    tt = 0;
    for (k = 0; k < 8; k++) tt += taco[route[k]].t;

    d = sqrt(pow(taco[ladder[0]].x, 2) + pow(taco[ladder[0]].y, 2));

    for (k = 1; k < 8; k++) {
        xd = taco[ladder[k]].x - taco[ladder[k - 1]].x;
        yd = taco[ladder[k]].y - taco[ladder[k - 1]].y;
        d += sqrt(pow(xd, 2) + pow(yd, 2));
    }

    printf("Tastiest route is (%d,%d), permutation %d\n", mr1, mr2, mi);

    for (k = 0; k < 8; k++) ladder[k] += 48;
    printf("Path is %s\n", ladder);
    for (k = 0; k < 8; k++) ladder[k] -= 48;

    printf("Taste %d, distance %f, ratio %f\n", tt, d, mx);

    SelectObject(hdc, CreatePen(0, 2, 0xff));
    MoveToEx(hdc, 0, 0, NULL);

    for (k = 0; k < 8; k++)
        LineTo(hdc, taco[ladder[k]].x * 20, taco[ladder[k]].y * 20);

    /*Tastiest route is (0, 7), permutation 15854
        Path is 82645193
        Taste 108, distance 38.766364, ratio 2.785920*/

    while (GetMessage(&msg, hw, 0, 0)) DispatchMessage(&msg);

    return 0;
}

The key insight here is the code to generate the 8! permutations:

h = 40320;
    for (j = 0; j < 8; j++) {
        for (i = 0; i < 40320; i++)
            prm[i][j] = (i % h) / (h / (j + 1));
        h /= (j + 1);
    }

which is translated into physical permutations by

   for (k = 0; k < 8; k++) {
                    b = 0; bp = prm[i][k];
                    for (j = k + 1; j < 8; j++) if (prm[i][j] <= bp + b) b++;
                    ladder[b + bp] = route[k];
                }

route[k] stores the stops to be used.

79796035
1

JMP, please note that the challenge says 'up to 8'. This means that routes with fewer than 8 steps are also eligible.

Quite few others here - myself included - have stumbled over this little thing and fixed our solutions. ;-)

79794844
2

TL;DR: 0.23 ms for trying the 1,814,400 possible routes via PLINQ (2.17 ms single-threaded), thanks to pre-computing distances into a matrix, a lean and mean permutation strategy, hoisting of branches and streamlined code for the last three steps.

Program output:

optimal route  : [7, 5, 6, 2, 10, 4]
total tastiness: 97
total distance : 33.51165531060739
score          : 2.8945153290979557

BenchmarkDotNet via LINQPad's facility for benchmarking selected source code lines:

unoptimised algorithm exposition (MinimalTacoCrawl.Main()):

| Method             | Mean     | Error     | StdDev    | Allocated |
|------------------- |---------:|----------:|----------:|----------:|
| SelectedSourceCode | 6.644 ms | 0.0052 ms | 0.0041 ms |    4.8 KB |

unoptimised code with precalculated distances (TacoCrawl.BruteForce(0)):

| Method             | Mean     | Error     | StdDev    | Allocated |
|------------------- |---------:|----------:|----------:|----------:|
| SelectedSourceCode | 5.930 ms | 0.0093 ms | 0.0087 ms |   2.18 KB |

optimised single-threaded (TacoCrawl.BruteForce(1)):

| Method             | Mean     | Error     | StdDev    | Allocated |
|------------------- |---------:|----------:|----------:|----------:|
| SelectedSourceCode | 2.171 ms | 0.0014 ms | 0.0013 ms |   2.16 KB |

optimised multi-threaded via PLINQ (TacoCrawl.BruteForce(2)):

| Method             | Mean     | Error   | StdDev  | Gen0   | Gen1   | Allocated |
|------------------- |---------:|--------:|--------:|-------:|-------:|----------:|
| SelectedSourceCode | 230.3 μs | 1.27 μs | 2.16 μs | 3.9063 | 0.9766 |  63.35 KB |

At the core of the implementation is a lean and mean scheme for enumerating permutations that I devised especially for this challenge. The main idea is that a single array represents both the choices made so far (entries to the left of the current step index) and the things still available to choose from (entries from the step index to the right end of the array).

A choice is made by swapping one of the available items to the slot corresponding to the step index, which is basically the slot 'owned' by the current invocation of the recursive function.

The next deeper invocation level operates only on the slots to the right of the home slot of its invoker. At the end of an invocation the array is back in the same state it was in upon function entry so that higher invocation levels are unaware that deeper invocation levels waltz all over the right part of the array.

As illustration, here is the state of things at the 7th recursive call level, during the third iteration of the loop (i.e. indices 7 and 8 have already been worked on). The code chooses the value of slot 9, swaps it into this step's home slot, makes the recursive call for the next level down, and then swaps things back.

Inline images are not allowed here, so I put the image on the web: spot-indices-step-7-9.png.

This scheme generates all possible permutations but it does not do so in lexicographic order. This is due to the fact that the available items are not considered/kept in sorted order. When an invocation level chooses one of the items to the right of its step index then it simply swaps that entry with its 'home' slot (thus making the spot in the home slot available for deeper invocations to choose from), which makes things a bit messy. :-)

Another consequence of this approach is that the code that records new best solutions must copy the index array because the array contents will keep changing until all permutations have been enumerated.

Here is a simple, non-optimised version of this algorithm:

private void simple_crawl_ (int[] spot_indices, int step, double tastiness, double distance, ConfinedMutableResult local_result)
{
    var previous_spot_index = spot_indices[step - 1];

    for (var i = step; i <= m_spot_count; ++i)
    {
        var current_spot_index = spot_indices[i];

        swap_(ref spot_indices[step], ref spot_indices[i]);

        var current_tastiness = tastiness + m_spots[current_spot_index].Tastiness;
        var current_distance = distance + distance_between_spots_(previous_spot_index, current_spot_index);
        var score = current_tastiness / current_distance;

        if (score >= local_result.BestScore)
            local_result.RecordBestResult(score, current_tastiness, current_distance, spot_indices, step);
        
        if (step < m_max_spots_to_visit)
            simple_crawl_(spot_indices, step + 1, current_tastiness, current_distance, local_result);
        else
            ++local_result.RoutesTried;

        swap_(ref spot_indices[step], ref spot_indices[i]);
    }
}

As can be seen from the code, each call tree gets its own structure for storing results in. The structure is allocated on the heap but during enumeration its reference is only ever known in the call tree that allocated it, which makes it effectively stack-confined and hence safe to mutate without further ado, obviating the need for synchronisation during enumeration.

When the code is used in conjunction with PLINQ then the internal synchronisation of the PLINQ logic ensures that things are visible to the thread that collects the results of the PLINQ query (per definitionem).

One optimisation that have not mentioned is a bit sleight of hand: it is not necessary to divide a tastiness value by a distance value in order to do a comparison with the current best score. The division can be replaced by a much faster multiplication.

Instead of:

var score = tastiness / distance;

if (score > best_score)
{
   ...
}

you can do

if (tastiness > best_score * distance)
{
   ...
}

Score updates are comparatively rare (29 for all the 1.814.400 routes that need checking), so the most expensive thing about the score updating logic overall is the test that governs it.

Finding strategies for cutting whole subtrees from the search tree seems difficult because there is no simple KO criterion (as in a minimal-cost scenario, for example, where you can stop when the current cost exceeds the current minimal cost).

The goal function is peculiar in that the result of combining two partial scores lies somewhere between the two. Mathematically the relation looks somewhat like this:

(t0 + t1) / (d0 + d1) = t0/d0 * x + t1/d1 * (1 - x) for x in [0,1] 

However, it seems difficult to turn this into a strategy for cutting whole branches from the search tree. E.g. if I have the score for the rest of the way from the current spot in addition to the score from the origin to here then I don't need heuristics to give a projection whether this could become a new high score. I can simply calculate the exact score and compare that.

While it seems difficult to cut subtrees, it is possible to reuse them, thus avoiding to recompute the final k steps for the same set of k spots again and again and again. For example, the computation for a given set of 4 last spots is performed again for each permutation of the corresponding first four spots, 24 times. However, using large amounts of pre-computed data runs afoul of the tiny L1 caches of current mainstream CPUs, typically 32k or 48k (or a mixture of the two, like in the i9 13900HX).

I roughed an experimental implementation where I pre-computed the results for the final 2 steps. The first try - result structs stored directly in an array indexed by four spot numbers - was a disaster. The pre-computation alone took more than 120 μs - more than half the time for my PLINQ result - because of cache thrashing. Using indirection - i.e. storing the actual results in a separate vector and putting only their UInt16 indices into the big array - cut the pre-computation time to 16 μs but overall performance of the taco crawl improved only by 10%, hardly worth the candle.

Hence I cut my losses and decided to post the solution I already had, the brutally optimised version of a brutally simple permutation explorer.

The attached source code is structured to facilitate experimentation above all else, which is why it contains a lot of things that are not strictly necessary for processing the one input vector that is relevant for this challenge: additional spots for giving the code a good workout, functions for verifying the integrity of results, pre-computed expected results for crawls using the 'stock' spots, and so on. The code is best consumed with an IDE that can work with #regions, e.g. JetBrains Rider.

Including the source code inline causes the size limit to be exceeded. Therefore I stashed it on PasteBin and put a password on it (LsKH3b03MM), to keep the stuff from being publicly visible before the deadline. A minimal, non-optimised version of the algorithm is at the end of this post. It is like BruteForce(0) from the full source, but without a precomputed distance matrix.

Anyway, I enjoyed this challenge immensely, and doing it taught a couple of new tricks even to this old dog. Kudos!

P.S.: like quite a few others here I overlooked the 'up to 8' in the problem description and implemented a 'k out of n' crawler instead of an 'up to k out of n' one. However, the fix was simple because it only required adding a score check to the intermediate steps, in addition to the existing one at step k. In the minimal version of the algorithm shown below the change required only the deletion of an 'else' (exchanging the positions of the two if statements is purely optional).

/// minimal rendition of the taco crawl with array-based permutation logic
public static class MinimalTacoCrawl
{
    public static void Main ()
    {
        var spot_indices = Enumerable.Range(0, SPOT_COUNT_INCLUDING_ORIGIN).ToArray();
        var local_result = new ConfinedMutableResult();

        simple_crawl_(spot_indices, 1, 0, 0, local_result);
        local_result.WriteToConsole();
    }

    private const int SPOT_COUNT_INCLUDING_ORIGIN = 1 + 10, MAX_SPOTS_TO_VISIT = 8;

    private record struct Spot (int X, int Y, int Tastiness);

    private static readonly Spot[] SPOTS =
    [
        new ( 0,  0,  0),  // origin prepended in order to simplify operations
        new ( 2,  8,  7),
        new (15,  3, 17),
        new ( 6,  6,  8),
        new (20, 15, 30),
        new (11,  2, 12),
        new (14,  1, 15),
        new ( 7,  3,  5),
        new ( 1, 12, 12),
        new ( 3,  6,  3),
        new (14, 13, 18)
    ];

    private class ConfinedMutableResult
    {
        public double BestScore => m_score;

        private int[]  m_best_route;
        private double m_score;
        private double m_tastiness;
        private double m_distance;

        public void RecordBestResult (double tastiness, double distance, int[] spot_indices, int step_count)
        {
            m_best_route = spot_indices.Skip(1).Take(step_count).ToArray();
            m_score      = tastiness / distance;
            m_tastiness  = tastiness;
            m_distance   = distance;
        }

        public void WriteToConsole ()
        {
            Console.WriteLine($"optimal route  : [{string.Join(", ", m_best_route)}]");
            Console.WriteLine($"total tastiness: {m_tastiness}");
            Console.WriteLine($"total distance : {m_distance}");
            Console.WriteLine($"score          : {m_score}");
        }
    }

    private static void simple_crawl_ (int[] spot_indices, int step, double tastiness, double distance, ConfinedMutableResult local_result)
    {
        var previous_spot_index = spot_indices[step - 1];

        for (int i = step; i < SPOT_COUNT_INCLUDING_ORIGIN; ++i)
        {
            var current_spot_index = spot_indices[i];

            swap_(ref spot_indices[step], ref spot_indices[i]);

            var current_tastiness = tastiness + SPOTS[current_spot_index].Tastiness;
            var current_distance = distance + distance_between_spots_(previous_spot_index, current_spot_index);
            var score = current_tastiness / current_distance;

            if (score >= local_result.BestScore)
                local_result.RecordBestResult(current_tastiness, current_distance, spot_indices, step);

            if (step < MAX_SPOTS_TO_VISIT)
                simple_crawl_(spot_indices, step + 1, current_tastiness, current_distance, local_result);

            swap_(ref spot_indices[step], ref spot_indices[i]);
        }
    }

    private static double distance_between_spots_ (int spot_index_1, int spot_index_2)
    {
        var delta_x = SPOTS[spot_index_1].X - SPOTS[spot_index_2].X;
        var delta_y = SPOTS[spot_index_1].Y - SPOTS[spot_index_2].Y;

        return Math.Sqrt(delta_x * delta_x + delta_y * delta_y);
    }

    private static void swap_ (ref int lhs, ref int rhs)
    {
        (lhs, rhs) = (rhs, lhs);
    }
}
79798216
1
Author

Thanks for explaining your algorithm. Impressive optimizations which show in the low compute time.

79794522
1

Solution

The path optimising for tastiness per distance (TPD) is 7,5,6,2,10,4 (starting from (0,0)) with a total tastiness of 97, a total distance of 33.51 and a TPD of 2.89. This path was found 3 times out of 5 attempts using my search algorithm.

visited spots reps with max TPD total reps path tastiness distance TPD
6 3 5 0,7,5,6,2,10,4 97 33.5116553106074 2.89451532909795

Search algorithm

I used a simulated annealing (SA) approach to find the path with the highest tastiness per distance (TPD) score. Each path in the search space is picked depending on their TPD and a temperature which allows for the selection of worse paths if set high. The temperature decreases gradually, until the search becomes a greedy algorithm to find the local (and hopefully global) maximum.

To reach the optimal solution faster, I refined transition probabilities between taco spots during both the SA and greedy searches to generate the next likely better paths to visit. Transition probabilities were incremented by 0.1 every time two tacos were next to each other in a new best path and decremented by 0.005 otherwise. The caveat is that these transition probabilities depend on the path length, so the searches should be separated for each path size ranging from 1 to 8.

During the greedy phase, I also updated transition probabilities when a path was not superseded after 5 iterations so that the local optimum was reached faster.

set.seed(123456)

local_optimum <- list()
reps <- 5
repi <- 0
for (rep in seq_len(reps)){
    for (path_size in c(1:8)){
        repi <- repi+1
        #### Initialize probs for each path size
        prob_table$prob <- 0.1

        #### Initial path
        current_path <- c(0, sample(c(1:10), path_size, replace = FALSE))
        best_path <- current_path
        best_tpd <- taste_per_distance(current_path, tacos_coords)
        tpd_max <- best_tpd[1]/best_tpd[2]
        time_best_path <- 1

        #### Optimization
        # Number of iterations depends on total search space
        combs <- taco_combs(path_size, 10)
        if ( combs < 10000){
            iterations <- 1000
        } else if (combs < 1000000){
            iterations <- 5000
        } else {
            iterations <- 10000
        }
        # 90/10 % of iterations for simulated annealing (SA)/greedy search
        temperature <- round(0.9*iterations)
        # Coefs to adjust decision probability
        temp_coef <- 1/(2*temperature)
        diff_coef <- 1.5
        # Number of times a path stays top before updating transitions probs
        best_path_limit <- 5

        #### Search
        for (i in seq_len(iterations)){
            tpd_current <- taste_per_distance(current_path, tacos_coords)

            new_path <- generate_path(path_size, prob_table)
            tpd_new <- taste_per_distance(new_path, tacos_coords)

            tpd_diff <- 1 - (tpd_current[1]/tpd_current[2])/
                        (tpd_new[1]/tpd_new[2])
            
            # Decision prob depends on TPD diff and decreasing temperature
            decision_prob <- min(tpd_diff*diff_coef + temperature*temp_coef, 1)
            decision_prob <- max(0, decision_prob)

            if (temperature > 0){ # SA search
                if (decision_prob > sample(seq_len(100)-1, 1)/100){
                    current_path <- new_path
                }
            } else { # Greedy search
                if (tpd_diff > 0) {
                    current_path <- new_path
                    time_best_path <- time_best_path + 1
                }else {
                    time_best_path <- 1
                }
            }
            
            # Update transition probs if path is better (SA + greedy)
            if(tpd_new[1]/tpd_new[2] > tpd_max){
                tpd_max <- tpd_new[1]/tpd_new[2]
                best_path <- new_path
                best_tpd <- tpd_new
                prob_table <- update_prob_table(new_path, prob_table)
            }
            
            # Update transition probs for persisting paths (greedy only)
            if (time_best_path >= best_path_limit){
                prob_table <- update_prob_table(current_path, prob_table)
            }

            temperature <- temperature - 1
            
        }
        local_optimum[[repi]] <- c(rep = rep,
                                    size = path_size,
                                    iterations = iterations,
                                    combinations = combs,
                                    path = paste(best_path, collapse = ","),
                                    tastiness = best_tpd[1],
                                    distance = best_tpd[2])
    }
}

Results

By design, the algorithm runs independently for each path size, leading to an optimal solution for each of them. The best of these 8 paths is the path with the overall best TPD.

The total search space is made out of 2,606,500 possible paths (for a path of size n: 10!/(10-n)!). I configured my algorithm to run 5 times with 29,000 iterations each (including all path lengths). This represents a little bit more than 5 % of the total search space.

#### Results
# Formatting
local_optimum <- data.frame(do.call("rbind", local_optimum))
local_optimum[,c("rep", "size", "iterations", "combinations", "tastiness", "distance")] <-
    apply(local_optimum[,c("rep", "size", "iterations", "combinations", "tastiness", "distance")],
            2, as.numeric)
local_optimum$TPD <- local_optimum$tastiness/local_optimum$distance

# Extract best reps for each path size
library(dplyr)
local_optimum_best <- local_optimum %>%
                group_by(size) %>%
                filter(TPD == max(TPD)) %>%
                mutate("reps with max TPD" = n(), "total reps" = reps, .after = "rep")  %>%
                slice(1) %>%
                rename("visited spots" = "size") %>%
                select("visited spots", "reps with max TPD", "total reps",  
                        "path", "tastiness", "distance", "TPD")

# Paths with maximum Tastiness Per Distance for each path size
local_optimum_best

# Path with maximum Tastiness Per Distance
local_optimum_best[which.max(local_optimum_best$TPD),]
visited spots reps with max TPD total reps path tastiness distance TPD
1 5 5 0,4 30 25 1.2
2 5 5 0,6,2 32 16.271736825118 1.96660014501973
3 5 5 0,5,6,2 44 16.5786855251671 2.65401017066198
4 2 5 0,7,5,6,2 49 17.1372243691497 2.85927282881407
5 2 5 0,5,6,2,10,4 92 32.9531164666248 2.79184519901717
6 3 5 0,7,5,6,2,10,4 97 33.5116553106074 2.89451532909795
7 1 5 0,3,7,5,6,2,10,4 105 37.5434412391504 2.79676014063691
8 2 5 0,9,3,7,5,6,2,10,4 108 38.7663637974112 2.78592030360124

Functions

# Combinations between taco spots
 taco_combs <- function(spots, allspots){
    res <- 1
    i <- allspots - spots + 1
    while (i <= allspots){
        res <- res*i
        i <- i+1
    }
    return(res)
 }

# Distance between two tacos
distance <- function (taco1x, taco1y, taco2x, taco2y){
    return(sqrt((taco2y - taco1y)**2 +
                (taco2x - taco1x)**2))

}

# Sum of tastiness and distance along the path
taste_per_distance <- function (path, tacos){
    tpd <- c(0,0)
    for (i in seq_along(path[-1])){
        source <- path[[i]]
        dest <- path[[i+1]]
        taco1 <- tacos[tacos$Taco == source,]
        taco2 <- tacos[tacos$Taco == dest,]
        tacos_distance <- distance(taco1$CoordX, taco1$CoordY,
                                    taco2$CoordX, taco2$CoordY)
        tpd[1] <- tpd[1] + taco2$Tastiness
        tpd[2] <- tpd[2] + tacos_distance
    }
    return(tpd)

}

# Adjust probabilities of transitions given a path
update_prob_table <- function(path, probs){
    all_neighbours <- F
    for (i in seq_along(path[-1])){
        # Neighbours are two consecutive spots in both direction
        neighbours <- probs$Taco1 == path[i] & probs$Taco2 == path[i+1]
        neighbours.rev <- probs$Taco2 == path[i] & probs$Taco1 == path[i+1]
        
        all_neighbours <- all_neighbours | neighbours | neighbours.rev
        # Increment prob of transition of neighbours with 0.1
        probs$prob[neighbours] <- min(0.999, probs$prob[neighbours] + 0.1)
        probs$prob[neighbours.rev] <- min(0.999, probs$prob[neighbours.rev] + 0.1)
    }

    # Decrement prob of transition of non-neighbours with 0.005
    probs$prob[!all_neighbours] <- pmax(0.001, probs$prob[!all_neighbours] - 0.005)

    return(probs)

}

# Generate a path depending on probabilities of transition
generate_path <- function(size, probs) {
    all_tacos <- unique(c(probs$Taco1, probs$Tazco2))
    new_path <- c(0)
    # Add one taco sopt at a time
    for (tacoi in seq_len(size)){
        remaining_tacos <- setdiff(all_tacos, new_path)
        last_taco <- new_path[length(new_path)]
        probs_subset <- probs$prob[probs$Taco1 == last_taco & 
                            probs$Taco2 %in% remaining_tacos]
        # Add taco sopt depending on probability
        new_path <- c(new_path, 
                    sample(remaining_tacos, 1, replace = FALSE,
                            prob = probs_subset))
    }
    return(new_path)
}

Input data

coords.tsv

Taco    CoordX  CoordY  Tastiness
0   0   0   0
1   2   8   7
2   15  3   17
3   6   6   8
4   20  15  30
5   11  2   12
6   14  1   15
7   7   3   5
8   1   12  12
9   3   6   3
10  14  13  18
# Tacos coordinates and tastiness
tacos_coords <- read.table("coords.tsv", h = T, sep = "\t")
# Transition probabilities between taco spots
prob_table <- expand.grid(tacos_coords$Taco, tacos_coords$Taco)
colnames(prob_table) <- c("Taco1", "Taco2")
# Can only visit each spot once
prob_table <- prob_table[!prob_table$Taco1 == prob_table$Taco2,]
79795667
0

Your solution happened to find the correct solution but it seems that it did so purely by accident. For another run with different spots/parameters it will likely return a wrong result.

What is completely missing from your exposition is the explanation which parts of the search space can be excluded for which reason, while still ensuring correctness of the solution. Absent that you have just a heuristic whose result may or may not be useable/correct.

A similar heuristic - but much faster - would be:

return new Random().NextDouble() < 0.50 ? "[7, 5, 6, 2, 10, 4]" : "[42]";
79795815
0

You are right that I am not guaranteed to find the optimal solution using my approach, as I don't scan all the possible paths.

However, I did find it (along with the optimal paths for each number of spots), so:

  • Either you are right and I am very lucky to have found the solution by visiting just 5 % of the search space.
  • Or - more likely - I used heuristics (which are all clearly explained by the way) that bring me very close to/at the optimal solution.

In fact, the challenge stated that you can use something else than a trivial brute force. I concluded that you don't need to ensure your solution is the optimal path, just find it, which my approach does rather consistently.

At the scale of this challenge, a brute force approach could probably work. That's valid. Other approaches are welcome also.

79796045
0

marcquery, 'using brute-force' means enumerating the whole search space without trying to find heuristics for pruning parts of the search space. Solutions still have to be correct all the time, for all inputs, not just if the stars are aligned favourably and for only one given input vector.

I've already shown you a minimal solution that 'finds' the solution for the given input vector exactly 50% of the time. By adjusting the constant in the comparison you can adjust the probability of a correct answer to any value you like. That doesn't make it a valid algorithm for solving the challenge.

Yes, you are very lucky to have found the solution after scanning only 5% of the search space. Try your solution on different inputs (i.e. spot coordinates and tastiness values chosen randomly from the same range as those of the challenge spots) and see how often you find the correct solution. There are currently about 50 different correct implementations in various languages here that you can use to find the correct solution(s) for a randomly generated/shuffled problem, in order to check the output of your algorithm.

79796151
0

Actually 'brute force' could mean anything, that is your interpretation, which is different than mine and that is fine. There is nothing about correctness in the challenge instructions, that is something you also (rightfully) deduced.

My approach is less random than you would like to think, feel free to test it and you will see for yourself.

In any case, I believe our argument stems mainly from a lack of details in the challenge instructions, so I will let the OP decides the issue of this.

79796217
1

In programming and computer science the term 'brute force' has always had a well-defined meaning. To pretend that it means anything other than that for this coding challenge would be disingenuous.

https://en.wikipedia.org/wiki/Brute-force_search

You are right insofar as this challenge is unusual in having no sample vectors with results and only a single judgement vector that is moreover known up front.

Normally we get a precise definition of the problem space and a set of test vectors, and the judgement vectors are kept secret while the challenge is open. Both test vectors and judgement vectors are chosen carefully to weed out solutions that produce the right result for the sample input(s) but do not actually solve the general problem implied by the challenge. Solutions entered by contestants are run on the judgement computer against the secret judgement vectors to ascertain that they do indeed work as expected.

Anyway, the 'judgement' part of the current coding challenge seems to be some sort of popularity contest. I do not think that anyone at Stack Overflow will go and install R or LINQPad or Node.js to find out whether the code actually gives the result that the write-up claims to have been obtained from it.

79794420
2

Code Challenge #11: Taco crawl optimization

Edited because I originally thought the challenge was to visit exactly 8 Taco Spots, not "up to" 8.

Optimal routing: [7, 5, 6, 2, 10, 4]

Total tastiness: 97

Total distance: 33.512

Total tastiness per distance: 2.895

Output for lengths 1-8

Optimal crawl sz 1: Crawl: [1.200 =   30.0 / 25.000] l=1 path: 0 -> 4
Optimal crawl sz 2: Crawl: [1.967 =   32.0 / 16.272] l=2 path: 0 -> 6 -> 2
Optimal crawl sz 3: Crawl: [2.654 =   44.0 / 16.579] l=3 path: 0 -> 5 -> 6 -> 2
Optimal crawl sz 4: Crawl: [2.859 =   49.0 / 17.137] l=4 path: 0 -> 7 -> 5 -> 6 -> 2
Optimal crawl sz 5: Crawl: [2.792 =   92.0 / 32.953] l=5 path: 0 -> 5 -> 6 -> 2 -> 10 -> 4
Optimal crawl sz 6: Crawl: [2.895 =   97.0 / 33.512] l=6 path: 0 -> 7 -> 5 -> 6 -> 2 -> 10 -> 4
Optimal crawl sz 7: Crawl: [2.797 =  105.0 / 37.543] l=7 path: 0 -> 3 -> 7 -> 5 -> 6 -> 2 -> 10 -> 4
Optimal crawl sz 8: Crawl: [2.786 =  108.0 / 38.766] l=8 path: 0 -> 9 -> 3 -> 7 -> 5 -> 6 -> 2 -> 10 -> 4```

Code:

Main.java

package org.sceaj.stackoverflow.challenge11;

import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

public class Main {
    
    public static final int MAX_CRAWL_SIZE = 8;
    
    private static List<TacoSpot> allTacoSpots;

    public static void main(String[] args) {
        allTacoSpots = createTacoSpots();
        for (int maxCrawlSize = 1; maxCrawlSize <= MAX_CRAWL_SIZE; maxCrawlSize++) {
            Crawl optimalCrawl = findOptimalCrawl(allTacoSpots, maxCrawlSize);
            System.out.println("Optimal crawl sz %d: %s".formatted(maxCrawlSize, optimalCrawl));
        }
    }
    
    private static Crawl findOptimalCrawl(List<TacoSpot> allSpots, int crawlSize) {
        List<Crawl> activeCrawls = new ArrayList<>();
        activeCrawls.add(new Crawl());
        for (int i = 0; i < crawlSize; i++) {
            List<Crawl> newCrawls = new ArrayList<>();
            for (Crawl workCrawl : activeCrawls) {
                newCrawls.addAll(extendCrawl(workCrawl));
            }
            activeCrawls = newCrawls;
        }
        // Find the best crawl from activeCrawls
        Crawl bestCrawl = activeCrawls.stream()
                .max((c1, c2) -> Double.compare(c1.score(), c2.score()))
                .orElse(null);
        return bestCrawl;
    }
    
    private static List<Crawl> extendCrawl(Crawl crawl) {
        List<TacoSpot> tacoSpots = findCandidatesForNextSpot(crawl);
        List<Crawl> extendedCrawls = new ArrayList<>();
        for (TacoSpot candidate : tacoSpots) {
            Crawl extendedCrawl = new Crawl(crawl);
            extendedCrawl.addTacoSpot(candidate);
            extendedCrawls.add(extendedCrawl);
        }
        return extendedCrawls;
    }
    
    private static List<TacoSpot> findCandidatesForNextSpot(Crawl crawl) {
        TacoSpot currentSpot = crawl.getCurrentTacoSpot();
        List<TacoSpot> candidates = new ArrayList<>(allTacoSpots);
        if (currentSpot.id() != 0) candidates.removeAll(crawl.visited());
        List<TacoSpot> sortedCandidates = sortByValueMetricFromTacoSpot(candidates, currentSpot);
        return sortedCandidates;
    }
        
    /*
     * from: https://stackoverflow.com/beta/challenges/79785895/code-challenge-11-taco-crawl-optimization
     *  Taco Spot   Coordinates Tastiness
                1   (2, 8)  7
                2   (15, 3) 17
                3   (6, 6)  8
                4   (20, 15)    30
                5   (11, 2) 12
                6   (14, 1) 15
                7   (7, 3)  5
                8   (1, 12) 12
                9   (3, 6)  3
                10  (14, 13)    18
     */
    private static List<TacoSpot> createTacoSpots() {
        return List.of(
                new TacoSpot(1, new Point(2, 8), 7), 
                new TacoSpot(2, new Point(15, 3), 17),
                new TacoSpot(3, new Point(6, 6), 8),
                new TacoSpot(4, new Point(20, 15), 30),
                new TacoSpot(5, new Point(11, 2), 12),
                new TacoSpot(6, new Point(14, 1), 15),
                new TacoSpot(7, new Point(7, 3), 5),
                new TacoSpot(8, new Point(1, 12), 12),
                new TacoSpot(9, new Point(3, 6), 3),
                new TacoSpot(10, new Point(14, 13), 18));
    }
    
    private static List<TacoSpot> sortByValueMetricFromTacoSpot(List<TacoSpot> tacoSpots, TacoSpot startingSpot) {
        return tacoSpots.stream()
                .sorted((ts1, ts2) -> Double.compare(ts2.valueMetricFromPoint(startingSpot.location()), ts1.valueMetricFromPoint(startingSpot.location())))
                .collect(Collectors.toList());
    }

}

Crawl.java

package org.sceaj.stackoverflow.challenge11;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

public class Crawl {
    
    private List<TacoSpot> path = new ArrayList<>();
    
    public Crawl() {
        path = new ArrayList<>();
        path.add(new TacoSpot(0, new Point(0, 0), 0)); // Start at origin)
    }
    
    public Crawl(Crawl other) {
        this.path = new ArrayList<>(other.path);
    }
    
    public Set<TacoSpot> visited() {
        return Collections.unmodifiableSet(new HashSet<>(path));
    }
    
    public boolean visits(TacoSpot spot) {
        return path.contains(spot);
    }
    
    
    public double addTacoSpot(TacoSpot spot) {
        path.add(spot);
        return score();
    }
    
    public TacoSpot getCurrentTacoSpot() {
        TacoSpot lastTacoSpot = null;
        if (!path.isEmpty()) {
            lastTacoSpot = path.get(path.size() - 1);
        }
        return lastTacoSpot;
    }
    
    public double score() {
        if (path.isEmpty()) { return 0.0; }
        
        double totalTastiness = calculateTotalTastiness();
        double totalDistance = calculateTotalDistance();
        return totalTastiness / totalDistance;
    }
    
    // Don't count the origin (starting) point
    public int length() {
        return path.size() - 1;
    }
    
    private double calculateTotalTastiness() {
        return path.stream().mapToDouble(TacoSpot::tastiness).sum();
    }
    
    private double calculateTotalDistance() {
        double totalDistance = 0.0;
        Point start = new Point(0, 0); // Starting point
        for (int i = 0; i < path.size(); i++) {
            Point end = path.get(i).location();
            totalDistance += start.distance(end);
            start = end;
        }
        return totalDistance;
    }
    
    public String toString() {
        StringBuilder sb = new StringBuilder();
        sb.append("Crawl: [%4.3f = %6.1f / %6.3f] l=%d path: ".formatted(score(), calculateTotalTastiness(), calculateTotalDistance(), length()));
        for (int i = 0; i < path.size(); i++) {
            sb.append(path.get(i).id());
            if (i < path.size() - 1) {
                sb.append(" -> ");
            }
        }
        return sb.toString();
    }

}

TacoSpot.java

package org.sceaj.stackoverflow.challenge11;

public record TacoSpot(int id, Point location, int tastiness) {
    
    public double valueMetricFromPoint(Point p) {
        double distance = location.distance(p);
        return tastiness / distance;
    }
}

Point.java

package org.sceaj.stackoverflow.challenge11;

public record Point(int x, int y) {
    /**
     * Calculate the Euclidean distance between this point and another point.
     * 
     * @param other The other point to which the distance is calculated.
     * @return The Euclidean distance between the two points.
     */
    public double distance(Point other) {
        return Math.hypot(other.x - this.x, other.y - this.y);
    }
}
79794391
2

EDIT 1: just realised the question said "up to 8" so I redid it as I previously thought you must visit 8 spots.

My approach is simply computing the score for each permutation, by visiting 1 to 8 spots.

My result is

Best routing: [7, 5, 6, 2, 10, 4]
Tastiness: 97
Total distance: 33.51165531060739
Tastiness per distance: 2.8945153290979557
Got results in 8.036053657531738 seconds

My code is available on github at https://github.com/genevieve-le-houx/SO_challenge_11_taco_crawl_optimization

My code in Python is

import csv
import itertools
import math
import time
from dataclasses import dataclass, field
from pathlib import Path
from typing import List, Tuple

from dataclasses_json import dataclass_json, config



@dataclass_json
@dataclass
class TacoSpot:
    number: int = field(metadata=config(field_name="TacoSpot"))
    x: int
    y: int
    tastiness: int = field(metadata=config(field_name="Tastiness"))


starting_spot = TacoSpot(0, 0, 0, 0)


def get_taco_spots(filepath: Path) -> List[TacoSpot]:
    data: List[TacoSpot] = []

    with open(filepath, "r") as file:
        reader = csv.DictReader(file)
        for line in reader:
            data.append(TacoSpot.from_dict(line))

    return data


def get_distance_between_spots(spot1: TacoSpot, spot2: TacoSpot) -> float:
    return math.sqrt((spot1.x - spot2.x)**2 + (spot1.y - spot2.y)**2)


def get_permutation_score(permutation: Tuple[TacoSpot, ...]) -> Tuple[float, int, float]:
    total_tastiness = sum(x.tastiness for x in permutation)

    # One start at (0, 0) so add this distance first
    total_distance = get_distance_between_spots(permutation[0], starting_spot)

    # Add the distance of the others
    for i, taco_spot in enumerate(permutation[1:], 1):
        total_distance += get_distance_between_spots(taco_spot, permutation[i - 1])

    return total_tastiness / total_distance, total_tastiness, total_distance


def get_best_permutation(permutations: List[Tuple[TacoSpot, ...]]) -> Tuple[Tuple[TacoSpot, ...], float, int, float]:
    scores, tastiness, distance = zip(*map(get_permutation_score, permutations))

    max_score_index = scores.index(max(scores))

    return (
        permutations[max_score_index], scores[max_score_index], tastiness[max_score_index],
        distance[max_score_index]
    )


def main():
    tic = time.time()
    data = get_taco_spots(Path("tacos_data.csv"))


    all_routing = []
    all_scores = []
    all_tastiness = []
    all_distances = []
    for n in range(1, 9):
        permutations = list(itertools.permutations(data, n))
        best_routing, max_score, tastiness, distance = get_best_permutation(permutations)

        all_routing.append(best_routing)
        all_scores.append(max_score)
        all_tastiness.append(tastiness)
        all_distances.append(distance)

    best_index = all_scores.index(max(all_scores))

    best_routing = all_routing[best_index]
    best_score = all_scores[best_index]
    best_tastiness = all_tastiness[best_index]
    best_distance = all_distances[best_index]

    print(f"Best routing: {[x.number for x in best_routing]}")
    print(f"Tastiness: {best_tastiness}")
    print(f"Total distance: {best_distance}")
    print(f"Tastiness per distance: {best_score}")

    tac = time.time()

    print(f"Got results in {tac - tic} seconds")


if __name__ == '__main__':
    main()

I tried to optimize by choosing the best spot at each step, but realized I could fall in local minima. So I ended up doing all the possible permutations.

79793969
6

Brute force solution using SIMD.

This overuses 128bit registers where 64bit would suffice, but I don't have time to clean it up.

AI generated python script solves this in ~7.5s, this solution takes ~75ms (100x faster) and isn't optimised.

Tested on g++ on linux.

Best distance = 33.5117
Best tastiness = 97
Best value = 2.89452
Best path len = 6
Best path = [7, 5, 6, 2, 10, 4]

Compile with:

g++ -Wall -Wextra -pedantic -O3 -std=c++23 -ggdb3 -mavx512f -mavx512vl -mavx512bw -mavx512vbmi -mavx512vbmi2 -mavx512dq -o crawl crawl.cpp

Code:

#include <iostream>
#include <algorithm>
#include <vector>
#include <array>
#include <span>
#include <optional>
#include <immintrin.h>
#include <emmintrin.h>
#include <climits>

using B = std::byte;
using Arr = std::array<B, 10>;

std::ostream& operator<<(std::ostream& os, const B& b) {
    os << (int)b;
    return os;
}

template <typename T, std::size_t N>
std::ostream& operator<<(std::ostream& os, const std::array<T, N>& arr) {
    os << "[";
    for (std::size_t i = 0; i < N; ++i) {
        os << arr[i];
        if (i != N - 1) {
            os << ", ";
        }
    }
    os << "]";
    return os;
}

template <typename T>
std::ostream& operator<<(std::ostream& os, const std::span<T>& arr) {
    os << "[";
    for (std::size_t i = 0; i < arr.size(); ++i) {
        os << arr[i];
        if (i != arr.size() - 1) {
            os << ", ";
        }
    }
    os << "]";
    return os;
}

// Only sums least significant 8 values
static inline __attribute__((always_inline))
__m128i plus_scan_epi8(__m128i v0)
{
    __m128i shift = _mm_set_epi8(-1, -1, -1, -1, -1, -1, -1, -1, 6, 5, 4, 3, 2, 1, 0, -1);
    __m128i v1 = _mm_shuffle_epi8(v0, shift);
    __m128i v2 = _mm_shuffle_epi8(v1, shift);
    __m128i v3 = _mm_shuffle_epi8(v2, shift);
    __m128i v4 = _mm_shuffle_epi8(v3, shift);
    __m128i v5 = _mm_shuffle_epi8(v4, shift);
    __m128i v6 = _mm_shuffle_epi8(v5, shift);
    __m128i v7 = _mm_shuffle_epi8(v6, shift);
    return v0 + v1 + v2 + v3 + v4 + v5 + v6 + v7;
}

static inline __attribute__((always_inline))
__m128i plus_scan_epi16(__m128i v0)
{
    __m128i shift = _mm_set_epi8(13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0, -1, -1);
    __m128i v1 = _mm_shuffle_epi8(v0, shift);
    __m128i v2 = _mm_shuffle_epi8(v1, shift);
    __m128i v3 = _mm_shuffle_epi8(v2, shift);
    __m128i v4 = _mm_shuffle_epi8(v3, shift);
    __m128i v5 = _mm_shuffle_epi8(v4, shift);
    __m128i v6 = _mm_shuffle_epi8(v5, shift);
    __m128i v7 = _mm_shuffle_epi8(v6, shift);
    return v0 + v1 + v2 + v3 + v4 + v5 + v6 + v7;
}

static inline __attribute__((always_inline))
__m256 plus_scan_ps(__m256 v0)
{
    __m256 zero = _mm256_setzero_ps();
    __m256i shift = _mm256_set_epi32(6, 5, 4, 3, 2, 1, 0, 0);
    __m256 v1 = _mm256_blend_ps(_mm256_permutevar8x32_ps(v0, shift), zero, 0x01);
    __m256 v2 = _mm256_blend_ps(_mm256_permutevar8x32_ps(v1, shift), zero, 0x01);
    __m256 v3 = _mm256_blend_ps(_mm256_permutevar8x32_ps(v2, shift), zero, 0x01);
    __m256 v4 = _mm256_blend_ps(_mm256_permutevar8x32_ps(v3, shift), zero, 0x01);
    __m256 v5 = _mm256_blend_ps(_mm256_permutevar8x32_ps(v4, shift), zero, 0x01);
    __m256 v6 = _mm256_blend_ps(_mm256_permutevar8x32_ps(v5, shift), zero, 0x01);
    __m256 v7 = _mm256_blend_ps(_mm256_permutevar8x32_ps(v6, shift), zero, 0x01);
    return v0 + v1 + v2 + v3 + v4 + v5 + v6 + v7;
}

int main()
{
    Arr bytes = {B{0}, B{1}, B{2}, B{3}, B{4}, B{5}, B{6}, B{7}, B{8}, B{9}};

    std::vector<Arr> permutations;
    permutations.reserve(3628800); // 10!

    do {
        permutations.push_back(bytes);
    } while (std::next_permutation(bytes.begin(), bytes.end()));

    Arr Xs = {B{2}, B{15}, B{6}, B{20}, B{11}, B{14}, B{7}, B{1}, B{3}, B{14}};
    Arr Ys = {B{8}, B{3}, B{6}, B{15}, B{2}, B{1}, B{3}, B{12}, B{6}, B{13}};
    Arr Tastiness = {B{7}, B{17}, B{8}, B{30}, B{12}, B{15}, B{5}, B{12}, B{3}, B{18}};

    __mmask16 mask8 = (1 << 8) - 1;
    __mmask16 mask10 = (1 << 10) - 1;
    __m128i zero = _mm_set1_epi8(0);
    __m256 zerof = _mm256_set1_ps(0.0f);

    __m128i xs = _mm_mask_loadu_epi8(zero, mask10, Xs.data());
    __m128i ys = _mm_mask_loadu_epi8(zero, mask10, Ys.data());
    __m128i tastiness = _mm_mask_loadu_epi8(zero, mask10, Tastiness.data());

    std::optional<size_t> best_n;
    float best_val = 0.0f;
    float best_dist = float(INT_MAX);
    int best_t = INT_MAX;
    int best_len = 0;

    for (size_t n = 0; n < permutations.size(); n++)
    {
        __m128i path = _mm_mask_loadu_epi8(zero, mask8, permutations[n].data());
        __m128i x = _mm_mask_permutexvar_epi8(zero, mask8, path, xs);
        __m128i y = _mm_mask_permutexvar_epi8(zero, mask8, path, ys);
        __m128i t = _mm_mask_permutexvar_epi8(zero, mask8, path, tastiness);
        __m128i shift = _mm_set_epi8(-1, -1, -1, -1, -1, -1, -1, -1, 6, 5, 4, 3, 2, 1, 0, -1);
        __m128i x0 = _mm_shuffle_epi8(x, shift);
        __m128i y0 = _mm_shuffle_epi8(y, shift);
        __m128i dx = _mm_sub_epi8(x, x0);
        __m128i dy = _mm_sub_epi8(y, y0);
        __m128i tt = plus_scan_epi8(t);
        __m128i xx = _mm_cvtepi8_epi16(dx);
        __m128i yy = _mm_cvtepi8_epi16(dy);
        __m128i x2 = _mm_mullo_epi16(xx, xx);
        __m128i y2 = _mm_mullo_epi16(yy, yy);
        __m128i x2y2 = _mm_add_epi16(x2, y2);
        __m256 x2y2f = _mm256_cvtepi32_ps(_mm256_cvtepi16_epi32(x2y2));
        __m256 sqrts = _mm256_sqrt_ps(x2y2f);
        __m256 tdist = plus_scan_ps(sqrts);
        __m256 ttf = _mm256_cvtepi32_ps(_mm256_cvtepi8_epi32(tt));
        __m256 v = _mm256_div_ps(ttf, tdist);
        __m256 bests = _mm256_set1_ps(best_val);
        __mmask8 m = _mm256_cmp_ps_mask(bests, v, _CMP_LT_OS);
        unsigned k = _cvtmask8_u32(m);
        int bits = _mm_popcnt_u32(k);
        __m256 vc = _mm256_mask_compress_ps(zerof, m, v);
        __m256 tdistc = _mm256_mask_compress_ps(zerof, m, tdist);
        __m128i ttc = _mm_mask_compress_epi8(zero, m, tt);
        for (int b = 0; b < bits; b++)
        {
            int len = _bit_scan_forward(k);
            (k) &= ~(1ULL << (len));
            alignas(32) float vals[8]; _mm256_store_ps(vals, vc);
            alignas(32) float tdists[8]; _mm256_store_ps(tdists, tdistc);
            std::byte tts[16]; _mm_storeu_epi8(tts, ttc);
            float val = vals[b];
            best_len = val > best_val ? len : best_len;
            best_n = val > best_val ? n : best_n;
            best_dist = val > best_val ? tdists[b] : best_dist;
            best_t = val > best_val ? int(tts[b]) : best_t;
            best_val = std::max(best_val, val);
        }
    }
    if (best_n)
    {
        Arr path = permutations[best_n.value()];
        for (auto& p : path) p = (std::byte)((int)p+1);
        std::cout << "Best n =  " << best_n.value_or(-1) << "\n"
            << "Best distance = " << best_dist << "\n"
            << "Best tastiness = " << best_t << "\n"
            << "Best val = " << best_val << "\n"
            << "Best len = " << (best_len+1) << "\n"
            << "Path = " << (std::span<B>(path.data(), best_len+1)) << "\n";
    }
}
79795636
0

Does single core fully utilize RAM bandwidth? Can it scale to multiple cores?

79795672
1

The SIMD code is an order of magnitude slower than a simple, non-optimised solution in C# (5 ms). Can you refine it to make the CPU burn? :-)

Bonus points for working several parts of the search space in parallel at the same time (like 4 or 8 streams). It's SIMD after all. And then load that beast on all CPU cores in parallel. That should be a marvellous monster that no-one else here could possible beat.

Using 32-bit floats for tastiness, distance and scores should be fine for this particular problem; I've tested this thoroughly.

79795989
0

@DarthGizka I wouldn't call your solution non-optimised. You pre-compute distances, which pretty important optimisation. It would likely improve my solution too. I don't have the time to do it as part of this challenge though. I don't really understand what you mean by 'working several parts of the search space in parallel'. I don't think using more cores would help here, because the data set is way too small.

79796504
1

Imagine 8 separate crawls. One starts with the first spot, the next with the second spot on so on. These crawls clearly operate independently and could be run concurrently.

Now imagine that the variables used by these 8 separate crawls were packed into SIMD words. One SIMD word might contain the current spot index for each of the 8 crawls, another might hold 8 distance values and so on. That way, executing a single SIMD instruction would operate on 8 crawls at the same time. That's what SIMD is all about in the first place.

Certain parts of the algorithm will have to be modified in order to 'SIMDise' it.

For example, instead of putting spot numbers into the working array (requiring a memory lookup for retrieving the tastiness from the spot array) one might put 16-bit or 32-bit words in there that hold both the spot number and the tastiness. This is doable because a tastiness values easily fit into a byte and the some holds for spot numbers; those actually require only a nibble instead of a whole byte. Doing it this way trades the memory lookup for 2 MOVZX type operations that the CPU can execute in the same cycle (1 for unpacking the spot number and one for unpacking the tastiness).

Memo to self: avoiding the tastiness lookup could be beneficial even for non-SIMD code. Investigate. :-)

79797180
0

@DarthGizka I'm still not sure I understand. Isn't that what I'm doing? (except for the byte packing). One register holds 8 spot numbers, another one 8 relative distances calculated from x,y pairs. The third one holds tastiness values for the selected spots. Basically one register holds the whole path from beginning to the end.

79797477
1

Take a version of the algorithm that 'walks' the problem space and records new best routes whenever they are encountered, instead of recording all possible routes/paths into a std::vector and then looping over that. You can take the simple_crawl_() function from my solution, for example.

Then think about it like this. When the first two spots have been chosen then the normal, 'single-barrelled' algorithm will loop over the reminaing eight spots, performing a series of steps on the chosen spot and then do the same for the next spot and so on.

At this point you can take the remaining eight spots and stuff them into a SIMD/AVX word (and adapting the rest of data as well, of course). Then you perform the rest of the steps analogous to the single-lane case once, except that each step processes a vector of eight values instead of a single value. The SIMD/AVX word is like the cross-section of eight different threads of execution.

One instruction adds the tastinesses of the 8 current spots to the 8 tastinesses accumulated so far in these 8 different paths. The spot numbers will have to be unpacked to perform the distance lookups and the 8 resulting distances will have to be merged into a SIMD/AVX word so that the rest can continue in vectorised fashion. Then one instruction adds these 8 distances to the 8 accumulated distances. Then this 8-distance SIMD word is multiplied by a scalar - the best score so far - and the result can be compared to the tastiness vector. I.e. the 8-lane equivalent of

if (tastiness > best_score * distance)
{
   // handle new best score
}

The branch body needs to be entered only when the result mask shows that at least one of the comparisons resulted in true, which means that at least one new best score was found. This is exceedingly rare, so the branch body can be as messy as it wants without affecting overall performance.

Does that render the idea?

79797786
1

Instead of a lengthy explanation, here's a draft 256-bit SIMD rendition (8 lanes) of my simple_crawl_() function.

internal void crawl_SIMD_ (
        ulong[] spot_indices, // using ulong as Vector64<byte>
        Vector256<float>[] taste_values, 
        Vector256<float> tastiness,
        Vector256<float> distance,
        ConfinedMutableResult local_result,
        int step )
{
    var previous_spots8 = spot_indices[step - 1];  // needed for computing the distances

    for (int i = step, n = m_spot_count; i <= n; ++i)
    {
        var current_spots8 = spot_indices[i];       
        var current_taste8 = tastiness + taste_values[i];

        swap_(ref spot_indices[step], ref spot_indices[i]);
        swap_(ref taste_values[step], ref taste_values[i]);
        
        Vector256<float> current_distance8 = Vector256.Create(
            distance32_between_spots_((byte) (previous_spots8 >>  0), (byte) (current_spots8 >>  0)),
            distance32_between_spots_((byte) (previous_spots8 >>  8), (byte) (current_spots8 >>  8)),
            distance32_between_spots_((byte) (previous_spots8 >> 16), (byte) (current_spots8 >> 16)),
            distance32_between_spots_((byte) (previous_spots8 >> 24), (byte) (current_spots8 >> 24)),
            distance32_between_spots_((byte) (previous_spots8 >> 32), (byte) (current_spots8 >> 32)),
            distance32_between_spots_((byte) (previous_spots8 >> 40), (byte) (current_spots8 >> 40)),
            distance32_between_spots_((byte) (previous_spots8 >> 48), (byte) (current_spots8 >> 48)),
            distance32_between_spots_((byte) (previous_spots8 >> 56), (byte) (current_spots8 >> 56)) );
        var comparand = current_distance8 * (float) local_result.BestScore;
//-     var result_bits = Avx.Compare(current_taste8, comparand, FloatComparisonMode.OrderedGreaterThanNonSignaling);
        var result_lo = Avx.Compare(current_taste8.GetLower(), comparand.GetLower(), FloatComparisonMode.OrderedGreaterThanNonSignaling);
        var result_hi = Avx.Compare(current_taste8.GetUpper(), comparand.GetUpper(), FloatComparisonMode.OrderedGreaterThanNonSignaling);

        if (result_lo != Vector128<float>.Zero || result_hi != Vector128<float>.Zero)
        {
            // ...
        }

        if (step < m_max_spots_to_visit)
            crawl_SIMD_(spot_indices, taste_values, tastiness, distance, local_result, step + 1);
        else
            ++local_result.RoutesTried;

        swap_(ref spot_indices[step], ref spot_indices[i]);
        swap_(ref taste_values[step], ref taste_values[i]);
    }
}

As you can see, it looks almost exactly like the scalar code but uses SIMD to crawl 8 different routes in parallel.

79802237
0

@DarthGizka gotcha, thanks for explanation.

79793803

This reply has been deleted.

79796048
1

Nandini, please review your code. It does not find the correct solution.

79793274
2

Answers:

The optimal routing I determined is: 7, 5, 6, 2, 10, 4

The total "Tastiness" achieved is: 97

The total distance travelled is approximately: 33.511655

The "tastiness per distance" score for this route is approximately: 2.894515

Here is my code, in C:

#include <stdio.h>
#include <math.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdlib.h>
#include <strings.h>
#include <time.h>

#define NUM_SPOTS 11 //10 actual spots, plus "origin" of 0,0
#define MAX_CRAWL_LOCATIONS 8 //Maximum number of locations to visit on crawl

typedef struct coords
{
    double X;
    double Y;
} Coordinates;

typedef struct spot
{
    int index;
    Coordinates location;
    int tastiness;
} TacoSpot;

typedef struct crawl
{
    TacoSpot spots[MAX_CRAWL_LOCATIONS + 1];
    int count;
    
} SpotPath;

SpotPath bruteForce(const TacoSpot* spotTable, int numSelectable, int maxSelections);

double score(TacoSpot A, TacoSpot B);
double distance(Coordinates A, Coordinates B);
int factorial(int n);
int permutations(int n, int r);
double crawlscore(const TacoSpot* crawlTable, int numEntries, bool showall);

void factTest();
void permTest();
void printCoords(Coordinates theseCoords, bool lb);
void printSpot(TacoSpot thisSpot);
void printSpotTable(const TacoSpot* thisTable, int numEntries);

const TacoSpot allSpots[NUM_SPOTS] = 
{
    { 0, {0,0}, 0},
    { 1, {2,8}, 7 },
    { 2, {15,3}, 17 },
    { 3, {6,6}, 8 },
    { 4, {20,15}, 30 },
    { 5, {11,2}, 12 },
    { 6, {14,1}, 15 },
    { 7, {7,3}, 5 },
    { 8, {1,12}, 12 },
    { 9, {3,6}, 3 },
    { 10, {14,13}, 18 },
};

int main()
{
    clock_t start_clk;
    clock_t end_clk;
    clock_t elapsed_clk;
    double elapsed_time;
    SpotPath bruteCrawl;

    printf("Spot Table to be analyzed:\n");
    
    printSpotTable( allSpots, NUM_SPOTS );
    printf("\n");

    //**Brute force method**//
    start_clk = clock();
    bruteCrawl = bruteForce( allSpots, NUM_SPOTS, MAX_CRAWL_LOCATIONS);
    end_clk = clock();
    
    elapsed_clk = end_clk - start_clk;
    elapsed_time = (double)elapsed_clk / (double)CLOCKS_PER_SEC;
    
    printf("Brute force best path is:\n");
    printSpotTable(bruteCrawl.spots, bruteCrawl.count + 1);
    printf( "Score: %f\n", crawlscore( bruteCrawl.spots, bruteCrawl.count, true ) );

    printf("Elapsed Brute Force time: %f\n", elapsed_time);

}

SpotPath bruteForce(const TacoSpot* spotTable, int numSelectable, int maxSelections)
{
 
    int p;
    int i;

    SpotPath thisPath;
    SpotPath thisbestPath;
    SpotPath bestPath;
    
    double bestScore;
    double thisbestScore;
    double thisScore;
    double checkProgress;
    double newProgress;
    double thisProgress;
    
    bool available[numSelectable];
    bool increment;
    bool docalculation;
    
    int elements[maxSelections + 1]; //Rather than recursive loops, will track elements within loops within loops
    
    int numPerms; //Number of Permutations
    
    //Set the 0 index to the (assumed) origin
    thisPath.spots[0] = spotTable[0];
    elements[0] = 0; //0th element is always 0 (home/origin, {0,0})
    available[0] = false; //0th element is never "available"
    
    bestScore = 0;
    for( thisPath.count=1; thisPath.count<=maxSelections; thisPath.count++ )
    {
        
        printf("Stop Count: %d\n", thisPath.count);

        for(i=1; i<thisPath.count; i++)
        {
         
            elements[i] = 1;
            
        }
        elements[thisPath.count] = 0; //Set last element to 0 as it will auto increment to 1
        
        //Not a true number of permutations - includes replaceable / repeatable values
        numPerms = pow( numSelectable - 1, thisPath.count);
        checkProgress = 0;
        thisbestScore = 0; //Reset best score for this set      
        for( p=1; p<=numPerms; p++)
        {
         
            //assume we will do the calculation unless told otherwise
            docalculation = true;
            
            //Reset availability for each Permutation
            for(i=1; i<numSelectable; i++)
            {
             
                available[i] = true;
                
            } //for i=1 to <numSelectable
         
            //This increments only the appropriate numbers
            increment = true;
            i = thisPath.count;
            while ( i >= 1 )
            {
               
                if (increment)
                {
                    
                    if ( elements[i] < ( numSelectable - 1 ) )
                    {   //Roll over if max reached
                        elements[i]++;
                        increment = false;
                    }
                    else
                    {
                        elements[i] = 1;
                    }
                    
                }
           
                if ( available[elements[i]] )
                {
                    available[elements[i]] = false;  
                }
                else
                {
                    //If doubling value ("not available") then don't calculate
                    docalculation = false;
                }         
                
                i--;
               
            }
            
            if (docalculation)
            {
                for( i=1; i<=thisPath.count; i++ )
                {
             
                    thisPath.spots[i] = spotTable[elements[i]];
                    
                }
                thisScore = crawlscore(thisPath.spots, thisPath.count, false);
                
                if (thisScore > thisbestScore)
                {
                    thisbestPath = thisPath;
                    thisbestScore = thisScore;
                    //printf("New Highest Score for Count! %f\n", thisbestScore);
                    //printf("Permutation #%d/%d:\n", p, numPerms);
                    //printSpotTable(thisbestPath.spots, thisbestPath.count + 1);
                } //if new best score for this number of stops
                
            } //If do calculation
            
            thisProgress = (double)p/(double)numPerms * 100;
            newProgress = checkProgress + 10;
            
            if ( thisProgress > newProgress )
            {
                printf("Progress at: %f%%    ", thisProgress );
                printf("Permutation #%d/%d:\n", p, numPerms);
                checkProgress = newProgress;
            }
            
            
        } //for permutations (p) = 1 to total

        //printf("Finished Permutations\n");
        //printf("Final Permutation Check:\n");
        //printSpotTable(thisPath.spots, thisPath.count + 1);
        //printf("Score: %f\n", thisScore);
        
        if (thisbestScore > bestScore)
        {
         
            bestPath = thisbestPath;
            bestScore = thisbestScore;
            //printf("New Overall High Score! %f\n", bestScore);
            //Print report
            //printSpotTable(bestPath.spots, bestPath.count + 1);
            
        } //If new best score
        
        printf("Best Score for %d stops:\n", thisPath.count);
        printSpotTable(thisbestPath.spots, thisbestPath.count + 1);
        printf( "Score: %f\n", crawlscore( thisbestPath.spots, thisbestPath.count, true ) );    
        
        printf("\n");
    
    }
    
    return bestPath;
    
}

double crawlscore(const TacoSpot* crawlTable, int numEntries, bool showall)
{
    int n;
    double thisDistance = 0;
    double myDistance = 0;
    double myTastiness = 0;
    
    for (n=1; n<=numEntries; n++)
    {
        thisDistance = distance(crawlTable[n-1].location, crawlTable[n].location);
        if (thisDistance > 0) //In case we accidentally put the same destination in twice
        {
            myDistance += thisDistance;
            myTastiness += crawlTable[n].tastiness;
        }
    }

    if (myDistance > 0)
    {
        if (showall) 
        {
            printf("Total Tastiness: %f\n", myTastiness);
            printf("Total Distance: %f\n", myDistance);
        }
        return myTastiness / myDistance;
    }
    else
    {
        return 0;
    }

}

double score(TacoSpot A, TacoSpot B)
{

    return B.tastiness / distance(A.location, B.location);
    
}


double distance(Coordinates A, Coordinates B)
{
    
    return sqrt( pow( B.X - A.X, 2 ) + pow( B.Y - A.Y, 2 ) );
    
}

int factorial(int n)
{
    
    int i;
    int f;
    
    //By definition 0! = 1, also 1 makes first multiplication identity
    f = 1;
    
    for (i=n; i>1; i--)
    {
        f = f * i;
    }
    
    return f;
    
}

int permutations(int n, int r)
{
 
    if ( r <= n )
    {
        
        return ( factorial(n) / factorial(n-r) );
    
    }
    else
    {
     
        return 0; //Permutations not possible
        
    }
       
}

void printCoords(Coordinates theseCoords, bool lb)
{
    if (lb)
    {
        printf("( %f, %f )\n", theseCoords.X, theseCoords.Y);
    }
    else
    {
        printf("( %f, %f )", theseCoords.X, theseCoords.Y);
    }
}

void printSpot(TacoSpot thisSpot)
{
    printf("%d    ", thisSpot.index);
    printCoords(thisSpot.location, false);
    printf("    %d", thisSpot.tastiness);
    printf("\n");
}

void printSpotTable(const TacoSpot* thisTable, int numEntries)
{
    int n;
    
    for(n=0; n<numEntries; n++)
    {
        printSpot(thisTable[n]);
    }
    
}

Interesting Challenges:

Your wording of "up to 8 taco spots" added a dimension of challenge to this, as I had to account for making paths of varying numbers of stops and not a fixed number of stops.

Things I learned:

Big thing is if a challenge says "brute force is acceptable" then the brute force method probably isn't as easy as you think it will be! I initially intended to do brute force first, then make a more elegant solution that I could check against the brute force method. Unfortunately, the brute force method took a longer time than expected and I did not have enough time to go for a more elegant solution, so that is what I'm delivering!

Also, "brute brute force" is sometimes easier and faster than a solution that appears to be a slightly more elegant brute force. Since this involved doing loops of 1->8 different locations, I didn't actually do 8 loops, but rather 2 loops, one a loop that incremented how many spots would be in a given path, then the second loop that tracked up to 8 loop variables in an array. Each main loop increased the number of variables and left unused variables as 0. I first had the "genius" idea to use my knowledge of permutations to cut the number of loops in the second loop short by using the actual permutations formula (n!/(n-r)!) to determine my number of loops in a master loop, then automatically incrementing past the "invalid" values before completing each loop. However, I found this required the logic for the final loop of each length to be very convoluted and also took extra processing time each loop. It was simpler to just do n^r total loops, incrementing one variable once and rolling over and incrementing others as appropriate. Then, at the start of the loop, determining if a permutation was valid (e.g. no duplicates) and if not valid, just go straight to the end of the loop and start a new one which required relatively trivial additional computation time each loop to determine validity.

This brute force method ran faster than I was expecting. I ran it remotely on the codehs server as I can't locally compile and run on the machine I was using, and it generally finished in 8-10 seconds. That being said, for something like a GPS application that would have to go through hundreds or thousands of nodes to determine an optimal route, you would need a much more elegant solution. I assume this challenge came about due to the recent implementation of a new GPS algorithm.

FYI since I brute forced it, I was able to get data on all path lengths from 1->8 spots along the way. Here are the best paths for each length / number of stops:

Number of Stops: 1

Path: 4

Tastiness: 30

Distance: 25

Score: 1.2

Number of Stops: 2

Path: 6, 2

Tastiness: 32

Distance: 16.271737

Score: 1.9666

Number of Stops: 3

Path: 5, 6, 2

Tastiness: 44

Distance: 16.578686

Score: 2.654

Number of Stops: 4

Path: 7,5,6,2

Tastiness: 49

Distance: 17.137224

Score: 2.859273

Number of Stops: 5

Path: 5,6,2,10,4

Tastiness: 92

Distance: 32.953116

Score: 2.791845

Number of Stops: 6

Path: 7,5,6,2,10,4

Tastiness: 97

Distance: 33.511655

Score: 2.894515

Number of Stops: 7

Path: 3,7,5,6,2,10,4

Tastiness: 105

Distance: 37.543441

Score: 2.79676

Number of Stops: 8

Path: 9,3,7,5,6,2,10,4

Tastiness: 108

Distance: 38.766364

Score: 2.78592

79793192
1

Given the size of the problem, as hinted in the problem description, it's possible to solve via brute force. We will also use the brute force to validate the solution.

(edited after submission -- noticed that I was using squared distance)

(edit 2 after submission -- up to 8 locations )

Visualization and intuition

Looking at the data, with this snippet to visualize the points:

import matplotlib.pyplot as plt
# List of tuple - x, y, tastiness, label
COORDS_AND_TASTINESS: list[tuple[int, int, int, int]] = [
    ( 2,  8,  7, 1),
    # put here the other 9
]

x = [coord[0] for coord in COORDS_AND_TASTINESS]
y = [coord[1] for coord in COORDS_AND_TASTINESS]
tastiness = [coord[2] for coord in COORDS_AND_TASTINESS]

plt.scatter(x, y, s=[t * 10 for t in tastiness])
for x, y, t, label in COORDS_AND_TASTINESS:
    plt.text(x, y, f"{label}. {t}", fontsize=9, ha='right')

We notice that the two points with the highest tastiness (30 and 18) are far from the other points.

So, it's likely that a permutation of the other 8 points would give the right solution.

Eval function

Per problem statement, the journey starts at (0, 0) and ends at the 8th venue.

def evaluate_sequence(coords: list[tuple[int, int, int]]) -> float:
    total_distance = 0.
    total_tastiness = 0.
    for c1, c2 in zip([(0, 0, 0)] + coords, coords):
        total_distance += ((c1[0] - c2[0]) ** 2 + (c1[1] - c2[1]) ** 2) ** 0.5
        total_tastiness += c2[2]
    # from the data, total distance is > 0.
    return total_tastiness / total_distance 

Brute force solution

itertools.permutations (documentation) supports getting all permutations of a subset of a given list (i.e. 8 out of 10). For the brute-force solution, we just rely on itertools, to evaluate all the possible permutations of 8 elements:

def brute_force_baseline():
    # returns a list of tuples of (x, y, tastiness)
    coords_and_tastiness = get_coords()
    best_score = 0.
    best_sequence = []
    for perm in tqdm(itertools.permutations(range(0, len(coords_and_tastiness)), 8)):
        sequence = [coords_and_tastiness[i] for i in perm]
        score = evaluate_sequence(sequence)
        if score > best_score:
            best_score = score
            best_sequence = sequence
    print(f"Best score: {best_score} with sequence {best_sequence}")

tqdm gives the number of iterations and approximate time to run. Bruteforce approach: 1814400 iterations (takes ~5s on i7 laptop). That's about 8! * (10, 8) = 8! * 10 * 9 / 2 = 8! * 45.

Total tastiness=108
Total distance=38.766
Score=2.786
Indices=[9, 3, 7, 5, 6, 2, 10, 4]

After looking through other solutions and reading (again) the instructions, we need to put the brute force solution in a for loop, and add a param for number of points to consider:

for max_limit in range(1, 9):
    print(f"Brute force with limit {max_limit}:")
    score, sequence = brute_force_baseline(limit=max_limit)
    if score > best_score:
        best_score = score
        best_sequence = sequence.copy()
print(f"Overall best score from brute force: {best_score} with sequence {best_sequence}")

tastiness, distance, final_score, indices = get_eval_metrics(list(best_sequence))
print(f"Eval metrics: {tastiness=}\n {distance=:.3f}\n{final_score=:.3f}\n {indices=}\n============ ")

Results (a shorter sequence is better):

Overall best score from brute force: 2.8945153290979557 with sequence [(7, 3, 5, 7), (11, 2, 12, 5), (14, 1, 15, 6), (15, 3, 17, 2), (14, 13, 18, 10), (20, 15, 30, 4)]

Total tastiness = 97.0
Total distance = 33.512
Final_score = 2.895
Indices=[7, 5, 6, 2, 10, 4]

Exclude farthest 2

(incorrect initial intuition) Based on looking at the data, we exclude the 2 furthest points, and try all permutations of remaining points.

def permutation_cluster():
    coords_and_tastiness = get_coords()
    # exclude the largest 2 by tastiness which are further.
    coords_and_tastiness = sorted(coords_and_tastiness, key=lambda x: x[2])[:8]
    best_score = 0.
    for perm in tqdm(itertools.permutations(coords_and_tastiness)):
        score = evaluate_sequence(list(perm))
        if score > best_score:
            best_score = score
            best_sequence = perm
    print(f"Best clustered permutation score: {best_score} with sequence {best_sequence}")

per tqdm, it takes 40320 iterations, i.e. 8! and takes less than 1s. It's ~45 times faster than brute force solution.

Total tastiness=79.0
Total distance=33.561
Final_score=2.353
Indices=[9, 1, 8, 3, 7, 5, 6, 2]

NOTE: The intuition that the furthest points should be excluded was incorrect.

Nearest neighbor heuristic

From looking at the visualization, one can think of picking always the nearest point, which we didn't visit yet.

def nearest_neighbor_heuristic():
    coords_and_tastiness = get_coords()
    
    visited = [False] * len(coords_and_tastiness)
    current_pos = (0, 0)

    route = []
    for _ in range(8):
        best_dist = float('inf')
        best_idx = -1
        for idx, (x, y, _) in enumerate(coords_and_tastiness):
            if visited[idx]:
                continue
            dist = (current_pos[0] - x) ** 2 + (current_pos[1] - y) ** 2
            if dist < best_dist:
                best_dist = dist
                best_idx = idx
        if best_idx == -1:
            break
        visited[best_idx] = True
        route.append(coords_and_tastiness[best_idx])
        current_pos = (coords_and_tastiness[best_idx][0], coords_and_tastiness[best_idx][1])
    score = evaluate_sequence(route)
    print(f"Heuristic score: {score} with route {route}")

Initial intuition was not correct. This gives the same result as the 8! permutation, just faster.

Adding a limit and considering shorter sequences doesn't get better scores.

Total tastiness=79.0
Total distance=33.561
Final_score=2.354
Indices=[9, 1, 8, 3, 7, 5, 6, 2]

Learnings

  1. Read the statement carefully; maybe add tests (e.g. for correct distance computation).

  2. Read the statement carefully. I.e. initially missed that there are up to 8 points.

  3. tqdm is great for a quick sense of how long a code snippet would run. 1a. using a progressbar for brute force:

eight_factorial = 40320  # 8!
    # There are C(10,8) = 45 ways to choose 8 out of 10, and for each we have 8! permutations
    with tqdm(total=eight_factorial * 9 * 5) as pbar:
        for perm in itertools.permutations(range(0, len(coords_and_tastiness)), 8):
            sequence = [coords_and_tastiness[i] for i in perm]
            score = evaluate_sequence(sequence)
            if score > best_score:
                best_score = score
                best_sequence = sequence
            pbar.update(1)

len(itertools.permutations... would not work, because it's a generator.

  1. visualizing the data helps getting an intuition into the solution

  2. itertools is useful for dealing with generating permutations and combinations.

79792724
-1

My solution leveraged JavaScript to enumerate all possible solutions:

PathRepresentation {steps: Array(9), ttlLength: 38.76636379741122, ttlTastiness: 108, ttlMetric: 2.78592030360124, fullGraph: {…}}
steps: (9) [0, 9, 3, 7, 5, 6, 2, 10, 4]
ttlLength: 38.76636379741122
ttlMetric: 2.78592030360124
ttlTastiness: 108

Optimised path: [0, 9, 3, 7, 5, 6, 2, 10, 4]
Total tastiness achieved: 108
Total distance travelled: 38.76636379741122
Metric: 2.78592030360124

This code brute forced the solution above:

<html><head><script>
//raw facts
var spotArray = new Array();
spotArray.push([0,0,0,0]);
spotArray.push([1,2,8,7]);
spotArray.push([2,15,3,17]);
spotArray.push([3,6,6,8]);
spotArray.push([4,20,15,30]);
spotArray.push([5,11,2,12]);
spotArray.push([6,14,1,15]);
spotArray.push([7,7,3,5]);
spotArray.push([8,1,12,12]);
spotArray.push([9,3,6,3]);
spotArray.push([10,14,13,18]);

class TacoSpot {
    key=0; x=0; y=0; tastiness=0;
    constructor(key, x, y, tastiness) {
        this.key = key;
        this.x = x;
        this.y = y;
        this.tastiness = tastiness;
    }
    renderEdges(forest){
        var distances = {};
        for (var key in forest) {
            var value = forest[key];
            var local = Math.sqrt(Math.pow(this.x-value.x, 2)+Math.pow(this.y-value.y, 2));
            distances[key] = local;
        }
        this.distances = distances;
    }
}

class PathFinder {
    options = new Array(); fullGraph;
    constructor(fullGraph) {
        this.fullGraph = fullGraph;
    }
    startPath(){
        var visitedSet = new Set();
        visitedSet.add(0);
        
        var currentLoc = this.fullGraph[0];
        var path = new Array();
        path.push(currentLoc.key);

        this.findPath(path, currentLoc, visitedSet, 0);
    }
    findPath(i_path, i_currentLoc, i_visitedSet, steps){
        var path = [...i_path];
        var visitedSet = new Set([...i_visitedSet]);
        
        if(steps > 7){
            this.options.push(path);
            return;
        }
        visitedSet.add(i_currentLoc.key);
        for (var key in i_currentLoc.distances) {
            //walk these paths if...
            //not a location I have visited before on this path
            if(visitedSet.has(parseInt(key))){
                continue;
            }
            //not the current loc
            if(key==i_currentLoc.key){
                continue;
            }
            var currentLoc = this.fullGraph[key];
            path.push(currentLoc.key);
            this.findPath(path, this.fullGraph[key], visitedSet, ++steps);
            path.pop(currentLoc.key);
            steps = steps-1;
        }
        visitedSet.delete(i_currentLoc.key);
    }
}

class PathRepresentation{
    steps = new Array();
    ttlLength = 0;
    ttlTastiness = 0;
    ttlMetric = 0;
    fullGraph = {};
    calculateMetrics(fullGraph, i_path){
        this.steps = i_path;
        for(var i=0;i<i_path.length;i++){
            if(typeof i_path[i-1] !== 'undefined'){
                this.ttlLength = this.ttlLength + fullGraph[i_path[i]].distances[i_path[i-1]];
            }
            this.ttlTastiness = this.ttlTastiness + fullGraph[i_path[i]].tastiness;
        }
        this.ttlMetric = this.ttlTastiness / this.ttlLength;
    }
}

var spotShortlist = {};

document.addEventListener("DOMContentLoaded", init);

function init() {

    //parse raw input to TacoSpot object
    for(var i=0;i<spotArray.length;i++){
        var localTacoSpot = new TacoSpot(spotArray[i][0], spotArray[i][1], spotArray[i][2], spotArray[i][3]);
        spotShortlist[spotArray[i][0]] = localTacoSpot;
    }

    //Render edges for each taco spot to all taco spots
    for (const key in spotShortlist) {
        spotShortlist[key].renderEdges(spotShortlist);
    }

    //Execute pathfinder
    var pathFinder = new PathFinder(spotShortlist);
    pathFinder.startPath();

    //Init defaults
    var best_value = 0;
    var best_option;
    
    //Porject to all taco spots, find the best option
    for(var i=0;i<pathFinder.options.length;i++){
        var localPath = new PathRepresentation();
        localPath.calculateMetrics(spotShortlist, pathFinder.options[i]);
        if(localPath.ttlMetric > best_value){
            best_value = localPath.ttlMetric;
            best_option = localPath;
        }
    }
    console.log(best_option);
}
</script></head><body></body></html>

AI Disclosure:

No LLMs or Artificial Intelligence were conscripted in this life cycle.

Learning opportunities:

This problem is compatible with the elusive Travelling Salesman Problem, albeit tighter restrictions.

Although optimisations could be implemented to improve the resources used throughout the recursion in PathFinder.findPath() (that could have ensured an older device with fewer resources available could have successfully found the optimal solution) the complexity would still be directly related to the input quantity.

79796050
1

Phil, please note that the challenge says 'up to 8'. This means that routes with fewer than 8 steps are also eligible.

Quite few others here - myself included - have stumbled over this little thing and fixed our solutions. ;-)

79797296
1

Thanks DarthGizka, I felt I should avoid a dirty write, but this modification achieves an optimised solution of up to 8 steps:


diff --git a/optimised.html b/optimised.html
index 
--- a/optimised.html
+++ b/optimised.html
@@ -52,10 +52,9 @@ class PathFinder {
     findPath(i_path, i_currentLoc, i_visitedSet, steps){
         var path = [...i_path];
         var visitedSet = new Set([...i_visitedSet]);

+        this.options.push(path);
         if(steps > 7){
         //if(steps > 3){
-            this.options.push(path);
             return;
         }
         visitedSet.add(i_currentLoc.key);

The related best option outputs after 2906.5ms

Path:

PathRepresentation {
    steps: [0, 7, 5, 6, 2, 10, 4], 
    ttlLength: 33.51165531060739, 
    ttlTastiness: 97, 
    ttlMetric: 2.8945153290979557, 
}
79792631
2

Optimal routing: [7, 5, 6, 2, 10, 4]
Total tastiness: 97
Total distance: 33.51166
Highest total tastiness per distance traveled: 2.89452

The code:

using System.Diagnostics;
using System.Linq;

namespace TacoCrawl;

static class Program
{
    private record Spot(int X, int Y, int Tastiness);

    private static readonly Spot[] Spots =
    [
        new Spot(2, 8, 7),
        new Spot(15, 3, 17),
        new Spot(6, 6, 8),
        new Spot(20, 15, 30),
        new Spot(11, 2, 12),
        new Spot(14, 1, 15),
        new Spot(7, 3, 5),
        new Spot(1, 12, 12),
        new Spot(3, 6, 3),
        new Spot(14, 13, 18)
    ];

    private const int MaxPathSize = 8;
    private static readonly Spot Start = new(0, 0, 0);

    private static double _bestMetric;
    private static double _bestDist;
    private static int _bestTaste;
    private static HashSet<int> _bestPath = [];

    private static int Pow2(int x) => x * x;
    private static double Dist(Spot a, Spot b) => Math.Sqrt(Pow2(a.X - b.X) + Pow2(a.Y - b.Y));

    private static void Iterate(HashSet<int> path)
    {
        var prev = Start;
        double totalDist = 0;
        int totalTastiness = 0;
        foreach (int index in path)
        {
            var cur = Spots[index];
            totalDist += Dist(cur, prev);
            totalTastiness += cur.Tastiness;
            prev = cur;
        }

        var metric = totalTastiness / totalDist;
        if (metric > _bestMetric)
        {
            _bestMetric = metric;
            _bestDist = totalDist;
            _bestTaste = totalTastiness;
            _bestPath = path;
        }

        if (path.Count < MaxPathSize)
        {
            var indicesLeft = Enumerable.Range(0, Spots.Length).Except(path);
            foreach (int index in indicesLeft)
            {
                Iterate([.. path, index]);
            }
        }
    }

    private static void Main()
    {
        var sw = Stopwatch.StartNew();
        Iterate([]);
        sw.Stop();

        Debug.WriteLine($"Total tastiness = {_bestTaste:0}");
        Debug.WriteLine($"Total distance = {_bestDist:0.00000}");
        Debug.WriteLine($"Metric = {_bestMetric:0.00000}");
        Debug.WriteLine($"Path = [{string.Join(", ", _bestPath.Select(x => (x + 1).ToString()))}]");
        Debug.WriteLine($"Elapsed: {sw.ElapsedMilliseconds} ms");
    }
}
79792328
1

Original Submission: I chose to do a brute force method for this challenge, as I can't remember much from my graph theory module at Uni, and I don't really have time to look up appropriate algorithms. Instead, I have calculated the distance and tastiness for the 1,814,400 different routes I found. As I'm neglecting to find a good algorithm for the problem, I have included a small regression model at the end to fit a rough algorithm to find the expected taco tastiness for distance travelled. I hope this makes up for my laziness on this challenge :p

To begin, I established the Taco DataFrame and created a function to calculate Euclidean distance between 2 points:

import pandas as pd

#############Set up
#Taco dataframe
tacos = pd.DataFrame({'Taco Spot':[1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
                      'Coordinates':[(2, 8), (15, 3), (6, 6), (20, 15), (11, 2),
                                     (14, 1), (7, 3), (1, 12), (3, 6), (14, 13)],
                      'Tastiness':[7, 17, 8, 30, 12, 15, 5, 12, 3, 18]})

#Distance function
def travel_distance(xn, yn, xn_1, yn_1):
    """Function to calculate euclidean distance between two points n and n_1
    Args:
        xn (int): position n's x coordinate
        yn (int): position n's y coordinate
        xn_1 (int): position n_1's x coordinate
        yn_1 (int): position n_1's y coordinate
    Returns:
        float: The euclidean distance between n and n_1
    """
    return (abs(xn_1 - xn)**2 + abs(yn_1 - yn)**2)**0.5

When I first wrote the code, I calculated the distance between two points for each route. This is obviously incredibly inefficient (took 7 minutes to run for all routes), so I decided to try to be a bit smarter and create a matrix of the distances between each point to all other points once at the start. I then used this as a look up when calculating the almost 2 million routes to save duplication:

###########Calculate all distances
import numpy as np
from itertools import combinations, permutations

#Add starting position (0,0) to the taco dataframe
tacos = tacos.set_index('Taco Spot')
tacos.loc[0] = [(0,0), np.nan]
tacos = tacos.sort_index()

#Get a list of all n to n_1 possibilities in the Taco dataset
journeys = [i for i in combinations(tacos.index.sort_values().values.tolist(), 2)]
#Create a matrix of all distances between each taco location to the others
distances = []
for journey in journeys:
    xn, yn = tacos.loc[journey[0], 'Coordinates']
    xn_1, yn_1 = tacos.loc[journey[1], 'Coordinates']
    distances.append(travel_distance(xn, yn, xn_1, yn_1))

#Doubling lists to make symettrical matrix (makes look ups quicker later)
distances = pd.DataFrame({'x':[i[0] for i in journeys] + [i[1] for i in journeys],
                          'y':[i[1] for i in journeys] + [i[0] for i in journeys],
                          'Distance':distances + distances}
                          ).pivot(index='y', columns='x', values='Distance')

This changed my overall run time to 1 minute. Much better than 7 (although I'm sure there will be much much faster solutions to this challenge). This then gave me a table of all distances. Then it was just a case of iterating over all possible journeys, calculating the distance and tastiness, and working out which one gives you more taste per mile:

###########Evaluate every journey
#Create a list of all the possible taco journeys (not including start point)
routes = [comb for comb in permutations(tacos[1:].index, 8)]
tastiness = tacos['Tastiness'].values.tolist() #convert to list as faster lookup
results = []

#For each route
for route in routes:
    #Set start point of 0
    start = 0
    #Establish distance and tastiness sums
    distance_sum = 0
    tastiness_sum = 0
    #For each taco spot on the route
    for spot in route:
        #Add distance between current spot and previous spot
        distance_sum += distances.loc[spot, start]
        #Add current sport tastiness
        tastiness_sum += tastiness[spot]
        #Update start/previous sport to current spot
        start = spot
    #Record the results for this route
    results.append([route, distance_sum, tastiness_sum])

#Create a dataframe of the results of all possible routes
results_df = pd.DataFrame(results, columns=['Route', 'Distance', 'Tastiness'])

#Calculate the tastiness per distance for each route
results_df['tastiness per distance'] = (results_df['Tastiness']
                                        / results_df['Distance'])

#Find and print the optimum route
optimum = results_df.loc[results_df['tastiness per distance'].idxmax()]

This gave me the optimum taco journey as (9, 3, 7, 5, 6, 2, 10, 4) with a tastiness per distance of 2.78592 (I really hope this is right! embarrassing if not :p)

Since I was lazy, I now have the distance and tastiness of all possible taco crawls. This means I can look at the relationship between distance travelled and tastiness. Generally, the further one is willing to travel, the tastier the tacos! I used the below code to give a formulae for the expected overall tastiness a taco crawler can expect based on the distance they are prepared to travel:

###########Aanalysis of distance vs tastiness
from sklearn.linear_model import LinearRegression

#Fitting a linear regression to predict the expected tastiness for distance
# travelled.
reg = LinearRegression().fit(results_df['Distance'].values.reshape(-1, 1),
                             results_df['Tastiness'].values.reshape(-1, 1))

print(f'Tastiness formulae: Tastiness = ({reg.coef_[0][0] :.2f} * Distance) + {reg.intercept_[0] :.2f}')

Which gave:

Tastiness = (0.38 * Distance) + 69.50

Suggesting that a lazy crawler, who doesn't want to travel at all could expect a 69.50 tastiness rating (is this the reward for those who choose to order in?)

I also wrote some code to make a plot of the optimum path, with the taco spots plotted where their size correlates to their tastiness. I cannot add the plot here, so you will just have to make do with the code, and know that it is a thing of beauty (don't argue with me if you run it yourself and disagree).

###########Plot of journey
import matplotlib.pyplot as plt

#Get the x and y from the coordinates
tacos[['x', 'y']] = tacos['Coordinates'].values.tolist()
#Create a size column to scale tastiness and make the plot look better
tacos['Size'] = tacos['Tastiness'] * 50
#Plot of the optimum path
ax = tacos.loc[[0] + list(optimum['Route'])].plot(x='x', y='y', style='r--', legend=False)
#Plot of each taco spot, with it's size being relative to its tastiness
tacos.plot(kind='scatter', x='x', y='y', s='Size', ax=ax)
#Label taco spots
for spot, coord in tacos[['Coordinates']].iterrows():
    ax.annotate(spot, coord[0], xytext=(10,-5), textcoords='offset points',
                family='sans-serif', fontsize=20, color='darkslategrey')
ax.set_title('Optimum Taco Route')
plt.show()

~fin

Edit 1 after submission: Now I've submitted and seen other solutions, I see that the question says up to 8 taco spots. Maybe one day I'll learn to read properly -_- it seems the final 6 spots in my solution match the optimal solution, so I guess I'm not far off. If I get chance I'll try to amend my answer to look at shorter routes, but I'll leave my solution as is for now, so you can all laugh at me :p

Edit 2 after submission:

Ok, so I had a quick few minutes to change my code to account for all routes up to 8 taco spots. I'm still being lazy and now I have 2,606,490 routes to calculate, which is even slower than before! If I had the time I could write something iterative which would make use of the fact that routes of length n contain a route of n-1 length, and only calculate the distance and tastiness once for each combination. Alas, I am still lazy, so I've just changed 1 line in my original code to get a list of all possible routes:

routes = []
for i in range(2, 9):
    routes += [comb for comb in permutations(tacos[1:].index, i)]

With this change I get the correct route of (7, 5, 6, 2, 10, 4) as others have found, with a tastiness per distance of 2.89.

As before, my code also returns a 'tastiness formulae'. The updated formulae with the additional routes included is now:

Tastiness formulae: Tastiness = (0.62 * Distance) + 46.53

Now our lazy taco-ers who order in get an even lower tastiness score of 46.53 compared to my original solution score of 69.50. I guess we've all learnt that being lazy not only means you might mis-read a coding challenge, but also leads to less tasty tacos.

With this new solution of visiting up to 8 spots, my original solution is only the 5th best route, with the below 4 solutions all achieving a better tastiness per distance:

Route                   Distance  Tastiness  tastiness per distance
(7, 5, 6, 2, 10, 4)     33.51      97.0      2.894515
(7, 5, 6, 2)            17.14      49.0      2.859273
(3, 7, 5, 6, 2, 10, 4)  37.54      105.0     2.796760
(5, 6, 2, 10, 4)        32.95      92.0      2.791845

to think, I could've missed out on that extra 0.1 tastiness per distance!

79798224
0
Author

Thanks for the writeup. It was enjoyable to follow your journey to the right answer.

79792189
3

Best tastiness: 2,8945153290979557

Distance: 33,51165531060739

Total tastiness: 97

Order to visit:

Taco Spot 7 at (7,3) with tastiness 5

Taco Spot 5 at (11,2) with tastiness 12

Taco Spot 6 at (14,1) with tastiness 15

Taco Spot 2 at (15,3) with tastiness 17

Taco Spot 10 at (14,13) with tastiness 18

Taco Spot 4 at (20,15) with tastiness 30

using ConsoleApp2;

const int MAXNUMBER = 8;

var spots = new Dictionary<int, TacoSpot>
{
    { 1, new TacoSpot(2, 8, 7) },
    { 2, new TacoSpot(15, 3, 17) },
    { 3, new TacoSpot(6, 6, 8) },
    { 4, new TacoSpot(20, 15, 30) },
    { 5, new TacoSpot(11, 2, 12) },
    { 6, new TacoSpot(14, 1, 15) },
    { 7, new TacoSpot(7, 3, 5) },
    { 8, new TacoSpot(1, 12, 12) },
    { 9, new TacoSpot(3, 6, 3) },
    { 10, new TacoSpot(14, 13, 18) },
};

var bestOrder = new List<int>();
double bestTastiness = 0;
double distanceBestTastiness = 0;
int totalscoreBestTastiness = 0;

foreach (List<int> order in helpers.GetPermutations(spots.Keys.ToArray(), MAXNUMBER))
{
    double currentDistance = 0;
    int currentScore = 0;
    double currentHighest = 0;
    double currentHighestDistancy = 0;
    int currentHighestTastiness = 0;
    int currentHighestPosition = 0;

    int xPrevious = 0;
    int yPrevious = 0;

    for (int i = 0; i < MAXNUMBER; i++)
    {
        currentDistance += Math.Sqrt(Math.Pow(spots[order[i]].X - xPrevious,2) + Math.Pow(spots[order[i]].Y - yPrevious, 2));
        currentScore += spots[order[i]].Tastiness;

        xPrevious = spots[order[i]].X;
        yPrevious = spots[order[i]].Y;

        if (currentScore / currentDistance > currentHighest)
        {
            currentHighest = currentScore / currentDistance;
            currentHighestDistancy = currentDistance;
            currentHighestTastiness = currentScore;
            currentHighestPosition = i;
        }
    }

    if (currentHighest > bestTastiness)
    {
        bestTastiness = currentHighest;
        distanceBestTastiness = currentHighestDistancy;
        totalscoreBestTastiness = currentHighestTastiness;
        bestOrder = order.Take(currentHighestPosition + 1).ToList();
    }
}

Console.WriteLine($"Best tastiness: {bestTastiness}");
Console.WriteLine($"Distance: {distanceBestTastiness}");
Console.WriteLine($"Total tastiness: {totalscoreBestTastiness}");
Console.WriteLine("Order to visit:");
foreach (var spot in bestOrder)
{
    Console.WriteLine($"Taco Spot {spot} at ({spots[spot].X},{spots[spot].Y}) with tastiness {spots[spot].Tastiness}");
}
internal class TacoSpot
{
    public TacoSpot(int x, int y, int tastiness)
    {
        X = x;
        Y = y;
        Tastiness = tastiness;
    }

    public int X { get; }
    public int Y { get;  }
    public int Tastiness { get; }
}
internal static class helpers
{
    public static IEnumerable<List<int>> GetPermutations(int[] numbers, int length)
    {
        if (length == 1)
        {
            foreach (var n in numbers)
                yield return new List<int> { n };
        }
        else
        {
            foreach (var perm in GetPermutations(numbers, length - 1))
            {
                foreach (var n in numbers)
                {
                    if (!perm.Contains(n))
                    {
                        var next = new List<int>(perm) { n };
                        yield return next;
                    }
                }
            }
        }
    }
}
79791423
2

Here's a simple, relatively* efficient brute force approach:

function factorial(n) {
    let z = n
    while (n-- > 1) z *= n
    return z
}

function* permuteCombinations(list, length) {
    if (!length) length = list.length
    const permutations = length == list.length ? factorial(list.length) : factorial(list.length) / factorial(list.length - length)
    for (let i = 0; i < permutations; i++) {

        const listCopy = list.slice()
        const permutedList = []
        let c = i

        while (permutedList.length < length) {
            const nextIndex = c % listCopy.length
            c = (c - nextIndex) / listCopy.length
            permutedList.push(listCopy.splice(nextIndex, 1)[0])
        }

        yield permutedList
    }
}

function computeScore(crawlOrder) {
    let lastX = 0, lastY = 0, totalDist = 0, totalTastiness = 0
    for (let spot of crawlOrder) {
        totalDist += Math.hypot(spot.x - lastX, spot.y - lastY)
        totalTastiness += spot.tastiness
        lastX = spot.x
        lastY = spot.y
    }
    return totalTastiness / totalDist
}

const spots = [
    {id: 1, x: 2, y: 8, tastiness: 7 },
    {id: 2, x: 15, y: 3, tastiness: 17 },
    {id: 3, x: 6, y: 6, tastiness: 8 },
    {id: 4, x: 20, y: 15, tastiness: 30 },
    {id: 5, x: 11, y: 2, tastiness: 12 },
    {id: 6, x: 14, y: 1, tastiness: 15 },
    {id: 7, x: 7, y: 3, tastiness: 5 },
    {id: 8, x: 1, y: 12, tastiness: 12 },
    {id: 9, x: 3, y: 6, tastiness: 3 },
    {id: 10, x: 14, y: 13, tastiness: 18 },
]

let bestScore = 0, bestCrawl = undefined
for (let numStops = 1; numStops <= 8; numStops++) {
    for (let crawl of permuteCombinations(spots, numStops)) {
        const score = computeScore(crawl)

        if (score > bestScore) {
            bestScore = score
            bestCrawl = crawl
        }
    }
}

console.log('bestCrawl', bestCrawl, bestScore)

The interesting part here is the permuteCombinations generator function, which will efficiently and lazily iterate over permutations of any N choose K combination.

Kind of fun to find a practical use for a generator function!

Edit: forgot to include the output with the answer:

bestCrawl [
  { id: 7, x: 7, y: 3, tastiness: 5 },
  { id: 5, x: 11, y: 2, tastiness: 12 },
  { id: 6, x: 14, y: 1, tastiness: 15 },
  { id: 2, x: 15, y: 3, tastiness: 17 },
  { id: 10, x: 14, y: 13, tastiness: 18 },
  { id: 4, x: 20, y: 15, tastiness: 30 }
] 2.8945153290979557

*At first I was assuming that the crawl should include exactly 8 spots, not up to eight spots. One optimization that I didn't make that could be make while still taking a (mostly) brute-force approach would be to check paths as they are built, and stop checking paths if it becomes impossible to beat the best score before reaching eight stops.

79792111
1

One optimization that I didn't make that could be make while still taking a (mostly) brute-force approach would be to check paths as they are built, and stop checking paths if it becomes impossible to beat the best score before reaching eight stops.

Finding a sharp and general criterion to determine that this is impossible would be a major issue here I imagine. I guess you can either choose something very general like assuming the remaining distance is the minimum distance within all spots for X remaining nodes, and the tastiness is the maximum combined sum of X nodes, but this will not kill many paths. Or you can calculate a more sophisticated measure while spending a similar amount of computation as for testing the remaining variants. Or do you have something concrete in mind?

79791124
9

Let SQL do the heavy lifting:

The real challenge in this problem is not just calculating distances or summing scores. it’s efficiently generating and keeping track of all possible paths. Instead of building a separate algorithm layer using imported Python libraries or another language, I decided to push all the logic down into SQL operators!

By combining the power of JOIN and Union operations with bitmasking and the bitwise operators & and | , SQL Server can naturally handle path expansion, track visited points in one integer, and explore all combinations. This way, the database engine isn't just storing data; it is doing the algorithmic work!

For who are not familiar with the concept of bitmasking, for example, if taco spots 2, 5, and 7 are visited, the bitmask for 10 points looks like this:

0001010010

Each bit represents one taco spot
if a bit is 1, that spot has been visited;
if it is 0, it hasn’t.

Then this is a built in binary comparison in SQL Server.

(p.mask & s.mask) = 0 (Comparision result can be 0 or 1)

If the result is 0, it means the spot is not visited yet and can be added to the path.

With this approach, SQL Server itself became the algorithm: join built the paths, bitmasks tracked the state, and recursion explored every combination. No external loops, no complex data structures, no external library, just smart use of SQL join and built in operators.

These few lines will generate all 2,606,500 possible paths and sort them by TotalTaste/distance . Using select top 1 can return only the top result which is:

Path: 7,5,6,2,10,4
Total taste: 97
Total distance: 33.5116553106074
Final score: 2.89451532909796

/* Creating a temporary table */
IF OBJECT_ID('tempdb..#TacoSpots') IS NOT NULL DROP TABLE #TacoSpots;
CREATE TABLE #TacoSpots (id INT PRIMARY KEY, x FLOAT NOT NULL, y FLOAT NOT NULL, taste INT NOT NULL);

/* Inserting points and tastes */
INSERT INTO #TacoSpots(id,x,y,taste) VALUES
(1,2,8,7),(2,15,3,17),(3,6,6,8),(4,20,15,30),(5,11,2,12),
(6,14,1,15),(7,7,3,5),(8,1,12,12),(9,3,6,3),(10,14,13,18);

/* Declaring max and min nubmer of spots */
DECLARE @K INT = 8, @MinStops INT = 1;

/* Generating bitmasks for each point and calculating its distance from origin */
WITH Spots AS (
  SELECT id, x, y, taste,
         CONVERT(BIGINT, POWER(CONVERT(BIGINT, 2), id - 1)) AS mask,
         SQRT(x * x + y * y) AS dist0
  FROM #TacoSpots
),
/* Starting journy */
Paths AS (
  SELECT
    s.id, s.x, s.y,
    s.mask,
    1 AS stops,
    CAST(CONVERT(VARCHAR(400), s.id) AS VARCHAR(400)) AS path_str,
    s.taste,
    s.dist0
  FROM Spots s

  /* Easy Recursion: expanding the path with a new unvisited point */
  UNION ALL
  SELECT
    s.id, s.x, s.y,
    p.mask | s.mask, --Expanding the path with new point
    p.stops + 1,
    CAST(p.path_str + ',' + CONVERT(VARCHAR(10), s.id) AS VARCHAR(400)),
    p.taste + s.taste,
    p.dist0 + SQRT( (s.x - p.x)*(s.x - p.x) + (s.y - p.y)*(s.y - p.y) )
  FROM Paths p
  JOIN Spots s ON (p.mask & s.mask) = 0 --Preventing duplicates
  WHERE p.stops < @K --Limiting path length to K=8
)
SELECT -- Preparing and sorting results
  p.stops AS Stops,
  p.path_str AS Path,
  p.taste AS TotalTaste,
  p.dist0 AS TotalDistance,
  p.taste / NULLIF(p.dist0, 0.0) AS TastePerDistance
FROM Paths p
WHERE p.stops BETWEEN @MinStops AND @K
ORDER BY TastePerDistance DESC, TotalDistance ASC

/* increasing default recursion limits */
OPTION (MAXRECURSION 32767);
79798226
1
Author

clever idea to use a database!

79790773
-2

Optimal Path: [2, 5, 6, 10, 4, 8, 1, 3]

Total Tastiness: 119

Total Distance: 68.73

Tastiness per Distance: 1.7317

import math
import itertools

# Taco spot data
spots = {
    1: {'coords': (2, 8), 'tastiness': 7},
    2: {'coords': (15, 3), 'tastiness': 17},
    3: {'coords': (6, 6), 'tastiness': 8},
    4: {'coords': (20, 15), 'tastiness': 30},
    5: {'coords': (11, 2), 'tastiness': 12},
    6: {'coords': (14, 1), 'tastiness': 15},
    7: {'coords': (7, 3), 'tastiness': 5},
    8: {'coords': (1, 12), 'tastiness': 12},
    9: {'coords': (3, 6), 'tastiness': 3},
    10: {'coords': (14, 13), 'tastiness': 18}
}

def euclidean_distance(coord1, coord2):
    """Calculate Euclidean distance between two points"""
    return math.sqrt((coord1[0] - coord2[0])**2 + (coord1[1] - coord2[1])**2)

def calculate_path_metrics(path):
    """Calculate total tastiness and distance for a given path"""
    total_tastiness = 0
    total_distance = 0
    
    # Start from origin (0,0)
    current_pos = (0, 0)
    
    for spot_id in path:
        spot_coords = spots[spot_id]['coords']
        total_tastiness += spots[spot_id]['tastiness']
        total_distance += euclidean_distance(current_pos, spot_coords)
        current_pos = spot_coords
    
    return total_tastiness, total_distance, total_tastiness / total_distance if total_distance > 0 else 0

def find_optimal_path():
    """Find the optimal path visiting up to 8 spots"""
    best_metric = 0
    best_path = None
    best_tastiness = 0
    best_distance = 0
    
    # Try all combinations of 1 to 8 spots
    for k in range(1, 9):
        for combo in itertools.combinations(range(1, 11), k):
            # Try all permutations of this combination
            for perm in itertools.permutations(combo):
                tastiness, distance, metric = calculate_path_metrics(perm)
                
                if metric > best_metric:
                    best_metric = metric
                    best_path = perm
                    best_tastiness = tastiness
                    best_distance = distance
    
    return best_path, best_tastiness, best_distance, best_metric

# Find optimal solution
optimal_path, total_tastiness, total_distance, metric = find_optimal_path()

print("OPTIMAL PATH:")
print(f"Path: {list(optimal_path)}")
print(f"Total Tastiness: {total_tastiness}")
print(f"Total Distance: {total_distance:.2f}")
print(f"Tastiness per Distance: {metric:.4f}")

# Verify the path
print("\nPATH DETAILS:")
current_pos = (0, 0)
cumulative_distance = 0
cumulative_tastiness = 0

print(f"Start at (0, 0)")
for i, spot_id in enumerate(optimal_path, 1):
    spot_coords = spots[spot_id]['coords']
    segment_distance = euclidean_distance(current_pos, spot_coords)
    cumulative_distance += segment_distance
    cumulative_tastiness += spots[spot_id]['tastiness']
    
    print(f"{i}. Spot {spot_id}: {spot_coords}, Tastiness: {spots[spot_id]['tastiness']}")
    print(f"   Segment distance: {segment_distance:.2f}, Cumulative: {cumulative_distance:.2f}")
    print(f"   Cumulative tastiness: {cumulative_tastiness}")
    
    current_pos = spot_coords

print(f"\nFinal Metric: {cumulative_tastiness}/{cumulative_distance:.2f} = {metric:.4f}")
79791176
1

Challenge asks up to 8 points, not always 8 points.

79790656
2

The optimal routing: [7, 5, 6, 2, 10, 4]
Total tastiness: 97
Total distance: 33,51165531060739
Tastiness per distance: 2,8945153290979557

I used brute force and recursive calls.

I learned to read carefully the requirements. All words are important: UP TO 8 spots.

namespace TacosChallenge;

// Tacos spot information
public class TacosSpot(int id, int x, int y, int tastiness)
{
    public int Id { get; set; } = id;
    public (int X, int Y) Coordinates { get; set; } = (x, y);
    public int Tastiness { get; set; } = tastiness;
}

// Path information
public class Path(double score, double distance, int TotalTastiness, List<TacosSpot> Spots)
{
    public double Score { get; set; } = score;
    public double Distance { get; set; } = distance;
    public int TotalTastiness { get; set; } = TotalTastiness;
    public List<TacosSpot> Spots { get; set; } = Spots;
}

public class Tests
{
    private TacosSpot[] TacoSpots;

    private int maxSpots = 8;

    [SetUp]
    public void Setup()
    {
        TacoSpots =
        [
            new TacosSpot(1, 2, 8, 7),
            new TacosSpot(2, 15, 3, 17),
            new TacosSpot(3, 6, 6, 8),
            new TacosSpot(4, 20, 15, 30),
            new TacosSpot(5, 11, 2, 12),
            new TacosSpot(6, 14, 1, 15),
            new TacosSpot(7, 7, 3, 5),
            new TacosSpot(8, 1, 12, 12),
            new TacosSpot(9, 3, 6, 3),
            new TacosSpot(10, 14, 13, 18)
        ];
    }

    [Test]
    public void Test1()
    {
        List<Path> paths = GetAllPaths();

        var bestPath = paths.OrderByDescending(p => p.Score).First();

        Console.WriteLine($"The optimal routing: [{string.Join(", ", bestPath.Spots.Select(s => s.Id))}]");
        Console.WriteLine($"Total tastiness: {bestPath.TotalTastiness}");
        Console.WriteLine($"Total distance: {bestPath.Distance}");
        Console.WriteLine($"Tastiness per distance: {bestPath.Score}");

        Assert.IsFalse(paths.Any(p => p.Score > bestPath.Score));

    }

    private List<Path> GetAllPaths()
    {
        var paths = new List<Path>();
        for (var i = 1; i <= maxSpots; i++)
        {
            var startPath = new Path(0, 0, 0, new List<TacosSpot>());

            paths.AddRange(PossiblePaths(startPath, i));
        }

        return paths;
    }

    private List<Path> PossiblePaths(Path basePath, int spotsRequested)
    {
        List<Path> newPaths = new List<Path>();

        var spotsNotInPath = TacoSpots.Except(basePath.Spots).ToArray();

        // Get the last spot in the current path to calculate distance from. If no spots, use origin (0,0).
        var lastSpot = basePath.Spots.Count > 0 ? basePath.Spots.Last() : new TacosSpot(0, 0, 0, 0);

        foreach (var spot in spotsNotInPath) // Add all possible spot from current location
        {
            double distance = CalculateDistance(lastSpot, spot);
            int totalTastiness = basePath.TotalTastiness + spot.Tastiness;
 
            var newSpotList = new List<TacosSpot>(basePath.Spots) { spot };
            var newPath = new Path(0, basePath.Distance + distance, totalTastiness, newSpotList);
            
            if (newPath.Spots.Count < spotsRequested)
            {
                newPaths.AddRange(PossiblePaths(newPath,spotsRequested)); // Recursively add paths
            }
            else
            {
                newPath.Score = newPath.TotalTastiness / newPath.Distance; // Calculate score only when path is complete
                newPaths.Add(newPath); // We are full at maxSpots, end of the path
            }
        }

        return newPaths.ToList();
    }
    
    private static double CalculateDistance(TacosSpot a, TacosSpot b)
    {
        return Math.Sqrt(Math.Pow(b.Coordinates.X - a.Coordinates.X, 2) + Math.Pow(b.Coordinates.Y - a.Coordinates.Y, 2));
    }
}
79795701
1

You could reduce runtime and memory requirements significantly by using yield return instead of materialising all possible paths (and then sorting them to boot).

An even bigger improvement would be possible by processing each path as it is found and memorising just the best result, instead of reifying the path sequence externally. :-)

79798122
0

Thank you @DarthGizka, when I'm back home I will try your second option and let you know how I reduced runtime. The current version takes 6 seconds on my 5 years old mac.

79790306
2

Not the most elegant code, just recursive brute force but it was fun...

Best Ratio: 2.8945
Best Path Full: [{"id":7,"coords":[7,3],"tastiness":5},{"id":5,"coords":[11,2],"tastiness":12},{"id":6,"coords":[14,1],"tastiness":15},{"id":2,"coords":[15,3],"tastiness":17},{"id":10,"coords":[14,13],"tastiness":18},{"id":4,"coords":[20,15],"tastiness":30}]
Best Path Ids: (6) [7, 5, 6, 2, 10, 4]
Total Tastiness: 97
Total Distance: 33.51165531060739

Code in node.js:


const tacoSpots = [
    { "id": 1, "coords": [2, 8], "tastiness": 7, },
    { "id": 2, "coords": [15, 3], "tastiness": 17, },
    { "id": 3, "coords": [6, 6], "tastiness": 8, },
    { "id": 4, "coords": [20, 15], "tastiness": 30, },
    { "id": 5, "coords": [11, 2], "tastiness": 12, },
    { "id": 6, "coords": [14, 1], "tastiness": 15, },
    { "id": 7, "coords": [7, 3], "tastiness": 5, },
    { "id": 8, "coords": [1, 12], "tastiness": 12, },
    { "id": 9, "coords": [3, 6], "tastiness": 3, },
    { "id": 10, "coords": [14, 13], "tastiness": 18, }
]

const MAX_VISITS = 8

function calculateDistance(tacoSpot0, tacoSpot1) {
    return Math.sqrt(
        (Math.pow((tacoSpot1.coords[0] - tacoSpot0.coords[0]), 2)) +
        (Math.pow((tacoSpot1.coords[1] - tacoSpot0.coords[1]), 2))
    )
}

function findBestPath(current, visited, tastinessSum, distanceSum, spotsLeft) {
    if (visited.length > 0 && distanceSum > 0) {
        const ratio = tastinessSum / distanceSum;
        if (ratio > bestRatio) {
            bestRatio = ratio;
            bestPath = visited.slice();
            bestTastiness = tastinessSum;
            bestDistance = distanceSum;
        }
    }

    if (visited.length === MAX_VISITS) return;

    for (let i = 0; i < spotsLeft.length; i++) {
        const nextSpot = spotsLeft[i];
        const nextDist = calculateDistance(current, nextSpot);
        // Prepare new state
        visited.push(nextSpot);
        const nextSpotsLeft = spotsLeft.slice(0, i).concat(spotsLeft.slice(i + 1));
        findBestPath(nextSpot, visited, tastinessSum + nextSpot.tastiness, distanceSum + nextDist, nextSpotsLeft);
        visited.pop();
    }
}

let bestRatio = 0
let bestPath = []
let bestTastiness = 0
let bestDistance = 0

// Start from spot (0,0)
findBestPath({ "id": -1, "coords": [0, 0], "tastiness": 0, }, [], 0, 0, tacoSpots);

console.log("Best Ratio:", bestRatio.toFixed(4));
console.log("Best Path Full:", JSON.stringify(bestPath));
console.log("Best Path Ids:", bestPath.map(e => e.id));
console.log("Total Tastiness:", bestTastiness);
console.log("Total Distance:", bestDistance);
79790228
-1
import itertools, math

spots = {
    1: ((2, 8), 7),
    2: ((15, 3), 17),
    3: ((6, 6), 8),
    4: ((20, 15), 30),
    5: ((11, 2), 12),
    6: ((14, 1), 15),
    7: ((7, 3), 5),
    8: ((1, 12), 12),
    9: ((3, 6), 3),
    10: ((14, 13), 18),
}

coords = {i: v[0] for i, v in spots.items()}
taste = {i: v[1] for i, v in spots.items()}

def dist(a, b):
    return math.hypot(a[0]-b[0], a[1]-b[1])

start = (0, 0)
all_ids = list(spots.keys())

D = {(i, j): dist(coords[i], coords[j]) for i in all_ids for j in all_ids if i != j}
Ds = {i: dist(start, coords[i]) for i in all_ids}

best_ratio = -1
best_path = None
best_taste = None
best_dist = None

for k in range(1, 9):
    for perm in itertools.permutations(all_ids, k):
        d = Ds[perm[0]] + sum(D[(a, b)] for a, b in zip(perm, perm[1:]))
        t = sum(taste[i] for i in perm)
        r = t / d
        if r > best_ratio:
            best_ratio = r
            best_path = perm
            best_taste = t
            best_dist = d

print("Melhor rota:", list(best_path))
print("Tastiness total:", best_taste)
print("Distância total:", best_dist)
79796054
1

Gustavo, you have not listed the output of your solution, i.e. the optimal path and the associated values (tastiness, distance, score) as specified in the challenge description.

You are still in time to fix this before the deadline. ;-)

79790128
1

Disclaimer: The new version of the assumption seems to work. Please, if you know whether it is true or false, don't hesitate to tell me!

Edit: Here's the new version

  • Assumption: The best path for k+1 visits contains a best path for k visits, with another taco spot added to the list, either at its beginning or end.

Code (Matlab):

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%% SO challenge taco crawl %%%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

% Inputs
Coord = [2,8;15,3;6,6;20,15;11,2;14,1;7,3;1,12;3,6;14,13];
Tast = [7;17;8;30;12;15;5;12;3;18];
N = size(Coord,1);
s = 8;

% distances to 0
d_0 = sqrt(Coord(:,1).^2+Coord(:,2).^2);

d_prev = d_0;
t_prev = Tast;

% distance matrix between taco spots
d_ij = zeros(N,N);
for i = 1:N
    
    d_ij(i,:) = sqrt((Coord(:,1)-Coord(i,1)).^2+(Coord(:,2)-Coord(i,2)).^2).';
    d_ij(i,i) = Inf;
    
end

% Array of paths. First column is going from 0 to i
a_path = zeros(N,s);
a_path(:,1) = (1:N).';

% Save best candidate after step 1
[metric_best,idx] = max(t_prev./d_prev);
d_best = d_prev(idx);
t_best = t_prev(idx);
a_best = a_path(idx);


% Main loop on paths
for k = 2:s
    
    % d_prev iteratively contains the cumulative distance of the current
    % path leading or starting at node 1 to 10 in k-1 steps.
    % d_k and d_k_ant gives the possible combinations for the distances in k
    % steps. Same idea for t_k and t_k_ant
    d_k = repmat(d_prev.',N,1) + d_ij;
    t_k = t_prev.' + Tast;

    % appending to the beginning of the list
    d_k_ant = Inf(N,N);

    for j = 1:N    
        if a_path(j,1) ~= 0        
            for l=1:N
                if a_path(l,1) ~= 0
                    
                    d_k_ant(j,l) = d_prev(l) + d_0(j) - d_0(a_path(l,1)) + d_ij(j,a_path(l,1));

                end
            end       
        end    
    end

    t_k_ant = t_prev.' + Tast;

    % Init loop variables
    idx_k = zeros(N,1);
    d_next = zeros(N,1);
    t_next = zeros(N,1);
    a_path_new = zeros(N,1);

    for i=1:N

        % max metric, discarding repetitions
        % available candidates are rows that
        % don't contain current row idx and are non zero
        idx_avail = [];
        for j=1:N
            if ~ismember(i,a_path(j,:))  && any(a_path(j,:))

                idx_avail = [idx_avail, j];

            end
        end

        if ~isempty(idx_avail)

            % maximize metric when adding node to the end of the list
            [met,idx_k] = max(t_k(i,idx_avail)./d_k(i,idx_avail));

            % maximize metric by adding node to the beginning of the list
            [met_ant,idx_k_ant] = max(t_k_ant(i,idx_avail)./d_k_ant(i,idx_avail));

            if met >= met_ant
            
                % save best path with node i to the end of the list, if
                % best option
                a_path_new(i,1:k)=[a_path(idx_avail(idx_k),1:(k-1)), i];
                % save distance and tastiness for best path in k
                % steps
                d_next(i) = d_k(i,idx_avail(idx_k));
                t_next(i) = t_k(i,idx_avail(idx_k));

            else

                % save best path with node i at the beginning of the list,
                % if best option
                a_path_new(i,1:k)=[i, a_path(idx_avail(idx_k_ant),1:(k-1))];
                % save distance and tastiness for best path in k
                % steps
                d_next(i) = d_k_ant(i,idx_avail(idx_k_ant));
                t_next(i) = t_k_ant(i,idx_avail(idx_k_ant));
                met = met_ant;

            end

            if met > metric_best
                % Save best candidate
                metric_best = met;
                d_best = d_next(i);
                t_best = t_next(i);
                a_best = a_path_new(i,1:k);

            end

        else

            % If no possible paths (due to repetitions) then discard
            a_path_new(i,1:k) = 0;
            d_next(i) = Inf;
            t_next(i) = 0;

        end

    end

    a_path = a_path_new;
    d_prev = d_next;
    t_prev = t_next;
    
end

metric_best
d_best
t_best
a_best

metric_best =

2.8945  

d_best =

33.5117

t_best =

97  

a_best =

 7     5     6     2    10     4

I believe this approach is not as time extensive as some others (takes around 5ms in Matlab).


How the algorithm works:

1-step paths (Init):

All the 1 step paths are kept:

0 Node  number after 1 Step
0 1
. .
. .
0 10

Along with the distances in the variable d_prev, and the tastiness in the variable t_prev

k-step paths to k+1 step paths:

For each of the 10 k-step paths generated in the previous step, append all possible node number before or after the path (excluding repetitions). This gives you a maximum of 20 k+1 step paths for every k-step path. Then, choose amongst those 20 the one with the best metric. It gives you a maximum of 10 k+1 Step paths for next iteration.

Example: Step 1 to Step 2:

0
0 1-->1 (not valid) 2-->1 . 10 --> 1 1-->1 (not valid) 1--> 2
.
.
0 1-->10 2-->10 . 10-->10 (not valid) 10-->1 etc

Results for 2-Step paths

Paths
[0, 1, 8]
[0, 6, 2]
[0, 3, 4]
[0, 10, 4]
[0, 5, 2]
[0, 6, 2]
[0, 7, 5]
[0, 1, 8]
[0, 9, 4]
[0, 10, 4]

Note: At some point, what will happen is that some rows have no valid paths anymore. I just fill those with 0s for the following Steps and set the corresponding distance to Inf so that they are discarded.

Results for Step 5 and Step 6 paths

5-Step paths 6-Step paths
[0, 1, 3, 5, 6, 2] [0, 9, 3, 5, 6, 2, 1] <-- path #9 of 5-step paths list, with a 1 appended to it
[0, 1, 3, 10, 4, 2] [0, 0, 0, 0, 0, 0, 0] <-- This path died
[0, 7, 5, 6, 2, 3] [0, 3, 5, 6, 2, 10, 4]
[0, 7, 5, 6, 2, 4] [0, 7, 5, 6, 2, 10, 4]<-- path #10 of 5-step path list with a 4 appended
[0, 5, 6, 2, 10, 4] [0, 3, 10, 4, 2, 6, 5]
[0, 3, 10, 4, 2, 6] [0, 1, 3, 10, 4, 2, 6]
[0, 7, 5, 6, 2, 4] [0, 7, 5, 6, 2, 10, 4]<-- path #5 of 5-step path list with a 7 appended
[0, 3, 5, 6, 2, 8] [0, 1, 3, 5, 6, 2, 8]
[0, 9, 3, 5, 6, 2] [0, 7, 5, 6, 2, 3, 9]
[0, 7, 5, 6, 2, 10] [0, 7, 5, 6, 2, 3, 10]

Results for Step 7 and Step 8 paths (you can see the paths "disappearing")

7-Step paths 8-Step paths
[0, 7, 5, 6, 2, 3, 9, 1] [0, 9, 3, 5, 6, 2, 10, 4, 1]
[0, 0, 0, 0, 0, 0, 0, 0] [0, 0, 0, 0, 0, 0, 0, 0, 0]
[0, 7, 5, 6, 2, 10, 4, 3] [0, 0, 0, 0, 0, 0, 0, 0, 0]
[0, 7, 5, 6, 2, 3, 10, 4] [0, 9, 3, 5, 6, 2, 1, 10, 4]
[0, 1, 3, 10, 4, 2, 6, 5] [0, 0, 0, 0, 0, 0, 0, 0, 0]
[0, 0, 0, 0, 0, 0, 0, 0] [0, 0, 0, 0, 0, 0, 0, 0, 0]
[0, 7, 3, 5, 6, 2, 10, 4] [0, 9, 3, 5, 6, 2, 10, 4, 7]
[0, 9, 3, 5, 6, 2, 1, 8] [0, 7, 5, 6, 2, 3, 9, 1, 8]
[0, 9, 3, 5, 6, 2, 10, 4] [0, 7, 5, 6, 2, 10, 4, 3, 9]
[0, 9, 3, 5, 6, 2, 1, 10] [0, 9, 3, 5, 6, 2, 1, 8, 10]

If I'm not mistaken this should run in O(n^2) instead of O(n!), right?

79790692
0

If I did my attempt correctly and understand your assumption, I don't think it holds in every instance. I listed what I think are the optimal paths for each of 1 to 8 points; and, although your assumption appears to hold in some instances, I don't think it holds for them all. Of course, I may be wrong. For example, from one to two points it's [4] to [6,2] (no 4) and from four to five points is [7,5,6,2] to [5,6,2,10,4] (no 7).

79790849
0

@Gary, Thank you for answering! I don't just keep one optimal path at each step. I will try to update my post with an explanation of what the algorithm does exactly so that it can be discussed better (once again, I am very not sure if it really works)

79791623
0

Thanks. I think I'm beginning to understand now. Do you mean that since [0,1,8] was optimal from [0,1], then all the [0,1,n!=8] can be discarded? I don't know but it seems that even if you eliminate them from that set of testing you are still catching them elsewhere. For example, you will test [0,1,7] when you test the [0,7,5] and evaluate 1 on the left as [0,1,7,5]. But will you ever evaluate [0,1,7,3] and do you need to?. Is your assumption that if [1,7] isn't optimal and [7,3] isn't optimal, then any path with [...1,7,3...] could not be optimal? I'm not bright enough to figure out how many paths this eliminates without running it because I find it hard to think through adding all possible points to both ends of a relative optimal solution. How many paths did you have to evaluate to get your answer?

79794326
0

Do you mean that since [0,1,8] was optimal from [0,1], then all the [0,1,n!=8] can be discarded?

Hmm. It is more about saying that if the optimum contained [0,1,n!=8] then [0,1,n!=8] would be in the list of optimums too.

For example, you will test [0,1,7] when you test the [0,7,5] and evaluate 1 on the left as [0,1,7,5]. But will you ever evaluate [0,1,7,3] and do you need to?

[0,1,7,3] would only be evaluated if [0,1,7] or [0,7,3] are in the list of optimums for the previous step.

Is your assumption that if [1,7] isn't optimal and [7,3] isn't optimal, then any path with [...1,7,3...] could not be optimal?

Hmm, yes it is (well, it is actually the contrapositive of what I was saying, I think)

How many paths did you have to evaluate to get your answer?

This is, I think, where if it works it would gain a lot of time. 10 for the first step, and then any further step is always 10x20 = 200. Meaning going to up to 8 spots requires 10 + 7*200 = 1410 evaluations.

Thanks again for taking the time to anwer

79798242
1
  • Assumption: The best path for k+1 visits contains a best path for k visits, with another taco spot added to the list, either at its beginning or end.

Can you guarantee that the k + 1th spot does not already exist among the original k spots?

79798384
0

Two observations.

(1) The intuition, that the optimal path for k + 1 steps must have the optimal path for k steps as a prefix (or, as formulated, that the best path for k + 1 steps can be derived by tacking an extra spot onto the best path for k steps) is simply erroneous. Greg has pointed that out already.

This can easily be seen from the fact that the optimal 1-spot crawl consists of a visit to spot 4 but the winning crawl for up to 8 spots has spot 7 as first stop.

The slightly weaker hypothesis that an optimal path for k + 1 steps must utilise all spots visited by the optimal path for k steps (even if not necessarily as a proper prefix) does not hold, either. This can be seen from the fact that the optimal crawl for two spots is [6,2], which does not have any overlap with the optimal 1-spot crawl l [4].

(2) It is not necessary to enumerate paths of length 1 through 7 explicitly. These paths are visited implicitly when visiting all paths of length 8, so to get the optimal path for up to 8 instead of exactly 8 you simply check the score after each step instead of only after step 8.

79798615
0

Thank you for your comments!

@Greg, I can guarantee it because I check for it. This is why some paths end, because all of them contain the number I'm checking for

@DarthGizka I do not keep only one "optimal" path. I keep 10 (this is why I say "contains a best path" instead of "the best path"). For each of those the 20 possibilities (appending a number before or after) are checked and the best is selected, keeping 10 paths for the next step. I also do not enumerate all the path, just 200 per step. The arrays I am showing in the post are the full sized arrays for each step.

I believe it would be fun to test the algorithm on other cases.

79798743
0

I see what you mean. It would make a nice challenge: devise a heuristic that explores at most, say, 5% of the search space and returns the correct result more often than the other contenders. I think marcguery might win that one with his approach based on simulated annealing. ;-)

The problem with generating test cases is not so much the generation as such. That can easily be accomplished by picking random integers from the same ranges from which the original spot values are drawn (1..20 for X, 1..15 for Y, 3..30 for tastiness). What's a bit more involved is avoiding constellations that have multiple solutions with the same tastiness/distance quotient. Of the implementations posted here, very few are able to report whether there a multiple solutions. Mine does, though. ;-) It makes things easier to have only a single valid solution. I stumbled over the multiple-optimal-solutions problem when I tested my implementation against a couple billion randomly generated cases. European billions, i.e. 10^12. I guess that would make it a couple trillions in Merkian parlance. ;-)

79790054
3

Optimal Path (Taco Spot Indices, excluding the starting point): [7, 5, 6, 2, 10, 4]
Optimal Path (Taco Spot Coordinates, including the starting point): [(0, 0), (7, 3), (11, 2), (14, 1), (15, 3), (14, 13), (20, 15)]
Best Tastiness per Distance: 2.8945153290979557

Brute force solution:

import math
from itertools import permutations

import matplotlib.pyplot as plt


def calculate_distance(coord1, coord2):
    """Calculates the Euclidean distance between two coordinates."""
    return math.sqrt((coord1[0] - coord2[0]) ** 2 + (coord1[1] - coord2[1]) ** 2)


def evaluate_path(path_indices, coordinates, tastiness):
    """Evaluates a given path of taco spots."""
    total_tastiness = 0
    total_distance = 0
    start_coord = (0, 0)

    for spot_index in path_indices:
        total_tastiness += tastiness[spot_index - 1]
        total_distance += calculate_distance(start_coord, coordinates[spot_index - 1])
        start_coord = coordinates[spot_index - 1]

    return total_tastiness / total_distance


def find_optimal_path(taco_spot_indices, coordinates, tastiness):
    """Finds the optimal path visiting up to 8 taco spots."""
    best_path_indices = None
    best_tastiness_per_distance = -1

    # Generate all possible paths of 1 to 8 taco spots
    for num in range(1, 9):
        for path_permutation in permutations(taco_spot_indices, num):
            tastiness_per_distance = evaluate_path(path_permutation, coordinates, tastiness)

            # update best path if current path is better
            if tastiness_per_distance > best_tastiness_per_distance:
                best_tastiness_per_distance = tastiness_per_distance
                best_path_indices = path_permutation

    best_path_indices = [taco_spot_indices[i - 1] for i in best_path_indices]
    best_path = [(0, 0)] + [coordinates[i - 1] for i in best_path_indices]
    return best_path_indices, best_path, best_tastiness_per_distance


# Taco spot data
taco_spots = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
coordinates = [(2, 8), (15, 3), (6, 6), (20, 15), (11, 2), (14, 1), (7, 3), (1, 12), (3, 6), (14, 13)]
tastiness = [7, 17, 8, 30, 12, 15, 5, 12, 3, 18]

# Find the optimal path
optimal_path_indices, optimal_path, best_metric = find_optimal_path(taco_spots, coordinates, tastiness)

print("Optimal Path (Taco Spot Indices, excluding the starting point):", optimal_path_indices)
print("Optimal Path (Taco Spot Coordinates, including the starting point):", optimal_path)
print("Best Tastiness per Distance:", best_metric)

# Create the scatter plot
x_coords = [coord[0] for coord in coordinates]
y_coords = [coord[1] for coord in coordinates]
plt.figure(figsize=(10, 10))
plt.scatter(x_coords, y_coords, s=[t * 10 for t in tastiness], alpha=0.5)  # Size of points based on tastiness

plt.xlabel("X Coordinate")
plt.ylabel("Y Coordinate")
plt.grid(True)
plt.axis('equal')
plt.title("Taco Spot Locations and Tastiness")
# Annotate each point with the taco spot number
for i, spot in enumerate(taco_spots):
    plt.annotate(f"Taco Spot {spot}\nTastiness: {tastiness[i]}", (x_coords[i], y_coords[i]),
                 textcoords="offset points", xytext=(5, 10), ha='center')
plt.plot([coord[0] for coord in optimal_path], [coord[1] for coord in optimal_path])

plt.show()
79789895
2

Best route (max tastiness per distance)

Route (spot indices in order): [7, 5, 6, 2, 10, 4]
Total tastiness: 97
Total distance: 33.511655
Tastiness per distance (metric): 2.894515

What I did (short, reproducible)

  • I checked every possible route that visits between 1 and 8 distinct spots (permutations of the 10 spots).

  • For each route I summed the tastiness, computed the Euclidean travel distance from start (0,0) through the route (no return), and computed tastiness / distance.

  • I selected the route with the highest tastiness/distance.

Best-route details (spot → (coord), tastiness)

  • Spot 7: (7, 3), tastiness = 5

  • Spot 5: (11, 2), tastiness = 12

  • Spot 6: (14, 1), tastiness = 15

  • Spot 2: (15, 3), tastiness = 17

  • Spot 10: (14, 13), tastiness = 18

  • Spot 4: (20, 15), tastiness = 30

Top candidates (quick glance)

I printed the top 10 candidates when searching; the runner-up (k=4) is route [7,5,6,2] with metric ≈ 2.8593 (tastiness 49, distance ≈ 17.1372). The top list shows several close alternatives.

Code (exact code I executed — copy & run to reproduce)

import itertools, math

spots = {
    1: {"coord": (2,8), "taste": 7},
    2: {"coord": (15,3), "taste": 17},
    3: {"coord": (6,6), "taste": 8},
    4: {"coord": (20,15), "taste": 30},
    5: {"coord": (11,2), "taste": 12},
    6: {"coord": (14,1), "taste": 15},
    7: {"coord": (7,3), "taste": 5},
    8: {"coord": (1,12), "taste": 12},
    9: {"coord": (3,6), "taste": 3},
    10: {"coord": (14,13), "taste": 18},
}

def euclid(a, b):
    return math.hypot(a[0]-b[0], a[1]-b[1])

start = (0,0)

results = []
for k in range(1, 9):  # visit up to 8 spots
    for combo in itertools.permutations(spots.keys(), k):
        taste_sum = sum(spots[i]["taste"] for i in combo)
        dist = euclid(start, spots[combo[0]]["coord"])
        for a,b in zip(combo, combo[1:]):
            dist += euclid(spots[a]["coord"], spots[b]["coord"])
        if dist == 0:
            continue
        metric = taste_sum / dist
        results.append((metric, taste_sum, dist, combo))

results.sort(reverse=True, key=lambda x: x[0])
best_metric, best_taste, best_dist, best_route = results[0]

print("Best route:", list(best_route))
print("Total tastiness:", best_taste)
print("Total distance: {:.6f}".format(best_dist))
print("Tastiness per distance (metric): {:.6f}".format(best_metric))
79789802
-2

Path: [4, 10, 2, 6, 5, 3, 7, 1]
Total Tastiness : 112
Total Distance : 63.40924628859251
Tastiness per Distance : 1.7663039155245264

const valueString = "2,8-7&15,3-17&6,6-8&20,15-30&11,2-12&14,1-15&7,3-5&1,12-12&3,6-3&14,13-18";
const pairs = valueString.split("&");

let origin = "0,0-0";

let tacoSpots = [];
let arrayTastiness = [];
let arrayDistance = [];

while(tacoSpots.length<8){
  let [oKey, oValue] = origin.split("-");
      const [okeyX, okeyY] = oKey.split(",");
        let array = [];
        let distanceArray = [];
pairs.forEach((pair, index)=>{
    const [key, value] = pair.split("-");
    const[keyX, keyY] = key.split(",");
    if(!arrayTastiness.includes(value)) {
          const distance = Math.pow(( Math.pow((parseInt(keyX) - parseInt(okeyX) ), 2) + Math.pow((parseInt(keyY) - parseInt(okeyY)), 2)  ),0.5);
          const optimizevalue = parseFloat(value) / distance;
          array.push(optimizevalue);
          distanceArray.push(distance);

    }
    else{
      array.push(0);
      distanceArray.push(0);
    }
});
    const maxvalue = Math.max(...array);
    const position = array.indexOf(maxvalue);
    origin = pairs[position];
    tacoSpots.push(position);
    const value = pairs[position].split("-")[1];
    arrayTastiness.push(value);
    arrayDistance.push(distanceArray[position]);
}
const totalDistance = arrayDistance.reduce((acc, curr) => acc + curr, 0);
const totalTastiness = arrayTastiness.reduce((acc, curr) => acc + parseInt(curr), 0);
const tacoSpotsBaseOne = tacoSpots.map(num => num + 1);

console.log("Distance",arrayDistance);
console.log("Tastiness",arrayTastiness);
console.log("Path",tacoSpotsBaseOne);
console.log("Total Tastiness",totalTastiness);
console.log("Total Distance",totalDistance);
console.log("Tastiness per Distance", totalTastiness/totalDistance);

79798230
0
Author

Doesn't seem to have the right solution.

79789696
1
The best route is: [9, 3, 7, 5, 6, 2, 10, 4]
With a total tastiness of '108',
requiring a travel distance of '38.76636379741122'
and having hence the highest tastiness per distance '2.78592030360124'.

I learned Python scripting, repeated math (variations and permutations).
And I found out, that my first approach, a path finding algorithm with a distance matrix, would be of no advantage, since the criteria is not a fix value like total travel dist, but the combination of tastiness (node weight) and travel distance. This left me with brute force. Luckily, python has got an permutation algo, which saved me from coming up with an own solution.

Code:

import math
import itertools

# greet
print("The taco route navigator T-4C0 starts...")

# declare entities
class POI:
    def __init__(self, spotId: int):
        self.spotId = spotId
    
    def __str__(self) -> str:
        return self.spotId
    
    def at(self, x: float, y: float):
        self.x = x
        self.y = y
        return self
    
    def scoring(self, score: float):
        self.score = score
        return self
        
    def calc_dist_to(self, x: float, y: float) -> float:
        dx = x - self.x
        dy = y - self.y
        return math.sqrt(dx*dx + dy*dy)

class RouteStats:
    def __init__(self, targets: 'list[POI]', start_pos: POI):
        
        # print("Create route stats for " + str(targets))
        
        self.targets = targets
        self.description = stringify_targets(targets)
        self.total_score = sum(list(map(lambda poi: poi.score, targets)), 0)
        
        total_distance = targets[0].calc_dist_to(start_pos.x, start_pos.y)
        for i in range(len(targets)-1):
            total_distance += targets[i].calc_dist_to(targets[i+1].x, targets[i+1].y)
        self.total_distance = total_distance
        
        self.efficiency = self.total_score / (self.total_distance if self.total_distance > 0 else 1)
    
    def __str__(self) -> str:
        return "{desc} - efficiency: {ef} - score(total): {score} - dist(total): {dist}".format(
            desc=self.description, ef=self.efficiency, score=self.total_score, dist=self.total_distance)

def stringify_targets(targets: 'list[POI]') -> str:
    return str(list(map(lambda poi: poi.spotId, targets)))

# set up POIs
all_spots: 'list[POI]' = [
    POI(1).at(2,8).scoring(7),
    POI(2).at(15, 3).scoring(17),
    POI(3).at(6, 6).scoring(8),
    POI(4).at(20, 15).scoring(30),
    POI(5).at(11, 2).scoring(12),
    POI(6).at(14, 1).scoring(15),
    POI(7).at(7, 3).scoring(5),
    POI(8).at(1, 12).scoring(12),
    POI(9).at(3, 6).scoring(3),
    POI(10).at(14, 13).scoring(18)
]

start_pos = POI(0).at(0,0).scoring(0)
max_targets = 8

def determine_best_route_by_brute_force() -> RouteStats:
    
    print("Taking brute force approach.")
    print("Create permutations from targets...")
    
    # generate permutations
    # expected amount = 
    # n!  / (n -k)!
    # 10! / (10-8)!
    # 10! / (2)!
    # 3628800 / 2
    # 1814400
    all_routes: list[list[POI]] = list(itertools.permutations(all_spots, max_targets))
    print("Got '" + str(len(all_routes)) + "' permutations. Should be '1814400'.")
    print("1st permutation: '" + stringify_targets(all_routes[0]) + "'.")
    print("last permutation: '" + stringify_targets(all_routes[-1]) + "'.")
    
    # create statistics
    print("Create statistics from all route options.")
    all_route_stats: list[RouteStats] = list(map(lambda route: RouteStats(route, start_pos), all_routes))
    
    # sort to determine the optimum
    print("Sorting route stats on efficiency.")
    sorted_route_stats = sorted(all_route_stats,
                                key = lambda routeStats: routeStats.efficiency,
                                reverse = True)
    
    amount_to_show: int = 100
    print("The " + str(amount_to_show) + " best routes sorted on efficiency:")
    
    for i in range(amount_to_show):
        print("\t- " + str(sorted_route_stats[i]))
    
    return sorted_route_stats[0]

# now compute

print("Computing optimal taco restaurant route...")

best_route_stats = determine_best_route_by_brute_force()

print("\n")
print("=================================================================================")
print("\n")
print("And the best route is:")
print("\n")
print("\t" + best_route_stats.description)
print("\n")
print("\tWith a total tastiness of '" + str(best_route_stats.total_score) + "',")
print("\trequiring a travel distance of '" + str(best_route_stats.total_distance) + "'")
print("\tand having hence the highest tastiness per distance '" + str(best_route_stats.efficiency) + "'.")
print("\n")
print("\n")
print("Please, download also our Super-Gym-Navigator. You might soon need that, too.")


-------------

Output:

The taco route navigator T-4C0 starts...
Computing optimal taco restaurant route...
Taking brute force approach.
Create permutations from targets...
Got '1814400' permutations. Should be '1814400'.
1st permutation: '[1, 2, 3, 4, 5, 6, 7, 8]'.
last permutation: '[10, 9, 8, 7, 6, 5, 4, 3]'.
Create statistics from all route options.
Sorting route stats on efficiency.
The 100 best routes sorted on efficiency:
        - [9, 3, 7, 5, 6, 2, 10, 4] - efficiency: 2.78592030360124 - score(total): 108 - dist(total): 38.76636379741122
        - [1, 3, 7, 5, 6, 2, 10, 4] - efficiency: 2.680932606674377 - score(total): 112 - dist(total): 41.77650707114675
        - [9, 1, 3, 5, 6, 2, 10, 4] - efficiency: 2.6447197447535657 - score(total): 110 - dist(total): 41.5923086815574
        - [1, 9, 3, 5, 6, 2, 10, 4] - efficiency: 2.6405378218731608 - score(total): 110 - dist(total): 41.658180045293776
                ....
79790257
1

I also failed to read that 8 spots is just a maximum limit. A minimum requirement of nutrition / tastiness would've been a good hint / additional rule in the exercise description.

However, I tested now different amount of spots; k=3...8 - and i get the 6 spot route now, too.

minor changes to the code are:

# somehwat DIFF-ish
- max_targets = 8
+ max_targets_start = 3
+ max_targets_end = 8

- def determine_best_route_by_brute_force() -> RouteStats:
+ def determine_best_route_by_brute_force(max_targets: int) -> RouteStats:

...

- best_route_stats = determine_best_route_by_brute_force()

+ best_routes_per_target_count: 'list[RouteStats]' = []
+ 
+ for i in range(max_targets_start, max_targets_end + 1):
+     route_stats: RouteStats = determine_best_route_by_brute_force(i)
+     best_routes_per_target_count.append(route_stats)
+ 
+ best_route_stats: RouteStats = max(best_routes_per_target_count, key = lambda stats: stats.efficiency)

...

+ print("Best routes per target count:")
+ for i in range(len(best_routes_per_target_count)):
+     print(str(i+1) + ". " + str(best_routes_per_target_count[i]))

----------------

Best routes per target count:
1. [7, 5, 6, 2, 10, 4] - efficiency: 2.8945153290979557 - score(total): 97 - dist(total): 33.51165531060739
2. [7, 5, 6, 2] - efficiency: 2.8592728288140585 - score(total): 49 - dist(total): 17.13722436914974
3. [3, 7, 5, 6, 2, 10, 4] - efficiency: 2.7967601406369123 - score(total): 105 - dist(total): 37.543441239150425
4. [5, 6, 2, 10, 4] - efficiency: 2.79184519901717 - score(total): 92 - dist(total): 32.95311646662476
5. [9, 3, 7, 5, 6, 2, 10, 4] - efficiency: 2.78592030360124 - score(total): 108 - dist(total): 38.76636379741122
6. [5, 6, 2] - efficiency: 2.654010170661975 - score(total): 44 - dist(total): 16.578685525167117

Sequence 5-6-2 is repeatedly present.
79789464

This reply has been deleted.

79796057
3

Try 'correctness first' instead of 'scalability first'. If your code does not compute the correct answer then it does not matter how scalable it is ...

79789392
3

A little quick C brute-force search. output somewhat compact but does it.

Some oldschool programm with bits-packing. No AI would have coded it that way! :-)

Brute-force solver runs for just 35 ms as measured with 'time' tool and -O3 build.

(edited because I forgot the seperate numbers for taste/distance)

Path is 7 5 6 2 10 4

tastiness / pathlen: 97 / 33.512

My little not-so-nice program:

#include <math.h>
#include <stdio.h>

#define NUM 10

struct TacoDef {
    int id,x,y,taste;
};

const struct TacoDef list[] = 
{
    {1,   2, 8 ,  7},
    {2,  15, 3 , 17},
    {3,   6, 6 ,  8},
    {4,  20, 15, 30},
    {5,  11, 2 , 12},
    {6,  14, 1 , 15},
    {7,   7, 3 ,  5},
    {8,   1, 12, 12},
    {9,   3, 6 ,  3},
    {10, 14, 13, 18},
    {0,   0,  0,  0}    // pseudo-item for starting point.
};


static float search(unsigned int prevs,int prev_count,unsigned int prev_mask,float current_sum_dist,int current_sum_taste,unsigned int *out_prevs,int *out_sum_taste);

int main()
{
    float best;
    unsigned int bestpath;
    int best_tastiness;
    unsigned int i,bits;

    // run search with recursive function call.
    best = search(~0u,0,0,0.0f,0,&bestpath,&best_tastiness);

    printf("best tastiness/distance: %.4f\n",best);
    printf("best tastiness: %d\n",best_tastiness);
    printf("best distance: %f\n",best_tastiness/best);
    printf("best path: 0x%X\n",bestpath);
    for(i=0;i<8;i++)
    {
        bits = ( bestpath>>(4*(7-i)) ) & 0x0F;
        if(bits<15)
            printf(" %d",list[bits].id);
    }
    printf("\n");
    return 0;
}


static float search(unsigned int prevs,int prev_count,unsigned int prev_mask,float current_sum_dist,int current_sum_taste,unsigned int *out_prevs,int *out_sum_taste)
{
    float result = -1.0f;
    int i;
    int prev_id;

    // one option is to end here.
    if(prev_count>0)
    {
        result = current_sum_taste/current_sum_dist;
        *out_prevs = prevs;
        *out_sum_taste = current_sum_taste;
    }
    // if already have max of 8, exit without trying more path elements.
    if(prev_count>=8)
        return result;


    // try adding path items.
    prev_id = ( prev_count>0 ? prevs&0x0F : NUM );
    for(i=0;i<NUM;i++)
    {
        float dx,dy,dist;
        float q;
        unsigned int op;
        int sum_t;
        // this already in path?
        if(prev_mask&(1<<i))
            continue;
        dx = (float)(list[prev_id].x-list[i].x);
        dy = (float)(list[prev_id].y-list[i].y);
        dist = sqrtf(dx*dx+dy*dy);

        // recursive call to self with modified values.
        q = search(
                (prevs<<4) | i,     // prevs is digit-list of path history
                prev_count+1 ,      // count is one more
                prev_mask|(1<<i) ,  // in-use mask with extra bit in it
                current_sum_dist + dist ,
                current_sum_taste + list[i].taste ,
                &op ,               // resulting best-path in local var.
                &sum_t              // same with sum-taste
        );
        if(q>result)
        {
            // is best so far. replace result value and upstream best-path
            result = q;
            *out_prevs = op;
            *out_sum_taste = sum_t;
        }
    }

    return result;
}


79798231
0
Author

You get the "most non-AI" award 😀

79789255
1

Best path: [9, 3, 7, 5, 6, 2, 10, 4]

tastiness: 108

distance: 38.76636379741122

score: 2.78592030360124

I did this in python.

I used an in-built method, permutations to get the possible 8-node paths which could be taken through the graph.

Then I scored each, using a dynamic programming technique, where I scored a path of length 7, and then added the 8th node, performing this recursively to the base-case where a path has length 1 remaining.

from itertools import permutations
from math import sqrt, pow
from collections import namedtuple

Location = namedtuple('Location', ['x', 'y' ])
Node = namedtuple('Node', ['id', 'location', 'tastiness' ])

nodes = [
    Node(1,  Location(2, 8),    7),
    Node(2,  Location(15, 3),   17),
    Node(3,  Location(6, 6),    8),
    Node(4,  Location(20, 15),  30),
    Node(5,  Location(11, 2),   12),
    Node(6,  Location(14, 1),   15),
    Node(7,  Location(7, 3),    5),
    Node(8,  Location(1, 12),   12),
    Node(9,  Location(3, 6),    3),
    Node(10, Location(14, 13),  18)]


def score_path(n):
    node = n[-1]
    if len(n) == 1:
        distance = sqrt(pow((node.location.x-0),2)+pow((node.location.y-0),2))
        tastiness = node[2]
        return distance, tastiness

    prior_node = n[-2]
    predistance, pretastiness = score_path(n[:-1])
    distance = predistance + sqrt(pow((node.location.x-prior_node.location.x),2)+pow((node.location.y-prior_node.location.y),2))
    tastiness = pretastiness + node.tastiness

    return distance, tastiness

Path = namedtuple('Path', ['path','tastiness','distance','score'])

best_path = Path(None, None, None, 0)
for n in permutations(nodes, 8):
    distance, tastiness = score_path(n)
    score = tastiness/distance
    if(score>best_path.score):
        best_path = Path(n, tastiness, distance, score)

print(f"Best path: {[i[0] for i in best_path.path]}")
print(f"tastiness: {best_path.tastiness}")
print(f"distance: {best_path.distance}")
print(f"score: {best_path.score}")

I tried caching the results of scoring paths, so that the score of sub-paths could be reused, but it didn't improve the speed enough to warrant the memory, maybe it would if the path size increased to 9 or higher.

On that note this doesn't scale very well with the node count, but solved sufficiently quickly for this problem size.

I hadn't used named tuples before but thought these may act as a good compromise between tuples and classes. So far I haven't seen any advantage over writing a class however.

79789267
1

With the seeing the other answers, I see my code didn't consider paths of less than 8 destinations. Whoops.

With this replacement

for i in range(1,8):
    for n in permutations(nodes, i):

I get

Best path: [7, 5, 6, 2, 10, 4]

tastiness: 97

distance: 33.51165531060739

score: 2.8945153290979557

79789135
2

As the problem states, a brute force solution is feasible, but for this problem size it takes some time to complete. Regardless of the solver, the basic setup looks like this:

import numpy as np
import pandas as pd

from tacos_bf import taste_over_distance_bf
from tacos_dp import taste_over_distance_dp

taco_spots = [
    {'x': 2, 'y': 8, 'tasty': 7},
    {'x': 15, 'y': 3, 'tasty': 17},
    {'x': 6, 'y': 6, 'tasty': 8},
    {'x': 20, 'y': 15, 'tasty': 30},
    {'x': 11, 'y': 2, 'tasty': 12},
    {'x': 14, 'y': 1, 'tasty': 15},
    {'x': 7, 'y': 3, 'tasty': 5},
    {'x': 1, 'y': 12, 'tasty': 12},
    {'x': 3, 'y': 6, 'tasty': 3},
    {'x': 14, 'y': 13, 'tasty': 18}
]


def distance(p1, p2):
    return np.sqrt((p1['x'] - p2['x']) ** 2 + (p1['y'] - p2['y']) ** 2)


def get_distances(df):
    result = np.zeros((len(df), len(df)))
    for i in range(len(df)):
        for j in range(len(df)):
            if i != j:
                result[i][j] = distance(df.iloc[i], df.iloc[j])
            else:
                result[i][j] = np.inf

    return result


def main():
    df = pd.DataFrame(taco_spots)
    rewards = df['tasty'].to_numpy(dtype=float)
    starts = (
            (df['x'].to_numpy(dtype=float) ** 2 +
             df['y'].to_numpy(dtype=float) ** 2) ** 0.5)
    distances = get_distances(df)
    visit_n = 4

    trip, score = taste_over_distance_bf(rewards, starts, distances, visit_n)
    print(f'Best trip of {visit_n} taco spots:', trip)
    print('Best score (tasty/distance):', score)

    trip, score, t_tastiness, t_distance = taste_over_distance_dp(rewards, starts, distances, visit_n)
    print(f'Best trip of {visit_n} taco spots:', trip)
    print('Best score (tasty/distance):', score)
    print('Total tastiness:', t_tastiness)
    print('Total distance:', t_distance)


if __name__ == '__main__':
    main()

This solution needs pip install numpy pandas and runs the brute force solution first, and then my more optimal one. I would recommend commenting out the brute force solution for visit_n over 6.

The brute force solution simply computes the total tastiness for all permutations of suitable length and in the end reports the best total tastiness over total distance.

This is tacos_bf.py:

def taste_over_distance_bf(rewards, starts, distances, max_length):
    from itertools import permutations

    n = len(rewards)
    best_score = 0
    best_combination = None

    for length in reversed(range(1, max_length+1)):
        print(f'Checking length {length}')
        for perm in permutations(range(n), length):
            total_tasty = sum(rewards[i] for i in perm)
            total_distance = starts[perm[0]]
            for a, b in zip(perm, perm[1:]):
                total_distance += distances[a, b]
            if total_tasty / total_distance > best_score:
                print(total_tasty / total_distance, perm)
                best_score = total_tasty / total_distance
                best_combination = perm

    return best_combination, best_score

It prints solutions as it finds better ones.

The much more efficient solution finds the optimal route by using Dinkelbach’s method (see https://en.wikipedia.org/wiki/Fractional_programming) to transform the fractional objective max R/C into a series of simpler parametric subproblems max (R - lam C) for varying lam (lambda), where R is total tastiness and C is total distance. The parameter lam is iteratively updated until it converges. Each subproblem balances tastiness gain vs. distance cost and steady improvement toward the globally optimal ratio is guaranteed for sufficiently small epsilon.

To solve each subproblem, it uses a dynamic programming (DP) over subsets approach, similar to the Held–Karp algorithm for the Travelling Salesman Problem but adapted for orienteering: paths start at (0,0), and may visit up to k spots without repeats, and end anywhere without needing to return. The inner part explores all valid paths up to size k using bitmasks, and keeps only the best path to each (subset, last_node) state. The method is suitable for small to medium-sized problems (up to ~25 locations with ~10 visits) and much faster than the brute force since it avoids enumerating full permutations directly, and can exit early when it has converged very closely.

This is tacos_dp.py:

from typing import Optional


class BitMask(int):
    def __new__(cls, value):
        return super().__new__(cls, value)

    def __repr__(self):
        return f"BitMask({self:#018b})"

    def __str__(self):
        return f"{self:#018b}"


def max_total_reward_minus_lambda_total_cost(lam, k, n, rewards, starts, distances):
    layers: list[Optional[dict[tuple[int, int], float]]] = [None] * (k + 1)

    # initialise layer 1 (single node visits)
    layer1 = {}
    parent = {}  # parent[(mask,last)] = (prev_mask, prev_last)
    for j in range(n):
        m = 1 << j
        v = rewards[j] - lam * starts[j]
        layer1[(m, j)] = v
        parent[(m, j)] = (0, -1)  # depot parent
    layers[1] = layer1

    best_val = float('-inf')
    best_key: Optional[tuple[int, int]] = None  # (mask, last)
    best_size = 1

    # find best in layer 1 as a baseline
    for key, v in layer1.items():
        if v > best_val:
            best_val, best_key, best_size = v, key, 1

    all_mask = (1 << n) - 1
    for s in range(1, k):
        if not layers[s]:
            break
        next_layer = {}
        # mask: BitMask of visited nodes, last: index of last visited node
        for (mask, last), val in layers[s].items():
            # mask of unvisited nodes
            free: BitMask = (~mask) & all_mask
            # pre-binding required lookups for speed
            get_next = next_layer.get
            r = rewards
            ds = distances
            while free:
                lsb: BitMask = free & -free
                u = (lsb.bit_length() - 1)
                free &= free - 1
                new_mask: BitMask = mask | lsb
                candidate = val + r[u] - lam * ds[last, u] # +reward -penalty
                key = (new_mask, u)
                prev_best = get_next(key)
                if prev_best is None or candidate > prev_best:
                    next_layer[key] = candidate
                    parent[key] = (mask, last)
                    # update global best
                    if candidate > best_val:
                        best_val, best_key, best_size = candidate, key, s + 1
        layers[s + 1] = next_layer

    # pick the best single node if nothing better is found
    if best_key is None:
        j = max(range(n), key=lambda jj: rewards[jj] - lam * starts[jj])
        best_key = (1 << j, j)
        best_val = rewards[j] - lam * starts[j]
        parent[best_key] = (0, -1)
        best_size = 1

    path = []
    mask, last = best_key
    for _ in range(best_size):
        path.append(last)
        mask, last = parent[(mask, last)]
    path.reverse()

    # total_reward R, total_cost C
    total_reward = float(sum(rewards[idx] for idx in path))
    if not path:
        total_cost = 0.0
    else:
        total_cost = float(starts[path[0]])
        for a, b in zip(path, path[1:]):
            total_cost += float(distances[a, b])

    return best_val, total_reward,total_cost, tuple(path)


def taste_over_distance_dp(
        rewards, starts, distances, max_length, eps=1e-9, max_iter=50):
    # Dinkelbach's method for fractional programming
    n = len(rewards)
    k = max(1, min(max_length, n))  # cap at n, require at least 1 node

    lam = 0.0
    best_ratio = 0.0
    best_path = ()
    best_tr = 0.0
    best_tc = 0.0
    for _ in range(max_iter):
        f_val, total_reward, total_cost, path = \
            max_total_reward_minus_lambda_total_cost(
                lam, k, n, rewards, starts, distances)
        # deal with the edge case where a place would be at the start
        # (infinite reward/distance)
        if total_cost <= eps:
            return path, 0.0, 0.0, 0.0
        gap = total_reward - lam * total_cost
        if abs(gap) <= eps:
            best_ratio, best_path, best_tr, best_tc = \
                total_reward / total_cost, path, total_reward, total_cost
            break
        lam = total_reward / total_cost
        best_ratio, best_path = lam, path

    return best_path, best_ratio, best_tr, best_tc

For n_visit equals 4, the output is:

Checking length 4
1.2835176801392247 (0, 1, 2, 3)
1.2944733015866365 (0, 1, 3, 4)
1.3688806987089908 (0, 1, 3, 5)
1.7349750919604081 (0, 1, 3, 9)
1.7405687909220895 (0, 1, 5, 3)
1.8496391913449162 (0, 1, 5, 4)
1.8677512539845096 (0, 1, 9, 3)
1.9229787154964553 (0, 2, 1, 5)
1.9271459833366686 (0, 2, 5, 1)
2.1231387917953564 (0, 2, 9, 3)
2.2310421616226326 (1, 5, 9, 3)
2.4473376225281536 (2, 4, 1, 5)
2.563249253706544 (2, 4, 5, 1)
2.707473660374164 (6, 4, 1, 5)
2.8592728288140585 (6, 4, 5, 1)
Checking length 3
Checking length 2
Checking length 1
Best trip of 4 taco spots: (6, 4, 5, 1)
Best score (tasty/distance): 2.8592728288140585
Best trip of 4 taco spots: (6, 4, 5, 1)
Best score (tasty/distance): 2.8592728288140585
Total tastiness: 49.0
Total distance: 17.13722436914974

The answer for n_visits equals 8 is:

Best trip of 8 taco spots: (6, 4, 5, 1, 9, 3)
Best score (tasty/distance): 2.8945153290979557
Total tastiness: 97.0
Total distance: 33.51165531060739

Note that the indices are off by one, so the actual answer is 7, 5, 6, 2, 10, 4, total tastiness of 97, a distance of ~33.52, and a "tastiness per distance" metric of ~2.895.

Also note that the same result could be achieved by running for n_visits equal to 6, which completes reasonably fast on brute force as well (with an identical result of course).

Note that the BitMask class is there for debugging, and may be helpful if you're trying to step through the solution (your debugger will make use of it if you wrap the bitmask assignments in the type to cast the values).

79789627
0

I enjoyed this exercise because the problem is very easy to understand, but deceptively complex. It was good to remind myself of things I learnt in CS decades ago, and have them work again. Modern hardware makes taking the easy way out very attractive, but finding a nice solution is much more satisfying.

79791594
0

I came back to have a look at other answers and then noticed a spelling error in the explanation - the code had the right answer all along, but I failed to increment the last number in the correct answer in the text by 1... hence the edit.

79788856
2
  • Route: [7, 5, 6, 2, 10, 4],

  • Total tastiness: 97,

  • Total distance: 33.51165531061,

  • Tastiness per distance: 2.894515329098.

    Basic idea is to count minimum distance for every subset of spots using dynamic programming over pairs of a subset and last spot in the subset. After that we can consider only allowed subsets (of at most $8$ spots) and find the answer. Takes Θ(2^n · n^2) of time and Θ(2^n · n) of memory, or 16 ms for the given input.

    Note that saving distance only per DP state saves time and memory. (Ok, it is not essential for a small input, but would make sense for a bigger one.) Also in route reconstruction part we have right to (and should!) compare distances using equality without any tolerance, because we recompute the same value of distance in the same way as in the first part.

#include <iostream>
#include <vector>
#include <algorithm>
#include <cmath>

using namespace std;

struct Spot
{
    int x, y;
    int tastiness;
};

double sqr(double x)
{
    return x * x;
}

void CrawlTacos(vector<Spot> &&spots, int maxSpots)
{
    const double infinity = 1e308;
    int n = spots.size();
    vector<vector<double>> distanceBetweenSpots(n, vector<double>(1 << n, 0.));
    for (int start = 0; start < n; start++)
        for (int finish = 0; finish < n; finish++)
            distanceBetweenSpots[start][finish] = sqrt(sqr(spots[start].x - spots[finish].x) + sqr(spots[start].y - spots[finish].y));

    vector<vector<double>> distanceForSubsetWithLast(n, vector<double>(1 << n));

    for (int subset = 1; subset < 1 << n; subset++)
    {
        for (int last = 0; last < n; last++)
            if ((subset >> last & 1) > 0)
            {
                if (subset == (1 << last))
                {
                    distanceForSubsetWithLast[last][subset] = sqrt(sqr(spots[last].x) + sqr(spots[last].y));
                    break;
                }

                double distance = infinity;

                for (int previous = 0; previous < n; previous++)
                    if (previous != last && (subset >> previous & 1) > 0)
                        distance = min(distance, distanceForSubsetWithLast[previous][subset ^ 1 << last] + distanceBetweenSpots[previous][last]);

                distanceForSubsetWithLast[last][subset] = distance;
            }
    }

    int bestSubset = 0;
    int bestSubsetTastiness = 0;
    double bestSubsetDistance = 0;
    double bestSubsetMetric = 0;

    for (int subset = 1; subset < 1 << n; subset++)
    {
        int totalTastiness = 0;
        int spotsVisited = 0;
        double minDistance = infinity;

        for (int spot = 0; spot < n; spot++)
            if ((subset >> spot & 1) > 0)
            {
                totalTastiness += spots[spot].tastiness;
                spotsVisited++;
                minDistance = min(minDistance, distanceForSubsetWithLast[spot][subset]);
            }

        double metric = totalTastiness / minDistance;

        if (spotsVisited <= maxSpots && metric > bestSubsetMetric)
        {
            bestSubset = subset;
            bestSubsetTastiness = totalTastiness;
            bestSubsetDistance = minDistance;
            bestSubsetMetric = metric;
        }
    }

    vector<int> route;
    for (int last = 0; last < n; last++)
        if ((bestSubset >> last & 1) > 0 && distanceForSubsetWithLast[last][bestSubset] == bestSubsetDistance)
        {
            double partialDistance = bestSubsetDistance;
            while ((bestSubset & bestSubset - 1) > 0)
            {
                route.push_back(last);
                bestSubset ^= 1 << last;

                for (int previous = 0; previous < n; previous++)
                    if (previous != last && distanceForSubsetWithLast[previous][bestSubset] + distanceBetweenSpots[previous][last] == partialDistance)
                    {
                        last = previous;
                        partialDistance = distanceForSubsetWithLast[previous][bestSubset];
                        break;
                    }
            }

            for (int first = 0; first < n; first++)
                if ((bestSubset >> first & 1) > 0)
                {
                    route.push_back(first);
                    break;
                }

            break;
        }

    reverse(route.begin(), route.end());

    cout << "Route: [";
    for (int i = 0; i < route.size(); i++)
        cout << (i > 0 ? ", " : "") << route[i] + 1;
    cout << "]" << endl;

    cout << "Total tastiness: " << bestSubsetTastiness << endl;
    cout.precision(13);
    cout << "Total distance: " << bestSubsetDistance << endl;
    cout << "Tastiness per distance: " << bestSubsetMetric << endl;
}

int main()
{
    CrawlTacos(vector<Spot>{
        { 2, 8, 7 },
        { 15, 3, 17 },
        { 6, 6, 8 },
        { 20, 15, 30 },
        { 11, 2, 12 },
        { 14, 1, 15 },
        { 7, 3, 5 },
        { 1, 12, 12 },
        { 3, 6, 3 },
        { 14, 13, 18 }}, 8);

    return 0;
}
79788853
3

Path Length: 6 spots
Best Ratio (Tastiness/Distance): 2.89451532909796
Path (Spot IDs): [7, 5, 6, 2, 10, 4]

"Brute force" example in SQL (PostgreSql)

With data as

spotid cx cy tastiness idx k
1 2 8 7 1 1
2 15 3 17 2 2
3 6 6 8 4 3
4 20 15 30 8 4
5 11 2 12 16 5
6 14 1 15 32 6
7 7 3 5 64 7
8 1 12 12 128 8
9 3 6 3 256 9
10 14 13 18 512 A

Added two columns "idx" - to check the node traverse no more than once. Since the number of nodes is small, I use a bitmask for accumulation nodes in path.
"k" - key for node as char(1) - path as symbols of nodes.

Recursive query to find all possible path with 8 or less step.

with recursive r as (
select 1 step,SpotId root,SpotId,t.cx,t.cy, tastiness as sumTastiness
  ,sqrt((cx-0)*(cx-0)+(cy-0)*(cy-0)) sumDist
  ,t.idx as npath
  ,cast(t.k as varchar(30)) as spath
from Spots t
  union all
select step+1 step,r.root,t.SpotId, t.cx,t.cy, r.sumTastiness+t.tastiness as sumTatiness
  ,r.sumdist+ sqrt((t.cx-r.cx)*(t.cx-r.cx)+(t.cy-r.cy)*(t.cy-r.cy)) sumDist
  ,npath+t.idx
  ,cast(concat(r.spath,',',t.k) as varchar(30)) as spath
from r inner join Spots t  on (t.idx & r.npath)=0
  where step<8
)
select root as start_spot,SpotId as end_spot
  ,sumTastiness as totalTastiness
  ,sumdist as totalDistance
  ,bestRate
  ,spath bestPath
  ,step as countSpot
from(
  select *,sumTastiness/sumDist bestRate
    ,dense_rank()over(order by sumTastiness/sumDist desc) rnk
  from r 
 ) a
where rnk=1
start_spot end_spot totalTastiness totalDistance bestRate bestPath countspot
7 4 97 33.5116553106074 2.89451532909796 7,5,6,2,10,4 6

Traverse path accumulated as bitmask "npath+t.idx".
Check for traversed nodes "(t.idx & r.npath)=0".
Path (spath - string) for output "concat(r.spath,t.k)"

This sqlfiddle as example.

fiddle
This fiddle only for example, max level <5.

79788684
2

Optimal route: 7, 5, 6, 2, 10, 4

Distance: 33.51165531060739

Tastiness: 97

Tastiness per distance: 2.8945153290979557

Python code:

from __future__ import annotations

from itertools import chain, combinations, permutations
from typing import Iterable, Optional


class Taco:
    """
    Taco class to contain data and have a few useful functions
    """

    def __init__(self, x: float, y: float, tastiness: float):
        self.x = x
        self.y = y
        self.tastiness = tastiness

    def __repr__(self):
        return f"({self.x},{self.y})-{self.tastiness}"

    def __eq__(self, __value):
        return super().__eq__(__value)

    def __ne__(self, __value):
        return super().__ne__(__value)

    def __hash__(self):
        return super().__hash__()

    def distance(self, other: Taco):
        return ((self.x - other.x) ** 2 + (self.y - other.y) ** 2) ** 0.5


def powerset(iterable: Iterable, min_size: int = 1, max_size: int = -1) -> Iterable:
    """
    Returns a powerset of the given iterable of all sizes from min_size to max_size (inc)
    :param iterable: The set to make a powerset of
    :param min_size: minimum size of the sets in the powerset
    :param max_size: maximum size of the sets in the powerset, default: len(iterable)
    :return: an iterable of the powerset
    """
    s: list = list(iterable)
    max_size: int = max_size if max_size > 0 else len(s)
    return chain.from_iterable(combinations(s, r) for r in range(min_size, max_size + 1))


def get_taco_problem() -> dict[int, Taco]:
    return {
        1: Taco(2, 8, 7),
        2: Taco(15, 3, 17),
        3: Taco(6, 6, 8),
        4: Taco(20, 15, 30),
        5: Taco(11, 2, 12),
        6: Taco(14, 1, 15),
        7: Taco(7, 3, 5),
        8: Taco(1, 12, 12),
        9: Taco(3, 6, 3),
        10: Taco(14, 13, 18),
    }


def main():
    tacos: dict[int, Taco] = get_taco_problem()
    start_taco: Taco = Taco(0, 0, 0)
    max_value: float = 0
    best_distance: float = 0
    best_tastiness: float = 0
    best_route: Optional[tuple[int]] = None
    # Iterate over all orderings (i.e., permutations) for all subsets in the powerset of the taco problem (up to size 8)
    groups: list[tuple[int]] = [route for subset in powerset(tacos, 1, 8) for route in permutations(subset)]
    for route in groups:
        distance: float = start_taco.distance(tacos[route[0]])
        distance += sum(tacos[route[i]].distance(tacos[route[i + 1]]) for i in range(len(route) - 1))
        tastiness: float = sum(tacos[i].tastiness for i in route)
        if tastiness / distance > max_value:
            max_value = tastiness / distance
            best_distance = distance
            best_tastiness = tastiness
            best_route = route
    print(best_route, best_tastiness, best_distance, max_value)


if __name__ == '__main__':
    main()
79788662
2

--- Taco Crawl Result ---

Optimal Path Length: 6 spots

Optimal Ratio (Tastiness/Distance): 2.89

Optimal Path (Spot IDs): [7, 5, 6, 2, 10, 4]

(function computeOptimalTacoPath(tacoPlaces, MAX_STOPS = 8){
    const start = performance.now();
    const dist = ([x1, y1], [x2, y2]) => Math.hypot(x2 - x1, y2 - y1);
    let bestRatio = 0;
    let bestPath = [];
    
    const stack = [{
        lastCoord: [0, 0],
        totalD: 0,
        totalT: 0,
        visitedMask: 0,
        path: []
    }];
    
    for (var iterations = 0;stack.length > 0; iterations++) {
        const currentState = stack.pop();
        const { lastCoord, totalD, totalT, visitedMask, path } = currentState;
    
        // 1. Check current path for optimality (only if we've visited at least one spot)
        if (path.length > 0) {
            const currentRatio = totalT / totalD;
            if (currentRatio > bestRatio) {
                bestRatio = currentRatio;
                bestPath = path;
            }
        }
    
        // 2. Pruning: Stop if we've reached the maximum number of stops
        if (path.length >= MAX_STOPS) continue;
        // 3. Explore next possible spots
        for (let n = 0; n < tacoPlaces.length; n++) {
            // Check if the n'th spot has NOT been visited
            if (!(visitedMask & (1 << n))) {
                const spot = tacoPlaces[n];
                // Create the new state for the next level and push to the stack
                const nextState = {
                    lastCoord: [spot.x, spot.y],
                    totalD: totalD + dist(lastCoord, [spot.x, spot.y]),
                    totalT: totalT + spot.t,
                    visitedMask: visitedMask | (1 << n),
                    path: [...path, spot.id]
                };
                stack.push(nextState);
            }
        }
    }
    
    console.log(`-------------------------`);
    console.log(`--- Taco Crawl Result ---`);
    console.log(`-------------------------`);
    console.log(`Compute time: ${(performance.now()-start).toFixed(2)}`);
    console.log(`Iterations: ${iterations}`);
    console.log(`Optimal Path Length: ${bestPath.length} spots`);
    console.log(`Optimal Ratio (Tastiness/Distance): ${bestRatio.toFixed(2)}`);
    console.log(`Optimal Path (Spot IDs): [${bestPath.join(', ')}]`);
})([
    { id: 1, x: 2, y: 8, t: 7, index: 0 },
    { id: 2, x: 15, y: 3, t: 17, index: 1 },
    { id: 3, x: 6, y: 6, t: 8, index: 2 },
    { id: 4, x: 20, y: 15, t: 30, index: 3 },
    { id: 5, x: 11, y: 2, t: 12, index: 4 },
    { id: 6, x: 14, y: 1, t: 15, index: 5 },
    { id: 7, x: 7, y: 3, t: 5, index: 6 },
    { id: 8, x: 1, y: 12, t: 12, index: 7 },
    { id: 9, x: 3, y: 6, t: 3, index: 8 },
    { id: 10, x: 14, y: 13, t: 18, index: 9 },
]);
79788621
4

Result

Optimal route: [7, 5, 6, 2, 10, 4] (6 stops)

Total tastiness: 97

Total distance: 33,512 (rounded)

Ratio: 2.895 (rounded)

Code

Its a GPU powered brute force approach.

First i create all permutations (10! = 3.628.800) with a PermutationShader and then calculate all the routes with their optimum. After that, i read back the route-data and search the entry with the highest tastiness / distance ratio.

TacoCrawlShader

using ComputeSharp;
using ParalellComputing.Models.TacoCrawl;

namespace ParalellComputing.Shader;

[ThreadGroupSize(DefaultThreadGroupSizes.X)]
[GeneratedComputeShaderDescriptor]
public readonly partial struct TacoCrawlShader(
    int maxStops,
    ConstantBuffer<TacoSpot> tacoSpots,
    ReadWriteBuffer<Int12> permutationOutput,
    ReadWriteBuffer<TacoCrawlRoute> tacoCrawlRoutes)
    : IComputeShader
{
    public void Execute()
    {
        int optimum = 0;
        float maxRatio = 0;
        float totalDistance = 0;
        int totalTastiness = 0;
        int2 currentPosition = new(0, 0);

        // full iteration
        for (int i = 0; i < maxStops; i++)
        {
            TacoSpot spot = tacoSpots[permutationOutput[ThreadIds.X].Get(i)];
            totalDistance += Hlsl.Sqrt(Hlsl.Pow(currentPosition.X - spot.Coordinates.X, 2) + Hlsl.Pow(currentPosition.Y - spot.Coordinates.Y, 2));
            totalTastiness += spot.Tastiness;

            float newRatio = totalTastiness / totalDistance;

            if (newRatio > maxRatio)
            {
                optimum = i;
                maxRatio = newRatio;
            }

            currentPosition = spot.Coordinates;
        }

        // get values with optimum and clear other route points
        totalDistance = 0;
        totalTastiness = 0;
        currentPosition = new(0, 0);

        for (int i = 0; i < Int12.Size; i++)
        {
            if (i <= optimum)
            {
                TacoSpot spot = tacoSpots[permutationOutput[ThreadIds.X].Get(i)];
                totalDistance += Hlsl.Sqrt(Hlsl.Pow(currentPosition.X - spot.Coordinates.X, 2) + Hlsl.Pow(currentPosition.Y - spot.Coordinates.Y, 2));
                totalTastiness += spot.Tastiness;
                currentPosition = spot.Coordinates;
            }
            else
            {
                permutationOutput[ThreadIds.X].Set(i, -1);
            }
        }

        tacoCrawlRoutes[ThreadIds.X].Route = permutationOutput[ThreadIds.X];
        tacoCrawlRoutes[ThreadIds.X].TotalDistance = totalDistance;
        tacoCrawlRoutes[ThreadIds.X].TotalTastiness = totalTastiness;
        tacoCrawlRoutes[ThreadIds.X].TastinessRatio = totalTastiness / totalDistance;
    }
}
79795282
0

How fast is it when you throw all that GPU power at the problem? Can it open the way to higher spot counts than a puny general CPU? :-)

BTW, it's 10! / 2! = 1,814,400 routes that need to be checked, not 10!.

79796792
0

The Calculation takes 250ms to 350ms with a GTX 1060. When i adapt the permutations to remove duplications it takes roughly the same amount.

I have another playground with the Travelling Salesman Problem and there it takes around the same time no matter how big the amount of towns is (up to 13).

Additionally while graphic cards can compute many thing in paralell, the available "amount of paralellization" can not keep up with a factorial. The Travelling Salesman runs into a problem with 14 town (13! / 2 permutations) stating that there are "not enough gpu kernels" to execute the shader.

So i conclude that GPU paralellization only excels when the calculations are fairly complex (e.g. multiple multiplications, divisions and vector operations like Marching Cubes) and the data required to execute the calculations is fairly low (otherwise we would waste too much time copying them back and forth to the GPU).

79796851
0

That's really interesting! Thanks for sharing!

79788432
-3


最优路线: PathPlan{totalScore=97.00, totalDistance=33.51, ratio=2.8945, path=[TacoSpot{x=7, y=3, score=5}, TacoSpot{x=11, y=2, score=12}, TacoSpot{x=14, y=1, score=15}, TacoSpot{x=15, y=3, score=17}, TacoSpot{x=14, y=13, score=18}, TacoSpot{x=20, y=15, score=30}]}
评分/距离比: 2.8945153290979557
79787838
1

This is my lazy attempt at a brute force method of evaluating all the possible point paths from one to eight points. The greatest taste per distance (tpd) appears to be at six points and is 2.89 for path [ 7, 5, 6, 2, 10, 4 ].

I had a difficult time figuring out how to have a variable level of nesting rather than writing out all the nested loops for eight points down to two.

function evalu8(path,cnt) {
  /*
    Argument path is an array of eight integers, each of
    which is also the index in array pts. Thus, iterate
    through the points.
  */
  let
     s = pts[0],
     f,
     d=0,
     t=0,
     tpd
  ;
  path.forEach( (v,i) => {
     f = pts[v];
     d += Math.sqrt( Math.pow(f[1]-s[1],2) + Math.pow(f[0]-s[0],2) );
     t += f[2];
     s = f;
  });
  tpd = t/d;
  if (tpd > best_tpd.tpd) {
    best_tpd.path = [...path];
    best_tpd.tpd = tpd;
    best_tpd.distance = d;
    best_tpd.taste = t;
  }
}
function nloop(points,path,n,j,q) {
  for (let i=0;i<n;i++) {
    path[j] = points[j][i];
    points[j+1] = points[j].toSpliced(i,1);
    if ( n-1 > q ) {
      nloop(points,path,n-1,j+1,q);
    } else {
      cnt +=1;
      evalu8(path,cnt);
   }
  }
}
let
   points,
   cnt,
   best_tpd
;
const pts = [
  [0,0,0],
  [2,8,7],
  [15,3,17],
  [6,6,8],
  [20,15,30],
  [11,2,12],
  [14,1,15],
  [7,3,5],
  [1,12,12],
  [3,6,3],
  [14,13,18]
];
for (let n=2;n<10;n++) {
  points = [[1,2,3,4,5,6,7,8,9,10]];
  cnt = 0;
  best_tpd = {
   path: [],
   tpd: 0,
   distance: 0,
   taste: 0
  };
  nloop(
     points,
     [],
     10,
     0,
     n
  );
  console.log(`Testing for points equal 10-${n}=${10-n}`);
  console.log(best_tpd);
  console.log(`count is ${cnt}.`)
}
/*
Testing for points equal 10-2=8
{
  path: [
    9, 3,  7, 5,
    6, 2, 10, 4
  ],
  tpd: 2.78592030360124,
  distance: 38.76636379741122,
  taste: 108
}
count is 1814400.
Testing for points equal 10-3=7
{
  path: [
    3,  7, 5, 6,
    2, 10, 4
  ],
  tpd: 2.7967601406369123,
  distance: 37.543441239150425,
  taste: 105
}
count is 604800.
Testing for points equal 10-4=6
{
  path: [ 7, 5, 6, 2, 10, 4 ],
  tpd: 2.8945153290979557,
  distance: 33.51165531060739,
  taste: 97
}
count is 151200.
Testing for points equal 10-5=5
{
  path: [ 5, 6, 2, 10, 4 ],
  tpd: 2.79184519901717,
  distance: 32.95311646662476,
  taste: 92
}
count is 30240.
Testing for points equal 10-6=4
{
  path: [ 7, 5, 6, 2 ],
  tpd: 2.8592728288140585,
  distance: 17.13722436914974,
  taste: 49
}
count is 5040.
Testing for points equal 10-7=3
{
  path: [ 5, 6, 2 ],
  tpd: 2.654010170661975,
  distance: 16.578685525167117,
  taste: 44
}
count is 720.
Testing for points equal 10-8=2
{
  path: [ 6, 2 ],
  tpd: 1.9666001450197348,
  distance: 16.27173682511799,
  taste: 32
}
count is 90.
Testing for points equal 10-9=1
{ path: [ 4 ], tpd: 1.2, distance: 25, taste: 30 }
count is 10.
*/

(Edited only to add this note: I hope someone submits a solution other than evaluation of all possible paths. When I was about ten years old, I used to skip school to go with my dad to "hustle freight" as he referred to it. It was delivering freight within a 300 or so mile radius and being home each night. He had about eight to fifteen stops a day and had to figure out the order so that he could load the truck in reverse order, try to unload the heaviest stuff first to keep fuel costs down, and meet some deadlines such as union docks that closed at three and often seemed to cause delays on purpose. Anyway, there were a lot of men with less than a high school education and who likely didn't use a calculator that handled similar issues each day and did it well, plus the freight office that determined which stops should go together. They could read the addresses and weights and whether palletized or hand-stacked and just figure out the route. Perhaps not the optimal path but a very good one. So, I feel a bit stupid just running 1.8 millions paths through without applying any thought up front.)

79798238
0
Author

Computation power is readily available these days but yes I agree, it is impressive what was done by intuition previously.

79787763
1

I really enjoyed this challenge. I tried several different approaches but this one was the fastest by far. The whole script took 12.4s to run on my very basic laptop. I know there are likely faster ways but this is good enough for me.

I made the assumption that we would want to visit as many taco spots as possible, so I didn't bother looking at shorter routes that would focus only on high tastiness to get a better tastiness-per-distance rate. Might as well get the most tacos for the cost of this taco crawl ticket.

from io import StringIO
import numpy as np, pandas as pd
from shapely import Point, distance
from itertools import permutations

df = pd.read_csv(StringIO('''Taco Spot  Coordinates Tastiness
1   (2, 8)  7
2   (15, 3) 17
3   (6, 6)  8
4   (20, 15)    30
5   (11, 2) 12
6   (14, 1) 15
7   (7, 3)  5
8   (1, 12) 12
9   (3, 6)  3
10  (14, 13)    18'''), sep='\t')

origin = (0, 0)
df.loc[len(df)] = [0, str(origin), np.nan] # adding in the origin as taco spot 0 with blank tastiness, for the next step
df[['x', 'y']] = df['Coordinates'].str[1:-1].str.split(',', expand=True).astype(int) # separating out the coords into numeric x and y values
df['geometry'] = df.apply(lambda x: Point(x['x'], x['y']), axis=1) # coordinate point using shapely.Point for easier distance measuring in the next step
df = df[['Taco Spot', 'Tastiness', 'geometry']] # keeping only necessary columns

df2 = pd.merge(df, df, how='cross', suffixes=('', '_dest')) # full self merge
df2 = df2[(df2['Taco Spot'].ne(df2['Taco Spot_dest'])) & (df2['Taco Spot_dest'].ne(0))].reset_index(drop=True) # removing rows where the starting point is the same as the destination point and/or the origin (0, 0) is a destination
df2['distance'] = distance(df2['geometry'], df2['geometry_dest']) # quick distance measure
df2['step'] = df2[['Taco Spot', 'Taco Spot_dest']].apply(lambda x: str(list(x)), axis=1) # origin and destination points as one value, for use in the next step

taco_spots = df['Taco Spot'].tolist()
all_routes = [list(x) for x in permutations(taco_spots, 9) if x[0]==0] # all possible routes to visit 8 taco spots, starting with the origin spot 0
all_steps = [[str([route[n-1], route[n]]) for n in range(1, len(route))] for route in all_routes] # route segmented into steps, like the steps in df2
df3 = pd.DataFrame({'route':[str(x) for x in all_routes],
                    'step':all_steps})
df3 = pd.merge(df3.explode('step'), df2[['step', 'Tastiness_dest', 'distance']].rename(columns={'Tastiness_dest':'tastiness'}), on='step', how='left') # steps exploded into their own row, and merged with the step data from df2
df3 = df3.groupby('route')[['tastiness', 'distance']].sum() # total tastiness and distance per route
df3['tastiness_per_distance'] = df3['tastiness'].div(df3['distance']) # tastiness per distance
df3 = df3.sort_values('tastiness_per_distance', ascending=False) # sorted so that requested route is at the top

And finally, the route with the highest tastiness-per-distance rate (there is only one):

>>> df3.iloc[0]

route                     [0, 9, 3, 7, 5, 6, 2, 10, 4]
tastiness                                        108.0
distance                                     38.766364
tastiness_per_distance                         2.78592
Name: 0, dtype: object

So the best route (ignoring the 0) is [9, 3, 7, 5, 6, 2, 10, 4].

Update:

After seeing the other answers point out the "up to 8 taco spots" wording, I'm adding this update for anyone who decides they'd rather not be full/happy. The code now takes 2m 20.4s

Just replace all_routes with:

all_routes = [list(x) for x in sum([list(permutations(taco_spots, n)) for n in range(2, 10)], []) if x[0]==0] 

Now, the route visiting "up to 8 taco spots" with the highest tastiness-per-distance rate:

>>> df3.iloc[0]

route                     [0, 7, 5, 6, 2, 10, 4]
tastiness                                   97.0
distance                               33.511655
tastiness_per_distance                  2.894515
Name: 0, dtype: object

This way you only visit 6 taco spots: [7, 5, 6, 2, 10, 4].

79787700
2

Best route: 7, 5, 6, 2, 10, 4

Tastiness metric: 2.8945153290979557

Total distance travelled: 33.51165531060739

Total tastiness: 97

I computed the distances between each node once and stored the results in a two dimensional array [to, from].

I used a simple approach to traversal by treating decimal integers as expressions of the routes and just iterating through them from 0 to 98765432, skipping any with repeated digits, and adding a leading 0 where appropriate. (0 = node ten.)


using System.Drawing;

var crawler = new TacoCrawler();
crawler.ShowBestCrawl();


public class TacoCrawler
{
    readonly int[] _tastinessArray;
    readonly double[,] _distances;

    int _bestTotalTastiness = 0;
    double _bestMetric = 0;
    string _bestRoute = "";
    double _bestRouteDistance = 0;


    public TacoCrawler()
    {
        _tastinessArray = InitializeTastinesses();
        _distances = InitializeDistances();
    }


    public void ShowBestCrawl()
    {
        var startedAt = DateTime.Now;
        for (var n = 0; n < 100000000; n++) // Iterate through every route expressed as a number with up to eight digits. Filter out any with the same digit twice.
        {
            var routeString = n.ToString();

            if (!CheckForRepeatedDigits(routeString))
            {
                ComputeTastiness(routeString);

                if (n < 10000000 && !routeString.Contains('0'))  // If the route doesn't contain 0 (node 10) and includes fewer than 8 nodes, also check starting from 0 .
                {
                    routeString = "0" + routeString;
                    ComputeTastiness(routeString);
                }
            }
        }

        var finishedAt = DateTime.Now;
        var timeTaken = finishedAt - startedAt;

        Console.WriteLine("Best route: " + PrettifyRouteString(_bestRoute));
        Console.WriteLine("Tastiness metric: " + _bestMetric.ToString());
        Console.WriteLine("Total distance travelled: " + _bestRouteDistance.ToString());
        Console.WriteLine("Total tastiness: " + _bestTotalTastiness.ToString());
        Console.WriteLine("Computation time (s): " + timeTaken.Seconds);

    }


    static double GetDistanceBetweenPoints(Point p1, Point p2)
    {
        return Math.Sqrt((p2.X - p1.X) * (p2.X - p1.X) + (p2.Y - p1.Y) * (p2.Y - p1.Y));
    }

    // Check if the string contains more than one instance of any character.
    static bool CheckForRepeatedDigits(string iString)
    {
        for (var j = 0; j < iString.Length - 1; j++)
        {
            if (iString[(j + 1)..].Contains(iString[j]))
            {
                return true;
            }
        }
        return false;

    }

    static string PrettifyRouteString(string route)
    {
        var prettyRoute = route[..1];
        for (var i = 1; i < route.Length; i++)
        {
            prettyRoute += ", " + route[i];
        }
        prettyRoute = prettyRoute.Replace("0", "10");
        return prettyRoute;
    }

    // Compute the distances between each node and store the results in a 2d array [from,to]. An additional element in the to category is used to store the distance between the node and the origin.
    static double[,] InitializeDistances()
    {

        var locations = InitializeLocations();

        var distances = new double[10, 11];
        for (var from = 0; from < 10; from++)
        {
            for (var to = 0; to < 10; to++)
            {
                distances[from, to] = GetDistanceBetweenPoints(locations[from], locations[to]);
            }
            distances[from, 10] = GetDistanceBetweenPoints(locations[from], new Point(0, 0));
        }
        return distances;
    }

    static int[] InitializeTastinesses()
    {
        var tastinessArray = new int[]
        {
        18, // 10 = 0
        7,
        17,
        8,
        30,
        12,
        15,
        5,
        12,
        3
        };
        return tastinessArray;
    }

    static Point[] InitializeLocations()
    {
        var locationArray = new Point[]
        {
        new Point(14, 13),
        new Point(2, 8),
        new Point(15, 3),
        new Point(6, 6),
        new Point(20, 15),
        new Point(11, 2),
        new Point(14, 1),
        new Point(7, 3),
        new Point(1, 12),
        new Point(3, 6)
        };
        return locationArray;
    }

    void ComputeTastiness(string routeString)
    {
        // Get the distance to and tastiness of the first node.
        var firstNode = int.Parse(routeString[0].ToString());
        int currentRouteTotalTastiness = _tastinessArray[firstNode];
        double currentRouteDistance = _distances[firstNode, 10];

        // Get the distance to and tastiness of every subsequent node
        for (var i = 0; i < routeString.Length - 1; i++)
        {
            var from = int.Parse(routeString[i].ToString());
            var to = int.Parse(routeString[i + 1].ToString());
            currentRouteDistance += _distances[from, to];
            currentRouteTotalTastiness += _tastinessArray[to];
        }

        var totalTastinessMetric = currentRouteTotalTastiness / currentRouteDistance;

        if (totalTastinessMetric > _bestMetric)
        {
            _bestMetric = totalTastinessMetric;
            _bestRoute = routeString;
            _bestRouteDistance = currentRouteDistance;
            _bestTotalTastiness = currentRouteTotalTastiness;
        }
    }

}
79787663
2

Best taco crawl (6 stops): 7, 5, 6, 2, 10, 4

Combined tastiness: 97

Total distance: 33.5117

Efficiency: 2.8945

Lessons learned:

  • there is no standard unit of tastiness

Lessons reinforced:

  • brute force is beautiful, when you can afford it
  • 10!/2 is not a particularly big number for a modern computer

Code (ISO C):

#include <stdio.h>
#include <math.h>

/*
 * Challenge data
 */

static const struct {
    int id;
    int x, y;
    int tastiness;
} spots[] = {
    { 1, 2, 8, 7},
    { 2 ,15, 3, 17 },
    { 3 ,6, 6, 8 },
    { 4 ,20, 15, 30 },
    { 5 ,11, 2, 12 },
    { 6 ,14, 1, 15 },
    { 7 ,7, 3, 5 },
    { 8 ,1, 12, 12 },
    { 9 ,3, 6, 3 },
    { 10 ,14, 13, 18 }
};

#define NUM_SPOTS ((sizeof spots) / (sizeof spots[0]))
#define MAX_VISITS 8

/*
 * A structure representing a position being evaluated
 */
struct state {
    // spot indexes in traversal order.  The first num_visits of them have been traversed already.
    int spot_list[NUM_SPOTS];
    // the coordinates of the current position (for convenience)
    int current_x;
    int current_y;
    // the number of spots already visited
    unsigned int num_visits;
    // the distance traveled so far
    double path_length;
    // the combined tastiness of the spots visited so far. int is wide enough for all our purposes,
    // given the specific data for this problem.
    int path_tastiness;
};

/*
 * Helper functions
 */

inline static int square(int x) {
    return x * x;
}

static void print_state(struct state *state) {
    const char *delim = "";

    printf("Improved path (%d stops):", state->num_visits);
    for (unsigned int i = 0; i < state->num_visits; i++) {
        printf("%s %d", delim, state->spot_list[i] + 1);
        delim = ",";
    }
    printf("\ntastiness: %3d; distance: %8.4f; efficiency: %8.4f\n\n", state->path_tastiness,
            state->path_length, state->path_tastiness / state->path_length);
}

static void update_state(struct state *state) {
    int next_spot = state->spot_list[state->num_visits++];

    state->path_length += sqrt(
            square(spots[next_spot].x - state->current_x)
            + square(spots[next_spot].y - state->current_y));
    state->path_tastiness += spots[next_spot].tastiness;
    state->current_x = spots[next_spot].x;
    state->current_y = spots[next_spot].y;
}

/*
 * Finds the optimal path starting from the specified state, recording the
 * best result discovered in another state object.
 *
 * This recursive, brute-force implementation tests all possible paths, up
 * to the maximum length.
 */
static void optimize(struct state *state, struct state *best_state) {
    if (best_state->path_length * state->path_tastiness
            > state->path_length * best_state->path_tastiness) {
        *best_state = *state;
        print_state(best_state);
    }

    if (state->num_visits < MAX_VISITS) {
        int temp = state->spot_list[state->num_visits];

        for (unsigned int i = state->num_visits; i < NUM_SPOTS; i++) {
            struct state next_state = *state;

            next_state.spot_list[state->num_visits] = state->spot_list[i];
            next_state.spot_list[i] = temp;
            update_state(&next_state);

            optimize(&next_state, best_state);
        }
    }
}

/*
 * Entry point
 */
int main(void) {
    struct state working_state = { { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 } };
    struct state best_state = working_state;

    // A minor kludge to make the efficiency of the initial best state 0 instead of infinite
    best_state.path_length = 1;

    optimize(&working_state, &best_state);
}

Notes

This program runs in about 0.024 s on the good-but-not-great Linux system on which it was developed.

79787665
2

Side note: although there's no standard unit, tastiness does seem to be inversely proportional to size, at least in some domains.

79795286
0

Kudos for comparing scores by way of cross-multiplication instead of doing divisions. :-)

You can shave another cycle from the total by doing the division whenever you assign a new best score. The test then becomes if (tastiness > best_score * distance), one less multiplication. Score updates are extremely rare, like 29 for trying 1,814,400 routes.

79798240
1
Author

nice solution and interesting share from xkcd!

79787537
-1

struct TacoSpot
{
    public int Id;
    public double X;
    public double Y;
    public int Tastiness;
}

internal class TacoCrawlSolver
{
    static readonly TacoSpot[] spots =
    [
        new TacoSpot { Id=1, X=2, Y=8, Tastiness=7 },
        new TacoSpot { Id=2, X=15, Y=3, Tastiness=17 },
        new TacoSpot { Id=3, X=6, Y=6, Tastiness=8 },
        new TacoSpot { Id=4, X=20, Y=15, Tastiness=30 },
        new TacoSpot { Id=5, X=11, Y=2, Tastiness=12 },
        new TacoSpot { Id=6, X=14, Y=1, Tastiness=15 },
        new TacoSpot { Id=7, X=7, Y=3, Tastiness=5 },
        new TacoSpot { Id=8, X=1, Y=12, Tastiness=12 },
        new TacoSpot { Id=9, X=3, Y=6, Tastiness=3 },
        new TacoSpot { Id=10, X=14, Y=13, Tastiness=18 }
    ];

    static double maxMetric = 0;
    static List<int> bestRoute = [];
    static readonly List<TacoSpot> allSpots = spots.ToList();

    static double Dist(double x1, double y1, double x2, double y2)
        => Math.Sqrt((x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1));

    static void Search(List<int> route, double distPercorsa, int gustosita, double lastX, double lastY)
    {
        if (distPercorsa > 0)
        {
            double metric = gustosita / distPercorsa;
            if (metric > maxMetric)
            {
                maxMetric = metric;
                bestRoute = new List<int>(route);
            }
        }

        if (route.Count == 8)
            return;

        var unvisited = allSpots.Where(s => !route.Contains(s.Id)).ToList();
        if (distPercorsa > 0)
        {
            int remaining = 8 - route.Count;
            int topTastiness = unvisited.OrderByDescending(s => s.Tastiness).Take(remaining).Sum(s => s.Tastiness);
            if ((gustosita + topTastiness) / distPercorsa < maxMetric)
                return;
        }

        foreach (var s in unvisited)
        {
            route.Add(s.Id);
            double d = Dist(lastX, lastY, s.X, s.Y);
            Search(route, distPercorsa + d, gustosita + s.Tastiness, s.X, s.Y);
            route.RemoveAt(route.Count - 1);
        }
    }

    public static (double Metric, List<int> Route, double Distance, int Tastiness) Solve()
    {
        maxMetric = 0;
        bestRoute.Clear();
        Search(new List<int>(), 0, 0, 0, 0);

        double dist = 0;
        int gust = 0;
        double x = 0, y = 0;
        foreach (var id in bestRoute)
        {
            var s = allSpots.First(sp => sp.Id == id);
            dist += Dist(x, y, s.X, s.Y);
            gust += s.Tastiness;
            x = s.X;
            y = s.Y;
        }
        return (maxMetric, bestRoute, dist, gust);
    }

    static void Main()
    {
        var r = Solve();
        Console.WriteLine("Percorso ottimale: " + string.Join(" -> ", r.Route));
        Console.WriteLine($"Gustosità: {r.Tastiness}, Distanza: {r.Distance:F2}, Metrica: {r.Metric:F2}");
    }
}
79787490
1

Here is my submission in Rust:

  • Optimal route: [9, 3, 7, 5, 6, 2, 10, 4]

  • Total tastiness: 108

  • Total distance: 38.766365

  • Tastiness per distance: 2.7859201

EDIT (solution below first solution):

  • Route: [7, 5, 6, 2, 10, 4]

  • Total tastiness: 97

  • Total distance: 33.511654

  • Total tastiness per distance: 2.8945155

Cargo.toml:

[package]
name = "taco-crawl"
version = "0.1.0"
edition = "2024"

[dependencies]
itertools = "0.14.0"
ordered-float = "5.1.0"
pathfinding = "4.14.0"

src/main.rs:

use core::fmt;
use std::collections::LinkedList;

use itertools::Itertools;

fn main() {
    let mut spots: LinkedList<TacoSpot> = LinkedList::new();

    spots.push_front(TacoSpot::new(1, (2, 8), 7));
    spots.push_front(TacoSpot::new(2, (15, 3), 17));
    spots.push_front(TacoSpot::new(3, (6, 6), 8));
    spots.push_front(TacoSpot::new(4, (20, 15), 30));
    spots.push_front(TacoSpot::new(5, (11, 2), 12));
    spots.push_front(TacoSpot::new(6, (14, 1), 15));
    spots.push_front(TacoSpot::new(7, (7, 3), 5));
    spots.push_front(TacoSpot::new(8, (1, 12), 12));
    spots.push_front(TacoSpot::new(9, (3, 6), 3));
    spots.push_front(TacoSpot::new(10, (14, 13), 18));

    let mut best_route = find_best_route(spots);

    // Recompute tastiness
    let mut tastiness = 0;
    let mut distance = 0.0;
    let mut prev = TacoSpot::new(0, (0, 0), 0);
    let mut curr = TacoSpot::new(0, (0, 0), 0);

    println!("Route:");
    for i in 0..best_route.len() {
        tastiness += prev.tastiness;
        curr = best_route.get(i).unwrap().clone();
        distance += prev.distance_to(&curr);
        println!("  Spot: {}", curr);
        prev = curr;
    }

    tastiness += prev.tastiness;

    println!(
        "Route:                        [{}]",
        best_route.iter().map(|x| x.id).join(", ")
    );
    println!("Total tastiness:              {}", tastiness);
    println!("Total distance:               {}", distance);
    println!(
        "Total tastiness per distance: {}",
        tastiness as f32 / distance
    );
}

pub(crate) fn find_best_route(spots: LinkedList<TacoSpot>) -> Vec<TacoSpot> {
    let start = TacoSpot::new(0, (0, 0), 0);

    let mut max: f32 = 0.0;
    let mut best_route = Vec::new();

    // Create combinations of all spots
    for combo in spots.iter().combinations(8) {
        // Create all permutations of those combinations
        let routes = combo.into_iter().cloned().permutations(8);

        for mut route in routes {
            // Add start point
            // After testing, this seems useless, need to think on why
            // Because it could think starting from 20,15 spot would be better otherwise
            route.insert(0, start.clone());

            let total_tastiness_per_distance: f32 = route
                .iter()
                .tuple_windows()
                .map(|(a, b)| a.tastiness_per_distance_to(b))
                .sum();
            if total_tastiness_per_distance > max {
                max = total_tastiness_per_distance;
                // Remove start point
                route.remove(0);
                best_route = route;
            }
        }
    }

    best_route
}

#[derive(Clone)]
pub(crate) struct Coordinates {
    x: i16,
    y: i16,
}

impl Coordinates {
    pub(crate) fn new(x: i16, y: i16) -> Self {
        Self { x, y }
    }
}

impl fmt::Display for Coordinates {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "({x: >2},{y: >2})", x = self.x, y = self.y)
    }
}

#[derive(Clone)]
pub(crate) struct TacoSpot {
    id: u16,
    coordinates: Coordinates,
    tastiness: u16,
}

impl TacoSpot {
    pub(crate) fn new(id: u16, coordinates: (i16, i16), tastiness: u16) -> Self {
        Self {
            id,
            coordinates: Coordinates::new(coordinates.0, coordinates.1),
            tastiness,
        }
    }

    pub fn tastiness_per_distance_to(&self, that: &TacoSpot) -> f32 {
        that.tastiness as f32 / self.distance_to(that)
    }

    pub fn distance_to(&self, that: &TacoSpot) -> f32 {
        ((self.coordinates.x - that.coordinates.x).pow(2) as f32
            + (self.coordinates.y - that.coordinates.y).pow(2) as f32)
            .sqrt()
    }
}

impl fmt::Display for TacoSpot {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(
            f,
            "{id: >2} | {coordinates} | {tastiness: >2}",
            id = self.id,
            coordinates = self.coordinates,
            tastiness = self.tastiness
        )
    }
}

Output:

Route:
  Spot:  9 | ( 3, 6) |  3
  Spot:  3 | ( 6, 6) |  8
  Spot:  7 | ( 7, 3) |  5
  Spot:  5 | (11, 2) | 12
  Spot:  6 | (14, 1) | 15
  Spot:  2 | (15, 3) | 17
  Spot: 10 | (14,13) | 18
  Spot:  4 | (20,15) | 30
Route:                        [9, 3, 7, 5, 6, 2, 10, 4]
Total tastiness:              108
Total distance:               38.766365
Total tastiness per distance: 2.7859201

Learned: there is no best algorithm apart from bruteforce for such cases: Dijkstra can not optimize partial paths.


New solution taking into account the up-to 8...

use core::fmt;
use std::collections::LinkedList;

use itertools::Itertools;

fn main() {
    let mut spots: LinkedList<TacoSpot> = LinkedList::new();

    spots.push_front(TacoSpot::new(1, (2, 8), 7));
    spots.push_front(TacoSpot::new(2, (15, 3), 17));
    spots.push_front(TacoSpot::new(3, (6, 6), 8));
    spots.push_front(TacoSpot::new(4, (20, 15), 30));
    spots.push_front(TacoSpot::new(5, (11, 2), 12));
    spots.push_front(TacoSpot::new(6, (14, 1), 15));
    spots.push_front(TacoSpot::new(7, (7, 3), 5));
    spots.push_front(TacoSpot::new(8, (1, 12), 12));
    spots.push_front(TacoSpot::new(9, (3, 6), 3));
    spots.push_front(TacoSpot::new(10, (14, 13), 18));

    let best_route = find_best_route(spots);

    // Recompute tastiness
    let mut tastiness = 0;
    let mut distance = 0.0;
    let mut prev = TacoSpot::new(0, (0, 0), 0);
    let mut curr;

    println!("Route:");
    for i in 0..best_route.len() {
        tastiness += prev.tastiness;
        curr = best_route.get(i).unwrap().clone();
        distance += prev.distance_to(&curr);
        println!("  Spot: {}", curr);
        prev = curr;
    }

    tastiness += prev.tastiness;

    println!(
        "Route:                        [{}]",
        best_route.iter().map(|x| x.id).join(", ")
    );
    println!("Total tastiness:              {}", tastiness);
    println!("Total distance:               {}", distance);
    println!(
        "Total tastiness per distance: {}",
        tastiness as f32 / distance
    );
}

pub(crate) fn find_best_route(spots: LinkedList<TacoSpot>) -> Vec<TacoSpot> {
    let start = TacoSpot::new(0, (0, 0), 0);

    let mut best: f32 = 0.0;
    let mut best_route = Vec::new();

    // Create combinations of all spots
    for i in 1..9 {
        for combo in spots.iter().combinations(i) {
            // Create all permutations of those combinations
            let routes = combo.into_iter().cloned().permutations(i);

            for mut route in routes {
                // Add start point
                // After testing, this seems useless, need to think on why
                // Because it could think starting from 20,15 spot would be better otherwise
                route.insert(0, start.clone());

                let mut accumulated_tastiness: u16 = 0;
                let mut accumulated_distance = 0.0;
                let mut curr;
                let mut next;

                for j in 0..route.len() - 1 {
                    curr = route.get(j).unwrap();
                    next = route.get(j + 1).unwrap();

                    accumulated_tastiness += curr.tastiness + next.tastiness;
                    accumulated_distance += curr.distance_to(&next);
                }

                let average_tastiness_per_distance =
                    accumulated_tastiness as f32 / accumulated_distance as f32;
                if average_tastiness_per_distance > best {
                    best = average_tastiness_per_distance;
                    // Remove start point
                    route.remove(0);
                    best_route = route;
                }
            }
        }
    }

    best_route
}

#[derive(Clone)]
pub(crate) struct Coordinates {
    x: i16,
    y: i16,
}

impl Coordinates {
    pub(crate) fn new(x: i16, y: i16) -> Self {
        Self { x, y }
    }
}

impl fmt::Display for Coordinates {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "({x: >2},{y: >2})", x = self.x, y = self.y)
    }
}

#[derive(Clone)]
pub(crate) struct TacoSpot {
    id: u16,
    coordinates: Coordinates,
    tastiness: u16,
}

impl TacoSpot {
    pub(crate) fn new(id: u16, coordinates: (i16, i16), tastiness: u16) -> Self {
        Self {
            id,
            coordinates: Coordinates::new(coordinates.0, coordinates.1),
            tastiness,
        }
    }

    pub fn distance_to(&self, that: &TacoSpot) -> f32 {
        ((self.coordinates.x - that.coordinates.x).pow(2) as f32
            + (self.coordinates.y - that.coordinates.y).pow(2) as f32)
            .sqrt()
    }
}

impl fmt::Display for TacoSpot {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(
            f,
            "{id: >2} | {coordinates} | {tastiness: >2}",
            id = self.id,
            coordinates = self.coordinates,
            tastiness = self.tastiness
        )
    }
}

Which outputs:

Route:
  Spot:  7 | ( 7, 3) |  5
  Spot:  5 | (11, 2) | 12
  Spot:  6 | (14, 1) | 15
  Spot:  2 | (15, 3) | 17
  Spot: 10 | (14,13) | 18
  Spot:  4 | (20,15) | 30
Route:                        [7, 5, 6, 2, 10, 4]
Total tastiness:              97
Total distance:               33.511654
Total tastiness per distance: 2.8945155
79787472
3
'use strict';

// SOLUTION:
// Optimal path: [7, 5, 6, 2, 10, 4]
// Total tastiness: 97
// Total distance: 33.51165531062575
// Tastiness per distance: 2.89451532909637

const start = process.hrtime.bigint();

// Set this number to change how many places you want to visit at 
// most. The challenge is asking for 8 places at most. Initially I
// thought it was asking for exactly 8 places.
const NUMBER_OF_PLACES_TO_VISIT = 8;

const places = [
  // I've included the starting point as a place with 0 tastiness. 
  // You need this location in order to calculate the distances 
  // between the starting point and all Taco places. It could also
  // be used if you wanted to return to the starting point at the
  // end of the trip, but the current challenge doesn't require 
  // that. Anyway, having that at index 0 makes the array indexes 
  // match the place numbers in the challenge.
  {x:  0, y:  0, tastiness:  0},
  {x:  2, y:  8, tastiness:  7},
  {x: 15, y:  3, tastiness: 17},
  {x:  6, y:  6, tastiness:  8},
  {x: 20, y: 15, tastiness: 30},
  {x: 11, y:  2, tastiness: 12},
  {x: 14, y:  1, tastiness: 15},
  {x:  7, y:  3, tastiness:  5},
  {x:  1, y: 12, tastiness: 12},
  {x:  3, y:  6, tastiness:  3},
  {x: 14, y: 13, tastiness: 18},
];

// Precalculate distances between all places so I don't have to do
// it repeatedly over and over again during the recursive search.
// You could argue that only half of the distance matrix needs to 
// be calculated because the distance between any two places is 
// the same in either direction. However, in that case I would 
// need to do a check to see which place index is lower to be able
// to look up the distance. That check would need to be done 
// inside every recursion and inside every loop iteration of the 
// recursion.
let distances = [];
for(let i=0; i<places.length; i++) {
  distances[i] = [];
  for(let j=0; j<places.length; j++) {
    // I don't always need to do a full distance calculation.
    if(i === j) {
      // This will always be zero.
      distances[i][j] = 0;
    } else if(i > j) {
      // The reverse has already been calculated.
      distances[i][j] = distances[j][i];
    } else {
      distances[i][j] = Math.sqrt (
                            Math.pow(places[i].x-places[j].x, 2)
                          + Math.pow(places[i].y-places[j].y, 2) 
                        );
    }
  }
}

// I'm not going to store all calculated paths, and then sort that 
// huge array to find the best one. That would take ages. The 
// challenge only requires that I find the best path, so I'll just 
// keep track of the best path found so far. This will save a ton 
// of memory and processing time. I won't know if there is a tie 
// for the best path, but I don't need to.
const bestTrip = {
  path: [], 
  tastiness: 0, 
  distance: 0,
  tastinessPerDist: 0
};
const currentTrip = {
  path: [], 
  tastiness: 0, 
  distance: 0,
  tastinessPerDist: 0
};
visitTacoPlace(0);

function visitTacoPlace(currentPlace) {
  // I can start the loop from index 1 because I never want to go 
  // back to the starting point at index 0.
  for(let i=1; i<places.length; i++) {
    // You can't visit a place you've already been to, including 
    // your current place, which is already in the path.
    if(currentTrip.path.includes(i)) continue;

    currentTrip.path.push(i);
    currentTrip.tastiness += places[i].tastiness;
    currentTrip.distance += distances[currentPlace][i];

    currentTrip.tastinessPerDist 
      = currentTrip.tastiness / currentTrip.distance;
    // If the current path is better than the best path found so 
    // far, save it. I'm avoiding pushing them to an array and 
    // sorting at the end, which would be a waste of resources.
    if(currentTrip.tastinessPerDist > bestTrip.tastinessPerDist) {
      bestTrip.path = [...currentTrip.path];
      bestTrip.tastiness = currentTrip.tastiness;
      bestTrip.distance = currentTrip.distance;
      bestTrip.tastinessPerDist = currentTrip.tastinessPerDist;
    }

    if(currentTrip.path.length < NUMBER_OF_PLACES_TO_VISIT) {
      visitTacoPlace(i);
    } else {
      // Initially when I thought the challenge required exactly 8
      // places to visit, this is where I calculated the tastiness
      // per distance and compared it to the best path so far. To
      // solve the challenge for 'up to 8 places', I just moved 
      // the few lines of code that was here up a little so it 
      // executes in every iteration regardless of the current 
      // path length.
    }

    currentTrip.path.pop();
    currentTrip.tastiness -= places[i].tastiness;
    currentTrip.distance -= distances[currentPlace][i];
  }
}

const execTime = Number(process.hrtime.bigint()-start)/1000000;
console.log('Execution time: ' + execTime + ' ms');
console.log(bestTrip);

Originally I thought the Challenge was asking to visit exactly 8 places. After I submitted my initial solution, I saw that most people calculated their solutions upto 8 places. I went back and moved a couple of lines of my code out of a condition check to account for that. I hope my solution can be accepted with this little post submission change.

I discovered several ways to optimize my code and brought done execution time of this node.js code to 140 ms.

Optimal path: 7, 5, 6, 2, 10, 4
Total tastiness: 97
Total distance: 33.51165531062575
Tastiness per distance: 2.89451532909637

79787318
2
import itertools
import math

# Taco data (spot_id: ((x, y), tastiness))
taco_spots = {
    1: ((2, 8), 7),
    2: ((15, 3), 17),
    3: ((6, 6), 8),
    4: ((20, 15), 30),
    5: ((11, 2), 12),
    6: ((14, 1), 15),
    7: ((7, 3), 5),
    8: ((1, 12), 12),
    9: ((3, 6), 3),
    10: ((14, 13), 18)
}

def euclidean(a, b):
    """Calculate straight-line distance between two points."""
    return math.sqrt((a[0]-b[0])**2 + (a[1]-b[1])**2)

# Starting point
start_point = (0, 0)
spot_ids = list(taco_spots.keys())

best_path = []
best_taste = 0
best_distance = 0
best_ratio = 0

# Try visiting 1 to 8 taco spots
for r in range(1, 9):
    for combo in itertools.combinations(spot_ids, r):
        for route in itertools.permutations(combo):
            total_taste = sum(taco_spots[i][1] for i in route)
            
            # Calculate travel distance
            total_dist = euclidean(start_point, taco_spots[route[0]][0])
            for i in range(len(route) - 1):
                total_dist += euclidean(taco_spots[route[i]][0], taco_spots[route[i + 1]][0])
            
            # Avoid division by zero
            if total_dist == 0:
                continue

            ratio = total_taste / total_dist

            if ratio > best_ratio:
                best_ratio = ratio
                best_path = route
                best_taste = total_taste
                best_distance = total_dist

print("=== Taco Crawl Optimization ===")
print("Optimal Path:", best_path)
print("Total Tastiness:", best_taste)
print("Total Distance:", round(best_distance, 2))
print("Tastiness per Distance:", round(best_ratio, 4))

When executed, the program outputs:
=== Taco Crawl Optimization ===

Optimal Path: (7, 5, 6, 2, 10, 4)

Total Tastiness: 97

Total Distance: 33.51

Tastiness per Distance: 2.8945
........................................................................
code : python

I add up (sum) the tastiness of all spots visited.

I calculate the total travel distance using the Euclidean distance formula
d = √((x₂ - x₁)² + (y₂ - y₁)²)
ration is score = total tastiness/total distance

Final Summary

Best Route: [7, 5, 6, 2, 10, 4]

Total Tastiness: 97

Total Distance: 33.51

Tastiness per Distance: 2.8945

79787287
3

Optimal route: [7, 5, 6, 2, 10, 4]

Total tastiness: 97

Total distance: 33.51165531060739

Tastiness per distance: 2.8945153290979557

Code (Rust):

use std::cmp::Ordering;
use std::collections::BinaryHeap;
use std::fmt::Display;
use std::num::ParseIntError;
use std::str::FromStr;

use thiserror::Error; // 2.0.16

fn main() {
    const DATA: &str = "\
        1   (2, 8)  7\n\
        2   (15, 3) 17\n\
        3   (6, 6)  8\n\
        4   (20, 15)    30\n\
        5   (11, 2) 12\n\
        6   (14, 1) 15\n\
        7   (7, 3)  5\n\
        8   (1, 12) 12\n\
        9   (3, 6)  3\n\
        10  (14, 13)    18\
    ";
    let mut places = DATA
        .lines()
        .map(str::parse)
        .collect::<Result<Vec<TacoPlace>, _>>()
        .unwrap();
    let mut best_paths = BinaryHeap::<PriorityItem<f64, _>>::new();
    taco_permutations(&mut places, 0, 8, TacoStats::new(), &mut |stats, path| {
        let &TacoStats {
            tastiness,
            distance,
            count,
            ..
        } = stats;
        if count == 0 {
            return;
        }
        let score = tastiness / distance;
        if best_paths.len() == 5 && best_paths.peek().is_some_and(|item| item.0 > score) {
            return;
        }
        best_paths.push(PriorityItem(score, (tastiness, distance, path.to_vec())));
        if best_paths.len() > 5 {
            best_paths.pop();
        }
    });
    let best_paths = best_paths.into_sorted_vec();
    for PriorityItem(score, (tastiness, distance, path)) in best_paths.into_iter() {
        println!("Score: {score}");
        println!("Distance: {distance}");
        println!("Tastiness: {tastiness}");
        for (ix, place) in path.iter().enumerate() {
            println!("{}) {place}", ix + 1);
        }
        println!();
    }
}

fn taco_permutations(
    path: &mut [TacoPlace],
    index: usize,
    max_len: usize,
    stats: TacoStats,
    report: &mut impl FnMut(&TacoStats, &[TacoPlace]),
) {
    report(&stats, &path[..index]);
    if index == max_len {
        return;
    }
    for next in index..path.len() {
        path.swap(index, next);
        taco_permutations(path, index + 1, max_len, stats.append(&path[index]), report);
        path.swap(index, next);
    }
}

#[derive(Debug, Clone, Copy, Default)]
struct TacoStats {
    count: usize,
    last_position: Position,
    distance: f64,
    tastiness: f64,
}

impl TacoStats {
    fn new() -> Self {
        Self::default()
    }

    fn append(mut self, place: &TacoPlace) -> Self {
        self.distance += place.position.dist(self.last_position);
        self.count += 1;
        self.tastiness += place.tastiness as f64;
        self.last_position = place.position;
        self
    }
}

#[derive(Debug, Copy, Clone, PartialEq, Eq, Default)]
struct Position {
    x: i32,
    y: i32,
}

impl Position {
    const fn new(x: i32, y: i32) -> Self {
        Self { x, y }
    }

    fn dist(self, other: Self) -> f64 {
        let dx = self.x.abs_diff(other.x) as f64;
        let dy = self.y.abs_diff(other.y) as f64;
        (dx * dx + dy * dy).sqrt()
    }
}

impl Display for Position {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        let &Self { x, y } = self;
        write!(f, "({x}, {y})")
    }
}

#[derive(Debug, Clone, Copy)]
struct TacoPlace {
    id: u8,
    position: Position,
    tastiness: u8,
}

#[derive(Debug, Error)]
enum ParseError {
    #[error("Syntax error")]
    SyntaxError,
    #[error(transparent)]
    InvalidNumber(#[from] ParseIntError),
}

impl FromStr for TacoPlace {
    type Err = ParseError;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        let (id, rest) = s.split_once('(').ok_or(ParseError::SyntaxError)?;
        let (x, rest) = rest.split_once(',').ok_or(ParseError::SyntaxError)?;
        let (y, tastiness) = rest.split_once(')').ok_or(ParseError::SyntaxError)?;
        Ok(Self {
            id: id.trim_ascii().parse()?,
            position: Position::new(x.trim_ascii().parse()?, y.trim_ascii().parse()?),
            tastiness: tastiness.trim_ascii().parse()?,
        })
    }
}

impl Display for TacoPlace {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        let &Self {
            id,
            position,
            tastiness,
        } = self;
        write!(f, "{id} {position} {tastiness}")
    }
}

#[derive(Debug, Clone)]
struct PriorityItem<K, T>(K, T);

impl<K, T> PartialEq for PriorityItem<K, T>
where
    K: PartialEq<K>,
{
    fn eq(&self, other: &Self) -> bool {
        self.0.eq(&other.0)
    }
}

impl<K, T> Eq for PriorityItem<K, T> where K: PartialEq<K> {}

impl<K, T> Ord for PriorityItem<K, T>
where
    K: PartialOrd,
{
    fn cmp(&self, other: &Self) -> Ordering {
        self.0.partial_cmp(&other.0).unwrap().reverse()
    }
}
impl<K, T> PartialOrd for PriorityItem<K, T>
where
    K: PartialOrd,
{
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
        Some(self.cmp(&other))
    }
}

Output:

Score: 2.8945153290979557
Distance: 33.51165531060739
Tastiness: 97
1) 7 (7, 3) 5
2) 5 (11, 2) 12
3) 6 (14, 1) 15
4) 2 (15, 3) 17
5) 10 (14, 13) 18
6) 4 (20, 15) 30

Score: 2.8592728288140585
Distance: 17.13722436914974
Tastiness: 49
1) 7 (7, 3) 5
2) 5 (11, 2) 12
3) 6 (14, 1) 15
4) 2 (15, 3) 17

Score: 2.7967601406369123
Distance: 37.543441239150425
Tastiness: 105
1) 3 (6, 6) 8
2) 7 (7, 3) 5
3) 5 (11, 2) 12
4) 6 (14, 1) 15
5) 2 (15, 3) 17
6) 10 (14, 13) 18
7) 4 (20, 15) 30

Score: 2.79184519901717
Distance: 32.95311646662476
Tastiness: 92
1) 5 (11, 2) 12
2) 6 (14, 1) 15
3) 2 (15, 3) 17
4) 10 (14, 13) 18
5) 4 (20, 15) 30

Score: 2.78592030360124
Distance: 38.76636379741122
Tastiness: 108
1) 9 (3, 6) 3
2) 3 (6, 6) 8
3) 7 (7, 3) 5
4) 5 (11, 2) 12
5) 6 (14, 1) 15
6) 2 (15, 3) 17
7) 10 (14, 13) 18
8) 4 (20, 15) 30
79786952
2
#include <algorithm>
#include <cassert>
#include <cmath>
#include <iostream>
#include <vector>

#ifdef LOCAL
#include "print.h"
#else
template <typename... Args> inline void print(const Args&... args) {}
inline void                             newline() {}
#endif

using ll = long long;
#define endl                       '\n'
#define FOR1(n)                    for (int _ = 0; _ < n; _++)
#define FOR2(i, n)                 for (int i = 0; i < n; i++)
#define FOR3(i, a, b)              for (int i = a; i < b; i++)
#define overload3(a, b, c, d, ...) d
#define FOR(...)                   overload3(__VA_ARGS__, FOR3, FOR2, FOR1)(__VA_ARGS__)

using namespace std;

struct Shop {
    int  x, y;
    int  tastiness;
    int  idx;
    bool operator<(const Shop& other) const {
        return idx < other.idx;
    }
};

constexpr Shop shops [10] = {
    {2,  8,  7,  0},
    {15, 3,  17, 1},
    {6,  6,  8,  2},
    {20, 15, 30, 3},
    {11, 2,  12, 4},
    {14, 1,  15, 5},
    {7,  3,  5,  6},
    {1,  12, 12, 7},
    {3,  6,  3,  8},
    {14, 13, 18, 9}
};

inline double dist(const Shop& a, const Shop& b) {
    return sqrt((a.x - b.x) * (a.x - b.x) + (a.y - b.y) * (a.y - b.y));
}

signed main() {
    ios::sync_with_stdio(0);
    cin.tie(0);
#ifdef LOCAL
    freopen("input.txt", "r", stdin);
#endif

    double       best = 0;
    double       bestDist;
    int          bestTaste;
    vector<Shop> bestPerm;

    FOR(i, 1 << 10) {
        if (__builtin_popcount(i) > 8 || __builtin_popcount(i) < 1) {
            continue;
        }
        vector<Shop> chosen;
        FOR(j, 10) {
            if (i & (1 << j)) {
                chosen.push_back(shops [j]);
            }
        }
        do {
            double sumDist = sqrt(
                (chosen [0].x * chosen [0].x) + (chosen [0].y * chosen [0].y)
            );
            int sumTaste = 0;
            FOR(j, __builtin_popcount(i) - 1) {
                sumDist += dist(chosen [j], chosen [j + 1]);
                sumTaste += chosen [j].tastiness;
            }
            sumTaste += chosen.back().tastiness;
            double score = sumTaste / sumDist;
            if (score > best) {
                best      = score;
                bestDist  = sumDist;
                bestTaste = sumTaste;
                bestPerm  = chosen;
            }
        } while (next_permutation(chosen.begin(), chosen.end()));
    }
    cout << "Best tastiness per distance: " << best << endl;
    cout << "Tastiness: " << bestTaste << endl;
    cout << "Distance: " << bestDist << endl;
    cout << "Permutation: [";
    for (auto& shop: bestPerm) {
        cout << shop.idx + 1;
        if (shop.idx != bestPerm.back().idx) {
            cout << ", ";
        }
    }
    cout << "]" << endl;
}

Output:

Best tastiness per distance: 2.89452
Tastiness: 97
Distance: 33.5117
Permutation: [7, 5, 6, 2, 10, 4]

Interesting that I only visited 6 shops.

79786551
-1
  • The optimal routing determined to visit up to 8 taco spots - [9, 3, 7, 5, 6, 2, 10, 4]

  • The total tastiness achieved: 108

  • The total distance traveled: 38.7664

  • The "tastiness per distance": 2.78592

Code (Python):

import math
from itertools import combinations, permutations
from typing import List, Tuple, Dict

class TacoSpot:
    def __init__(self, id: int, x: float, y: float, tastiness: int):
        self.id = id
        self.x = x
        self.y = y
        self.tastiness = tastiness
    
    def __repr__(self):
        return f"Spot {self.id}: ({self.x}, {self.y}), Tastiness: {self.tastiness}"

def euclidean_distance(p1: Tuple[float, float], p2: Tuple[float, float]) -> float:
    return math.sqrt((p2[0] - p1[0])**2 + (p2[1] - p1[1])**2)

def calculate_path_metrics(path: List[int], spots: List[TacoSpot]) -> Dict:
    spot_dict = {spot.id: spot for spot in spots}

    total_distance = 0.0
    total_tastiness = 0
    current_pos = (0, 0)
    
    for spot_id in path:
        spot = spot_dict[spot_id]
        distance = euclidean_distance(current_pos, (spot.x, spot.y))
        total_distance += distance
        total_tastiness += spot.tastiness
        current_pos = (spot.x, spot.y)
    metric = total_tastiness / total_distance if total_distance > 0 else 0
    
    return {
        'path': path,
        'total_tastiness': total_tastiness,
        'total_distance': round(total_distance, 4),
        'metric': round(metric, 6)
    }

def find_optimal_path_brute_force(spots: List[TacoSpot], max_spots: int = 8) -> Dict:
    best_result = None
    spot_ids = [spot.id for spot in spots]
    print(f"Starting brute force optimization...")
    print(f"Testing combinations of {max_spots} spots from {len(spots)} available spots")
    print(f"Total combinations: {math.comb(len(spots), max_spots)}")
    print(f"Permutations per combination: {math.factorial(max_spots)}")
    print(f"Total paths to evaluate: ~{math.comb(len(spots), max_spots) * math.factorial(max_spots):,}\n")
    
    combos = list(combinations(spot_ids, max_spots))
    total_combos = len(combos)
    for i, combo in enumerate(combos):
        for perm in permutations(combo):
            metrics = calculate_path_metrics(list(perm), spots)
            if best_result is None or metrics['metric'] > best_result['metric']:
                best_result = metrics
        if (i + 1) % 5 == 0 or (i + 1) == total_combos:
            progress = ((i + 1) / total_combos) * 100
            print(f"Progress: {progress:.1f}% ({i + 1}/{total_combos} combinations)")
    return best_result

def print_results(result: Dict, spots: List[TacoSpot]):
    spot_dict = {spot.id: spot for spot in spots}
    print("\n" + "="*35)
    print("OPTIMAL TACO CRAWL ROUTE FOUND!")
    print("="*35)
    print(f"\nOptimal Route: {result['path']}")
    print(f"\nMetrics:")
    print(f"• Total Tastiness: {result['total_tastiness']}")
    print(f"• Total Distance: {result['total_distance']}")
    print(f"• Tastiness per Distance: {result['metric']}")
    print(f"\nPath Details:")
    print(f"Start: (0, 0)")
    current_pos = (0, 0)
    cumulative_distance = 0.0
    
    for idx, spot_id in enumerate(result['path'], 1):
        spot = spot_dict[spot_id]
        distance = euclidean_distance(current_pos, (spot.x, spot.y))
        cumulative_distance += distance
        print(f"{idx}. Spot {spot_id} at ({spot.x}, {spot.y}) - "
              f"Tastiness: {spot.tastiness}, "
              f"Segment distance: {distance:.2f}, "
              f"Cumulative: {cumulative_distance:.2f}")
        current_pos = (spot.x, spot.y)
    print("="*85 + "\n")

def main():
    taco_spots = [
        TacoSpot(1, 2, 8, 7),
        TacoSpot(2, 15, 3, 17),
        TacoSpot(3, 6, 6, 8),
        TacoSpot(4, 20, 15, 30),
        TacoSpot(5, 11, 2, 12),
        TacoSpot(6, 14, 1, 15),
        TacoSpot(7, 7, 3, 5),
        TacoSpot(8, 1, 12, 12),
        TacoSpot(9, 3, 6, 3),
        TacoSpot(10, 14, 13, 18)
    ]
    
    print("Taco Crawl Optimization Challenge\n")
    print("Available Taco Spots:")
    for spot in taco_spots:
        print(f"  {spot}")
    print()
    optimal_result = find_optimal_path_brute_force(taco_spots, max_spots=8)
    print_results(optimal_result, taco_spots)

if __name__ == "__main__":
    main()

What I learned/challenges faced:

  1. Balancing Tastiness and Distance:

    At first, I thought this would be a regular shortest-path or TSP problem. But optimizing for tastiness per distance turned out to be trickier because it’s really two goals in one — trying to get the best reward while keeping travel short. It made me focus more on ratios than totals, and I realized that greedy choices don’t always give the best result here.

  2. Handling Too Many Possibilities:

    With 10 taco spots and the option to visit up to 8, the number of possible routes got huge very quickly. My first brute-force attempt worked but was slow, so I had to be smart about it — skipping routes with low-tastiness or far-off spots to save time.

  3. Getting Distances Right:

    It was surprising how small rounding errors could change the final ratio. Switching to math.hypot() helped make the distance calculations cleaner and more accurate.

  4. Noticing Natural Clusters:

    When I plotted the taco spots, I noticed a tight cluster around spots (5, 6, 7, 2, 10, 4). Focusing on that group gave much better results than chasing higher-tastiness spots that were far away. Seeing that pattern really helped me understand why the best route worked.

  5. Testing Step by Step:

    Instead of jumping straight into all 8-spot combinations, I tested smaller groups first to make sure the logic and distance calculations were correct. That saved me a lot of debugging later.

  6. Final Takeaway:

    This small challenge was a fun reminder that optimization problems can get complex fast, even with only ten points. It made me think more about tradeoffs, heuristics, and where brute force makes sense. And honestly, solving it made me crave tacos — so that’s a win in itself.

79786664
2
Author

Nice solution and thanks for the learnings writeup! Is your solution constrained to visit exactly 8 spots? It doesn't seem that way from a quick glance at the code, but there is a more optimal solution with less spots visited.

79788573
0

@setman yes, in my current implementation it has constraints to visit exactly 8 spots. find_optimal_path_brute_force() function explicitly chooses combinations of size max_spots (set to 8). So, it won’t find shorter routes that might give a better tastiness/distance ratio.

For the second thing, there is another solution that I came across, if I want to make it optimal across any number of stops, I could loop through smaller subset sizes like this:

best_result = None
for k in range(1, len(spots) + 1):
    result = find_optimal_path_brute_force(spots, max_spots=k)
    if best_result is None or result['metric'] > best_result['metric']:
        best_result = result

but what it would do is increase the computation time, since the total number of combinations and permutations grows super fast as k varies. So, to keep it traceable I limited it to 8 stops. I hope this answers your query :)

79786518
0

I accept the challenge, and I am using Python to implement this challenge.

import math
import itertools
import time

# Taco spots data with coordinates and tastiness values
spots = {
    1: ((2, 8), 7),
    2: ((15, 3), 17),
    3: ((6, 6), 8),
    4: ((20, 15), 30),
    5: ((11, 2), 12),
    6: ((14, 1), 15),
    7: ((7, 3), 5),
    8: ((1, 12), 12),
    9: ((3, 6), 3),
    10: ((14, 13), 18)
}

def euclidean_distance(p1, p2):
    """Calculate Euclidean distance between two points"""
    return math.sqrt((p1[0] - p2[0])**2 + (p1[1] - p2[1])**2)

def calculate_path_metrics(path):
    """Calculate tastiness, distance, and ratio for a given path"""
    start_point = (0, 0)
    total_tastiness = 0
    total_distance = 0
    
    # Start from origin to first spot
    current_point = start_point
    for spot_id in path:
        spot_coords, tastiness = spots[spot_id]
        total_tastiness += tastiness
        total_distance += euclidean_distance(current_point, spot_coords)
        current_point = spot_coords
    
    ratio = total_tastiness / total_distance if total_distance > 0 else 0
    return total_tastiness, total_distance, ratio

def find_optimal_taco_crawl():
    """Find the optimal path that maximizes tastiness per distance"""
    start_time = time.time()
    
    best_ratio = -1
    best_path = None
    best_tastiness = 0
    best_distance = 0
    paths_evaluated = 0
    
    print("Evaluating all possible paths...")
    
    # Try all path lengths from 1 to 8 spots
    for k in range(1, 9):
        print(f"Checking paths with {k} spots...")
        
        # Generate all permutations of k spots from the 10 available
        for path in itertools.permutations(spots.keys(), k):
            tastiness, distance, ratio = calculate_path_metrics(path)
            paths_evaluated += 1
            
            if ratio > best_ratio:
                best_ratio = ratio
                best_path = path
                best_tastiness = tastiness
                best_distance = distance
    
    end_time = time.time()
    
    print(f"\n{'='*50}")
    print("TACO CRAWL OPTIMIZATION RESULTS")
    print(f"{'='*50}")
    print(f"Total paths evaluated: {paths_evaluated:,}")
    print(f"Computation time: {end_time - start_time:.2f} seconds")
    print(f"{'='*50}")
    
    return best_path, best_tastiness, best_distance, best_ratio

def main():
    """Main function to run the taco crawl optimization"""
    print("National Taco Day Challenge - Optimal Taco Crawl")
    print("Finding the path that maximizes tastiness per distance traveled...")
    print()
    
    # Display the taco spots information
    print("Taco Spots Information:")
    print("-" * 40)
    print(f"{'Spot':<6} {'Coordinates':<12} {'Tastiness':<10}")
    print("-" * 40)
    for spot_id, (coords, tastiness) in spots.items():
        print(f"{spot_id:<6} {str(coords):<12} {tastiness:<10}")
    print()
    
    # Find the optimal path
    optimal_path, tastiness, distance, ratio = find_optimal_taco_crawl()
    
    # Display the optimal path details
    print(f"Optimal Path: {list(optimal_path)}")
    print(f"Total Tastiness: {tastiness}")
    print(f"Total Distance Traveled: {distance:.2f}")
    print(f"Tastiness per Distance: {ratio:.4f}")
    print()
    
    # Display the route step-by-step
    print("Detailed Route:")
    print("-" * 50)
    current_point = (0, 0)
    total_dist = 0
    
    print(f"Start at: {current_point}")
    for i, spot_id in enumerate(optimal_path, 1):
        spot_coords, spot_tastiness = spots[spot_id]
        leg_distance = euclidean_distance(current_point, spot_coords)
        total_dist += leg_distance
        print(f"Step {i}: Go to Spot {spot_id} {spot_coords} "
              f"(tastiness: {spot_tastiness}, "
              f"distance: {leg_distance:.2f}, "
              f"cumulative: {total_dist:.2f})")
        current_point = spot_coords
    
    print(f"\nFinal cumulative distance: {total_dist:.2f}")
    print(f"Final ratio (tastiness/distance): {tastiness}/{total_dist:.2f} = {ratio:.4f}")

if __name__ == "__main__":
    main()
When you run this code, it will:
1. Evaluate all possible paths from 1 to 8 taco spots in all possible orders
2. Calculate the metrics for each path:
    * Total tastiness
    * Total distance traveled (from start through all visited spots)
    * Ratio of tastiness per distance
3. Find the optimal path that maximizes the ratio
4. Display detailed results including:
    * The optimal sequence of spots to visit
    * Total tastiness achieved
    * Total distance traveled
    * The optimized ratio value
    * Step-by-step route details
The code uses a brute-force approach since the problem size is manageable (about 2 million permutations total for paths of length 1-8). It will typically run in a few seconds to a minute depending on your computer's speed.
Key features:
* Clear, readable code with comments
* Progress reporting during computation
* Detailed output showing the reasoning
* Step-by-step route visualization
* Performance metrics (paths evaluated, computation time)
This solution will definitively find the globally optimal path for the taco crawl challenge according to the specified criteria.
79786659
0
Author

Nice code! What's your solution?

79786454
5

Absolutely worst brute-force unoptimized algorithm with CUDA: 16 milliseconds for kernel, maybe + 1 microsecond on CPU-side:

#define __CUDACC__
#include<iostream>
#include <cuda_runtime.h>
// Error-handling assert code from stackoverflow talonmies.
#define gpuErrchk(ans) { gpuAssert((ans), __FILE__, __LINE__); }
inline void gpuAssert(cudaError_t code, const char* file, int line, bool abort = true)
{
    if (code != cudaSuccess)
    {
        fprintf(stderr, "GPUassert: %s %s %d\n", cudaGetErrorString(code), file, line);
        if (abort) exit(code);
    }
}
__device__ float path[] = {
    2, 8,
    15, 3,
    6, 6,
    20, 15,
    11, 2,
    14, 1,
    7, 3,
    1, 12,
    3, 6,
    14, 13
};
__device__ float taste[] = {
    7,
    17,
    8,
    30,
    12,
    15,
    5,
    12,
    3,
    18
};
constexpr int P_10_8 = 1000000000;
constexpr int BLOCKS = 46 * 2;
constexpr int THREADS = 768;
__global__ void k_brute(float* scores, int* permutations) {
    const int thread = threadIdx.x + blockIdx.x * blockDim.x;
    const int numThreads = blockDim.x * gridDim.x;
    __shared__ float s_reduction[32];
    __shared__ int s_reduction2[32];
    int indices[10] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
    float score = 0.0f;
    int prm = 0;
    const int steps = (P_10_8 + numThreads - 1) / numThreads;
    for (int i = 0; i < steps; i++) {
        const int permutation = i * numThreads + thread;
        if (permutation < P_10_8) {
            indices[0] = (permutation / 1) % 10;
            indices[1] = (permutation / 10) % 10;
            indices[2] = (permutation / 100) % 10;
            indices[3] = (permutation / 1000) % 10;
            indices[4] = (permutation / 10000) % 10;
            indices[5] = (permutation / 100000) % 10;
            indices[6] = (permutation / 1000000) % 10;
            indices[7] = (permutation / 10000000) % 10;
            bool skip = false;
            for (int j = 0; j < 8; j++) {
                for (int k = 0; k < 8; k++) {
                    if (j != k && indices[j] == indices[k]) {
                        skip = true;
                    }
                }
            }
            if (skip)
                continue;
            float distance = 0.0f; 
            float tastiness = 0.0f;
            float lastX = 0;
            float lastY = 0;
            for (int i = 0; i < 8; i++) {
                const float dx = path[indices[i] * 2] - lastX;
                const float dy = path[indices[i] * 2 + 1] - lastY;
                const float t = taste[indices[i]];
                distance += sqrtf(fmaf(dx, dx, fmaf(dy, dy, 0.0f)));
                tastiness += t;
                lastX = path[indices[i] * 2];
                lastY = path[indices[i] * 2 + 1];
                if (distance > 0.0f) {
                    if (tastiness / distance > score) {
                        prm = permutation;
                        score = tastiness / distance;
                    }
                }
            }
        }
    }
    const int warpLane = threadIdx.x & 31;
    const int warpIndex = threadIdx.x >> 5;
    for (int i = 16; i >= 1; i >>= 1) {
        const float gather = __shfl_down_sync(0xFFFFFFFF, score, i);
        const int gather2 = __shfl_down_sync(0xFFFFFFFF, prm, i);
        if (warpLane < i) {
            prm = (score < gather ? gather2 : prm);
            score = (score < gather ? gather : score);
        }
    }
    if (warpLane == 0) {
        s_reduction[warpIndex] = score;
        s_reduction2[warpIndex] = prm;
    }
    __syncthreads();
    if (warpIndex == 0) {
        if (warpLane < (blockDim.x >> 5)) {
            score = s_reduction[warpLane];
            prm = s_reduction2[warpLane];
        }
        else {
            score = 0.0f;
            prm = 0;
        }
        for (int i = 16; i >= 1; i >>= 1) {
            const float gather = __shfl_down_sync(0xFFFFFFFF, score, i);
            const int gather2 = __shfl_down_sync(0xFFFFFFFF, prm, i);
            if (warpLane < i) {
                prm = (score < gather ? gather2 : prm);
                score = (score < gather ? gather : score);
            }
        }
        if (warpLane == 0) {
            scores[blockIdx.x] = score;
            permutations[blockIdx.x] = prm;
        }
    }
}


int main() {
    cudaStream_t stream;
    cudaEvent_t event1;
    cudaEvent_t event2;
    gpuErrchk(cudaStreamCreate(&stream));
    gpuErrchk(cudaEventCreate(&event1));
    gpuErrchk(cudaEventCreate(&event2));
    int size1 = 64;
    int size2 = 128;
    int size3 = 128;
    int size4 = 128;
    size_t tmp[4] = { size1, size2, size3, size4 };
    int size = size1 * size2 * size3 * size4;
    float* scores;
    int* permutations;
    float init = 1e35f;
    gpuErrchk(cudaMallocAsync(&scores, sizeof(float) * BLOCKS, stream));
    gpuErrchk(cudaMallocAsync(&permutations, sizeof(int) * BLOCKS, stream));
    gpuErrchk(cudaEventRecord(event1, stream));
    constexpr int BENCHMARK_ITERATIONS = 10;
    for (int i = 0; i < BENCHMARK_ITERATIONS; i++) {
        k_brute << <BLOCKS, THREADS, 0, stream >> > (scores, permutations);
    }
    gpuErrchk(cudaEventRecord(event2, stream));
    gpuErrchk(cudaStreamSynchronize(stream));
    float results[BLOCKS];
    int results2[BLOCKS];
    gpuErrchk(cudaMemcpy(results, scores, sizeof(float) * BLOCKS, cudaMemcpyDeviceToHost));
    gpuErrchk(cudaMemcpy(results2, permutations, sizeof(int) * BLOCKS, cudaMemcpyDeviceToHost));
    float finalScore = 0.0f;
    int finalPermutation = 0;

    for (int i = 0; i < BLOCKS; i++) {
        if (finalScore < results[i]) {
            finalScore = results[i];
            finalPermutation = results2[i];
        }
    }
    std::cout << "tastiness / distance = " << finalScore << std::endl;
    std::cout << "distance / tastiness = " << (1.0f / finalScore) << std::endl;
    float milliseconds = 0;
    cudaEventElapsedTime(&milliseconds, event1, event2);
    std::cout << "milliseconds = " << (milliseconds / BENCHMARK_ITERATIONS) << std::endl;
    int indices[10] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
    indices[0] = (finalPermutation / 1) % 10;
    indices[1] = (finalPermutation / 10) % 10;
    indices[2] = (finalPermutation / 100) % 10;
    indices[3] = (finalPermutation / 1000) % 10;
    indices[4] = (finalPermutation / 10000) % 10;
    indices[5] = (finalPermutation / 100000) % 10;
    indices[6] = (finalPermutation / 1000000) % 10;
    indices[7] = (finalPermutation / 10000000) % 10;

    float distance = 0.0f;
    float tastiness = 0.0f;
    float lastX = 0;
    float lastY = 0;
    float path[] = {
        2, 8,
        15, 3,
        6, 6,
        20, 15,
        11, 2,
        14, 1,
        7, 3,
        1, 12,
        3, 6,
        14, 13
    };
    float taste[] = {
        7,
        17,
        8,
        30,
        12,
        15,
        5,
        12,
        3,
        18
    };
    for (int i = 0; i < 8; i++) {
        std::cout << "path: " << (indices[i] + 1) << std::endl;
        const float dx = path[indices[i] * 2] - lastX;
        const float dy = path[indices[i] * 2 + 1] - lastY;
        const float t = taste[indices[i]];
        distance += sqrtf(fmaf(dx, dx, fmaf(dy, dy, 0.0f)));
        tastiness += t;
        lastX = path[indices[i] * 2];
        lastY = path[indices[i] * 2 + 1];
        if (distance > 0.0f) {
            if (fabsf(tastiness / distance - finalScore) < 0.001f) {
                std::cout << tastiness / distance << " score reached! " << std::endl;
                break;
            }
        }
    }

    gpuErrchk(cudaFreeAsync(scores, stream));
    gpuErrchk(cudaFreeAsync(permutations, stream));
    gpuErrchk(cudaEventDestroy(event1));
    gpuErrchk(cudaEventDestroy(event2));
    gpuErrchk(cudaStreamDestroy(stream));
    return 0;
}

output:

tastiness / distance = 2.89452
distance / tastiness = 0.345481
milliseconds = 16.5473
path: 7
path: 5
path: 6
path: 2
path: 10
path: 4
2.89452 score reached!

Some parts can be optimized:

  • 8 x 8 skip loop: this can be replaced by an array that keeps track of used indices to reduce number of checks --> can speedup 10x

  • modulo arithmetic + math: a lot of iterations are wasted currently. Using exact number of permutations required + some 1to1 mapping would also make it fast --> can speedup 100x

  • total speedup would be 1000x (10-15 microseconds)

  • A second gpu could be given half of the work, to make it 2000x speedup (5 microseconds). Scaling isn't good beyond this, due to already-low latency.

79786666
0
Author

Great solution! It's amazing how fast computers are!

79786732
0

@setman thank you. But something like a minimum spanning tree could be more efficient.

79786200
4

Output of the 🌮 code posted below:

Best path: 7 -> 5 -> 6 -> 2 -> 10 -> 4
 Total tastiness:             97
 Total distance:              33.51
 Tastiness to distance ratio: 2.89

Code used to solve the 🌮 challenge, I included an optional plot_taco_spots method that visualizes the data and path:

# Taco spot data (x, y, tastiness)
TACO_SPOTS = {
    1:   (2,  8,  7),
    2:  (15,  3, 17),
    3:  ( 6,  6,  8),
    4:  (20, 15, 30),
    5:  (11,  2, 12),
    6:  (14,  1, 15),
    7:  ( 7,  3,  5),
    8:  ( 1, 12, 12),
    9:  ( 3,  6,  3),
    10: (14, 13, 18)
}

# Parameters
HUNGER = 8       # Number of taco spot that can be visited
START  = (0, 0)  # Starting point

def plot_taco_spots(spots, path=None):
    """ Visualize taco spots and optionally the path taken.
    spots: dict of taco spots with index as key and (x, y, tastiness) as value
    path:  list of spot indices representing the path taken (optional)
    """
    import matplotlib.pyplot as plt

    # Some contract checks on the inputs to avoid plotting garbage
    assert(isinstance(spots, dict) and len(spots) > 0), "Spots must be a non-empty dictionary"
    assert(all(isinstance(v, tuple) and len(v) == 3 for v in spots.values())), "Invalid spots format"
    assert(path is None or all(i in spots for i in path)), "Path contains invalid spot indices"

    x = [spots[i][0] for i in spots]
    y = [spots[i][1] for i in spots]
    labels = [str(i) for i in spots]

    plt.scatter(x, y, s=[spots[i][2]*20 for i in spots])  # Size proportional to tastiness

    for i, label in enumerate(labels):
        plt.annotate(label, (x[i], y[i]), textcoords="offset points", xytext=(0,10), ha='center')

    if path:
        path_x = [START[0]] + [spots[i][0] for i in path]
        path_y = [START[1]] + [spots[i][1] for i in path]
        plt.plot(path_x, path_y, 'r--')

    plt.xlabel('X Coordinate')
    plt.ylabel('Y Coordinate')
    plt.xticks(range(0, 21, 1), [])
    plt.yticks(range(0, 21, 1), [])
    plt.title('Taco Spots')
    plt.grid(True)
    plt.axis('equal')
    plt.show()

def get_distance_L2(p1, p2):
    """ Calculate Euclidean distance between two given points, p1 and p2. Works without math import. """
    return ((p1[0] - p2[0]) ** 2 + (p1[1] - p2[1]) ** 2) ** 0.5

def get_ratio(tastiness, distance):
    """ Calculate tastiness to distance ratio. """
    if distance == 0:
        return 0.0  # Avoid division by zero
    return tastiness / distance

def print_path(path, tastiness, distance):
    """ Print the path in a readable format. """
    print(" -> ".join(map(str, path)))
    print(f" Total tastiness:             {tastiness}")
    print(f" Total distance:              {distance:.2f}")
    print(f" Tastiness to distance ratio: {get_ratio(tastiness, distance):.2f}")


if __name__ == "__main__":
    # Stores all completed paths (no further improvements possible)
    completed_paths = []

    # Each path is a tuple: (path, current_position, current_tastiness, current_distance, remaining_spots, current_hunger)
    initial_state = ([], START, 0, 0, set(i for i in TACO_SPOTS.keys()), HUNGER)
    routes        = [initial_state]

    # Visit all possible paths, breadth-first
    while routes:
        new_paths = []
        for path, position, tastiness, distance, rem_spots, hunger_left in routes:

            # Look for next spots to visit
            for spot_index in rem_spots:
                spot = TACO_SPOTS[spot_index]

                # Try to visit this spot, if hunger allows
                if hunger_left > 0:
                    spot_pos      = (spot[0], spot[1])
                    spot_dist     = get_distance_L2(position, spot_pos)
                    new_tastiness = tastiness + spot[2]
                    new_distance  = distance + spot_dist
                    prev_ratio    = get_ratio(tastiness, distance)
                    new_ratio     = get_ratio(new_tastiness, new_distance)
                    new_path      = path + [spot_index]

                    # Remove the visited spot from remaining spots
                    new_rem_spots = rem_spots.copy()
                    new_rem_spots.remove(spot_index)
                    new_paths.append((new_path, spot_pos, new_tastiness, new_distance, new_rem_spots, hunger_left - 1))

                completed_paths.append((path, tastiness, distance))

        # Follow all active paths in the next iteration
        routes = new_paths

    # Find the best completed path
    best_path      = None
    best_tastiness = 0
    best_distance  = float('inf')
    for path, tastiness, distance in completed_paths:

        if get_ratio(tastiness, distance) > get_ratio(best_tastiness, best_distance):
            best_path      = path
            best_tastiness = tastiness
            best_distance  = distance

    print("Best path: ", end="")
    print_path(best_path, best_tastiness, best_distance)
    # plot_taco_spots(TACO_SPOTS, best_path)

Visualization of the taco spots and the best path: Best path for starting at (0,0)

It really helped visualizing the data and I also found that the criterion avoids an all-you-can-eat being the best solution. Thanks for an interesting challenge!

If you don't like to stay hungry, try (1, 16) as the starting point.

79786487
2

I misunderstood the question as exactly 8 nodes to travel. But after seeing your answer, I fixed mine to find 6-node path. You helped me. so +1 for you.

79786667
0
Author

Nice solution and the visualization is a nice touch as well!