Число всех самых длинных возрастающих подпоследовательностей

Я практикую алгоритмы и одна из моих задач заключается в подсчете количество всех самых длинных возрастающих подпоследовательностей для этого 0 цифры. Решение O (n^2) - это не вариант.

Я уже реализовал поиск LIS и его длины ( алгоритм LIS), но этот алгоритм переключает числа на минимально возможные. Поэтому невозможно определить, являются ли суб-последовательности с предыдущим число (большее) могло бы достичь самой длинной длины, иначе я мог бы просто сосчитать эти переключатели, я думаю.

любой идеи, как получить это в о O (nlogn)? Я знаю, что его нужно решать с помощью динамического программирования.

я реализовал одно решение, и оно работает хорошо, но для этого требуется два вложенных цикла (i в 1..n) x (j в 1..i-1).
Так это O (n^2) Я думаю, тем не менее, это слишком медленно.

Я попытался даже переместить эти числа из массива в двоичное дерево (потому что в каждом Я итерация я ищу все меньшие числа, чем номер[i] - прохождение элементов i-1..1), но это было еще медленнее.

пример тестов:

1 3 2 2 4
result: 3 (1,3,4 | 1,2,4 | 1,2,4)

3 2 1
result: 3 (1 | 2 | 3)

16 5 8 6 1 10 5 2 15 3 2 4 1
result: 3 (5,8,10,15 | 5,6,10,15 | 1,2,3,4)

4 ответов


Поиск числа всех самых длинных возрастающих подпоследовательностей

Ниже приведен полный Java-код улучшенного алгоритма LIS, который обнаруживает не только длину самой длинной возрастающей подпоследовательности, но и количество подпоследовательностей такой длины. Я предпочитаю использовать дженерики, чтобы разрешить не только целые числа, но и любые сопоставимые типы.

@Test
public void testLisNumberAndLength() {

    List<Integer> input = Arrays.asList(16, 5, 8, 6, 1, 10, 5, 2, 15, 3, 2, 4, 1);
    int[] result = lisNumberAndlength(input);
    System.out.println(String.format(
            "This sequence has %s longest increasing subsequenses of length %s", 
            result[0], result[1]
            ));
}


/**
 * Body of improved LIS algorithm
 */
public <T extends Comparable<T>> int[] lisNumberAndLength(List<T> input) {

    if (input.size() == 0) 
        return new int[] {0, 0};

    List<List<Sub<T>>> subs = new ArrayList<>();
    List<Sub<T>> tails = new ArrayList<>();

    for (T e : input) {
        int pos = search(tails, new Sub<>(e, 0), false);      // row for a new sub to be placed
        int sum = 1;
        if (pos > 0) {
            List<Sub<T>> pRow = subs.get(pos - 1);            // previous row
            int index = search(pRow, new Sub<T>(e, 0), true); // index of most left element that <= e
            if (pRow.get(index).value.compareTo(e) < 0) {
                index--;
            } 
            sum = pRow.get(pRow.size() - 1).sum;              // sum of tail element in previous row
            if (index >= 0) {
                sum -= pRow.get(index).sum;
            }
        }

        if (pos >= subs.size()) {                             // add a new row
            List<Sub<T>> row = new ArrayList<>();
            row.add(new Sub<>(e, sum));
            subs.add(row);
            tails.add(new Sub<>(e, 0));

        } else {                                              // add sub to existing row
            List<Sub<T>> row = subs.get(pos);
            Sub<T> tail = row.get(row.size() - 1); 
            if (tail.value.equals(e)) {
                tail.sum += sum;
            } else {
                row.add(new Sub<>(e, tail.sum + sum));
                tails.set(pos, new Sub<>(e, 0));
            }
        }
    }

    List<Sub<T>> lastRow = subs.get(subs.size() - 1);
    Sub<T> last = lastRow.get(lastRow.size() - 1);
    return new int[]{last.sum, subs.size()};
}



/**
 * Implementation of binary search in a sorted list
 */
public <T> int search(List<? extends Comparable<T>> a, T v, boolean reversed) {

    if (a.size() == 0)
        return 0;

    int sign = reversed ? -1 : 1;
    int right = a.size() - 1;

    Comparable<T> vRight = a.get(right);
    if (vRight.compareTo(v) * sign < 0)
        return right + 1;

    int left = 0;
    int pos = 0;
    Comparable<T> vPos;
    Comparable<T> vLeft = a.get(left);

    for(;;) {
        if (right - left <= 1) {
            if (vRight.compareTo(v) * sign >= 0 && vLeft.compareTo(v) * sign < 0) 
                return right;
            else 
                return left;
        }
        pos = (left + right) >>> 1;
        vPos = a.get(pos);
        if (vPos.equals(v)) {
            return pos;
        } else if (vPos.compareTo(v) * sign > 0) {
            right = pos;
            vRight = vPos;
        } else {
            left = pos;
            vLeft = vPos;
        }
    } 
}



