Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Given n points where each point has its own range, adjust all points to maximize the minimum distance of adjacent points

Take n points in one dimension:

enter image description here

Each point can be moved within a certain range indicated by the arrow above it. These ranges are known. Ranges can overlap. How can these points be adjusted so that minimum distance of adjacent points is maximized?

I would be okay with an approximation as well.

Edit: I am not really looking for code but a general strategy. I have really no idea how to solve this.

Originally, I thought a greedy algorithm might work.

  1. Let p0 be the starting point and pn the ending point. Let dist(p0,p1) represent the distance between p0 and p1.Well obviously adjust p0 as far left as possible and pn as far right as possible.

  2. Next, find dist(p0,pn)/n-1. This represents the optimal distance each point should be from each other to be as spread out as possible. Move p1 as close to this distance.

  3. Find dist(p1, pn)/n-2. Move p2 as close to this distance as possible.

  4. Repeat for rest of points.

This doesn't work because adjusting another pointer later on might ruin the previous points.

like image 496
Apprentice_Programmer Avatar asked Nov 15 '22 20:11

Apprentice_Programmer


1 Answers

Here's some Ruby code, followed by sample results. The idea is to start with a random allocation of points to values within their ranges. Then we repeatedly do the following:

1. Find the minimum gap
2. Find all points that border a minimum gap
3. Re-randomize the values of those points

All the while, we remember the best solution so far. This won't give you the best possible result, but should give you a reasonably good result.

For the sample run, I gave a 'good' set of points by allocating 100 points evenly in a range of about 1100, then gave each point a range around that val. The algorithm was just given the ranges in random order. It's possible to get an input set of points where the best possible min gap is small or zero, which no algorithm can help with.

def test(n, range, max_attempts)
  points = get_points_with_optimal_min_gap(n, range)
  puts "input: #{points}"
  allocation = allocate_points(points, max_attempts)
  puts "solution: #{allocation.keys.sort}"
  puts "best gap: #{get_min_gap(allocation, points)}" 
  puts "\n\nallocation:"
  allocation.keys.sort.each do |k|
    puts "  #{k} => #{allocation[k]}"
  end
end

def get_points_with_optimal_min_gap(n, range)
  points = []
  1.upto(n) do |num|
    points.append([[0, num * range/n - rand(5*range/n)].max, num * range/n + rand(5*range/n)])
  end
  return points.shuffle
end




def allocate_points(input_points, max_attempts)
  val_to_points = Hash.new {|h, k| h[k] = []}
  input_points.each do |point|
    val = get_rand_val_for_point(point)
    val_to_points[val].append(point)
  end
  
  
  best_min_gap = -1
  best_allocation = nil
  attempts = 0
  while attempts < max_attempts do
    attempts += 1
    min_gap = get_min_gap(val_to_points, input_points)
    if min_gap > best_min_gap
      best_min_gap = min_gap
      best_allocation = deep_copy(val_to_points)
      puts "found new gap after #{attempts} tries: #{best_min_gap}"
      attempts = 0
    end
    vals_to_adjust = get_vals_bordering_min_gap(val_to_points, min_gap)
    points_to_reallocate = []
    vals_to_adjust.each do |val|
      points_to_reallocate += val_to_points.delete(val)
    end
  

    
    points_to_reallocate.each do |point|
      new_val = get_rand_val_for_point(point)
      val_to_points[new_val].append(point)
    end
  end
  return best_allocation
end


def get_vals_bordering_min_gap(val_to_points, min_gap)
  vals_bordering_min_gap = Set.new()
  if min_gap == 0
    val_to_points.each do |val, points|
      vals_bordering_min_gap.add(val) if points.size > 1
    end
  else
    sorted_vals = val_to_points.keys.sort
    
    prior_val = sorted_vals[0]
  
    1.upto(sorted_vals.size - 1) do |i|
      cur_val = sorted_vals[i]
      cur_gap = cur_val - prior_val
      if cur_gap == min_gap
        vals_bordering_min_gap.add(cur_val)
        vals_bordering_min_gap.add(prior_val)
      end
      prior_val = cur_val
    end
  end
  return vals_bordering_min_gap.to_a
