Festival Charging Carts

MediumsimulationAlso known as k-Means Clustering

A music festival runs a few phone-charging carts. Each evening the organisers look at where people stood (from wristband pings) and move the carts. Their routine: every person is served by whichever cart is closest; then each cart is rolled to the average position of the people it served. With the carts in new places, some people now have a different closest cart, so they repeat, until nobody switches carts or they run out of time.

That routine is k-means, the classic way to find groups in data nobody has labelled. It is used to group similar documents by their embeddings, to compress colours in images, and to split a large vector-search index into regions so a query only searches the nearest few.

Your task

Write placeCarts(people, carts, maxRounds). people[i] and carts[j] are positions with the same number of coordinates. Repeat for at most maxRounds rounds:

  1. Assign. Give every person to the nearest cart by straight-line distance, using the cart positions from the start of this round. If two carts are equally near, pick the lower cart index.
  2. Check. If every person has the same cart as in the previous round's assignment, stop now. In round 1 there is no previous assignment, so always go on to Move.
  3. Move. Move each cart to the average of its people's positions (each coordinate averaged separately). A cart with nobody assigned stays where it is.

Return the final cart positions, in the original cart order.

placeCarts(people: float[][], carts: float[][], maxRounds: int) → float[][] Numbers are accepted within 1e-6.

Examples

Example 1
Input
people = [[0, 0], [0, 2], [10, 0], [10, 2]]
carts = [[0, 1], [1, 1]]
maxRounds = 10
Output
[[0, 1], [10, 1]]
Why

Round 1: (0, 0) and (0, 2) are nearer cart 0; (10, 0) and (10, 2) are nearer cart 1. Cart 0 moves to the average of its two people, (0, 1). Cart 1 moves to (10, 1). Round 2: everyone keeps the same cart, so stop. Answer [[0, 1], [10, 1]].

Example 2
Input
people = [[0], [2], [4], [10]]
carts = [[1], [3]]
maxRounds = 1
Output
[[1], [7]]
Why

Only 1 round is allowed. Carts start at 1 and 3. The person at 0 is nearer cart 0. The person at 2 is 1 away from both carts, so the tie goes to cart 0. The people at 4 and 10 are nearer cart 1. Cart 0 moves to (0 + 2) / 2 = 1, cart 1 to (4 + 10) / 2 = 7. Answer [[1], [7]].

Example 3
Input
people = [[1, 1], [2, 2]]
carts = [[0, 0], [50, 50]]
maxRounds = 5
Output
[[1.5, 1.5], [50, 50]]
Why

Cart 1 is far from everyone and never gets a person, so it stays at (50, 50). Cart 0 moves to (1.5, 1.5) in round 1, and round 2 changes nothing.

Constraints

  • 1 ≤ people.length ≤ 1,000
  • 1 ≤ carts.length ≤ 20
  • 1 ≤ number of coordinates ≤ 5
  • -1,000 ≤ each coordinate ≤ 1,000
  • 1 ≤ maxRounds ≤ 100
  • Answers within 0.000001 of the expected values are accepted.
Start with brute force

The "perfect" answer would be the grouping of people into carts that makes the total squared distance smallest. Finding it by trying every grouping means k^n options: with 20 carts and 1,000 people that number has over 1,300 digits.

k-means does not search all groupings. Each round can only lower (or keep) the total squared distance, so it settles quickly, usually at a good answer, though not always the best one. Each round costs O(n · k · d) for n people, k carts and d coordinates.

Hints

0 of 4 shown

Walkthrough and solution

Unlocks when you pass every test. Stuck for a while? Looking is fine too, as long as you then close it and write the code yourself.

Ctrl+Enter runs

Run your code to see results here. Run checks the examples. Submit checks every test, including hidden edge cases.

Write code