Попытка оптимизировать этот код ruby ​​для сортировки / сопоставления массивов - PullRequest
1 голос
/ 01 октября 2019

У меня есть некоторые проблемы во время моей стажировки. Мне нужно получить все последовательности из массива: при условии, что у меня есть этот массив [1,2,3,4,5], мне нужно сгенерировать все последовательные подмассивы, такие как [1], [2], [3], [4], [5], [1,2], [2,3], [3,4], [4,5], [1,2,3], [2,3,4], [3,4,5], [1,2,3,4], [2,3,4,5], [1,2,3,4,5].

После этого мне нужноудалить из этого сгенерированного массива некоторые вложенные массивы. Я назвал это "не разрешено".

Например, если недопустимыми являются [1,3] и [3,5], мне нужно удалить последовательности [1,2,3] (в этом * * [1,3]), [3,4,5] ([3,5] - это подпрограмма). массив), [1,2,3,4] ([1,3] - это подмассив), [2,3,4,5] ([3,5] - это подмассив) и [1,2,3,4,5] ([1,3] и [3,5] - это подмассивof).

Я сделал некоторый код, который работает, проблема: большие массивы (n может быть 10^5), код очень медленный (Если вы попробуете с n = 1000, вы увидите). Мой код ниже. Недопустимые значения генерируются из [a[i], b[i]].

require 'set'

def not_allowed(a, b)
  count = a.size - 1
  (0..count).map { |i| [a[i], b[i]] }
end

def combinations(n)
  combinations = []
  elements = (1..n).to_a
  elements_sequence = (0..elements.size - 1)

  elements_sequence.each do |i|
    elements_sequence.each do |j|
      next if elements[j..i].size == 0

      combinations << elements[j..i]
    end
  end
  combinations
end

def adjustCombinations(n, a, b)
  sequences = combinations(n)
  not_alloweds = not_allowed(a, b).map { |not_allowed| not_allowed if not_allowed[0] != not_allowed[1] }.compact
  final_not_alloweds = not_alloweds.map { |not_allowed|  sequences.map { |sequence| sequence if not_allowed.to_set.subset?(sequence.to_set) }.compact }.flatten(1)

  (sequences - final_not_alloweds).count
end

adjustCombinations(5, [2,1,2], [2,3,5])

. В этом примере [1,3] и [2,5] не допускаются. Если недопустимые значения равны, например, [1,1], [2,2], [3,3], это не проблема.

n означает, что массив работает до 5.

ОБНОВЛЕНИЕ 1: у меня есть ограничение в 9 ~ 10 секунд для запуска этого кода

ОБНОВЛЕНИЕ 2: Для not_alloweds [1,3] и [2,5] ,нам нужно удалить, например, последовательности [ 1 , 2, 3 ], [ 1 , 2, 3 , 4][ 2 , 3,4, 5 ], [ 1 , 2 , 3 , 4, 5 ] (если посмотреть, что not_alloweds является подпоследовательностью перечисленных последовательностей)

ОБНОВЛЕНИЕ 3: мне нужно количество возможных разрешенных_последовательностей. В случае:

n = 5
a = [2,1,2]
b = [2,3,5]

Ожидаемый результат 11

Ответы [ 3 ]

1 голос
/ 06 октября 2019

ОП определил 2-элементные массивы "не разрешено", [i,j], j >= i. Я считаю более информативным ссылаться на диапазоны "не подлежать покрытию" ( NTBC ), i..j, j >= i. Учитывая положительное целое число n и массив arr диапазонов NTBC, цель состоит в том, чтобы определить число диапазонов, охватываемых 1..n, которые охватывают none диапазонов NTBC;то есть подсчитайте те диапазоны i..j, для которых:

arr.any? { |r| (i..j).cover? r } == false

Здесь и далее, когда я ссылаюсь на диапазон i..j, следует предполагать, что i и j удовлетворяютусловие 1 <= i <= j <= n.

