Is there an efficient way to remove subsets from an array of sets
E.g. array of arrays
[[2, 3, 4, 7, 8, 9, 10], [1, 5, 6], [3, 7, 10], [4, 8, 9], [5, 6], [7, 10], [8, 9], [6], [9]]
to output an array
[[2, 3, 4, 7, 8, 9, 10], [1, 5, 6]]
The key is guaranteeing the source sets are sorted in descending order of size. That way all supersets precede their subsets.
Here’s a generic function to do it. You could adapt it to take any kind of sequence of sequence of hashable and convert them to an array of sets on the way in:
func removeSubsets<T: Hashable>(source: [Set<T>]) -> [Set<T>] {
let sets = source.sorted { $0.count > $1.count }
var supersets: [Set<T>] = []
for set in sets {
if !contains(supersets, { set.isSubsetOf($0) }) {
supersets.append(set)
}
}
return supersets
}
removeSubsets([[2, 3, 4, 7, 8, 9, 10], [1, 5, 6], [3, 7, 10], [4, 8, 9], [5, 6], [7, 10], [8, 9], [6], [9]])
// returns [{10, 2, 9, 4, 7, 3, 8}, {5, 6, 1}]
It's still cubic unfortunately since contains is linear and so is isSubsetOf.
EDIT: here's the fully generic version:
func removeSubsets
<S0: SequenceType, S1: SequenceType
where S0.Generator.Element == S1,
S1.Generator.Element: Hashable>
(source: S0) -> [Set<S1.Generator.Element>]
{
let sets = map(source) { Set($0) }.sorted { $0.count > $1.count }
var supersets: [Set<S1.Generator.Element>] = []
for set in sets {
if !contains(supersets, { set.isSubsetOf($0) }) {
supersets.append(set)
}
}
return supersets
}
let a: [[Int]] = [
[2, 3, 4, 7, 8, 9, 10],
[1, 5, 6], [3, 7, 10],
[4, 8, 9], [5, 6],
[7, 10], [8, 9],
[6], [9]]
removeSubsets(a) // returns [{10, 2, 9, 4, 7, 3, 8}, {5, 6, 1}]
EDIT2: if you want the result to be an array of the original arrays (since converting them to sets loses their ordering), you could make the following change, which takes more space but is also slightly more efficient since it only converts the supersets to sets, not the subsets:
func removeSubsets<T: Hashable>(source: [[T]]) -> [[T]] {
// note, this is quite efficient since arrays are copy-on-write,
// so it is only really creating a new array of pointers
let sets = source.sorted { $0.count > $1.count }
var supersets: [Set<T>] = []
var result: [[T]] = []
for set in sets {
if !contains(supersets, { $0.isSupersetOf(set) }) {
supersets.append(Set(set))
result.append(set)
}
}
return result
}
removeSubsets([[2, 3, 4, 7, 8, 9, 10], [1, 5, 6], [3, 7, 10], [4, 8, 9], [5, 6], [7, 10], [8, 9], [6], [9]])
// returns [[2, 3, 4, 7, 8, 9, 10], [1, 5, 6]]
EDIT3: if you want to keep the original order of the sets (just with the subsets removed), you could tag them with a number on the way in before sorting, then re-sort them using it and strip it off the result at the end:
func removeSubsets<T: Hashable>(source: [[T]]) -> [[T]] {
let sets = sorted(enumerate(source)) { $0.1.count > $1.1.count }
var supersets: [Set<T>] = []
var result: [(Int,[T])] = []
for (n,set) in sets {
if !contains(supersets, { $0.isSupersetOf(set) }) {
supersets.append(Set(set))
result.append(n,set)
}
}
return result.sorted { $0.0 < $1.0 }.map { $1 }
}
// note, input not sorted in order of length
removeSubsets([[1, 5, 6], [2, 3, 4, 7, 8, 9, 10], [3, 7, 10], [4, 8, 9], [5, 6], [7, 10], [8, 9], [6], [9]])
// returns [[1, 5, 6], [2, 3, 4, 7, 8, 9, 10]]
Just like with any other (non-2D/set) array you could use an array extension like this ...
extension Array
{
func slice(indices:Int...) -> Array
{
var s = indices[0];
var e = self.count - 1;
if (indices.count > 1)
{
e = indices[1];
}
if (e < 0)
{
e += self.count;
}
if (s < 0)
{
s += self.count;
}
let count = (s < e ? e - s : s - e) + 1;
let inc = s < e ? 1 : -1;
var result = Array();
var idx = s;
for i in 0 ..< count
{
result.append(self[idx]);
idx += inc;
}
return result;
}
}
Usage:
let a = [[2, 3, 4, 7, 8, 9, 10], [1, 5, 6], [3, 7, 10], [4, 8, 9], [5, 6], [7, 10], [8, 9], [6], [9]];
let b = a.slice(0, 1);
let c = a.slice(3);
If your array does not contain duplicated int values, you can convert to Set to use some feature from Swift:
(Take a look at Performing Set Operations)
https://developer.apple.com/library/prerelease/ios/documentation/Swift/Conceptual/Swift_Programming_Language/CollectionTypes.html
Here is my code to get another Array which does not contain subsets. This method is not optimized, it works however.
//let arrayOfArray = [[2, 3, 4, 7, 8, 9, 10], [1, 5, 6], [3, 7, 10], [4, 8, 9], [5, 6], [7, 10], [8, 9], [6], [9]]
//use set instead
var setArray : [Set<Int>] = [[2, 3, 4, 7, 8, 9, 10], [1, 5, 6], [3, 7, 10], [4, 8, 9], [5, 6], [7, 10], [8, 9], [6], [9]]
setArray.sort({$0.count > $1.count}) //sort to have ordered array (biggest set at first)
var result = [Set<Int>]() //you will get your result in this variable.
for _aSet in setArray {
var isSubSet = false
for _exitSet in result {
if _aSet.isSubsetOf(_exitSet) {
isSubSet = true
break;
}
}
if (!isSubSet) {
result.append(_aSet)
}
}
This is the most efficient way I could think of:
let nArrays = [[2, 3, 4, 7, 8, 9, 10], [1, 5, 6], [3, 7, 10], [4, 8, 9], [5, 6], [7, 10], [8, 9], [6], [9]]
nArrays
.reduce([Set<Int>]()) {
accu, el in let setEl = Set(el)
return contains(accu) {setEl.isSubsetOf($0)} ? accu : accu + [setEl]
}
//[{10, 2, 9, 4, 7, 3, 8}, {5, 6, 1}]
Instead of checking if every array is a subset of every other array, you just check if they're a subset of the already checked arrays. Of course, that returns an array of Sets, not an array of Arrays, but you can map() over it to convert it back:
let nArrays = [[2, 3, 4, 7, 8, 9, 10], [1, 5, 6], [3, 7, 10], [4, 8, 9], [5, 6], [7, 10], [8, 9], [6], [9]]
nArrays
.reduce([Set<Int>]()) {
accu, el in let setEl = Set(el)
return contains(accu) {setEl.isSubsetOf($0)} ? accu : accu + [setEl]
}
.map{Array($0)}
//[[10, 2, 9, 4, 7, 3, 8], [5, 6, 1]]
You could do this:
let arrayOfArray = [[2, 3, 4, 7, 8, 9, 10], [1, 5, 6], [3, 7, 10], [4, 8, 9], [5, 6], [7, 10], [8, 9], [6], [9]]
let output = arrayOfArray[0...1]
Related
From:
arr1 = np.array([ [1, 2, 3], [4, 5, 6], [7, 8, 9] ])
To:
arr1 = np.array([ [0, 1, 2, 3], [0, 4, 5, 6], [0, 7, 8, 9] ])
You can try something like this with numpy.full:
x = 0
new = np.full((arr1.shape[0], arr1.shape[1] + 1), x)
new[:, 1:] = arr1
Output
new
array([[0, 1, 2, 3],
[0, 4, 5, 6],
[0, 7, 8, 9]])
Note that you can assign any value to x.
Your (3,3) 2d array:
In [100]: arr1 = np.array([ [1, 2, 3], [4, 5, 6], [7, 8, 9] ])
In [101]: arr1
Out[101]:
array([[1, 2, 3],
[4, 5, 6],
[7, 8, 9]])
A new (3,4) array:
In [102]: np.concatenate((np.zeros((3,1),int),arr1), axis=1)
Out[102]:
array([[0, 1, 2, 3],
[0, 4, 5, 6],
[0, 7, 8, 9]])
Any other (3,1) array (or even a (3,n)) could be added "at the start" like this.
Is there any method to split an array like this?
[1, 2, 3, 4, 5, 6, 7, 8, 9].split(3, 4, 2)
#=> [[1, 2, 3],[4, 5, 6, 7],[8, 9]]
Immutable version with λ:
▶ splitter = ->(array, *parts) do
parts.reduce([[], 0]) do |acc, i|
right = acc.last + i
[acc.first << (acc.last...right), right]
end.first.map { |r| array[r] }
end
#⇒ #<Proc:0x0055ae3d9ae7c8#(pry):18 (lambda)>
▶ splitter.((1..9).to_a, 3, 4, 2)
#⇒ [[1, 2, 3], [4, 5, 6, 7], [8, 9]]
No, there is none, but you can easily write one yourself.
class Array
def in_groups_of_n(*sizes)
sizes.map(&method(:shift))
end
end
Example:
arr = [1, 2, 3, 4, 5, 6, 7, 8, 9]
arr.in_groups_of_n(3, 4, 2)
# => [[1, 2, 3], [4, 5, 6, 7], [8, 9]]
Demonstration
In case you want a none-destructive version, you can use a dup method:
class Array
def in_groups_of_n(*sizes)
duplicate = dup
sizes.map { |size| duplicate.shift(size) }
end
end
arr = [1, 2, 3, 4, 5, 6, 7, 8, 9]
arr.in_groups_of_n(3,4,2)
# => [[1, 2, 3], [4, 5, 6, 7], [8, 9]
arr
# => [1, 2, 3, 4, 5, 6, 7, 8, 9]
Demonstration
Here's a naive Array implementation:
class Array
def multi_split(*sizes)
r = []
e = self.each
sizes.each do |size|
t = []
size.times do
t << e.next
end
r << t
end
r
end
end
p [1, 2, 3, 4, 5, 6, 7, 8, 9].multi_split(3, 4, 2)
# [[1, 2, 3], [4, 5, 6, 7], [8, 9]]
#Stefan mentioned it might make sense to implement it on Enumerable:
module Enumerable
def multi_split(*sizes)
Enumerator.new do |yielder|
e = self.each
sizes.each do |size|
yielder << Array.new(size){ e.next }
end
end
end
end
p [1, 2, 3, 4, 5, 6, 7, 8, 9].multi_split(3, 4, 2).to_a
# [[1, 2, 3], [4, 5, 6, 7], [8, 9]]
Another option (lossless in event the splits are not equal to the array size
def split_at(arr,splits)
rest = arr.last(arr.size - splits.reduce(:+))
enum = arr.to_enum
splits.map do |n|
n.times.map { enum.next }
end.concat(rest.empty? ? [] : [rest])
end
Then called as
split_at (1..9), [3,4,2]
#=> [[1, 2, 3], [4, 5, 6, 7], [8, 9]]
split_at (1..22), [3,4,2]
#=> [[1, 2, 3], [4, 5, 6, 7], [8, 9], [10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22]]
Example
class Array
def split_by_number(*sizes)
sizes.each_with_object([]) { |n,a| a << [a.empty? ? 0 : a.last.sum, n] }.
map { |start, nbr| self[start, nbr] }
end
end
[1, 2, 3, 4, 5, 6, 7, 8, 9].split_by_number 3, 4, 2
#=> [[1, 2, 3], [4, 5, 6, 7], [8, 9]]
Note that
[3, 4, 2].each_with_object([]) { |n,a| a << [a.empty? ? 0 : a.last.sum, n] }
#=> [[0, 3], [3, 4], [7, 2]]
I would like to find all the permutations of plucking 3, 4 or 5 numbers from [2,3,4,5,6,7,8], repeats allowed, such that their sum is 16. So [8,5,3], [8,3,5] and [4,3,3,3,3] are valid permutations. Also circular permutations should be removed so [3,3,3,3,4] wouldn't also be added to the answer.
I can do this in Ruby without allowing repeats like this:
d = [2,3,4,5,6,7,8]
number_of_divisions = [3,4,5]
number_of_divisions.collect do |n|
d.permutation(n).to_a.reject do |p|
p[0..n].inject(0) { |sum,x| sum + x } != 16
end
end
How could I allow repeats so that [3,3,3,3,4] was included?
For all permutations, including duplicates, one might use Array#repeated_permutation:
d = [2,3,4,5,6,7,8]
number_of_divisions = [3,4,5]
number_of_divisions.flat_map do |n|
d.repeated_permutation(n).reject do |p| # no need `to_a`
p.inject(:+) != 16
end
end
or, even better with Array#repeated_combination:
number_of_divisions.flat_map do |n|
d.repeated_combination(n).reject do |p| # no need `to_a`
p.inject(:+) != 16
end
end
There are far fewer repeated combinations than repeated permutations, so let's find the repeated combinations that sum to the given value, then permute each of those. Moreover, by applying uniq at each of several steps of the calculation we can significantly reduce the number of repeated combinations and permutations considered.
Code
require 'set'
def rep_perms_for_all(arr, n_arr, tot)
n_arr.flat_map { |n| rep_perms_for_1(arr, n, tot) }
end
def rep_perms_for_1(arr, n, tot)
rep_combs_to_rep_perms(rep_combs_for_1(arr, n, tot)).uniq
end
def rep_combs_for_1(arr, n, tot)
arr.repeated_combination(n).uniq.select { |c| c.sum == tot }
end
def rep_combs_to_rep_perms(combs)
combs.flat_map { |c| comb_to_perms(c) }.uniq
end
def comb_to_perms(comb)
comb.permutation(comb.size).uniq.uniq do |p|
p.size.times.with_object(Set.new) { |i,s| s << p.rotate(i) }
end
end
Examples
rep_perms_for_all([2,3,4,5], [3], 12)
#=> [[2, 5, 5], [3, 4, 5], [3, 5, 4], [4, 4, 4]]
rep_perms_for_all([2,3,4,5,6,7,8], [3,4,5], 16).size
#=> 93
rep_perms_for_all([2,3,4,5,6,7,8], [3,4,5], 16)
#=> [[2, 6, 8], [2, 8, 6], [2, 7, 7], [3, 5, 8], [3, 8, 5], [3, 6, 7],
# [3, 7, 6], [4, 4, 8], [4, 5, 7], [4, 7, 5], [4, 6, 6], [5, 5, 6],
# [2, 2, 4, 8], [2, 2, 8, 4], [2, 4, 2, 8], [2, 2, 5, 7], [2, 2, 7, 5],
# [2, 5, 2, 7], [2, 2, 6, 6], [2, 6, 2, 6], [2, 3, 3, 8], [2, 3, 8, 3],
# ...
# [3, 3, 3, 7], [3, 3, 4, 6], [3, 3, 6, 4], [3, 4, 3, 6], [3, 3, 5, 5],
# [3, 5, 3, 5], [3, 4, 4, 5], [3, 4, 5, 4], [3, 5, 4, 4], [4, 4, 4, 4],
# ...
# [2, 2, 4, 5, 3], [2, 2, 5, 3, 4], [2, 2, 5, 4, 3], [2, 3, 2, 4, 5],
# [2, 3, 2, 5, 4], [2, 3, 4, 2, 5], [2, 3, 5, 2, 4], [2, 4, 2, 5, 3],
# ...
# [2, 5, 3, 3, 3], [2, 3, 3, 4, 4], [2, 3, 4, 3, 4], [2, 3, 4, 4, 3],
# [2, 4, 3, 3, 4], [2, 4, 3, 4, 3], [2, 4, 4, 3, 3], [3, 3, 3, 3, 4]]
Explanation
rep_combs_for_1 uses the method Enumerable#sum, which made its debut in Ruby v2.4. For earlier versions, use c.reduce(:0) == tot.
In comb_to_perms, the first uniq simply removes duplicates. The second uniq, with a block, removes all but one of the p.size elements (arrays) that can be obtained by rotating any of the other p-1 elements. For example,
p = [1,2,3]
p.size.times.with_object(Set.new) { |i,s| s << p.rotate(i) }
#=> #<Set: {[1, 2, 3], [2, 3, 1], [3, 1, 2]}>
I have a program for finding peaks (local maxima) in ruby that passes all but one test. Personally, I think my program is ok, but maybe I'm not taking into account an assumed neighborhood size that isn't specified in the problem, but was suggested by someone else who also attempted it.
Here's what I have so far.
def pick_peaks(arr)
pos = []
peaks =[]
peak_set = {pos: [], peaks: []}
for i in 1..arr.length-2
if arr[i-1] < arr[i] && arr[i] >= arr[i+1]
unless edge_plateau?(arr, i)
peak_set[:pos] << i
peak_set[:peaks] << arr[i]
end
end
end
peak_set_alt = peak_set.collect{|k,v| [k.to_s, v]}.to_h
peak_set_alt
end
def edge_plateau?(array, position)
edge_plateau_left = true
edge_plateau_right = true
i = 1
until i == position
edge_plateau_left = false if array[0] != array[i]
i += 1
end
i = array.length-2
until i == position
edge_plateau_right = false if array[i] != array.last
i -= 1
end
edge_plateau_left or edge_plateau_right
end
Here's the test that it needs to pass but I don't know the original array, so that's a bit of a challenge.
Expected: {"pos"=>[2, 7, 14, 20], "peaks"=>[5, 6, 5, 5]}, instead got: {"pos"=>[2, 7, 11, 14, 20], "peaks"=>[5, 6, 3, 5, 5]}
I'm getting an extra peak in the middle but that should be ok if it's a local maxima, right?
UPDATE
Thanks to a suggestion I found the test array
[1, 2, 5, 4, 3, 2, 3, 6, 4, 1, 2, 3, 3, 4, 5, 3, 2, 1, 2, 3, 5, 5, 4, 3]
This is a more Ruby-like way to find the local maxima.
Code
def locale_maxima(arr)
last_idx = arr.size - 1
peaks, pos =
([[-Float::INFINITY, nil]] +
arr.each_with_index.reject { |v,i| i < last_idx && v == arr[i+1] } +
[[-Float::INFINITY, nil]]
).each_cons(3).
select { |(n1,_), (n2,_), (n3,_)| n1 < n2 && n2 > n3 }.
map { |_,max_pair,_| max_pair }.
transpose
{ pos: pos, peaks: peaks }
end
Example
arr = [1, 2, 5, 4, 3, 2, 3, 6, 4, 1, 2, 3, 3, 4, 5, 3, 2, 1, 2, 3, 5, 5, 4, 3]
locale_maxima arr
#=> { :pos =>[2, 7, 14, 21],
# :peaks=>[5, 6, 5, 5] }
Explanation
The steps are as follows.
last_idx = arr.size - 1
#=> 23
Where there are consecutive equal values, which may represent inflection points (a complication), remove all but the last. To report the indices of the local maxima we therefore need to save indices before removing the duplicates.
b = arr.each_with_index.reject { |v,i| i < last_idx && v == arr[i+1] }
#=> [[1, 0], [2, 1], [5, 2], [4, 3], [3, 4], [2, 5], [3, 6], [6, 7],
# [4, 8], [1, 9], [2, 10], [3, 12], [4, 13], [5, 14], [3, 15],
# [2, 16], [1, 17], [2, 18], [3, 19], [5, 21], [4, 22], [3, 23]]
Notice that [3,11] and [5, 20] have been removed.
Tack on pairs at the beginning and end that cannot be local maxima (nil is arbitrary).
c = [[-Float::INFINITY, nil]] + b + [[-Float::INFINITY, nil]]
#=> [[-Infinity, nil], [1, 0], [2, 1], [5, 2], [4, 3], [3, 4], [2, 5], [3, 6],
# [6, 7], [4, 8], [1, 9], [2, 10], [3, 12], [4, 13], [5, 14], [3, 15], [2, 16],
# [1, 17], [2, 18], [3, 19], [5, 21], [4, 22], [3, 23], [-Infinity, nil]]
Use Enumerable#each_cons to produce an enumerator that will be used to identify the local maxima.
d = c.each_cons(3)
#=> #<Enumerator:
# [[-Infinity, nil], [1, 0], [2, 1], [5, 2], [4, 3], [3, 4], [2, 5],
# [3, 6], [6, 7], [4, 8], [1, 9], [2, 10], [3, 12], [4, 13], [5, 14],
# [3, 15], [2, 16], [1, 17], [2, 18], [3, 19], [5, 21], [4, 22],
# [3, 23], [-Infinity, nil]]:each_cons(3)>
e = d.select { |(n1,_), (n2,_), (n3,_)| n1 < n2 && n2 > n3 }
#=> [[[2, 1], [5, 2], [4, 3]],
# [[3, 6], [6, 7], [4, 8]],
# [[4, 13], [5, 14], [3, 15]],
# [[3, 19], [5, 21], [4, 22]]]
f = e.map { |_,max_pair,_| max_pair }
#=> [[5, 2], [6, 7], [5, 14], [5, 21]]
peaks, pos = f.transpose
#=> [[5, 6, 5, 5], [2, 7, 14, 21]]
{ pos: pos, peaks: peaks }
#=> {:pos=>[2, 7, 14, 21], :peaks=>[5, 6, 5, 5]}
Well there are more than one bug in your code.
Try your code on following data
pick_peaks([0,1,10,1,2,2,3,1,10,1,0])
You'll get
{"pos"=>[2, 4, 6, 8], "peaks"=>[10, 2, 3, 10]}
Obviously 2 here is a bug. So the source of the bug here is arr[i] >= arr[i+1]
Also unless it is shomehow explicitly stated in the task, you seem to handle edges wrong. Consider
pick_peaks([0,0,10,1,2,2,3,1,10,0,0])
You'll get
{"pos"=>[4, 6], "peaks"=>[2, 3]}
missing both 10 on the left and on the right.
Without exact task it is hard to say definitely but at the first glance it seems that your algorithm is to complicated both in terms of code and Big-O. Why you don't just go once through the array and track whether your are climbing up, staying plain or going down?
Update
Here is a piece of code to illustrate my last suggestion. I'm not good at Ruby so my example code would be in JavaScript so you can run it in your browser console:
function pick_peaks(arr) {
var prevUp = true; //let the start be a peak
var peaks = [];
var lastUpInd = 0;
for(var i = 0; i < arr.length-1; i++) {
if (arr[i] < arr[i+1]) {
prevUp = true;
lastUpInd = i + 1;
}
else if (arr[i] > arr[i+1]) {
if(prevUp) {
for(var j = lastUpInd; j <= i; j++) {
peaks.push([j, arr[j]]);
}
}
prevUp = false;
}
}
// additionally handle the end to let it be a peak
if(prevUp)
{
for(var j = lastUpInd; j <= i; j++) {
peaks.push([j, arr[j]]);
}
}
return peaks;
}
If you don't want the ends to be possible peaks just init prevUp with false and remove last if with inner for after a comment
I have an array:
[1, 4, 4, 4, 2, 9, 0, 4, 3, 3, 3, 3, 4]
and want to replace the repeating values with a string "repeat". The repeated 4 at indices 1, 2, 3 and 3 at indices 8, 9, 10, 11 should be replaced. I should get:
[1, "repeat", 2, 9, 0, 4, "repeat", 4]
How is this accomplished?
Here are two ways you could do that.
#1 Use Enumerable#chunk:
arr = [1,4,4,4,2,9,0,4,3,3,3,3,4]
arr.chunk(&:itself).map { |f,a| a.size==1 ? f : "repeat" }
#=> [1, "repeat", 2, 9, 0, 4, "repeat", 4]
The steps:
enum = arr.chunk(&:itself)
#=> #<Enumerator: #<Enumerator::Generator:0x007febc99fb160>:each>
We can view the elements of this enumerator by converting it to an array:
enum.to_a
#=> [[1, [1]], [4, [4, 4, 4]], [2, [2]], [9, [9]], [0, [0]],
# [4, [4]], [3, [3, 3, 3, 3]], [4, [4]]]
Object#itself was added in Ruby v2.2. For earlier version you would use
enum = arr.chunk { |e| e }
It is now a simple matter to map the elements of enum as required:
enum.map { |f,a| a.size==1 ? f : "repeat" }
#=> [1, "repeat", 2, 9, 0, 4, "repeat", 4]
#2 Use Enumerable#slice_when
arr.slice_when { |e,f| e !=f }.map { |a| a.size==1 ? a.first : "repeat" }
The steps:
enum = arr.slice_when { |e,f| e !=f }
#=> #<Enumerator: #<Enumerator::Generator:0x007febc99b8cc0>:each>
a = enum.to_a
#=> [[1], [4, 4, 4], [2], [9], [0], [4], [3, 3, 3, 3], [4]]
a.map { |a| a.size==1 ? a.first : "repeat" }
#=> [1, "repeat", 2, 9, 0, 4, "repeat", 4]
slice_when was introduced in Ruby v.2.2.