1
votes

Algorithm:

A sequence of numbers is called a wiggle sequence if the differences between successive numbers strictly alternate between positive and negative. The first difference (if one exists) may be either positive or negative. A sequence with fewer than two elements is trivially a wiggle sequence.

For example, [1,7,4,9,2,5] is a wiggle sequence because the differences (6,-3,5,-7,3) are alternately positive and negative. In contrast, [1,4,7,2,5] and [1,7,4,5,5] are not wiggle sequences, the first because its first two differences are positive and the second because its last difference is zero.

Given a sequence of integers, return the length of the longest subsequence that is a wiggle sequence. A subsequence is obtained by deleting some number of elements (eventually, also zero) from the original sequence, leaving the remaining elements in their original order.

Examples:

Input: [1,7,4,9,2,5]
Output: 6
The entire sequence is a wiggle sequence.

Input: [1,17,5,10,13,15,10,5,16,8]
Output: 7
There are several subsequences that achieve this length. One is [1,17,10,13,10,16,8].

Input: [1,2,3,4,5,6,7,8,9]
Output: 2

My soln:

def wiggle_max_length(nums)
    [   build_seq(nums, 0, 0, true, -1.0/0.0), 
        build_seq(nums, 0, 0, false, 1.0/0.0)
    ].max
end

def build_seq(nums, index, len, wiggle_up, prev)
    return len if index >= nums.length 
    if wiggle_up && nums[index] - prev > 0 || !wiggle_up && nums[index] - prev < 0
        build_seq(nums, index + 1, len + 1, !wiggle_up, nums[index])
    else
        build_seq(nums, index + 1, len, wiggle_up, prev)
    end
end