Я ссылаюсь на два диапазона NTBC i..j и p..q как с перекрытием , если i < p < q < j или p < i < q < j. Ниже я приведу очень быстрое решение для случая, когда нет перекрывающихся диапазонов NTBC. ОП заявил, что диапазоны NTBC могут перекрываться, поэтому мое решение не дает полного ответа. Проблема состоит в том, что наличие перекрывающихся диапазонов разрушает математическую структуру, требуемую комбинаторным алгоритмом, который я использую. Тем не менее я решил представить свое решение в надежде, что кто-то найдет способ расширить мои идеи до случая, когда диапазоны NTBC могут перекрываться.

Создать count_ranges метод

Давайте сначала создадим метод, который с учетом целых чисел i и j, i <= j подсчитывает количество диапазонов p..q, таких, что i <= p <= q <= j;то есть для чего (i..j).cover?(p,q) #=> true. Например,

(20..40).cover?(12..18) #=> false
(20..40).cover?(14..28) #=> false
(20..40).cover?(23..37) #=> true
(20..40).cover?(29..47) #=> false
(20..40).cover?(43..51) #=> false

См. Range # cover? для случая, когда аргумент является диапазоном. Метод следующий.

def count_ranges(i,j)
  return 0 if j < i
  n = (j-i+1)
  n**2 - (n-1)*n/2
end

Например:

count_ranges(1,4)   #=> 10

Это будут 10 диапазоны:

1..1, 1..2, 1..3, 1..4,
      2..2, 2..3, 2..4,
            3..3, 3..4,
                  4..4

Обратите внимание:

count_ranges(3,6)   #=> 10

или, в более общем смысле,

count_ranges(n,n+3) #=> 10

Обоснование расчетов, выполненных этим методом, заключается в следующем. Учитывая диапазон i..j, i <= j, число диапазонов, охватываемых этим диапазоном, рассчитывается следующим образом.

n = j-i+1
n + n-1 + n-2 + ... + n-(n-1)
  = n*n - (1 + 2 +...+ n-1)
  = n*n - (n-1)*n/2

n равно количеству диапазонов, которые начинаются с i и заканчиваются наk, где i <= k <= j. n-1 равно количеству диапазонов, которые начинаются с i+1 и заканчиваются k, i+1 <= k <= j и т. Д. n-(n-1) #=> 1 равно числу диапазонов, которые начинаются с i+(n-1) #=> i+(j-i+1-1) => j и заканчиваются k, j <= k <= j.

