17

Can anyone please help me understand the core logic behind the solution to a problem mentioned at http://www.topcoder.com/stat?c=problem_statement&pm=1259&rd=4493

A zig zag sequence is one that alternately increases and decreases. So, 1 3 2 is zig zag, but 1 2 3 is not. Any sequence of one or two elements is zig zag. We need to find the longest zig zag subsequence in a given sequence. Subsequence means that it is not necessary for elements to be contiguous, like in the longest increasing subsequence problem. So, 1 3 5 4 2 could have 1 5 4 as a zig zag subsequence. We are interested in the longest one.

I understand that this is a dynamic programming problem and it is very similar to How to determine the longest increasing subsequence using dynamic programming?.

I think any solution will need an outer loop that iterates over sequences of different lengths, and the inner loop will have to iterate over all sequences.

We will store the longest zig zag sequence ending at index i in another array, say dpStore at index i. So, intermediate results are stored, and can later be reused. This part is common to all Dynamic programming problems. Later we find the global maximum and return it.

My solution is definitely wrong, pasting here to show what I've so far. I want to know where I went wrong.

    private int isZigzag(int[] arr)
{
    int max=0;
    int maxLength=-100;
    int[] dpStore = new int[arr.length];

    dpStore[0]=1;

    if(arr.length==1)
    {
        return 1;
    }
    else if(arr.length==2)
    {
        return 2;
    }
    else 
    {           
        for(int i=3; i<arr.length;i++)
        {
            maxLength=-100;
            for(int j=1;j<i && j+1<=arr.length; j++)
            {
                if(( arr[j]>arr[j-1] && arr[j]>arr[j+1])
                    ||(arr[j]<arr[j-1] && arr[j]<arr[j+1]))
                {
                    maxLength = Math.max(dpStore[j]+1, maxLength);
                }
            }
            dpStore[i]=maxLength;               
        }
    }
    max=-1000;
    for(int i=0;i<arr.length;i++)
    {
        max=Math.max(dpStore[i],max);
    }
    return max; 
}
Community
  • 1
  • 1
Abhijeet Kashnia
  • 12,290
  • 8
  • 38
  • 50
  • Your first link requires registration to access. It would be much easier to answer if the problem description was embedded in your question. – hammar Aug 02 '11 at 16:05
  • I am sorry I didn't notice that... I'll put in a quick statement of the problem. – Abhijeet Kashnia Aug 02 '11 at 16:06
  • Do you understand how to solve the basic longest increasing subsequence (without the zigzag) as well? This is just a minor modification of that, using the same techniques to solve. – hugomg Aug 02 '11 at 16:24
  • In `1 3 5 4 2`, the entire sequence is `zig-zag`. You don't mention how equal numbers should be treated, but excluding equal numbers, aren't all sequences that are not increasing or decreasing (these have no zig-zag subsequences either, except the trivial 1 or 2 element ones). So, is `1 1 1` increasing or decreasing? – IVlad Aug 02 '11 at 16:27
  • Well, the problem you linked to is entirely different than what you're describing. Can you please decide on which one you need help with? – IVlad Aug 02 '11 at 16:36
  • The difference you mention is that I wanted the longest subsequence whereas the original problem wanted just its length. But I thought the problems would be closely related as it should involve some kind of back pointer or reference to the previous value which on traversal gives us the exact subsequence, this could be maintained just like the memoization of the length stored in the array. I might be wrong, this is why I did not focus too much on these two different problems. Thank you for helping, I'll try to find the exact sequence myself. I will mark your answer as soon as I understand it :) – Abhijeet Kashnia Aug 03 '11 at 05:47
  • Please feel free to comment if you think I understood the difference incorrectly. – Abhijeet Kashnia Aug 03 '11 at 05:49
  • @missingno Yes, I did that problem before attempting this. – Abhijeet Kashnia Aug 03 '11 at 05:54
  • @IVlad Regarding your previous comment that 1 3 5 4 2 is entirely sig zag, yes I realized that, I just wanted to make sure that the term "subsequence" was understood correctly. I uses the term "alternately increasing and decreasing". I think in general a sequence like 1 1 1 would be considered as non increasing. Maybe I should have said "strictly alternately increasing and decreasing". I hope there is no ambiguity now. – Abhijeet Kashnia Aug 03 '11 at 06:09
  • There is still ambiguity - `1 3 5 4 2` is zig zag according to YOUR description of the problem, but NOT according to the topcoder description. Do you see why? – IVlad Aug 03 '11 at 10:22
  • I think I got my example wrong. Yes, I see. Thanks a lot. 1 3 5 4 2 is not zig zag since 1 3 5 is increasing. – Abhijeet Kashnia Aug 03 '11 at 11:13
  • @AbhijeetKashnia can you please write a recursive relation for the answer of this question? – Mohsin Sep 25 '16 at 05:21