This is working for smaller inputs (e.g [1,1,1,3,2,4,1,6,3,10,8] and for all the sample inputs, but its failing for very large inputs (which is harder to debug) like:

[33,53,12,64,50,41,45,21,97,35,47,92,39,0,93,55,40,46,69,42,6,95,51,68,72,9,32,84,34,64,6,2,26,98,3,43,30,60,3,68,82,9,97,19,27,98,99,4,30,96,37,9,78,43,64,4,65,30,84,90,87,64,18,50,60,1,40,32,48,50,76,100,57,29,63,53,46,57,93,98,42,80,82,9,41,55,69,84,82,79,30,79,18,97,67,23,52,38,74,15]

which should have output: 67 but my soln outputs 57. Does anyone know what is wrong here?

4
what is the expected time complexity ? - marvel308
Not sure but not my concern atm -- right now I'm looking for accuracy - segue_segway

4 Answers

3
votes

The approach tried is a greedy solution (because it always uses the current element if it satisfies the wiggle condition), but this does not always work. I will try illustrating this with this simpler counter-example: 1 100 99 6 7 4 5 2 3.

One best sub-sequence is: 1 100 6 7 4 5 2 3, but the two build_seq calls from the algorithm will produce these sequences:

  • 1 100 99
  • 1

Edit: A slightly modified greedy approach does work -- see this link, thanks Peter de Rivaz.

1
votes

Dynamic Programming can be used to obtain an optimal solution.

Note: I wrote this before seeing the article mentioned by @PeterdeRivaz. While dynamic programming (O(n2)) works, the article presents a superior (O(n)) "greedy" algorithm ("Approach #5"), which is also far easier to code than a dynamic programming solution. I have added a second answer that implements that method.

Code

def longest_wiggle(arr)
  best = [{ pos_diff: { length: 0, prev_ndx: nil },
            neg_diff: { length: 0, prev_ndx: nil } }]
  (1..arr.size-1).each do |i|
    calc_best(arr, i, :pos_diff, best)
    calc_best(arr, i, :neg_diff, best)
  end
  unpack_best(best)
end

def calc_best(arr, i, diff, best)
  curr = arr[i]
  prev_indices = (0..i-1).select { |j|
    (diff==:pos_diff) ? (arr[j] < curr) : (arr[j] > curr) }
  best[i] = {} if best.size == i
  best[i][diff] =
    if prev_indices.empty?
      { length: 0, prev_ndx: nil }
    else
      prev_diff = previous_diff(diff)
      j = prev_indices.max_by { |j| best[j][prev_diff][:length] }
      { length: (1 + best[j][prev_diff][:length]), prev_ndx: j }
    end
end

def previous_diff(diff)
  diff==:pos_diff ? :neg_diff : :pos_diff·
end

def unpack_best(best)
  last_idx, last_diff =
    best.size.times.to_a.product([:pos_diff, :neg_diff]).
         max_by { |i,diff| best[i][diff][:length] }
  return [0, []] if best[last_idx][last_diff][:length].zero?
  best_path = []
  loop do
    best_path.unshift(last_idx)
    prev_index = best[last_idx][last_diff][:prev_ndx]
    break if prev_index.nil?
    last_idx = prev_index·
    last_diff = previous_diff(last_diff)
  end
  best_path
end

Examples

longest_wiggle([1, 4, 2, 6, 8, 3, 2, 5])
  #=> [0, 1, 2, 3, 5, 7]]

The length of the longest wiggle is 6 and consists of the elements at indices 0, 1, 2, 3, 5 and 7, that is, [1, 4, 2, 6, 3, 5].

A second example uses the larger array given in the question.

arr = [33, 53, 12, 64, 50, 41, 45, 21, 97, 35, 47, 92, 39, 0, 93, 55, 40, 46,
       69, 42, 6, 95, 51, 68, 72, 9, 32, 84, 34, 64, 6, 2, 26, 98, 3, 43, 30,
       60, 3, 68, 82, 9, 97, 19, 27, 98, 99, 4, 30, 96, 37, 9, 78, 43, 64, 4,
       65, 30, 84, 90, 87, 64, 18, 50, 60, 1, 40, 32, 48, 50, 76, 100, 57, 29,
arr.size           63, 53, 46, 57, 93, 98, 42, 80, 82, 9, 41, 55, 69, 84, 82, 79, 30, 79,
       18, 97, 67, 23, 52, 38, 74, 15]
  #=> 100
longest_wiggle(arr).size
  #=> 67
longest_wiggle(arr)
  #=> [0, 1, 2, 3, 5, 6, 7, 8, 9, 10, 12, 14, 16, 17, 19, 21, 22, 23, 25,
  #    27, 28, 29, 30, 32, 34,  35, 36, 37, 38, 39, 41, 42, 43, 44, 47, 49, 50,
  #    52, 53, 54, 55, 56, 57, 58, 62, 63, 65, 66, 67,   70, 72, 74, 75, 77, 80,
  #    81, 83, 84, 90, 91, 92, 93, 95, 96, 97, 98, 99]

As indicated, the largest wiggle is comprised of 67 elements of arr. Solution time was essentially instantaneous.

The values of arr at those indices are as follows.

[33, 53, 12, 64, 41, 45, 21, 97, 35, 47, 39, 93, 40, 46, 42, 95, 51, 68, 9,
 84, 34, 64, 6, 26, 3, 43, 30, 60, 3, 68, 9, 97, 19, 27, 4, 96, 37, 78, 43,
 64, 4, 65, 30, 84, 18, 50, 1, 40, 32, 76, 57, 63, 53, 57, 42, 80, 9, 41, 30,
 79, 18, 97, 23, 52, 38, 74, 15]

[33, 53, 12, 64, 41, 45, 21, 97, 35, 92, 0, 93, 40, 69, 6, 95, 51, 72, 9, 84, 34, 64, 2, 98, 3, 43, 30, 60, 3, 82, 9, 97, 19, 99, 4, 96, 9, 78, 43, 64, 4, 65, 30, 90, 18, 60, 1, 40, 32, 100, 29, 63, 46, 98, 42, 82, 9, 84, 30, 79, 18, 97, 23, 52, 38, 74]

Explanation

I had intended to provide an explanation of the algorithm and its implementation, but having since learned there is a superior approach (see my note at the beginning of my answer), I have decided against doing that, but would of course be happy to answer any questions. The link in my note explains, among other things, how dynamic programming can be used here.

0
votes

Let Wp[i] be the longest wiggle sequence starting at element i, and where the first difference is positive. Let Wn[i] be the same, but where the first difference is negative.

Then:

Wp[k] = max(1+Wn[k'] for k<k'<n, where A[k'] > A[k]) (or 1 if no such k' exists)
Wn[k] = max(1+Wp[k'] for k<k'<n, where A[k'] < A[k]) (or 1 if no such k' exists)

This gives an O(n^2) dynamic programming solution, here in pseudocode

Wp = [1, 1, ..., 1] -- length n
Wn = [1, 1, ..., 1] -- length n
for k = n-1, n-2, ..., 0
   for k' = k+1, k+2, ..., n-1
       if A[k'] > A[k]
           Wp[k] = max(Wp[k], Wn[k']+1)
       else if A[k'] < A[k]
           Wn[k] = max(Wn[k], Wp[k']+1)
result = max(max(Wp[i], Wn[i]) for i = 0, 1, ..., n-1)
0
votes

In a comment on @quertyman's answer, @PeterdeRivaz provided a link to an article that considers various approaches to solving the "longest wiggle subsequence" problem. I have implemented "Approach #5", which has a time-complexity of O(n).

The algorithm is simple as well as fast. The first step is to remove one element from each pair of consecutive elements that are equal, and continue to do so until there are no consecutive elements that are equal. For example, [1,2,2,2,3,4,4] would be converted to [1,2,3,4]. The longest wiggle subsequence includes the first and last elements of the resulting array, a, and every element a[i], 0 < i < a.size-1 for which a[i-1] < a[i] > a[i+1] ora[i-1] > a[i] > a[i+1]. In other words, it includes the first and last elements and all peaks and valley bottoms. Those elements are A, D, E, G, H, I in the graph below (taken from the above-referenced article, with permission).

enter image description here

Code

def longest_wiggle(arr)
  arr.each_cons(2).
      reject { |a,b| a==b }.
      map(&:first).
      push(arr.last).
      each_cons(3).
      select { |triple| [triple.min, triple.max].include? triple[1] }.
      map { |_,n,_| n }.
      unshift(arr.first).
      push(arr.last)
end

Example

arr = [33, 53, 12, 64, 50, 41, 45, 21, 97, 35, 47, 92, 39, 0, 93, 55, 40,
       46, 69, 42, 6, 95, 51, 68, 72, 9, 32, 84, 34, 64, 6, 2, 26, 98, 3,
       43, 30, 60, 3, 68, 82, 9, 97, 19, 27, 98, 99, 4, 30, 96, 37, 9, 78,
       43, 64, 4, 65, 30, 84, 90, 87, 64, 18, 50, 60, 1, 40, 32, 48, 50, 76,
       100, 57, 29, 63, 53, 46, 57, 93, 98, 42, 80, 82, 9, 41, 55, 69, 84,
       82, 79, 30, 79, 18, 97, 67, 23, 52, 38, 74, 15]    

a = longest_wiggle(arr)
  #=> [33, 53, 12, 64, 41, 45, 21, 97, 35, 92, 0, 93, 40, 69, 6, 95, 51, 72,
  #    9, 84, 34, 64, 2, 98, 3, 43, 30, 60, 3, 82, 9, 97, 19, 99, 4, 96, 9,
  #    78, 43, 64, 4, 65, 30, 90, 18, 60, 1, 40, 32, 100, 29, 63, 46, 98, 42,
  #    82, 9, 84, 30, 79, 18, 97, 23, 52, 38, 74, 15]
a.size
  #=> 67

Explanation

The steps are as follows.

arr = [3, 4, 4, 5, 2, 3, 7, 4]

enum1 = arr.each_cons(2)
  #=> #<Enumerator: [3, 4, 4, 5, 2, 3, 7, 4]:each_cons(2)>

We can see the elements that will be generated by this enumerator by converting it to an array.

enum1.to_a
  #=> [[3, 4], [4, 4], [4, 5], [5, 2], [2, 3], [3, 7], [7, 4]]

Continuing, remove all but one of each group of successive equal elements.

d = enum1.reject { |a,b| a==b }
  #=> [[3, 4], [4, 5], [5, 2], [2, 3], [3, 7], [7, 4]]
e = d.map(&:first)
  #=> [3, 4, 5, 2, 3, 7]

Add the last element.

f = e.push(arr.last)
  #=> [3, 4, 5, 2, 3, 7, 4]

Next, find the peaks and valley bottoms.

enum2 = f.each_cons(3)
  #=> #<Enumerator: [3, 4, 5, 2, 3, 7, 4]:each_cons(3)>
enum2.to_a
  #=> [[3, 4, 5], [4, 5, 2], [5, 2, 3], [2, 3, 7], [3, 7, 4]]
g = enum2.select { |triple| [triple.min, triple.max].include? triple[1] }
  #=> [[4, 5, 2], [5, 2, 3], [3, 7, 4]]
h = g.map { |_,n,_| n }
  #=> [5, 2, 7]

Lastly, add the first and last values of arr.

i = h.unshift(arr.first)
  #=> [3, 5, 2, 7]
i.push(arr.last)
  #=> [3, 5, 2, 7, 4]