I've got some good results with the algorithm below. See also the hand-crafted example and the Python implementation here.
Observations and Definitions
One way of summarising the task is: reduce gaps in a series of numbers to zero by renumbering as few numbers as possible.
Let's call a "run" a series of one or more consecutive integers like [45, 46, 47]. And a "gap" is one or more missing numbers in a list of integers.
E.g. [1, 2, 5, 6, 9, 11] contains:
- four runs
[1, 2], [5, 6], [9] and [11]
- three gaps
[3, 4], [7, 8] and [10]
Some observations (the hand-crafted example below may help to explain these better):
renumbering numbers off the runs at the end and the beginning is efficient. This is because when numbers in an "outer" run get renumbered, the run may be used up and disappear. When that happens, the gap next to it disappears too.
Moving "outer" numbers so they fill gaps in the middle has a chance of (a) eliminating a gap at the outer edge (b) eliminating a gap in the middle (c) increasing the length of a run. For example, renumbering [11] to 9 removes the [10] gap and increases the length of the [9] run but in this case reduces the size of, but does not eliminate, a gap in the middle. Both removing a gap (or gaps) and increasing the length of an existing run means there are more consecutive numbers and that we're making progress
conversely, renumbering a number near the middle of the list is not likely to be as efficient. At best, if the number has gaps on both sides then it is 2/3 as effective as renumbering an outer number, because it can never remove an outer gap
each number may be renumbered more than once during the process, but all we need is one renumber from initial value to final value. This gives us an opportunity to optimise
Some of the above observations are more probabilities than certainties, but a good strategy appears to be "renumber from the outside in."
Algorithm
- sort the list of numbers into ascending order so you can see where the runs and gaps are
- take the last run of consecutive numbers (e.g.
[98, 99, 100]) or the first run of consecutive numbers (e.g. [1, 2]), whichever has the fewest numbers. In this case [1, 2] would be picked because it has only 2 elements. If the first and last runs are of the same length, choose either
- take numbers from the last/first run and squeeze as many as possible into the gap that's nearest the middle
- repeat from 2 until there are no more gaps
- optimise renumbers-- see below.
This Python implementation uses the above algorithm. There's no guarantee that its solutions are optimal but the renumber counts are reassuringly low for small cases I've looked at closely. It solves for 5000 numbers and over 1000 gaps in a few seconds (test data in source) and the minimal number of moves is subjectively about right.
$ python continuous.py
Original numbers: [1, 2, 5, 6, 9, 10, 13, 14, 19]
19 -> 3
14 -> 4
13 -> 7
10 -> 8
(4 renumbers)
Original order with renumbers:
[1, 2, 5, 6, 9, 8, 7, 4, 3]
Gaps in final output (check=0): 0
Sorted:
[1, 2, 3, 4, 5, 6, 7, 8, 9]
no of integers in original: 9
Hand-crafted Example
Take the numbers [1, ..., 3, ..., 5, 6, ..., 9, 10, 11, ..., 13, ..., 19]:
The [7, 8] gap is nearest the middle (3rd gap of 5); The run [1] (1 number) is the same size as run [19] (1 number) so 1 is arbitrarily chosen from the first run and renumbers as 7. The [7, 8] gap is now reduced to [8]; because 1 has been renumbered, there is no longer a [2] gap either, as the sequence now starts from the number 3:
[1, 3, 5, 6, 9, 10, 11, 13, 19] -> [*x,* 3, 5, 6, *7,* 9, 10, 11, 13, 19]
(x is where the original number was before renumbering.)
The [8] gap is nearest the middle (2nd gap of 3); 3 peels off from the start and renumbers as 8, removing the [8] gap and the [4] gap:
[3, 5, 6, 7, 9, 10, 11, 13, 19] -> [*x,* 5, 6, 7, *8,* 9, 10, 11, 13, 19]
The [12] gap is nearest the middle-- or as near the middle as the [14..18] gap; [19] is shorter than [5..7] so the last run, [19] is renumbered first. The number 19 renumbers as 12, plugging the 12 gap and removing the [14..18] gap:
[5, 6, 7, 8, 9, 10, 11, 13, 19] -> [5, 6, 7, 8, 9, 10, 11, *12,* 13, *x*]
Because there are no more gaps, we have an unbroken sequence of numbers [5..13].
The answer is how many numbers you have moved:
1 -> 7, 3 -> 8, 19 -> 12 (3 moves)
Optimising Renumbers
What if the [14, 15, 16, 17, 18] gap had been chosen as the middle gap instead of the [12] gap? In that case we'd get two renumbers: 19 -> 14 followed by 14 -> 12.
You can optimise the answer further by collapsing redundant renumbers if there are any. As per observation 4, no number needs changing more than once, so 19 -> 14 followed by 14 -> 12 is equivalent to 19 -> 12. If you have two or more renumbers that are chained like this, they can be replaced by one renumber.
As a further example, the sequence [1, 3, 5, 7, 9] could be consolidated using the following renumbers:
9 -> 6, 7 -> 2, 6 -> 4
This can be reduced to:
9 -> 4, 7 -> 2
giving the continuous run [1, 2, 3, 4, 5] in 2 moves rather than 3.