/**
 * Class for 'sub' pairs
 */
public static class Sub<T extends Comparable<T>> implements Comparable<Sub<T>> {

    T value;
    int sum;

    public Sub(T value, int sum) { 
        this.value = value; 
        this.sum = sum; 
    }

    @Override public String toString() {
        return String.format("(%s, %s)", value, sum); 
    }

    @Override public int compareTo(Sub<T> another) { 
        return this.value.compareTo(another.value); 
    }
}

объяснение

поскольку мое объяснение кажется длинным, я назову начальную последовательность "seq" и любую ее подпоследовательность "sub". Так задача состоит в том, чтобы вычислить количество самых длинных увеличивающихся суб, которые могут быть получены из seq.

как я уже упоминал ранее, идея состоит в том, чтобы вести подсчет всех возможных длинных подлодок, полученных на предыдущих шагах. Итак, давайте создадим нумерованный список строк, где количество каждой строки равно длине подстановок, хранящихся в этой строке. И давайте сохраним subs как пары чисел (v, c), где "v" -" значение " конечного элемента, " c " - это "количество" подводных лодок данного длина этого конца на "v". Например:

1: (16, 1) // that means that so far we have 1 sub of length 1 which ends by 16.

мы будем строить такой список шаг за шагом, беря элементы из начальной последовательности по их порядку. На каждом шагу мы будем стараться добавьте этот элемент в самый длинный sub, который он может быть добавлен в и записи изменений.

создание списка

давайте построим список, используя последовательность из вашего примера, так как у него есть все возможные варианты:

 16 5 8 6 1 10 5 2 15 3 2 4 1

во-первых, взять элемент 16. Наш список пока пуст, поэтому мы просто помещаем в него одну пару:

1: (16, 1) <= one sub that ends by 16

далее 5. Его нельзя добавить в sub, который заканчивается на 16, поэтому он создаст новый sub с длиной 1. Создаем пару (5, 1) и ставим ее в строку 1:

1: (16, 1)(5, 1)

элемент 8 следующий. Он не может создать sub [16, 8] длины 2, но может создать sub [5, 8]. Итак, вот куда идет алгоритм. Во-первых, мы повторяем строки списка вверх ногами, глядя на" значения " последней пары. Если наш элемент больше значений всех последних элементов во всех строках, то мы можем добавить его к существующим sub(s), увеличив его длину на единицу. Таким образом, значение 8 создаст новую строку списка, потому что оно больше значений всех последних элементов, существующих в списке до сих пор (i. e. > 5):

1: (16, 1)(5, 1) 
2: (8, ?)   <=== need to resolve how many longest subs ending by 8 can be obtained

элемент 8 может продолжать 5, но не может продолжать 16. Поэтому нам нужно искать предыдущую строку, начиная с ее конца, вычисляя сумму "подсчитывает" в парах, "значение" которых меньше 8:

(16, 1)(5, 1)^  // sum = 0
(16, 1)^(5, 1)  // sum = 1
^(16, 1)(5, 1)  // value 16 >= 8: stop. count = sum = 1, so write 1 in pair next to 8

1: (16, 1)(5, 1)
2: (8, 1)  <=== so far we have 1 sub of length 2 which ends by 8.

почему бы нам не сохранить значение 8 в subs длины 1 (первая строка)? Потому что нам нужны подлодки максимально возможной длины, и 8 могут продолжить некоторые предыдущие подлодки. Таким образом, каждое следующее число больше 8 также будет продолжать такой sub, и нет необходимости держать 8 как sub длины меньше, чем это может быть.

далее. 6. Поиск вверх ногами по последним "значениям" в строках:

1: (16, 1)(5, 1)  <=== 5 < 6, go next
2: (8, 1)

1: (16, 1)(5, 1)
2: (8, 1 )  <=== 8 >= 6, so 6 should be put here

найти номер для 6, нужно рассчитать количество:

take previous line
(16, 1)(5, 1)^  // sum = 0
(16, 1)^(5, 1)  // 5 < 6: sum = 1
^(16, 1)(5, 1)  // 16 >= 6: stop, write count = sum = 1