11 Answers11

50

This is what the problem you linked to says:

A sequence of numbers is called a zig-zag 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 zig-zag sequence.

For example, 1,7,4,9,2,5 is a zig-zag 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 zig-zag sequences, the first because its first two differences are positive and the second because its last difference is zero.

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

This is completely different from what you described in your post. The following solves the actual topcoder problem.

dp[i, 0] = maximum length subsequence ending at i such that the difference between the
           last two elements is positive
dp[i, 1] = same, but difference between the last two is negative

for i = 0 to n do     
   dp[i, 0] = dp[i, 1] = 1

   for j = 0 to to i - 1 do
    if a[i] - a[j] > 0
      dp[i, 0] = max(dp[j, 1] + 1, dp[i, 0])
    else if a[i] - a[j] < 0
      dp[i, 1] = max(dp[j, 0] + 1, dp[i, 1])
    

Example:

i        = 0  1   2  3   4   5   6   7  8   9
a        = 1  17  5  10  13  15  10  5  16  8 
dp[i, 0] = 1  2   2  4   4   4   4   2  6   6    
dp[i, 1] = 1  1   3  3   3   3   5   5  3   7
           ^  ^   ^  ^
           |  |   |  -- gives us the sequence {1, 17, 5, 10}
           |  |   -- dp[2, 1] = dp[1, 0] + 1 because 5 - 17 < 0.
           |  ---- dp[1, 0] = max(dp[0, 1] + 1, 1) = 2 because 17 - 1 > 0
     1 element
   nothing to do
 the subsequence giving 7 is 1, 17, 5, 10, 5, 16, 8, hope I didn't make any careless
 mistakes in computing the other values)

Then just take the max of both dp arrays.

Community
  • 1
  • 1
IVlad
  • 43,099
  • 13
  • 111
  • 179
28

This is a simpler solution

Let the original array A be of length n. Build another array B of length n-1 of only 0s and 1s. B[i] = 0 if a[i]-a[i+1] >=0 else B[i] = 1. This can be done in O(n). Now we have an array of only 0s and 1, now the problem is to find alternating continuous 0s and 1s. A continuous sub array array in B of 0s will be represented by any one of its elements. For example: If B is = [0,0,0,0,0, 1,0,0,0,1,0,1,1,1,0] then we can reduce B to Br which = [0,1,0,1,0,1,0] in O(n), in fact we just need to find the size of Br which can be done by just one iteration. And that my friend is the answer to the given problem. So total complexity is O(n) + O(n) = O(n). In other words: Keep the first element. Then find the monotone growing or shrinking parts of the sequence and keep the last element from all of these sequences.

UPDATE: You need to add one to the answer coming out of this process, because you are counting the zigzags, not the length of the list. Beware of the fence post problem: https://betterexplained.com/articles/learning-how-to-count-avoiding-the-fencepost-problem/

n0rm1e
  • 3,796
  • 2
  • 28
  • 37