end


def get_min_gap(val_to_points, input_points)
  return 0 if val_to_points.size < input_points.size
  
  sorted_vals = val_to_points.keys.sort
  prior_val = sorted_vals[0]
  
  min_gap = nil
  1.upto(sorted_vals.size - 1) do |i|
    cur_val = sorted_vals[i]
    min_gap = cur_val - prior_val if min_gap.nil? || cur_val - prior_val < min_gap
    prior_val = cur_val
    return min_gap if min_gap == 1
  end
  return min_gap
end


def get_rand_val_for_point(point)
  return point[0] + rand(point[1] - point[0] + 1)
end

def deep_copy(int_to_arr_of_int_pairs)
  new_hash = Hash.new {|h, k| h[k] = []}
  int_to_arr_of_int_pairs.each do |int, arr|
    arr.each do |int_pair|
      new_hash[int].append([int_pair[0], int_pair[1]])
    end
  end
  return new_hash
end

Sample Results

input: [[207, 258], [754, 826], [957, 993], [127, 158], [332, 352], [539, 615], [213, 236], [572, 590], [668, 712], [802, 823], [385, 427], [595, 641], [924, 1018], [20, 50], [698, 749], [318, 335], [64, 120], [462, 521], [708, 760], [513, 569], [806, 877], [364, 380], [732, 799], [896, 923], [947, 997], [250, 281], [798, 857], [717, 795], [542, 583], [910, 964], [277, 315], [168, 221], [263, 337], [858, 920], [316, 348], [646, 705], [557, 573], [293, 315], [48, 131], [488, 522], [252, 271], [269, 326], [43, 106], [673, 728], [399, 432], [140, 184], [564, 630], [906, 973], [508, 566], [0, 17], [493, 547], [782, 817], [184, 246], [230, 305], [5, 90], [446, 507], [429, 449], [822, 917], [35, 127], [630, 699], [888, 944], [899, 917], [179, 201], [715, 785], [40, 65], [338, 415], [908, 958], [2, 31], [469, 517], [649, 718], [96, 150], [380, 428], [347, 401], [197, 289], [849, 912], [603, 642], [805, 830], [564, 629], [19, 88], [80, 151], [593, 660], [932, 994], [479, 541], [291, 331], [175, 192], [842, 877], [987, 1028], [382, 432], [796, 884], [102, 160], [535, 587], [621, 696], [461, 476], [131, 170], [437, 469], [343, 410], [660, 713], [122, 171], [699, 752], [776, 782]]
found new gap after 1 tries: 0
found new gap after 2 tries: 1
found new gap after 8 tries: 2
found new gap after 9 tries: 3
found new gap after 35 tries: 4
found new gap after 87 tries: 5
found new gap after 384 tries: 6
found new gap after 8043 tries: 7
solution: [4, 12, 20, 31, 43, 51, 59, 68, 82, 102, 110, 119, 127, 134, 141, 148, 161, 172, 179, 188, 202, 209, 223, 233, 244, 251, 258, 273, 284, 299, 308, 326, 335, 344, 351, 361, 369, 376, 383, 395, 408, 415, 423, 430, 438, 454, 468, 482, 491, 499, 508, 515, 524, 532, 539, 549, 557, 574, 582, 589, 596, 615, 623, 634, 642, 649, 656, 663, 676, 693, 720, 728, 741, 748, 758, 769, 776, 786, 801, 808, 815, 827, 834, 842, 851, 858, 865, 872, 882, 899, 911, 935, 943, 952, 962, 974, 987, 996, 1011, 1028]
best gap: 7