1: (16, 1)(5, 1)
2: (8, 1)(6, 1) 

после обработки 1:

1: (16, 1)(5, 1)(1, 1) <===
2: (8, 1)(6, 1)

после обработки 10:

1: (16, 1)(5, 1)(1, 1)
2: (8, 1)(6, 1)
3: (10, 2) <=== count is 2 because both "values" 8 and 6 from previous row are less than 10, so we summarized their "counts": 1 + 1

после обработки 5:

1: (16, 1)(5, 1)(1, 1)
2: (8, 1)(6, 1)(5, 1) <===
3: (10, 2)

после обработки 2:

1: (16, 1)(5, 1)(1, 1)
2: (8, 1)(6, 1)(5, 1)(2, 1) <===
3: (10, 2)

после обработки 15:

1: (16, 1)(5, 1)(1, 1)
2: (8, 1)(6, 1)(5, 1)(2, 1)
3: (10, 2)
4: (15, 2) <===

после обработки 3:

1: (16, 1)(5, 1)(1, 1)
2: (8, 1)(6, 1)(5, 1)(2, 1)
3: (10, 2)(3, 1) <===
4: (15, 2)  

после обработки 2:

1: (16, 1)(5, 1)(1, 1)
2: (8, 1)(6, 1)(5, 1)(2, 2) <===
3: (10, 2)(3, 1) 
4: (15, 2)  

если при поиске строк по последнему элементу мы находим равный элемент, мы снова вычисляем его " count "на основе предыдущей строки и добавляем к существующему"count".

после обработки 4:

1: (16, 1)(5, 1)(1, 1)
2: (8, 1)(6, 1)(5, 1)(2, 2)  
3: (10, 2)(3, 1) 
4: (15, 2)(4, 1) <===

после обработки 1:

1: (16, 1)(5, 1)(1, 2) <===
2: (8, 1)(6, 1)(5, 1)(2, 2)  
3: (10, 2)(3, 1) 
4: (15, 2)(4, 1)  

Итак, что мы имеем после обработки всей исходной последовательности? Глядя на последнюю строку, мы видим, что у нас есть 3 самых длинных subs, каждый из которых состоит из 4 элементов: 2 заканчивается на 15 и 1 заканчивается на 4.

насчет сложности?

на каждой итерации, принимая следующий элемент из начальной последовательности, мы делаем 2 цикла: первый при итерации строк, чтобы найти место для следующего элемента, и второй при суммировании подсчетов в предыдущей строке. Поэтому для каждого элемента мы делаем максимум до n итерации (в худших случаях: если начальный seq состоит из элементов в порядке возрастания, мы получим список из n строк с 1 парой в каждой строке; если seq сортируется в порядке убывания получим список из 1 строки с n элементами). Кстати, O (n2) сложность-это не то, чего мы хотим.

во-первых, это очевидно, что в каждом промежуточном состоянии строки сортируются по возрастанию их последних "ценностей". Поэтому вместо грубого цикла можно выполнить двоичный поиск, сложность которого равна O (log n).

во-вторых, нам не нужно суммировать "подсчеты" подводных лодок, каждый раз проходя через элементы строки. Мы можем суммировать их в процессе, когда новая пара добавляется в строку, например:

1: (16, 1)(5, 2) <=== instead of 1, put 1 + "count" of previous element in the row

так второй номер покажет не графа из самых длинных подводных лодок, которые могут быть получены с заданным значением в конце, но резюме графа of все самые длинные подводные лодки, которые заканчиваются любым элементом, который больше или равен "значению" из пары.

таким образом," подсчеты "будут заменены на"суммы". И вместо итерации элементов в предыдущей строке мы просто выполните двоичный поиск (это возможно, потому что пары в любой строке всегда упорядочены по их "значениям") и возьмите "sum" для новой пары как "sum" последнего элемента в предыдущей строке минус "sum" от элемента слева до найденной позиции в предыдущей строке плюс "sum" предыдущего элемента в текущей строке.

поэтому при обработке 4:

1: (16, 1)(5, 2)(1, 3)
2: (8, 1)(6, 2)(5, 3)(2, 5) 
3: (10, 2)(3, 3) 
4: (15, 2) <=== room for (4, ?)

search in row 3 by "values" < 4:
3: (10, 2)^(3, 3) 

4 будет сопряжено с (3-2+2): ("сумма" из последней пары предыдущей строки) - ("сумма" из пары слева в найденную позицию в предыдущая строка) + ("сумма" из предыдущей пары в текущей строке):

