|
| 1 | +require 'benchmark' |
| 2 | + |
| 3 | +using(Module.new { refine(Array) { |
| 4 | + def middle |
| 5 | + self[size / 2] |
| 6 | + end |
| 7 | +}}) |
| 8 | + |
| 9 | +LSHIFT = 30 |
| 10 | + |
| 11 | +bench_candidates = [] |
| 12 | + |
| 13 | +bench_candidates << def sort_func(order, bad) |
| 14 | + bad.sum { |b| b.sort { |x, y| order[x << LSHIFT | y] || 1 }.middle } |
| 15 | +end |
| 16 | + |
| 17 | +bench_candidates << def rand_pivot_quickselect(order, bad) |
| 18 | + quicksel = ->(a, i) { |
| 19 | + pivot = a.sample |
| 20 | + |
| 21 | + low = a.select { |x| order[x << LSHIFT | pivot] } |
| 22 | + high = a.select { |x| order[pivot << LSHIFT | x] } |
| 23 | + k = low.size |
| 24 | + |
| 25 | + if i < k |
| 26 | + quicksel[low, i] |
| 27 | + elsif i > k |
| 28 | + quicksel[high, i - k - 1] |
| 29 | + else |
| 30 | + pivot |
| 31 | + end |
| 32 | + } |
| 33 | + bad.sum { |b| quicksel[b, b.size / 2] } |
| 34 | +end |
| 35 | + |
| 36 | +bench_candidates << def rand_pivot_quickselect_mut(order, bad) |
| 37 | + quicksel = ->(a, l, r, i) { |
| 38 | + pivot_i = rand(l...r) |
| 39 | + pivot = a[pivot_i] |
| 40 | + a[pivot_i] = a[r - 1] |
| 41 | + a[r - 1] = pivot |
| 42 | + |
| 43 | + small = l |
| 44 | + |
| 45 | + (l...r).each { |i| |
| 46 | + next unless order[a[i] << LSHIFT | pivot] |
| 47 | + a[small], a[i] = [a[i], a[small]] |
| 48 | + small += 1 |
| 49 | + } |
| 50 | + |
| 51 | + if i < small |
| 52 | + quicksel[a, l, small, i] |
| 53 | + elsif i > small |
| 54 | + # for my implementation I've decided not to swap the pivot back into place, |
| 55 | + # so i will decrease by 1 here. |
| 56 | + quicksel[a, small, r - 1, i - 1] |
| 57 | + else |
| 58 | + pivot |
| 59 | + end |
| 60 | + } |
| 61 | + bad.sum { |b| |
| 62 | + b = b.dup |
| 63 | + quicksel[b, 0, b.size, b.size / 2] |
| 64 | + } |
| 65 | +end |
| 66 | + |
| 67 | +bench_candidates << def median_of_medians(order, bad) |
| 68 | + sort = ->a { a.sort { |x, y| order[x << LSHIFT | y] || 1 }.freeze } |
| 69 | + median_of_median = ->(a, i) { |
| 70 | + slices = a.each_slice(5).to_a.map(&:freeze).freeze |
| 71 | + medians = slices.map { |s| sort[s].middle }.freeze |
| 72 | + pivot = if medians.size <= 5 |
| 73 | + sort[medians].middle |
| 74 | + else |
| 75 | + median_of_median[medians, medians.size / 2] |
| 76 | + end |
| 77 | + |
| 78 | + low = a.select { |x| order[x << LSHIFT | pivot] } |
| 79 | + high = a.select { |x| order[pivot << LSHIFT | x] } |
| 80 | + k = low.size |
| 81 | + |
| 82 | + if i < k |
| 83 | + median_of_median[low, i] |
| 84 | + elsif i > k |
| 85 | + median_of_median[high, i - k - 1] |
| 86 | + else |
| 87 | + pivot |
| 88 | + end |
| 89 | + } |
| 90 | + bad.sum { |b| median_of_median[b, b.size / 2] } |
| 91 | +end |
| 92 | + |
| 93 | +seps = %w(| ,).map(&:freeze).freeze |
| 94 | +order, pages, empty = ARGF.each("\n\n", chomp: true).zip(seps).map { |section, sep| |
| 95 | + section.each_line.map { |l| l.split(sep).map(&method(:Integer)).freeze }.freeze |
| 96 | +} |
| 97 | +raise "bad #{empty}" if empty |
| 98 | + |
| 99 | +strict = true |
| 100 | +uniq_page = pages.flatten.to_h { |k| [k, true] }.freeze |
| 101 | + |
| 102 | +order = order.to_h { |a, b, *bad| |
| 103 | + raise "bad #{bad}" unless bad.empty? |
| 104 | + [a, b].each { |pg| raise "rule for unneeded page #{pg}" if strict && !uniq_page[pg] } |
| 105 | + [a << LSHIFT | b, -1] |
| 106 | +}.freeze |
| 107 | + |
| 108 | +n = uniq_page.size |
| 109 | +all_pairs = order.size == n * (n - 1) / 2 |
| 110 | + |
| 111 | +results = {} |
| 112 | + |
| 113 | +bad = pages.select { |ps| |
| 114 | + # any pair is in the *wrong* order. |
| 115 | + (all_pairs ? ps.each_cons(2) : ps.combination(2)).any? { |x, y| |
| 116 | + order[y << LSHIFT | x] |
| 117 | + } |
| 118 | +}.freeze |
| 119 | + |
| 120 | +Benchmark.bmbm { |bm| |
| 121 | + bench_candidates.each { |f| |
| 122 | + bm.report(f) { 100.times { results[f] = send(f, order, bad) } } |
| 123 | + } |
| 124 | +} |
| 125 | + |
| 126 | +# Obviously the benchmark would be useless if they got different answers. |
| 127 | +if results.values.uniq.size != 1 |
| 128 | + results.each { |k, v| puts "#{k} #{v}" } |
| 129 | + raise 'differing answers' |
| 130 | +end |
0 commit comments