allocation:
  4 => [[0, 17]]
  12 => [[5, 90]]
  20 => [[2, 31]]
  31 => [[20, 50]]
  43 => [[19, 88]]
  51 => [[40, 65]]
  59 => [[35, 127]]
  68 => [[43, 106]]
  82 => [[64, 120]]
  102 => [[96, 150]]
  110 => [[80, 151]]
  119 => [[48, 131]]
  127 => [[122, 171]]
  134 => [[102, 160]]
  141 => [[140, 184]]
  148 => [[127, 158]]
  161 => [[131, 170]]
  172 => [[168, 221]]
  179 => [[179, 201]]
  188 => [[175, 192]]
  202 => [[184, 246]]
  209 => [[207, 258]]
  223 => [[197, 289]]
  233 => [[213, 236]]
  244 => [[230, 305]]
  251 => [[250, 281]]
  258 => [[252, 271]]
  273 => [[263, 337]]
  284 => [[269, 326]]
  299 => [[277, 315]]
  308 => [[293, 315]]
  326 => [[291, 331]]
  335 => [[318, 335]]
  344 => [[316, 348]]
  351 => [[332, 352]]
  361 => [[347, 401]]
  369 => [[343, 410]]
  376 => [[364, 380]]
  383 => [[338, 415]]
  395 => [[382, 432]]
  408 => [[399, 432]]
  415 => [[380, 428]]
  423 => [[385, 427]]
  430 => [[429, 449]]
  438 => [[437, 469]]
  454 => [[446, 507]]
  468 => [[461, 476]]
  482 => [[479, 541]]
  491 => [[469, 517]]
  499 => [[462, 521]]
  508 => [[488, 522]]
  515 => [[493, 547]]
  524 => [[513, 569]]
  532 => [[508, 566]]
  539 => [[535, 587]]
  549 => [[539, 615]]
  557 => [[557, 573]]
  574 => [[542, 583]]
  582 => [[572, 590]]
  589 => [[564, 629]]
  596 => [[564, 630]]
  615 => [[595, 641]]
  623 => [[603, 642]]
  634 => [[630, 699]]
  642 => [[593, 660]]
  649 => [[621, 696]]
  656 => [[649, 718]]
  663 => [[646, 705]]
  676 => [[660, 713]]
  693 => [[668, 712]]
  720 => [[673, 728]]
  728 => [[699, 752]]
  741 => [[698, 749]]
  748 => [[717, 795]]
  758 => [[708, 760]]
  769 => [[715, 785]]
  776 => [[776, 782]]
  786 => [[732, 799]]
  801 => [[754, 826]]
  808 => [[802, 823]]
  815 => [[782, 817]]
  827 => [[805, 830]]
  834 => [[806, 877]]
  842 => [[796, 884]]
  851 => [[798, 857]]
  858 => [[822, 917]]
  865 => [[842, 877]]
  872 => [[858, 920]]
  882 => [[849, 912]]
  899 => [[899, 917]]
  911 => [[896, 923]]
  935 => [[906, 973]]
  943 => [[888, 944]]
  952 => [[908, 958]]
  962 => [[910, 964]]
  974 => [[957, 993]]
  987 => [[932, 994]]
  996 => [[947, 997]]
  1011 => [[924, 1018]]
  1028 => [[987, 1028]]

---- UPDATE ----

I added code to try to find the optimal gap for a given ordering. This is significantly better.

allocate_points() changed, and I added a new method called attempt_to_achieve_min_gap().

def attempt_to_achieve_min_gap(val_to_points, target_min_gap)
  new_val_to_points = Hash.new {|h, k| h[k] = []}
  
  leftmost_val = val_to_points.keys.min
  leftmost_point = val_to_points[leftmost_val][0]
  new_val_to_points[leftmost_point[0]].append(leftmost_point)
  
  sorted_vals = val_to_points.keys.sort
  prior_val = leftmost_val
  1.upto(sorted_vals.length - 1) do |i|
    cur_val = sorted_vals[i]
    cur_point = val_to_points[cur_val][0]
    target_val = prior_val + target_min_gap
    if target_val <= cur_point[0]
      new_val_to_points[cur_point[0]].append(cur_point)
      prior_val = cur_point[0]
    elsif target_val <= cur_point[1]
      new_val_to_points[target_val].append(cur_point)
      prior_val = target_val
    else
      return false
    end
  end
  return new_val_to_points