4: (15, 2)(4, 3)

в этом случае конечным числом всех самых длинных подстановок является "сумма" из последней пары последней строки списка i. e. 3, а не 3 + 2.

Итак, выполняя двоичный поиск как для поиска строк, так и для поиска суммы, мы будет поставляться с O (N * log N) сложностью.

как насчет потребляемой памяти, после обработки всего массива мы получаем максимум N пар, поэтому потребление памяти в случае динамического массивы будут O (n). Кроме того, при использовании динамических массивов или коллекций требуется дополнительное время для их выделения и изменения размера, но большинство операций выполняется за O(1) время, потому что мы не делаем никакой сортировки и перестановки во время процесса. Таким образом, оценка сложности кажется окончательной.


Саша Соловьев-это здорово, но мне не понятно, почему

sum -= pRow.get(index).sum;

вот мой код, основанный на той же идее

import java.math.BigDecimal;
import java.util.*;

class lisCount {
  static BigDecimal lisCount(int[] a) {
    class Container {
      Integer    v;
      BigDecimal count;

      Container(Integer v) {
        this.v = v;
      }
    }
    List<List<Container>> lisIdxSeq = new ArrayList<List<Container>>();
    int lisLen, lastIdx;
    List<Container> lisSeqL;
    Container lisEle;
    BigDecimal count;
    int pre;
    for (int i = 0; i < a.length; i++){
      pre = -1;
      count = new BigDecimal(1);
      lisLen = lisIdxSeq.size();
      lastIdx = lisLen - 1;
      lisEle = new Container(i);
      if(lisLen == 0 || a[i] > a[lisIdxSeq.get(lastIdx).get(0).v]){
        // lis len increased
        lisSeqL = new ArrayList<Container>();
        lisSeqL.add(lisEle);
        lisIdxSeq.add(lisSeqL);
        pre = lastIdx;
      }else{
        int h = lastIdx;
        int l = 0;

        while(l < h){
          int m = (l + h) / 2;
          if(a[lisIdxSeq.get(m).get(0).v] < a[i]) l = m + 1;
          else h = m;
        }

        List<Container> lisSeqC = lisIdxSeq.get(l);
        if(a[i] <= a[lisSeqC.get(0).v]){
          int hi = lisSeqC.size() - 1;
          int lo = 0;
          while(hi < lo){
            int mi = (hi + lo) / 2;
            if(a[lisSeqC.get(mi).v] < a[i]) lo = mi + 1;
            else hi = mi;
          }
          lisSeqC.add(lo, lisEle);
          pre = l - 1;
        }
      }
      if(pre >= 0){
        Iterator<Container> it = lisIdxSeq.get(pre).iterator();
        count = new BigDecimal(0);
        while(it.hasNext()){
          Container nt = it.next();
          if(a[nt.v] < a[i]){
            count = count.add(nt.count);
          }else break;
        }
      }
      lisEle.count = count;
    }

    BigDecimal rst = new BigDecimal(0);
    Iterator<Container> i = lisIdxSeq.get(lisIdxSeq.size() - 1).iterator();
    while(i.hasNext()){
      rst = rst.add(i.next().count);
    }
    return rst;
  }

  public static void main(String[] args) {
    System.out.println(lisCount(new int[] { 1, 3, 2, 2, 4 }));
    System.out.println(lisCount(new int[] { 3, 2, 1 }));
    System.out.println(lisCount(new int[] { 16, 5, 8, 6, 1, 10, 5, 2, 15, 3, 2, 4, 1 }));
  }
}

сортировка терпения также O (N * logN), но путь короче и проще, чем методы, основанные на двоичном поиске:

static int[] input = {4, 5, 2, 8, 9, 3, 6, 2, 7, 8, 6, 6, 7, 7, 3, 6};

/**
 * Every time a value is tested it either adds to the length of LIS (by calling decs.add() with it), or reduces the remaining smaller cards that must be found before LIS consists of smaller cards. This way all inputs/cards contribute in one way or another (except if they're equal to the biggest number in the sequence; if want't to include in sequence, replace 'card <= decs.get(decIndex)' with 'card < decs.get(decIndex)'. If they're bigger than all decs, they add to the length of LIS (which is something we want), while if they're smaller than a dec, they replace it. We want this, because the smaller the biggest dec is, the smaller input we need before we can add onto LIS.
 *
 * If we run into a decreasing sequence the input from this sequence will replace each other (because they'll always replace the leftmost dec). Thus this algorithm won't wrongfully register e.g. {2, 1, 3} as {2, 3}, but rather {2} -> {1} -> {1, 3}.
 *
 * WARNING: This can only be used to find length, not actual sequence, seeing how parts of the sequence will be replaced by smaller numbers trying to make their sequence dominate
 *
 * Due to bigger decs being added to the end/right of 'decs' and the leftmost decs always being the first to be replaced with smaller decs, the further a dec is to the right (the bigger it's index), the bigger it must be. Thus, by always replacing the leftmost decs, we don't run the risk of replacing the biggest number in a sequence (the number which determines if more cards can be added to that sequence) before a sequence with the same length but smaller numbers (thus currently equally good, due to length, and potentially better, due to less needed to increase length) has been found.
 */