(1 + 2 +...+ n-1) - это сумма арифметической последовательности, которая равна произведению числаэлементы в последовательности (n-1) и среднее значение в последовательности, равное сумме первого и последнего элементов (1+(n-1) #=> n), деленное на 2.

Этот метод выполняет всю работув коде ниже. Важно отметить, что скорость выполнения этого метода практически не зависит от величин двух его аргументов.

Удалить избыточные диапазоны NTBC

Пусть arr будетмассив диапазонов NTBC. Первым шагом является удаление диапазонов NTBC, которые охватывают другой диапазон NTBC. Если:

r1 = u..v
r2 = x..w

и u <= x =< w <= v (r1.cover?(r2) #=> true), r1 является избыточным и поэтому может игнорироваться, так как любой диапазон r, который исключается из подсчета, поскольку r.cover?(r1) #=> true будетдисквалифицируется r2, если r1 не было.

require 'set'

def remove_redundant_ranges(arr)
  set = Set.new
  a = arr.dup
  while a.any?
    r = a.shift
    set << r unless set.any? { |t| r.cover?(t) } || a.any? { |t| r.cover?(t) }
  end
  set.to_a  
end

Например:

arr = [2..4, 7..14, 8..12, 14..14, 4..5, 1..5, 9..12, 2..4]

remove_redundant_ranges(arr) 
  #=> [14..14, 4..5, 9..12, 2..4]

Метод подсчета

def count_non_covering_ranges(n, arr)
  (([0..0] + remove_redundant_ranges(arr).sort_by(&:begin)) << (n+1..n+1)).
    each_cons(2).
    sum { |r1,r2| net_count(r1, r2) }
end

def net_count(r1, r2)
  r1b, r1e = endpoints(r1)
  r2b, r2e = endpoints(r2)
  count_ranges(r1b+1, r2e-1) - count_ranges(r1b+1, r1e-1)
end

def endpoints(r)
  [r.begin, r.end]
end

Примеры

Пример 1

n = 9
arr = [2..4, 4..5, 7..9]

count_non_covering_ranges(n, arr)
  #=> 20

Диапазоны 20, охватываемые 1..9, которые не охватывают диапазоны NTBCв arr:

1..1, 1..2, 1..3,
      2..2, 2..3,
            3..3, 3..4,
                  4..4,
                        5..5, 5..6, 5..7, 5..8,
                              6..6, 6..7, 6..8,
                                    7..7, 7..8,
                                          8..8, 8..9
                                                9..9

Пример 2

n = 12
arr = [2..4, 4..7, 9..11]

count_non_covering_ranges(n, arr)
  #=> 38

38 диапазоны, охватываемые 1..12, которые не охватывают диапазоны NTBC в arr, следующие:

1..1, 1..2, 1..3,
      2..2, 2..3,
            3..3, 3..4, 3..5, 3..6,
                  4..4, 4..5, 4..6,
                        5..5, 5..6, 5..7, 5..8, 5..9,  5..10,
                              6..6, 6..7, 6..8, 6..9,  6..10,
                                    7..7, 7..8, 7..9,  7..10,
                                          8..8, 8..9,  8..10,
                                                9..9,  9..10,
                                                      10..10, 10..11, 10..12,
                                                              11..11, 11..12,
                                                                      12..12

Пример 3

n = 12
arr = [2..4, 6..8, 9..11]

count_non_covering_ranges(n, arr)
  #=> 34

Диапазоны 34, охватываемые 1..12, которые не охватывают диапазоны NTBC в arr, следующие:

1..1, 1..2, 1..3,
      2..2, 2..3,
            3..3, 3..4, 3..5, 3..6, 3..7
                  4..4, 4..5, 4..6, 4..7
                        5..5, 5..6, 5..7,
                              6..6, 6..7,
                                    7..7, 7..8, 7..9,  7..10,
                                          8..8, 8..9,  8..10, 
                                                9..9,  9..10, 
                                                      10..10, 10..11, 10..12,
                                                              11..11, 11..12,
                                                                      12..12

Пример 4

n = 15
arr = [2..4, 4..5, 9..12, 14..14]

count_non_covering_ranges(n, arr)
  #=> 44

Диапазоны 44, охватываемые 1..15, которые не охватывают диапазоны NTBC в arr следующие.

1..1, 1..2, 1..3,
      2..2, 2..3,
            3..3,
                  4..4,
                        5..5, 5..6, 5..7, 5..8,   5..9,  5..10,  5..11,
                              6..6, 6..7, 6..8,   6..9,  6..10,  6..11,
                                    7..7, 7..8,   7..9,  7..10,  7..11,
                                          8..8,   8..9,  8..10,  8..11,
                                                  9..9,  9..10,  9..11,
                                                        10..10, 10..11,
                                                                11..11,
                                                10..12, 10..13,
                                                11..12, 11..13,
                                                12..12, 12..13, 
                                                        13..13,
                                                                15..15

Пример 5

n = 15
arr = [2..4, 4..5, 9..12, 14..14]

require 'time'

n = 1_000_000_000_000
m = 1_000 # number of NTBC ranges
s = 1_000 # size of each NTBC range
q = n/m
arr = m.times.map do |i|
  from = rand(i*q..(i+1)*q-1-s)
  from..from+s-1
end
arr.first(3)
  #=> [737321450..737322449, 1803846784..1803847783, 2536962375..2536963374] 
arr.last(2)
  #=> [998900666529..998900667528, 999036273747..999036274746] 

t = Time.now
x = count_non_covering_ranges(n, arr)
  #=> 585_410_016_606_600_423_738 
puts "#{(Time.now-t).round(2)} seconds"
0.24 seconds

Объяснение

См. Enumerable # each_cons .

Первое, на что нужно обратить внимание, это то, что скорость выполнения count_non_covering_ranges практически не зависитвеличины его первого аргумента, n, и размеров диапазонов NTBC, и приблизительно пропорционально размеру его второго аргумента, arr, числу диапазонов NTBC.

Предположим:

n = 14
arr = [2..5, 7..9, 10..13]

Сначала мы изменим это следующим образом:

r1 = 0..0
r2 = 2..5
r3 = 7..9
r4 = 10..13
r5 = n+1..n+1 #=> 15..15
a = [r1, r2, r3, r4, r5]

Сначала мы вычислим:

c1 =  count_ranges(r1.begin+1, r2.end-1)
  #=> count_ranges(1, 4)   #=> 10

Это число диапазонов, охватываемых 1..4, ни один из которых не охватывает ни одного из NTBC. Эти 10 диапазоны 1..1, 1..2, 1..3, 1..4, 2..2, 2..3, 2..4, 3..3, 3..4 и 4..4.

Далее мы вычисляем:

c2 =  count_ranges(r2.begin+1, r3.end-1)
  #=> count_ranges(3, 8)   #=> 21

c3 =  count_ranges(r3.begin+1, r4.end-1)
  #=> count_ranges(8, 12)  #=> 15

c4 =  count_ranges(r4.begin+1, r5.end-1)
  #=> count_ranges(11, 14) #=> 10

Теперь сложим эти значения:

c1 + c2 + c3 + c4
  #=> 56

Давайте сравним это с тем, что мы вычислим для итогов:

count_non_covering_ranges(n, arr)
  #=> 49

Расхождение связано с тем, что у нас есть двойные счета 7 диапазонов:

  • c1 и c2 оба считают диапазоны 3..3, 3..4 и 4..4;
  • c2 и c3 оба считают диапазон 8..8
  • c3 и c4 оба считают диапазоны 11..11, 11..12 и 12..12.

Поэтому мы должны вычесть из 49:

count_ranges(r2.begin+1, r2.end-1) + count_ranges(r3.begin+1, r3.end-1) +
  count_ranges(r4.begin+1, r4.end-1)
  #=> 3 + 1 + 3 => 7

Для удобства я также вычту следующее:

count_ranges(r1.begin+1, r1.end-1) + count_ranges(r5.begin+1, r5.end-1)
  #=> count_ranges(1, -1) + count_ranges(16, 14) => 0 + 0 => 0   

Это разрешено, поскольку я построил count_ranges, так что count_ranges(i, j) возвращает ноль, когда j < i.

1 голос
/ 01 октября 2019

Вместо создания всех комбинаций следует удаление того, что вам не нужно. Возможно, быстрее будет создать только то, что вам действительно нужно.

Например, вы можете добавить блок к методу combinations, который определяет, будет ли комбинация добавлена ​​в вывод. Таким образом, вы не генерируете ненужный длинный массив, который нужно вычесть из более позднего.

require 'set'

def combinations(n)
  combinations = []
  elements = (1..n).to_a

  elements.each_index do |i|
    (elements.length - i).times do |j|
      combination = elements[j, i + 1]
      # add the combination if no block is given, or if it evaluates to truthy
      combinations << combination if !block_given? || yield(combination)
    end
  end

  combinations
end

def adjustCombinations(n, a, b)
  not_allowed = [a, b].transpose
  not_allowed.reject! { |n1, n2| n1 == n2 }
  not_allowed.map!(&:to_set)

  combinations(n) do |combination|
    combination = combination.to_set
    not_allowed.reject { |set| set.size > combination.size }
               .none?  { |set| (set - combination).empty? }
  end
end

adjustCombinations(5, [2,1,2], [2,3,5])
#=> [[1], [2], [3], [4], [5], [1, 2], [2, 3], [3, 4], [4, 5], [2, 3, 4], [3, 4, 5]]

Если вам нужно только количество элементов, вы можете заменить combinations = [] на combinations = 0 и combinations << combination наcombinations += 1. Таким образом, вам не нужно создавать экземпляр всего массива комбинаций, который экономит много памяти при больших числах.

0 голосов
/ 02 октября 2019

для больших массивов (n = 1000) код очень медленный ..

  • n = 1000 выполняется более 20 секунд, и у меня процессор i7 8750
  • n == 10 ^ 5 .. занимает более 10 минут подряд
  • У меня есть ограничение от 9 до 10 секунд .. для больших массивов ..

Решение с использованием побитовых операторов: n = 10 ^ 4 за 83 с

Вот мои лучшие усилия, с тестами и тестами . Это не достаточно быстро, но я думаю, что есть два хороших метода здесь , которые вы можете использовать, а именно:

  1. генерация «подмассивов» со сдвигом битов (<< вsequential_sub_arrays)
  2. выполнение «пропусков» с помощью битовой маскировки (& in omit?)

Это не методы, которые я использую часто или хорошо, поэтому я представляю ихтолько в качестве трамплина для других людей.

Для достижения вашей цели («от 9 до 10 секунд .. для больших массивов») вам, возможно, придется воспользоваться этими приемами и реализовать их на более быстром языке,как C или ржавчина.

# Convert omissions like [1,3] into bitmasks like 0b00000101. See use in `omit?`.
def omissions_to_masks(omissions)
  omissions.map { |o|
    len = o.max
    s = o.each_with_object('0' * len) { |e, a| a[e - 1] = '1' }
    eval('0b' + s.reverse)
  }
end

# Test 1: omissions_to_masks
expected = [
  0b00000101, # [1,3]
  0b00010100  # [3,5]
]
actual = omissions_to_masks([[1,3], [3,5]])
fail unless actual == expected

# bit_subarray is an Integer, whose bits represent an array of Integers.
# For example, 0b00111001 represents the array [1,4,5,6]
def omit?(bit_subarray, masks)
  masks.any? { |o| bit_subarray & o == o }
end

def sequential_sub_arrays(n, output, omissions)
  masks = omissions_to_masks(omissions)
  1.upto(n) do |size|
    proto = 2 ** size - 1
    0.upto(n - size) do |shift|
      value = proto << shift
      unless omit?(value, masks)
        output << value
      end
    end
  end
end

# Test 2: sequential_sub_arrays
expected = [
  0b00001,
  0b00010,
  0b00100,
  0b01000,
  0b10000,
  0b00011,
  0b00110,
  0b01100,
  0b11000,
  0b01110,
]
actual = []
sequential_sub_arrays(5, actual, [[1,3], [3,5]])
fail unless expected == actual

# Benchmarking. I use a `NullArray` to discard results, 
# because n=10^4 would produce 60 GB of results and I 
# don't have enough memory.
class NullArray
  def <<(x)
    # noop
  end
end

require 'benchmark'
Benchmark.bm do |x|
  [5, 100, 1_000, 5_000, 10_000].each do |max|
    x.report { sequential_sub_arrays(max, NullArray.new, [[1,3], [3,5]]) }
  end
end
#        user     system      total        real
#    0.000034   0.000002   0.000036 (  0.000032)
#    0.002266   0.000007   0.002273 (  0.002274)
#    0.444146   0.001798   0.445944 (  0.446747)
#   14.362543   0.375005  14.737548 ( 14.765235)
#   83.235438   8.440965  91.676403 ( 92.070415)
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...