【问题标题】:Trying optimize this ruby code to over array sorting/matching尝试优化此 ruby​​ 代码以进行数组排序/匹配
【发布时间】:2020-01-30 07:21:02
【问题描述】:

我在实习期间遇到了一些问题。我需要从数组中获取所有序列:假设我有这个数组[1,2,3,4,5],我需要生成所有顺序子数组,如[1][2][3][4][5]、@ 987654327@,[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] 的子数组)。

我做了一些有效的代码,问题是:大数组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:我需要可能的 allowed_sequences 的数量。在以下情况下:

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

预期结果是 11

【问题讨论】:

  • "对于大数组 (n=1000) 代码很慢" 有多慢?我第一次尝试 n=1000 需要 9 秒。是不是太慢了?
  • @JaredBeck 好吧,这里 n = 1000 需要超过 20 秒才能运行,我有一个 i7 8750 CPU,n 可以是 10^5,在这种情况下,需要超过 10 分钟运行
  • 您的数组大小的上限是多少?这类问题向我表明,bitset 可能是答案。做布尔数学比测试单个数组元素更容易。例如。 disallowed_pattern &amp; entry == disallowed_pattern 表示您点击了不允许的条目。如果您正在处理简单的数值,这将起作用。这可以将您的 O(n^2) 问题减少到 O(n)
  • 从您的代码看来,您希望计算满足特定条件的子序列的总数,而不是这些子序列的数组(每个子序列对应一个范围 @987654365 @)。然而,这并不是目标,没有通读您的代码的读者可能会认为您希望返回一个数组。这需要说清楚。
  • @CarySwoveland 我想计算满足某些条件的子序列的总数。我会更清楚地说明这一点

标签: arrays ruby performance sorting optimization


【解决方案1】:

而不是创建所有组合,然后删除不需要的内容。只创建您真正需要的内容可能会更快。

例如,您可以向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 &lt;&lt; combination 替换为combinations += 1。这样您就不需要实例化整个组合数组,这样可以节省大量内存。

【讨论】:

  • 我理解你的回答,但是 [1,2] 是有效的,因为不允许的是 [2,2]、[1,3] 和 [2,5] (a[i], b [i]),但是 [2,2] 不是问题,所以我们只考虑 [1,3] 和 [2,5] 是不允许的。
  • 啊,所以你 transpose 输入数组。虽然输出并不完全相同,但我希望解释是。更多的是关于yield
  • [[2,1,2], [2,3,5]].transpose #=&gt; [[2, 2], [1, 3], [2, 5]]
  • 我运行了你的代码,它非常好,比我的要好得多。我在实习时问过某人是否足够好,但我有 9 到 10 秒的限制来执行代码,对于大型数组,它仍然需要更长的时间。我仍然想不出另一种方法来统计所有病例,但我仍在尝试。
  • @Alecsander 您是否也尝试过答案中的combinations += 1?但是,是的,这个答案不是禁食的。更快的答案涉及更多的数学。您首先必须计算总组合,即1 + 2 + 3 + ... + (n - 1) + n。然后用not_allowed 减去可能的组合计数。我有一种工作版本,但它没有考虑到不同给定not_allowed 选项之间的重叠可能性。我会看看我是否能在以后打破我的大脑。
【解决方案2】:

OP 定义了 2 元素“不允许”数组,[i,j]j &gt;= i。我发现引用“不被覆盖”(NTBC)范围,i..jj &gt;= i 更具描述性。给定一个正整数n 和一个NTBC 范围数组arr,目标是确定1..n 覆盖的范围数,这些范围没有 的NTBC 范围;也就是说,计算那些范围i..j

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

在这里和下面,每当​​我提到一个范围i..j 时,都假定ij 满足条件1 &lt;= i &lt;= j &lt;= n

如果i &lt; p &lt; q &lt; jp &lt; i &lt; q &lt; j,我将两个NTBC 范围i..jp..q 称为重叠。下面我提出了一个非常快速的解决方案,适用于 no 重叠 NTBC 范围的情况。 OP 已经声明 NTBC 范围可以重叠,所以我的解决方案没有一个完整的答案。问题是重叠范围的存在破坏了我采用的组合算法所需的数学结构。尽管如此,我还是决定提出我的解决方案,希望有人能找到一种方法将我的想法扩展到 NTBC 范围可能重叠的情况。

创建count_ranges方法

让我们首先创建一个方法,给定整数iji &lt;= j,计算p..q 的范围数,使得i &lt;= p &lt;= q &lt;= j;也就是说,(i..j).cover?(p,q) #=&gt; 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..ji &lt;= 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 &lt;= k &lt;= jn-1 等于从 i+1 开始到 ki+1 &lt;= k &lt;= j 等结束的范围数。 n-(n-1) #=&gt; 1 等于从 i+(n-1) #=&gt; i+(j-i+1-1) =&gt; j 开始并以 kj &lt;= k &lt;= j 结束的范围数。

(1 + 2 +...+ n-1)是等差数列的和,等于序列中的元素个数(n-1)与序列中的平均值的乘积,等于第一个和最后一个元素之和( 1+(n-1) #=&gt; n) 除以2

这个方法完成了下面代码中的所有工作。需要注意的是,这个方法的执行速度几乎与它的两个参数的大小无关。

删除多余的 NTBC 范围

arr 成为 NTBC 范围的数组。第一步是删除覆盖另一个 NTBC 范围的 NTBC 范围。如果:

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

u &lt;= x =&lt; w &lt;= v (r1.cover?(r2) #=&gt; true),r1 是多余的,因此可以忽略,因为r 的任何范围都被取消资格,因为r.cover?(r1) #=&gt; true 将被r2 取消资格,如果@987654381 @ 不在场。

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

1..9 覆盖的20 范围不包括arr 中的任何 NTBC 范围如下。

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

1..12 覆盖的38 范围不包括arr 中的任何 NTBC 范围如下。

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

1..12 覆盖的34 范围不包括arr 中的任何 NTBC 范围如下。

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

1..15 覆盖的44 范围不包括arr 中的任何 NTBC 范围如下。

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..11..21..31..42..22..32..43..33..4 和@9876。

接下来我们计算:

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 范围:

  • c1c2 都计算范围 3..33..44..4
  • c2c3 都计算范围 8..8;和
  • c3c4 都计算范围 11..1111..1212..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 &lt; i 时返回零。

【讨论】:

    【解决方案3】:

    对于大数组 (n=1000),代码非常慢..

    • n = 1000 运行时间超过 20 秒,我有一个 i7 8750 CPU
    • n == 10^5 .. 运行时间超过 10 分钟
    • 我有 9 到 10 秒的限制 .. 对于大型阵列 ..

    使用位运算符的解决方案:n=10^4 in 83s

    这是我的最大努力,带有测试和基准。这还不够快,但我认为这里有两种很好的技术可供您使用,特别是:

    1. 使用位移生成“子数组”(sequential_sub_arrays 中的&lt;&lt;
    2. 通过位掩码执行“省略”(omit? 中的&amp;

    这些不是我经常使用或很好用的技术,所以我只是将它们作为其他人玩的垫脚石。

    为了实现您的目标(“9 到 10 秒......对于大型数组”),您可能需要采用这些技术并用更快的语言(如 C 或 rust)实现它们。

    # 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)
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2019-02-09
      • 2020-03-27
      • 2019-10-22
      • 1970-01-01
      • 2013-09-25
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多