Optimizing Multiplication Table
You only need to compute half the table because it is symmetrical; for your line

a1[1..-1].each do |y|


instead do 

a1[x..-1].each do |y|


and if you ever need to look up a * b where b &gt; a, instead look up b * a

This will create a multiplication table that looks like this:

# old table
  1  2  3  4  5
1 1  2  3  4  5
2 2  4  6  8 10
3 3  6  9 12 15
4 4  8 12 16 20
5 5 10 15 20 25

# new table
  1 2 3  4  5
1 1 2 3  4  5
2   4 6  8 10
3     9 12 15
4       16 20
5          25

# final multiplication table
arr = [
  [1,2,3,4,5],
  [4,6,8,10],
  [9,12,15],
  [16,20],
  [25]
]


To access a*b where a >= b, the answer is in arr[a-1][b-a]
To access 3*4, the answer is in row 3-1, element 4-3 = arr[3-1][4-3] = arr[2][1] = 12
To access 5*2 (= 2*5), the answer is in row 2-1, element 5-2 = arr[2-1][5-2] = arr[1][3] = 10
To access 100x100 (assuming the range goes high enough), arr[99][0]

You'll need to do something slightly different if you are passing an array that isn't 1..x - in that case, first look up the index of each of the two numbers in the original array and use THEM in the arr[a-1][b-a] lookup.  I suspect you'll need to subtract 1 from the indices but I haven't worked it out in my head.