1

Input array is something like:

[0,0,1,2,4,7,9,6,4,4,2,1,0,0,0,0,0,3,5,5,10,3,2,1,4,5,7,7,12,11,8,4,2,
 1,1,1,2,4,9,4,2,2,0,1,3,6,13,11,5,5,2,2,3,4,7,11,8...]

As we can see there are hills and valleys in an array with repeating (possibly) minimum values for same valley. I extended This (addresses arrays with distinct elements) idea to find solution to above as:

/// <summary>
/// Returns index array where valley lies
/// </summary>
/// <param name="arraySmoothed"></param>
/// <returns></returns>
private static List<int> GetValleys(List<int> arraySmoothed) {
    List<int> valleys = new List<int>();
    List<int> tempValley = new List<int>();

    bool contdValleyValues = false;
    for (int i = 0; i < arraySmoothed.Count; i++) {
        // A[i] is minima if A[i-1] >= A[i] <= A[i+1], <= instead of < is deliberate otherwise it won't work for consecutive repeating minima values for a valley
        bool isValley = ((i == 0 ? -1 : arraySmoothed[i - 1]) >= arraySmoothed[i]) 
            && (arraySmoothed[i] <= (i == arraySmoothed.Count - 1 ? -1 : arraySmoothed[i + 1]));

        // If several equal minima values for same valley, average the indexes keeping in temp list
        if (isValley) {
            if (!contdValleyValues)
                contdValleyValues = true;
            tempValley.Add(i);
        } else  {
            if (contdValleyValues) {
                valleys.Add((int)tempValley.Average());
                tempValley.Clear();
                contdValleyValues = false;
            }
        }
    }
    return valleys;
}

This method gets stuck at ...7,9,6,4,4,2,1,0,0,0,0,0,3,5,5,10,3... where it throws out three minima, but there should be one (the middle 0 among five). Complexity is not the issue for me, will worship O(n). I just want a generic solution. Any hint/help will be appreciated.

Community
  • 1
  • 1
Bishnu Rawal
  • 1,387
  • 16
  • 33
  • 1
    A side note: You could change the `for` condition to skip the first and last items, in order to simplify (and improve performance of) the `isValley` calculation... `for (int i = 1; i < arraySmoothed.Count - 1; i++)` – Rufus L Apr 30 '15 at 17:27

2 Answers2

8

You can code this up with a simple state machine with three states representing the recent history of the curve:

  • Going down
  • Equal range after going down
  • Not going down (i.e. going up or being flat after going up)

State transition diagram looks like this:

State transition diagram

The picture looks a little complicated, but the description is quite simple: - when the curve slopes up, the next state is NotGoingDown - when the curve slopes down, the next state is GoingDown - When the stretch is horizontal, the state becomes EqGoingDown if the curve was going down, or NotGoingDown if the curve was flat or going up.

Here is an implementation in C#:

enum CurveState {
    GoingDown=0, EqGoingDown=1, NotGoingDown=2
}
private static IList<int> GetValleys(IList<int> a) {
    var res = new List<int>();
    if (a.Count < 2) {
        return res;
    }
    int lastEq = 0;
    CurveState s = CurveState.NotGoingDown;
    for (var i = 1 ; i != a.Count ; i++) {
        switch(Math.Sign(a[i]-a[i-1])) {
            case -1:
                s = CurveState.GoingDown;
                break;
            case  0:
                if (s == CurveState.GoingDown) {
                    lastEq = i;
                }
                s = (s==CurveState.NotGoingDown)
                  ? CurveState.NotGoingDown
                  : CurveState.EqGoingDown;
                break;
            case 1:
                if (s == CurveState.GoingDown) {
                    res.Add(i-1);
                } else if (s == CurveState.EqGoingDown) {
                    res.Add((lastEq+i-1)/2);
                }
                s = CurveState.NotGoingDown;
            break;
        }
    }
    return res;
}

Demo uses your numbers, marking valleys with an asterisk.

lastEq variable is the index of the position at which the current equal range has started. Note that the transition table is set up in such a way that lastEq is always set when we are in the CurveState.EqGoingDown state. The (lastEq+i-1)/2 formula computes the average between the last position where values were equal and i-1.

Sergey Kalinichenko
  • 714,442
  • 84
  • 1,110
  • 1,523
2

The issue is that at at the end of a continued valley you need to check if the next value is lower, meaning that you were not actually at the bottom of the valley. Additionally you have to keep track of what the slope was before a flat area to determine if you were going up or down before the flat area. Also you don't need the contdValleyValues as you can just check tempValley to see if it has any values instead.

private static List<int> GetValleys(List<int> arraySmoothed)
{
    List<int> valleys = new List<int>();
    List<int> tempValley = new List<int>();
    int slope = 0;
    for (int i = 1; i < arraySmoothed.Count - 1; i++)
    {
        // A[i] is minima if A[i-1] >= A[i] <= A[i+1], <= instead of < is deliberate 
        // otherwise it won't work for consecutive repeating minima values for a 
        // valley
        bool isValley = arraySmoothed[i - 1] >= arraySmoothed[i] 
                     && arraySmoothed[i] <= arraySmoothed[i + 1];

        // If several equal minima values for same valley, average the indexes 
        // keeping in temp list
        if (isValley)
        {
            tempValley.Add(i);
        }
        else
        {
            if (tempValley.Any())
            {
                if (arraySmoothed[i - 1] < arraySmoothed[i] && slope == -1)
                {
                    valleys.Add((int)tempValley.Average());
                }

                tempValley.Clear();
            }
        }

        if (arraySmoothed[i - 1] > arraySmoothed[i])
        {
            slope = -1;
        }
        else if (arraySmoothed[i - 1] < arraySmoothed[i])
        {
            slope = 1;
        }
    }

    return valleys;
}
juharr
  • 31,741
  • 4
  • 58
  • 93
  • 1
    One other small fix would be to change the `for` condition to skip the first and last items, so that these conditions could be removed from the `isValley` calculation... `for (int i = 1; i < arraySmoothed.Count - 1; i++)` – Rufus L Apr 30 '15 at 17:26
  • @RufusL agreed and fixed. – juharr Apr 30 '15 at 17:29
  • Thanks juharr, Great hint. But what if array is like ...9,6,4,4,2,1,0,0,0,0,0,3,4,4,5.7,10,8.... Given array is just a example. @RufusL thanks for performance trick. – Bishnu Rawal Apr 30 '15 at 17:36
  • @BishnuRawal Yes, you'll need to actually look at the values on each side of a flat surface. – juharr Apr 30 '15 at 17:41
  • @juharr, could you please just tell me the logic that removes both side undesired valleys for example above. I tried but its kinda pendulum. – Bishnu Rawal Apr 30 '15 at 17:55
  • @BishnuRawal OK try my edit now. I've included a slope parameter to keep track of what the slope was before the level ground. If it is -1 (down hill) then we know the flat area was a the bottom, where as a slope of 1 means it's a plateau or shelf on the way back up. – juharr Apr 30 '15 at 18:08