end


def allocate_points(input_points, max_attempts)
  val_to_points = Hash.new {|h, k| h[k] = []}
  input_points.each do |point|
    val = get_rand_val_for_point(point)
    val_to_points[val].append(point)
  end
  
  
  best_min_gap = -1
  best_allocation = nil
  attempts = 0
  while attempts < max_attempts do
    attempts += 1
    min_gap = get_min_gap(val_to_points, input_points)
    if min_gap > 0
      found_improvement = true
      while found_improvement
        found_improvement = false
        trial_min_gap = [min_gap, best_min_gap].max + 1
        improved_results = attempt_to_achieve_min_gap(val_to_points, trial_min_gap)
        if improved_results
          found_improvement = true
          min_gap = trial_min_gap
          puts "found improvement!: #{min_gap}"
          val_to_points = improved_results
        end
      end
    end
    if min_gap > best_min_gap
      best_min_gap = min_gap
      best_allocation = deep_copy(val_to_points)
      puts "found new gap after #{attempts} tries: #{best_min_gap}"
      attempts = 0
    end
    vals_to_adjust = get_vals_bordering_min_gap(val_to_points, min_gap)
    points_to_reallocate = []
    vals_to_adjust.each do |val|
      points_to_reallocate += val_to_points.delete(val)
    end
  

    
    points_to_reallocate.each do |point|
      new_val = get_rand_val_for_point(point)
      val_to_points[new_val].append(point)
    end
  end
  return best_allocation
end

New sample results:

input: [[0, 21], [787, 819], [614, 642], [503, 553], [771, 854], [701, 740], [768, 807], [475, 480], [223, 251], [431, 473], [19, 67], [711, 764], [650, 673], [381, 401], [832, 907], [303, 357], [414, 480], [201, 215], [130, 182], [233, 269], [335, 417], [637, 704], [193, 242], [566, 632], [854, 927], [545, 577], [548, 574], [552, 607], [812, 868], [949, 962], [290, 334], [299, 372], [0, 31], [234, 304], [488, 540], [132, 215], [975, 994], [389, 406], [378, 437], [685, 721], [410, 453], [750, 800], [856, 919], [237, 270], [322, 355], [964, 1018], [67, 90], [925, 1015], [736, 792], [755, 823], [29, 78], [709, 761], [828, 897], [873, 927], [464, 533], [332, 380], [788, 831], [354, 413], [500, 545], [144, 196], [96, 138], [925, 982], [0, 67], [183, 207], [83, 116], [665, 679], [843, 904], [654, 694], [892, 955], [179, 203], [56, 87], [575, 636], [484, 542], [380, 439], [592, 689], [419, 431], [654, 700], [907, 978], [136, 151], [242, 313], [96, 126], [975, 1023], [449, 491], [866, 924], [117, 177], [291, 351], [460, 485], [959, 1004], [797, 846], [210, 277], [207, 240], [138, 184], [536, 606], [710, 774], [695, 714], [71, 83], [617, 629], [284, 302], [576, 633], [88, 137]]
found new gap after 1 tries: 0
found improvement!: 2
found improvement!: 3
found improvement!: 4
found improvement!: 5
found improvement!: 6
found new gap after 1 tries: 6
found improvement!: 7
found new gap after 5 tries: 7
found improvement!: 8
found improvement!: 9
found new gap after 2 tries: 9
found improvement!: 10
found new gap after 5686 tries: 10
solution: [0, 12, 22, 32, 42, 56, 67, 77, 87, 97, 107, 117, 127, 137, 147, 157, 167, 177, 187, 197, 207, 217, 227, 237, 247, 257, 267, 277, 287, 297, 307, 317, 327, 337, 347, 357, 367, 377, 389, 399, 409, 419, 429, 439, 449, 459, 469, 479, 489, 499, 509, 519, 529, 539, 549, 559, 569, 579, 589, 599, 614, 624, 634, 644, 654, 664, 674, 684, 694, 704, 714, 724, 734, 744, 754, 764, 774, 784, 794, 804, 814, 824, 834, 844, 854, 864, 874, 884, 894, 904, 914, 925, 935, 949, 959, 975, 985, 995, 1005, 1015]
best gap: 10