surya
  • 991
  • 1
  • 12
  • 22
  • can you prove the optimality of this algorithm? – Olayinka Oct 06 '13 at 17:06
  • 1
    Olayinka: You cannot have an algorithm which is better than O(n) if it has to read each element of a length n array. – hivert Oct 09 '13 at 17:15
  • 4
    @surya your solution does not working for `1 17 5 10 13 15 10 5 16 8` here `b` become `[1 0 1 1 1 0 0 1 0]` and `br` will become `[1 0 1 0 1 0]` so your algo give `6` but answer is `7` – EmptyData Mar 27 '14 at 07:31
  • 1
    @EmptyData correct me if I'm wrong, but I think you need to add 1 to the length of `br`. After all, if the entire array is zigzag, `br` will only have `n-1` elements. Think about `a = [1,2]`, `b=br=[1]` case. That said, this algorithm seems to work for me. Any other counterexamples? – danqing Oct 29 '14 at 04:28
  • Updated the solution to clarify this. – n0rm1e Aug 20 '16 at 15:57
  • 1
    I think this is a much neater solution - https://cs.stackexchange.com/q/75063. We do not need `Br` (extra space) – Angshuman Agarwal May 05 '19 at 09:09
6

There is a greedy approach also.

Take the first element. Then find out the minimum or maximum element in the contiguous sequence including the first element and select it.

That is if the sequence is 1, 5, 7, 9, 2,4, first select 1, and then 9 because 9 is the maximum in the contiguous sequence 1, 5, 7, 9.

Proceed in the same manner and select 2 and 5. Using same approach, the subsequence computed for the example:

1, 17, 5, 10, 13, 15, 10, 5, 16, 8

is: 1, 17, 5, 15, 5, 16, 8

Chris Zheng
  • 1,509
  • 1
  • 15
  • 20
user434345
  • 299
  • 5
  • 8
2

or you can use greedy algorithm

public static int longestZigZag(int[] sequence) {
    if (sequence.length==1) return 1;
    if (sequence.length==2) return 2;
    int[] diff = new int[sequence.length-1];

    for (int i=1;i<sequence.length;i++){
        diff[i-1]=sequence[i]-sequence[i-1];
    }
    int prevsign=sign(diff[0]);
    int count=0;
    if (prevsign!=0)
        count=1;
    for (int i=1;i<diff.length;i++){
        int sign=sign(diff[i]);
        if (prevsign*sign==-1){
            prevsign=sign;
            count++;
        }
    }
    return count+1;
}

public static int sign(int a){
    if (a==0) return 0;
    return a/Math.abs(a);
}
Aydar Omurbekov
  • 2,047
  • 4
  • 27
  • 53
2

