Ответ 1
Поиск числа всех длительно растущих подпоследовательностей
Полный Java-код улучшенного алгоритма LIS, который обнаруживает не только длину самой длинной возрастающей подпоследовательности, но и число подпоследовательностей такой длины. Я предпочитаю использовать generics, чтобы не только целые числа, но и любые сопоставимые типы.
@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.
Как я упоминал ранее, идея состоит в том, чтобы вести учет всех возможных дольше, полученных на предыдущих шагах. Поэтому позвольте создать пронумерованный список строк, где число каждой строки равно длине субмасок, хранящихся в этой строке. И пусть store 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.
Мы будем строить такой список шаг за шагом, беря элементы из начальной последовательности по их порядку. На каждом шаге мы попробуем добавить этот элемент в самый длинный элемент, который можно добавить в, и записать изменения.
Построение списка
Давайте создадим список, используя последовательность из вашего примера, так как он имеет все возможные варианты:
16 5 8 6 1 10 5 2 15 3 2 4 1
Сначала возьмите элемент 16. До сих пор наш список пуст, поэтому мы просто положили в него одну пару:
1: (16, 1) <= one sub that ends by 16
Далее 5. Он не может быть добавлен к югу, который заканчивается на 16, поэтому он создаст новый юг с длиной 1. Мы создаем пару (5, 1) и помещаем ее в строку 1:
1: (16, 1)(5, 1)
Далее появится элемент 8. Он не может создать sub [16, 8] длины 2, но может создать sub [5, 8]. Итак, вот где идет алгоритм. Во-первых, мы перебираем строки списка вверх ногами, глядя на "значения" последней пары. Если наш элемент больше, чем значения всех последних элементов во всех строках, мы можем добавить его к существующим sub (s), увеличивая его длину на единицу. Таким образом, значение 8 создаст новую строку списка, потому что она больше, чем значения всех последних элементов, существующих в списке до сих пор (например, > 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, также продолжит такой юг, и нет необходимости сохранять 8 как меньшую длину, чем это возможно.
Далее. 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)
Если при поиске строк по последнему элементу мы находим равный элемент, мы снова вычисляем его "счет" на основе предыдущей строки и добавляем к существующему "счету".
После обработки 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 самых длинных субмарины, каждая из которых состоит из 4 элементов: 2 конца на 15 и 1 заканчивается на 4.
Как насчет сложности?
На каждой итерации при взятии следующего элемента из начальной последовательности мы делаем 2 цикла: сначала, когда итерируем строки, чтобы найти место для следующего элемента, а во-вторых, суммируя подсчеты в предыдущей строке. Поэтому для каждого элемента мы делаем максимум n итераций (наихудшие случаи: если начальный seq состоит из элементов в порядке возрастания, мы получим список из n строк с 1 парой в каждой строке, если seq будет отсортирован в порядке убывания, мы получим список из 1 строки с n элементами). Кстати, сложность O (n 2) не то, что мы хотим.
Во-первых, очевидно, что в каждом промежуточном состоянии строки сортируются по возрастанию порядка их последнего "значения". Таким образом, вместо грубой петли может выполняться двоичный поиск, сложность которого - O (log n).
Во-вторых, нам не нужно суммировать "подсчеты" подсетей путем циклического обращения к элементам строки каждый раз. Мы можем суммировать их в процессе, когда новая пара добавляется в строку, например:
1: (16, 1)(5, 2) <=== instead of 1, put 1 + "count" of previous element in the row
Таким образом, второе число покажет не количество самых длинных субмарок, которые могут быть получены с заданным значением в конце, но суммарный подсчет всех самых длинных субмарок, которые заканчиваются любым элементом, который больше или равен "значению" из пары.
Таким образом, "counts" будет заменен на "sums". И вместо повторения элементов в предыдущей строке мы просто выполняем двоичный поиск (это возможно, потому что пары в любой строке всегда упорядочены по их "значениям" ) и берут "сумму" для новой пары как "сумму" последнего элемента в предыдущей строке минус "сумма" от элемента слева до найденного положения в предыдущей строке плюс "сумма" предыдущего элемента в текущей строке.
Поэтому при обработке 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)
В этом случае конечный счет всех самых длинных субмарок является "суммой" из последней пары последней строки списка, т.е. е. 3, а не 3 + 2.
Итак, выполняя двоичный поиск как для поиска строк, так и для поиска суммы, мы придем к сложности O (n * log n).
Что касается потребляемой памяти, то после обработки всего массива мы получаем максимум n пар, поэтому потребление памяти в случае динамических массивов будет O (n). Кроме того, при использовании динамических массивов или наборов требуется дополнительное время для их распределения и изменения, но большинство операций выполняется в течение O (1), потому что мы не делаем никакой сортировки и перегруппировки во время процесса. Поэтому оценка сложности кажется окончательной.