static void patienceFindLISLength() {
    ArrayList<Integer> decs = new ArrayList<>();
    inputLoop: for (Integer card : input) {
        for (int decIndex = 0; decIndex < decs.size(); decIndex++) {
            if (card <= decs.get(decIndex)) {
                decs.set(decIndex, card);
                continue inputLoop;
            }
        }
        decs.add(card);
    }
    System.out.println(decs.size());
}

cpp реализация вышеуказанной логики:

#include<bits/stdc++.h>
using namespace std;
#define pb push_back
#define pob pop_back
#define pll pair<ll, ll>
#define pii pair<int, int>
#define ll long long
#define ull unsigned long long
#define fori(a,b) for(i=a;i<b;i++)
#define forj(a,b) for(j=a;j<b;j++)
#define fork(a,b) for(k=a;k<b;k++)
#define forl(a,b) for(l=a;l<b;l++)
#define forir(a,b) for(i=a;i>=b;i--)
#define forjr(a,b) for(j=a;j>=b;j--)
#define mod 1000000007
#define boost std::ios::sync_with_stdio(false)

struct comp_pair_int_rev
{
    bool operator()(const pair<int,int> &a, const int & b)
    {
        return (a.first > b);
    }
    bool operator()(const int & a,const pair<int,int> &b)
    {
        return (a > b.first);
    }
};

struct comp_pair_int
{
    bool operator()(const pair<int,int> &a, const int & b)
    {
        return (a.first < b);
    }
    bool operator()(const int & a,const pair<int,int> &b)
    {
        return (a < b.first);
    }
};

int main()
{
    int n,i,mx=0,p,q,r,t;
    cin>>n;

    int a[n];
    vector<vector<pii > > v(100005);
    vector<pii > v1(100005);

    fori(0,n)
    cin>>a[i];

    v[1].pb({a[0], 1} );
    v1[1]= {a[0], 1};

    mx=1;
    fori(1,n)
    {
        if(a[i]<=v1[1].first)
        {
            r=v1[1].second;

            if(v1[1].first==a[i])
                v[1].pob();

            v1[1]= {a[i], r+1};
            v[1].pb({a[i], r+1});
        }
        else if(a[i]>v1[mx].first)
        {
            q=upper_bound(v[mx].begin(), v[mx].end(), a[i], comp_pair_int_rev() )-v[mx].begin();
            if(q==0)
            {
                r=v1[mx].second;
            }
            else
            {
                r=v1[mx].second-v[mx][q-1].second;
            }

            v1[++mx]= {a[i], r};
            v[mx].pb({a[i], r});
        }
        else if(a[i]==v1[mx].first)
        {
            q=upper_bound(v[mx-1].begin(), v[mx-1].end(), a[i], comp_pair_int_rev() )-v[mx-1].begin();
            if(q==0)
            {
                r=v1[mx-1].second;
            }
            else
            {
                r=v1[mx-1].second-v[mx-1][q-1].second;
            }
            p=v1[mx].second;
            v1[mx]= {a[i], p+r};

            v[mx].pob();
            v[mx].pb({a[i], p+r});


        }
        else
        {
            p=lower_bound(v1.begin()+1, v1.begin()+mx+1, a[i], comp_pair_int() )-v1.begin();
            t=v1[p].second;

            if(v1[p].first==a[i])
            {

                v[p].pob();
            }

            q=upper_bound(v[p-1].begin(), v[p-1].end(), a[i], comp_pair_int_rev() )-v[p-1].begin();
            if(q==0)
            {
                r=v1[p-1].second;
            }
            else
            {
                r=v1[p-1].second-v[p-1][q-1].second;
            }

            v1[p]= {a[i], t+r};
            v[p].pb({a[i], t+r});

        }


    }

    cout<<v1[mx].second;

    return 0;
}