Actually I think that answer with highest score is correct (IVlad's). But I'm pretty sure that the dynamic programming part (outer loop) is not necessary.

A greedy approach is used and we can obtain positive_end_seq[i] and negative_end_seq[i] by operations:

    positive_end_seq[i] = negative_end_seq[i-1];
    negative_end_seq[i] = positive_end_seq[i-1];
    if (A[i-1] > A[i]) { // next element for positive_end_seq
       positive_end_seq[i] += 1; 
    }
    if (A[i-1] < A[i]) { // next element for negqtive_end_seq
       negative_end_seq[i] += 1;
    }
    // if (A[i-1] == A[i]) values don't change

positive_end_seq[0] = 1 and negative_end_seq[0] = 1, both arrays for all i's contain length of the longest subsequence with pos/neg ending in i-th element. We don't have to look at 0..i-2 elements, and it would be nice to prove that.

Time complexity is O(n)

Of course, pos/neg array can be replaced by counters now, here is code in Java

    public static int subZigZag(int[] arr) {
      int pos_count = 1;
      int neg_count = 1;
      for(int i = 1; i < arr.length; ++i) {
        if (arr[i-1] < arr[i]) {
          pos_count = neg_count + 1;
        }
        if (arr[i-1] > arr[i]) {
          neg_count = pos_count+1;
        }
      }
      return Math.max(pos_count, neg_count);
    } 
The_Ham
  • 427
  • 3
  • 7
0
public static int longestZigZag(int[] sequence) {
    int max_seq = 0;

    if (sequence.length == 1) {
        return 1;
    }

    if (sequence.length == 1) {
        return 2;
    }

    int dp[] = new int[sequence.length];

    dp[0] = 1;
    dp[1] = 2;

    for (int i = 2; i < sequence.length; i++) {
        for (int j = i - 1; j > 0; j--) {
            if (((sequence[i] > sequence[j] &&
                sequence[j] < sequence[j - 1]) || 
                (sequence[i] < sequence[j] &&
                sequence[j] > sequence[j - 1])) &&
                dp[i] < dp[j] + 1) {
                dp[i] = dp[j] + 1;

                if (dp[i] > max_seq) {
                    max_seq = dp[i];
                }
            } 
        }
    }

    return max_seq;
}
vishpat
  • 133
  • 1
  • 7
0

This is my take on a simple greedy implementation.

Like others have previously mentioned, you simply need to look at the zag'ing of the three last points.

def zigzag(xs):
    res = xs[:2]
    for x in xs[2:]:
        if cmp(res[-1], x) == cmp(res[-1], res[-2]):
            res.append(x)
        else:
            res[-1] = x
    return res
Thomas Ahle
  • 30,774
  • 21
  • 92
  • 114
0

Here is how did it in O(n)

public static int longestZigZag(int[] sequence) {
    if (sequence == null) {
        return 0;
    }

    int len  = sequence.length;
    if (len <= 2) {
        return len;
    }
    int minima = sequence[0];
    int maxima = sequence[0];
    int maximalen = 1;
    int minimalen = 1;

    for (int i = 1; i < len; i++) {
        if (sequence[i] < maxima) {
            if (minimalen < maximalen + 1) {
                minimalen = maximalen + 1;
                minima = sequence[i];
            } else if (minimalen == maximalen + 1 && sequence[i] < minima) {
                minima = sequence[i];
            }
        }
        if (sequence[i] > minima) {
            if (maximalen < minimalen + 1) {
                maximalen = minimalen + 1;
                maxima = sequence[i];
            } else if (maximalen == minimalen + 1 && sequence[i] > maxima) {
                maxima = sequence[i];
            }
        }
    }

    return Math.max(maximalen, minimalen);
}
Sandip
  • 317
  • 1
  • 3
  • 8
0

int zigzag(int[] a) {

List<Integer> list= new ArrayList<>();
int max = 0;
if(a.length==0 || a.length==1) return 0;
if(a.length==2) return 1;
for(int i=1;i<a.length-1;i++){

    if((a[i-1]<a[i] && a[i+1]<a[i]) || (a[i-1]>a[i] && a[i+1]>a[i])){
        if(list.isEmpty()){
           list.add(a[i-1]); 
        }
        list.add(a[i]);

    }else{
        list.add(a[i+1]); 
        max = Math.max(max,list.size());
        list.clear();
    }

}
return max;

}

jchopra
  • 21
  • 4
-1

def ZigZag(tup):

length = len(tup)
lst = []
lst.append(1)
lst.append(2)
if length > 2:
    for i in range(2,length):
        if (tup[i]-tup[i-1]) * (tup[i-1]-tup[i-2]) < 0:
            d = lst[i-1] + 1
        else:
            d = lst[i-1]
        lst.append(d)

return lst[length-1]
-1

Pick local maxima and local minima, very simple.

vector<int> longest_oscilating_subsequence(const vector<int> seq) {
    vector<int> result; // the resulting subsequence 

    for (int i = 0; i < seq.size(); ++i) {
        if (i > 0 && seq[i] == seq[i - 1]) continue;

        // is this point a local extreme 
        bool local_max = true, local_min = true;
        if (i > 0) {
            local_max = local_max && (seq[i] >= seq[i - 1]);
            local_min = local_min && (seq[i] <= seq[i - 1]);
        }
        if (i < seq.size() - 1) {
            local_max = local_max && (seq[i] >= seq[i + 1]);
            local_min = local_min && (seq[i] <= seq[i + 1]);
        }

        // potentially add it to the sequence 
        if (local_max || local_min) result.push_back(seq[i]);
    }

    return result; 
}
Lukas Mach
  • 29
  • 2