3

We have 2 lists(black and red), each one contains multiple points in 3d space. We have to move each black point to a red point, and do this in such a way that the total distance to make the moves is the least it can be. The lists can be of different sizes.

Simple correct soltution in 2D space: Correct Example

Incorrect solution:Incorrect solution


If the sizes of the lists differ, then we have to either stack points on top of each other or split one point into multiple points.

Splitting example: Splitting example

Stacking example: Stacking example


Our best attempt at this problem follows these general steps:

  1. If there are more red points than black points, pick the black point that's furthest from all of the red point and match it with a red point that is closest to its position and has not been matched yet.

  2. Repeat step 1 until all of the black points are matched.

  3. Iterate over the leftover red points and match each one to their respective closest black point, thus stacking them. The result will look something like this:enter image description here

  4. Note: if there is more black points than red points, then step one will look for the furthest red point and match it to its closest black point and proceed all the same with the colors swapped.

Some C# code:

private void SaveFrames(List<List<Vector3>> frameList) {
        List<Dictionary<Vector3, List<Vector3>>> resultingPairs = new List<Dictionary<Vector3, List<Vector3>>>();
        for (int iFrame = 0; iFrame < frameList.Count+1; iFrame++) {
            List<Vector3> currentFrame = frameList[iFrame % frameList.Count];
            List<Vector3> nextFrame = frameList[(iFrame + 1) % frameList.Count];

            int maxIterations = Mathf.Min(currentFrame.Count, nextFrame.Count);
            Dictionary<Vector3, List<Vector3>> pairs = new Dictionary<Vector3, List<Vector3>>();
            HashSet<Vector3> takenRed = new HashSet<Vector3>();
            HashSet<Vector3> takenBlack = new HashSet<Vector3>();
            HashSet<Vector3> takenDestination = new HashSet<Vector3>();
            bool moreRed = currentFrame.Count < nextFrame.Count;

            if (moreRed) {
                for (int i = 0; i < maxIterations; i++) {
                    // Find furthest black point from any red point
                    float distance = 0;
                    Vector3 furthestBlack = Vector3.zero;
                    foreach (Vector3 black in currentFrame) {
                        if (takenBlack.Contains(black)) continue;
                        foreach (var red in nextFrame) {
                            if (Vector3.Distance(black, red) > distance) {
                                distance = Vector3.Distance(black, red);
                                furthestBlack = black;
                            }
                        }
                    }

                    // Find the closest red point to the furthest black point
                    distance = float.MaxValue;
                    Vector3 closestRed = Vector3.zero;
                    foreach (var red in nextFrame) {
                        if (takenRed.Contains(red)) continue;
                        if (Vector3.Distance(furthestBlack, red) < distance) {
                            distance = Vector3.Distance(furthestBlack, red);
                            closestRed = red;
                        }
                    }

                    if (!pairs.ContainsKey(furthestBlack)) {
                        pairs[furthestBlack] = new List<Vector3>();
                    }

                    if (!takenDestination.Contains(closestRed)) {
                        pairs[furthestBlack].Add(closestRed);
                        takenBlack.Add(furthestBlack);
                        takenRed.Add(closestRed);
                        takenDestination.Add(closestRed);
                    }

//                    Debug.Log("Pair: " + furthestBlack.ToString() + " to " + closestRed.ToString());
                }
            } else {
                for (int i = 0; i < maxIterations; i++) {
                    // Find furthest red point from any black point
                    float distance = 0;
                    Vector3 furthestRed = Vector3.zero;
                    foreach (Vector3 red in nextFrame) {
                        if (takenRed.Contains(red)) continue;
                        foreach (Vector3 black in currentFrame) {
                            if (Vector3.Distance(black, red) > distance) {
                                distance = Vector3.Distance(black, red);
                                furthestRed = red;
                            }
                        }
                    }

                    // Find the closest black point to the furthest red point
                    distance = float.MaxValue;
                    Vector3 closestBlack = Vector3.zero;
                    foreach (var black in currentFrame) {
                        if (takenBlack.Contains(black)) continue;
                        if (Vector3.Distance(furthestRed, black) < distance) {
                            distance = Vector3.Distance(furthestRed, black);
                            closestBlack = black;
                        }
                    }

                    if (!pairs.ContainsKey(closestBlack)) {
                        pairs[closestBlack] = new List<Vector3>();
                    }

                    if (!takenDestination.Contains(furthestRed)) {
                        pairs[closestBlack].Add(furthestRed);
                        takenBlack.Add(closestBlack);
                        takenRed.Add(furthestRed);
                        takenDestination.Add(furthestRed);
                    }

//                    Debug.Log("Pair: " + closestBlack.ToString() + " to " + furthestRed.ToString());
                }
            }




            if (currentFrame.Count < nextFrame.Count) {
                // For every nextFrame[i], find the closest black point and pair it.
                for (int i = currentFrame.Count; i < nextFrame.Count; i++) {
                    float distance = float.MaxValue;
                    Vector3 closestBlack = Vector3.zero;
                    foreach (var black in currentFrame) {
                        if (Vector3.Distance(nextFrame[i], black) < distance) {
                            distance = Vector3.Distance(nextFrame[i], black);
                            closestBlack = black;
                        }
                    }

                    if (!pairs.ContainsKey(closestBlack)) {
                        pairs[closestBlack] = new List<Vector3>();
                    }

                    if (!takenDestination.Contains(nextFrame[i])) {
                        pairs[closestBlack].Add(nextFrame[i]);
                        takenDestination.Add(nextFrame[i]);
                    }

//                    Debug.Log("Pair: " + closestBlack.ToString() + " to " + nextFrame[i].ToString());
                }
            }


            if (currentFrame.Count > nextFrame.Count) {
                // For every currentFrame[i], find the closest red point and pair it.
                for (int i = nextFrame.Count; i < currentFrame.Count; i++) {
                    float distance = float.MaxValue;
                    Vector3 closestRed = Vector3.zero;
                    foreach (var red in nextFrame) {
                        if (Vector3.Distance(currentFrame[i], red) < distance) {
                            distance = Vector3.Distance(currentFrame[i], red);
                            closestRed = red;
                        }
                    }

                    if (!pairs.ContainsKey(currentFrame[i])) {
                        pairs[currentFrame[i]] = new List<Vector3>();
                    }

                    if (!takenDestination.Contains(closestRed)) {
                        pairs[currentFrame[i]].Add(closestRed);
                        takenDestination.Add(closestRed);
                    }

//                    Debug.Log("Pair: " + currentFrame[i].ToString() + " to " + closestRed.ToString());
                }
            }
            resultingPairs.Add(pairs);
        }
}