allocation:
  0 => [[0, 31]]
  12 => [[0, 21]]
  22 => [[0, 67]]
  32 => [[19, 67]]
  42 => [[29, 78]]
  56 => [[56, 87]]
  67 => [[67, 90]]
  77 => [[71, 83]]
  87 => [[83, 116]]
  97 => [[88, 137]]
  107 => [[96, 126]]
  117 => [[117, 177]]
  127 => [[96, 138]]
  137 => [[132, 215]]
  147 => [[136, 151]]
  157 => [[144, 196]]
  167 => [[130, 182]]
  177 => [[138, 184]]
  187 => [[179, 203]]
  197 => [[183, 207]]
  207 => [[201, 215]]
  217 => [[207, 240]]
  227 => [[223, 251]]
  237 => [[193, 242]]
  247 => [[237, 270]]
  257 => [[210, 277]]
  267 => [[233, 269]]
  277 => [[234, 304]]
  287 => [[242, 313]]
  297 => [[284, 302]]
  307 => [[291, 351]]
  317 => [[290, 334]]
  327 => [[303, 357]]
  337 => [[322, 355]]
  347 => [[299, 372]]
  357 => [[354, 413]]
  367 => [[335, 417]]
  377 => [[332, 380]]
  389 => [[389, 406]]
  399 => [[381, 401]]
  409 => [[378, 437]]
  419 => [[419, 431]]
  429 => [[380, 439]]
  439 => [[410, 453]]
  449 => [[431, 473]]
  459 => [[414, 480]]
  469 => [[460, 485]]
  479 => [[475, 480]]
  489 => [[449, 491]]
  499 => [[464, 533]]
  509 => [[488, 540]]
  519 => [[484, 542]]
  529 => [[500, 545]]
  539 => [[503, 553]]
  549 => [[545, 577]]
  559 => [[548, 574]]
  569 => [[566, 632]]
  579 => [[552, 607]]
  589 => [[536, 606]]
  599 => [[576, 633]]
  614 => [[614, 642]]
  624 => [[617, 629]]
  634 => [[575, 636]]
  644 => [[592, 689]]
  654 => [[650, 673]]
  664 => [[637, 704]]
  674 => [[665, 679]]
  684 => [[654, 694]]
  694 => [[654, 700]]
  704 => [[695, 714]]
  714 => [[685, 721]]
  724 => [[709, 761]]
  734 => [[701, 740]]
  744 => [[711, 764]]
  754 => [[750, 800]]
  764 => [[710, 774]]
  774 => [[768, 807]]
  784 => [[736, 792]]
  794 => [[787, 819]]
  804 => [[755, 823]]
  814 => [[788, 831]]
  824 => [[771, 854]]
  834 => [[797, 846]]
  844 => [[812, 868]]
  854 => [[828, 897]]
  864 => [[832, 907]]
  874 => [[843, 904]]
  884 => [[866, 924]]
  894 => [[873, 927]]
  904 => [[856, 919]]
  914 => [[854, 927]]
  925 => [[925, 982]]
  935 => [[892, 955]]
  949 => [[949, 962]]
  959 => [[907, 978]]
  975 => [[975, 994]]
  985 => [[964, 1018]]
  995 => [[959, 1004]]
  1005 => [[925, 1015]]
  1015 => [[975, 1023]]
like image 158
Dave Avatar answered Nov 29 '22 05:11

Dave