This method works for simple shapes like cubes. enter image description here

However, it starts acting up when the cube positions overlap in 3d space from ne set of points to another.

enter image description here

And it does even funkier stuff with more complex points: enter image description here

I am not exactly sure why this breaks down and I could not come up with a simple 2D example of where this approach goes wrong.

We have tried 3 different methods over 3 very long days, and can not seem to find a solution to this seemingly simple problem.

Majiick
  • 183
  • 9
  • *"it starts acting up when the cube positions overlap in 3d space"*. If that's the case, the 2D equivalent is overlapping squares. – user3386109 Feb 15 '19 at 01:06
  • Does it resemble [ICP](https://en.wikipedia.org/wiki/Iterative_closest_point)? – MBo Feb 15 '19 at 04:23
  • @MBo it kind of resembles ICP but we need a separate rigid transformation for every individual point instead of the whole cloud so that the new points match the new cloud perfectly, ICP also doesn't deal with splitting one point into multiple or converging multiple points into one. – Majiick Feb 15 '19 at 11:32

2 Answers2

1

You can interpret this as the Assignment problem, where the black points are the "agents", the red points are the "tasks" (or vice versa) and the distance between them is the cost.

The problem instance has a number of agents and a number of tasks. Any agent can be assigned to perform any task, incurring some cost that may vary depending on the agent-task assignment. It is required to perform all tasks by assigning exactly one agent to each task and exactly one task to each agent in such a way that the total cost of the assignment is minimized.

The Assignment problem can be solved in polynomial time using The Hungarian algorithm. Variations on the problem involve more tasks than agents, which you can apply to your special cases where the sizes of the lists differ.

samgak
  • 22,290
  • 4
  • 50
  • 73
  • Doesn't quite work because of this case https://imgur.com/vBJrJ08 . Arguably we could create N amount of dummies for every single black point where N is the amount of red points, but then this algorithm would take forever. – Majiick Feb 15 '19 at 22:33
0

If you want a "quick 'n dirty" solution that should give decent results, consider adapting your current algorithm to be a probabilistic one. Weight each nearby red point according to how far away it is from the black point, and pick one at random by their weights. You may want to square (or even cube) the distances, to discourage picking farther away points. Overall it should pick many of the same points as your original algorithm, but with a few differences here and there. Repeat as many times as feasible, and pick the best result.

If you want something a bit less hand-wavey, consider modeling the problem as an asymmetric traveling salesman problem. Connect each black point to each red point, with a directional edge of weight proportional to its euclidean distance between them. Then connect each red point to each black point, with a directional edge of weight 0. Then solve with an existing asymmetric TSP solver, and then add extra nodes + connect as normal if necessary. Note however, this will throw away of lot of useful information (for instance, that we don't particularly care which black node we connect with next), in exchange for being able to use existing software with tried and proven heuristics and optimizations.

Dillon Davis
  • 3,905
  • 2
  • 12
  • 30