Как определить самую длинную возрастающую подпоследовательность с помощью динамического программирования?
У меня есть набор целых чисел. Я хочу найти самая длинная возрастающая подпоследовательность из этого набора с помощью динамического программирования.
14 ответов:
хорошо, я опишу сначала самое простое решение, которое является O (N^2), где N-размер коллекции. Существует также решение O(n log N), которое я также опишу. Смотри здесь для этого в разделе эффективные алгоритмы.
я буду считать, что индексы массива от 0 до N - 1. Так давайте определим
DP[i]
быть длиной LIS (самая длинная возрастающая подпоследовательность), которая заканчивается на элементе с индексомi
. Чтобы вычислитьDP[i]
смотрим при всех показателяхj < i
и проверить, еслиDP[j] + 1 > DP[i]
иarray[j] < array[i]
(мы хотим, чтобы он увеличивался). Если это так, то мы можем обновить текущий оптимум дляDP[i]
. Чтобы найти глобальный оптимум для массива, вы можете взять максимальное значение изDP[0...N - 1]
.int maxLength = 1, bestEnd = 0; DP[0] = 1; prev[0] = -1; for (int i = 1; i < N; i++) { DP[i] = 1; prev[i] = -1; for (int j = i - 1; j >= 0; j--) if (DP[j] + 1 > DP[i] && array[j] < array[i]) { DP[i] = DP[j] + 1; prev[i] = j; } if (DP[i] > maxLength) { bestEnd = i; maxLength = DP[i]; } }
я использую массив
prev
чтобы иметь возможность позже найти фактическую последовательность не только ее длину. Просто вернитесь рекурсивно отbestEnd
в цикле с помощьюprev[bestEnd]
. Элемент-1
значение знак остановка.
хорошо, теперь к более эффективным
O(N log N)
устранение:пусть
S[pos]
определяется как наименьшее целое число, которое завершает увеличивающуюся последовательность длиныpos
. Теперь перебираем каждое целое числоX
из входного набора и выполните следующие действия:
если
X
> последний элементS
, затем добавитьX
до концаS
. Это по существу означает, что мы нашли новый крупнейшийLIS
.в противном случае найти наименьший элемент
S
, которая составляет>=
чемX
иX
. Потому чтоS
сортируется в любое время, элемент можно найти с помощью двоичного поиска вlog(N)
.Общее время выполнения -
N
целые числа и двоичный поиск для каждого из них-N * log(N) = O (N log N)теперь давайте сделаем реальный пример:
коллекция целые:
2 6 3 4 1 2 9 5 8
действия:
0. S = {} - Initialize S to the empty set 1. S = {2} - New largest LIS 2. S = {2, 6} - New largest LIS 3. S = {2, 3} - Changed 6 to 3 4. S = {2, 3, 4} - New largest LIS 5. S = {1, 3, 4} - Changed 2 to 1 6. S = {1, 2, 4} - Changed 3 to 2 7. S = {1, 2, 4, 9} - New largest LIS 8. S = {1, 2, 4, 5} - Changed 9 to 5 9. S = {1, 2, 4, 5, 8} - New largest LIS
таким образом, длина LIS
5
(размер S).восстановить фактическое
LIS
мы снова будем использовать родительский массив. Пустьparent[i]
быть предшественником элемента с индексомi
наLIS
оканчивается на элемент с индексомi
.чтобы сделать вещи проще, мы можем сохранить в массиве
S
, не фактические целые числа, а их индексы(позиции) в наборе. Мы не держим{1, 2, 4, 5, 8}
, но сохранить{4, 5, 3, 7, 8}
.это вход[4]=1, input[5]=2, input[3]=4, input[7]=5, input[8]=8.
если мы правильно обновим Родительский массив, фактическая LIS будет:
input[S[lastElementOfS]], input[parent[S[lastElementOfS]]], input[parent[parent[S[lastElementOfS]]]], ........................................
теперь к важному - как мы обновляем Родительский массив? Есть два варианта:
если
X
> последний элементS
, тогдаparent[indexX] = indexLastElement
. Это означает, что родитель самого нового элемента является последним элементом. Мы просто добавимX
до концаS
.в противном случае найти индекс наименьшего элемента в
S
, которая составляет>=
чемX
иX
. Здесьparent[indexX] = S[index - 1]
.
объяснение Петара Минчева помогло мне прояснить ситуацию, но мне было трудно разобрать, что все было, поэтому я сделал реализацию Python с чрезмерно описательными именами переменных и множеством комментариев. Я сделал наивное рекурсивное решение, решение O(n^2) и решение o(n log n).
Я надеюсь, что это поможет прояснить алгоритмы!
Рекурсивное Решение
def recursive_solution(remaining_sequence, bigger_than=None): """Finds the longest increasing subsequence of remaining_sequence that is bigger than bigger_than and returns it. This solution is O(2^n).""" # Base case: nothing is remaining. if len(remaining_sequence) == 0: return remaining_sequence # Recursive case 1: exclude the current element and process the remaining. best_sequence = recursive_solution(remaining_sequence[1:], bigger_than) # Recursive case 2: include the current element if it's big enough. first = remaining_sequence[0] if (first > bigger_than) or (bigger_than is None): sequence_with = [first] + recursive_solution(remaining_sequence[1:], first) # Choose whichever of case 1 and case 2 were longer. if len(sequence_with) >= len(best_sequence): best_sequence = sequence_with return best_sequence
O(n^2) динамическое программирование Решение
def dynamic_programming_solution(sequence): """Finds the longest increasing subsequence in sequence using dynamic programming. This solution is O(n^2).""" longest_subsequence_ending_with = [] backreference_for_subsequence_ending_with = [] current_best_end = 0 for curr_elem in range(len(sequence)): # It's always possible to have a subsequence of length 1. longest_subsequence_ending_with.append(1) # If a subsequence is length 1, it doesn't have a backreference. backreference_for_subsequence_ending_with.append(None) for prev_elem in range(curr_elem): subsequence_length_through_prev = (longest_subsequence_ending_with[prev_elem] + 1) # If the prev_elem is smaller than the current elem (so it's increasing) # And if the longest subsequence from prev_elem would yield a better # subsequence for curr_elem. if ((sequence[prev_elem] < sequence[curr_elem]) and (subsequence_length_through_prev > longest_subsequence_ending_with[curr_elem])): # Set the candidate best subsequence at curr_elem to go through prev. longest_subsequence_ending_with[curr_elem] = (subsequence_length_through_prev) backreference_for_subsequence_ending_with[curr_elem] = prev_elem # If the new end is the best, update the best. if (longest_subsequence_ending_with[curr_elem] > longest_subsequence_ending_with[current_best_end]): current_best_end = curr_elem # Output the overall best by following the backreferences. best_subsequence = [] current_backreference = current_best_end while current_backreference is not None: best_subsequence.append(sequence[current_backreference]) current_backreference = (backreference_for_subsequence_ending_with[current_backreference]) best_subsequence.reverse() return best_subsequence
решение динамического программирования O (N log n)
def find_smallest_elem_as_big_as(sequence, subsequence, elem): """Returns the index of the smallest element in subsequence as big as sequence[elem]. sequence[elem] must not be larger than every element in subsequence. The elements in subsequence are indices in sequence. Uses binary search.""" low = 0 high = len(subsequence) - 1 while high > low: mid = (high + low) / 2 # If the current element is not as big as elem, throw out the low half of # sequence. if sequence[subsequence[mid]] < sequence[elem]: low = mid + 1 # If the current element is as big as elem, throw out everything bigger, but # keep the current element. else: high = mid return high def optimized_dynamic_programming_solution(sequence): """Finds the longest increasing subsequence in sequence using dynamic programming and binary search (per http://en.wikipedia.org/wiki/Longest_increasing_subsequence). This solution is O(n log n).""" # Both of these lists hold the indices of elements in sequence and not the # elements themselves. # This list will always be sorted. smallest_end_to_subsequence_of_length = [] # This array goes along with sequence (not # smallest_end_to_subsequence_of_length). Following the corresponding element # in this array repeatedly will generate the desired subsequence. parent = [None for _ in sequence] for elem in range(len(sequence)): # We're iterating through sequence in order, so if elem is bigger than the # end of longest current subsequence, we have a new longest increasing # subsequence. if (len(smallest_end_to_subsequence_of_length) == 0 or sequence[elem] > sequence[smallest_end_to_subsequence_of_length[-1]]): # If we are adding the first element, it has no parent. Otherwise, we # need to update the parent to be the previous biggest element. if len(smallest_end_to_subsequence_of_length) > 0: parent[elem] = smallest_end_to_subsequence_of_length[-1] smallest_end_to_subsequence_of_length.append(elem) else: # If we can't make a longer subsequence, we might be able to make a # subsequence of equal size to one of our earlier subsequences with a # smaller ending number (which makes it easier to find a later number that # is increasing). # Thus, we look for the smallest element in # smallest_end_to_subsequence_of_length that is at least as big as elem # and replace it with elem. # This preserves correctness because if there is a subsequence of length n # that ends with a number smaller than elem, we could add elem on to the # end of that subsequence to get a subsequence of length n+1. location_to_replace = find_smallest_elem_as_big_as(sequence, smallest_end_to_subsequence_of_length, elem) smallest_end_to_subsequence_of_length[location_to_replace] = elem # If we're replacing the first element, we don't need to update its parent # because a subsequence of length 1 has no parent. Otherwise, its parent # is the subsequence one shorter, which we just added onto. if location_to_replace != 0: parent[elem] = (smallest_end_to_subsequence_of_length[location_to_replace - 1]) # Generate the longest increasing subsequence by backtracking through parent. curr_parent = smallest_end_to_subsequence_of_length[-1] longest_increasing_subsequence = [] while curr_parent is not None: longest_increasing_subsequence.append(sequence[curr_parent]) curr_parent = parent[curr_parent] longest_increasing_subsequence.reverse() return longest_increasing_subsequence
говоря о решении DP, я удивился, что никто не упомянул о том, что LIS можно свести к LCS. Все, что вам нужно сделать, это отсортировать копию исходной последовательности, удалить все дубликаты и сделать LCS из них. В псевдокоде это:
def LIS(S): T = sort(S) T = removeDuplicates(T) return LCS(S, T)
и полная реализация написана в Go. Вам не нужно поддерживать всю матрицу N^2 DP, если вам не нужно восстанавливать решение.
func lcs(arr1 []int) int { arr2 := make([]int, len(arr1)) for i, v := range arr1 { arr2[i] = v } sort.Ints(arr1) arr3 := []int{} prev := arr1[0] - 1 for _, v := range arr1 { if v != prev { prev = v arr3 = append(arr3, v) } } n1, n2 := len(arr1), len(arr3) M := make([][]int, n2 + 1) e := make([]int, (n1 + 1) * (n2 + 1)) for i := range M { M[i] = e[i * (n1 + 1):(i + 1) * (n1 + 1)] } for i := 1; i <= n2; i++ { for j := 1; j <= n1; j++ { if arr2[j - 1] == arr3[i - 1] { M[i][j] = M[i - 1][j - 1] + 1 } else if M[i - 1][j] > M[i][j - 1] { M[i][j] = M[i - 1][j] } else { M[i][j] = M[i][j - 1] } } } return M[n2][n1] }
следующая реализация C++ включает также некоторый код, который создает фактический самая длинная возрастающая подпоследовательность С помощью массива под названием
prev
.std::vector<int> longest_increasing_subsequence (const std::vector<int>& s) { int best_end = 0; int sz = s.size(); if (!sz) return std::vector<int>(); std::vector<int> prev(sz,-1); std::vector<int> memo(sz, 0); int max_length = std::numeric_limits<int>::min(); memo[0] = 1; for ( auto i = 1; i < sz; ++i) { for ( auto j = 0; j < i; ++j) { if ( s[j] < s[i] && memo[i] < memo[j] + 1 ) { memo[i] = memo[j] + 1; prev[i] = j; } } if ( memo[i] > max_length ) { best_end = i; max_length = memo[i]; } } // Code that builds the longest increasing subsequence using "prev" std::vector<int> results; results.reserve(sz); std::stack<int> stk; int current = best_end; while (current != -1) { stk.push(s[current]); current = prev[current]; } while (!stk.empty()) { results.push_back(stk.top()); stk.pop(); } return results; }
реализация без стека просто перевернуть вектор
#include <iostream> #include <vector> #include <limits> std::vector<int> LIS( const std::vector<int> &v ) { auto sz = v.size(); if(!sz) return v; std::vector<int> memo(sz, 0); std::vector<int> prev(sz, -1); memo[0] = 1; int best_end = 0; int max_length = std::numeric_limits<int>::min(); for (auto i = 1; i < sz; ++i) { for ( auto j = 0; j < i ; ++j) { if (s[j] < s[i] && memo[i] < memo[j] + 1) { memo[i] = memo[j] + 1; prev[i] = j; } } if(memo[i] > max_length) { best_end = i; max_length = memo[i]; } } // create results std::vector<int> results; results.reserve(v.size()); auto current = best_end; while (current != -1) { results.push_back(s[current]); current = prev[current]; } std::reverse(results.begin(), results.end()); return results; }
вот три шага оценки проблемы с точки зрения динамического программирования:
- рецидива определение: свойство maxlength(я) == 1 + свойство maxlength(J), где 0 массив[Дж]
- граница параметра повторения: может быть от 0 до I - 1 подпоследовательностей, передаваемых в качестве параметра
- порядок оценки: поскольку он увеличивает подпоследовательность, он должен быть оценен от 0 до n
если взять в качестве примера последовательность {0, 8, 2, 3, 7, 9}, по индексу:
- [0], мы получим подпоследовательность {0} в качестве базового варианта
- [1] у нас есть 1 новая подпоследовательность {0, 8}
- [2] попытка оценить две новые последовательности {0, 8, 2} и {0, 2} путем добавления элемента с индексом 2 к существующим подпоследовательностям-допустима только одна, поэтому добавление третьей возможной последовательности {0, 2} только к списку параметров ...
вот рабочий код C++11:
#include <iostream> #include <vector> int getLongestIncSub(const std::vector<int> &sequence, size_t index, std::vector<std::vector<int>> &sub) { if(index == 0) { sub.push_back(std::vector<int>{sequence[0]}); return 1; } size_t longestSubSeq = getLongestIncSub(sequence, index - 1, sub); std::vector<std::vector<int>> tmpSubSeq; for(std::vector<int> &subSeq : sub) { if(subSeq[subSeq.size() - 1] < sequence[index]) { std::vector<int> newSeq(subSeq); newSeq.push_back(sequence[index]); longestSubSeq = std::max(longestSubSeq, newSeq.size()); tmpSubSeq.push_back(newSeq); } } std::copy(tmpSubSeq.begin(), tmpSubSeq.end(), std::back_insert_iterator<std::vector<std::vector<int>>>(sub)); return longestSubSeq; } int getLongestIncSub(const std::vector<int> &sequence) { std::vector<std::vector<int>> sub; return getLongestIncSub(sequence, sequence.size() - 1, sub); } int main() { std::vector<int> seq{0, 8, 2, 3, 7, 9}; std::cout << getLongestIncSub(seq); return 0; }
вот реализация Scala алгоритма O (n^2):
object Solve { def longestIncrSubseq[T](xs: List[T])(implicit ord: Ordering[T]) = { xs.foldLeft(List[(Int, List[T])]()) { (sofar, x) => if (sofar.isEmpty) List((1, List(x))) else { val resIfEndsAtCurr = (sofar, xs).zipped map { (tp, y) => val len = tp._1 val seq = tp._2 if (ord.lteq(y, x)) { (len + 1, x :: seq) // reversely recorded to avoid O(n) } else { (1, List(x)) } } sofar :+ resIfEndsAtCurr.maxBy(_._1) } }.maxBy(_._1)._2.reverse } def main(args: Array[String]) = { println(longestIncrSubseq(List( 0, 8, 4, 12, 2, 10, 6, 14, 1, 9, 5, 13, 3, 11, 7, 15))) } }
вот еще одна реализация O (n^2) JAVA. Нет рекурсии / memoization для создания фактической подпоследовательности. Просто массив строк, который хранит фактические LIS на каждом этапе и массив для хранения длины LIS для каждого элемента. Чертовски просто. Взгляните:
import java.io.BufferedReader; import java.io.InputStreamReader; /** * Created by Shreyans on 4/16/2015 */ class LNG_INC_SUB//Longest Increasing Subsequence { public static void main(String[] args) throws Exception { BufferedReader br=new BufferedReader(new InputStreamReader(System.in)); System.out.println("Enter Numbers Separated by Spaces to find their LIS\n"); String[] s1=br.readLine().split(" "); int n=s1.length; int[] a=new int[n];//Array actual of Numbers String []ls=new String[n];// Array of Strings to maintain LIS for every element for(int i=0;i<n;i++) { a[i]=Integer.parseInt(s1[i]); } int[]dp=new int[n];//Storing length of max subseq. int max=dp[0]=1;//Defaults String seq=ls[0]=s1[0];//Defaults for(int i=1;i<n;i++) { dp[i]=1; String x=""; for(int j=i-1;j>=0;j--) { //First check if number at index j is less than num at i. // Second the length of that DP should be greater than dp[i] // -1 since dp of previous could also be one. So we compare the dp[i] as empty initially if(a[j]<a[i]&&dp[j]>dp[i]-1) { dp[i]=dp[j]+1;//Assigning temp length of LIS. There may come along a bigger LIS of a future a[j] x=ls[j];//Assigning temp LIS of a[j]. Will append a[i] later on } } x+=(" "+a[i]); ls[i]=x; if(dp[i]>max) { max=dp[i]; seq=ls[i]; } } System.out.println("Length of LIS is: " + max + "\nThe Sequence is: " + seq); } }
код в действии:http://ideone.com/sBiOQx
Это можно решить в O (n^2) с помощью динамического программирования. Код Python для того же будет выглядеть так: -
def LIS(numlist): LS = [1] for i in range(1, len(numlist)): LS.append(1) for j in range(0, i): if numlist[i] > numlist[j] and LS[i]<=LS[j]: LS[i] = 1 + LS[j] print LS return max(LS) numlist = map(int, raw_input().split(' ')) print LIS(numlist)
для ввода:
5 19 5 81 50 28 29 1 83 23
вывод:
[1, 2, 1, 3, 3, 3, 4, 1, 5, 3] 5
list_index выходного списка-это list_index входного списка. Значение в заданном list_index в выходном списке обозначает самую длинную возрастающую длину подпоследовательности для этого list_index.
вот реализация java O(nlogn)
import java.util.Scanner; public class LongestIncreasingSeq { private static int binarySearch(int table[],int a,int len){ int end = len-1; int beg = 0; int mid = 0; int result = -1; while(beg <= end){ mid = (end + beg) / 2; if(table[mid] < a){ beg=mid+1; result = mid; }else if(table[mid] == a){ return len-1; }else{ end = mid-1; } } return result; } public static void main(String[] args) { // int[] t = {1, 2, 5,9,16}; // System.out.println(binarySearch(t , 9, 5)); Scanner in = new Scanner(System.in); int size = in.nextInt();//4; int A[] = new int[size]; int table[] = new int[A.length]; int k = 0; while(k<size){ A[k++] = in.nextInt(); if(k<size-1) in.nextLine(); } table[0] = A[0]; int len = 1; for (int i = 1; i < A.length; i++) { if(table[0] > A[i]){ table[0] = A[i]; }else if(table[len-1]<A[i]){ table[len++]=A[i]; }else{ table[binarySearch(table, A[i],len)+1] = A[i]; } } System.out.println(len); } }
это реализация Java в O (n^2). Я просто не использовал двоичный поиск, чтобы найти наименьший элемент в S, который >= чем X. Я просто использовал цикл for. Использование двоичного поиска сделало бы сложность при O (n logn)
public static void olis(int[] seq){ int[] memo = new int[seq.length]; memo[0] = seq[0]; int pos = 0; for (int i=1; i<seq.length; i++){ int x = seq[i]; if (memo[pos] < x){ pos++; memo[pos] = x; } else { for(int j=0; j<=pos; j++){ if (memo[j] >= x){ memo[j] = x; break; } } } //just to print every step System.out.println(Arrays.toString(memo)); } //the final array with the LIS System.out.println(Arrays.toString(memo)); System.out.println("The length of lis is " + (pos + 1)); }
проверка кода в java для самого длинного увеличения подпоследовательности с элементами массива
/** ** Java Program to implement Longest Increasing Subsequence Algorithm **/ import java.util.Scanner; /** Class LongestIncreasingSubsequence **/ class LongestIncreasingSubsequence { /** function lis **/ public int[] lis(int[] X) { int n = X.length - 1; int[] M = new int[n + 1]; int[] P = new int[n + 1]; int L = 0; for (int i = 1; i < n + 1; i++) { int j = 0; /** Linear search applied here. Binary Search can be applied too. binary search for the largest positive j <= L such that X[M[j]] < X[i] (or set j = 0 if no such value exists) **/ for (int pos = L ; pos >= 1; pos--) { if (X[M[pos]] < X[i]) { j = pos; break; } } P[i] = M[j]; if (j == L || X[i] < X[M[j + 1]]) { M[j + 1] = i; L = Math.max(L,j + 1); } } /** backtrack **/ int[] result = new int[L]; int pos = M[L]; for (int i = L - 1; i >= 0; i--) { result[i] = X[pos]; pos = P[pos]; } return result; } /** Main Function **/ public static void main(String[] args) { Scanner scan = new Scanner(System.in); System.out.println("Longest Increasing Subsequence Algorithm Test\n"); System.out.println("Enter number of elements"); int n = scan.nextInt(); int[] arr = new int[n + 1]; System.out.println("\nEnter "+ n +" elements"); for (int i = 1; i <= n; i++) arr[i] = scan.nextInt(); LongestIncreasingSubsequence obj = new LongestIncreasingSubsequence(); int[] result = obj.lis(arr); /** print result **/ System.out.print("\nLongest Increasing Subsequence : "); for (int i = 0; i < result.length; i++) System.out.print(result[i] +" "); System.out.println(); } }
Это можно решить в O (n^2) с помощью динамического программирования.
обрабатывайте входные элементы по порядку и ведите список кортежей для каждого элемента. Каждый кортеж (A, B), для элемента i будет обозначен, A = Длина самой длинной возрастающей подпоследовательности, заканчивающейся на i и B = индекс предшественника списка[i] в самой длинной возрастающей подпоследовательности, заканчивающейся на list[i].
начать с элемента 1, Список кортежа для элемента 1 будет [(1,0)] для элемента i отсканируйте список 0..я и найти список элементов[k] такой, что list[k]
в конце концов, найти все элементы с максимальным значением A (Длина Lis заканчивается на элементе) и вернуться с помощью кортежей, чтобы получить список.
Я поделился кодом для того же at http://www.edufyme.com/code/?id=66f041e16a60928b05a7e228a89c3799
o(n^2) реализация java:
void LIS(int arr[]){ int maxCount[]=new int[arr.length]; int link[]=new int[arr.length]; int maxI=0; link[0]=0; maxCount[0]=0; for (int i = 1; i < arr.length; i++) { for (int j = 0; j < i; j++) { if(arr[j]<arr[i] && ((maxCount[j]+1)>maxCount[i])){ maxCount[i]=maxCount[j]+1; link[i]=j; if(maxCount[i]>maxCount[maxI]){ maxI=i; } } } } for (int i = 0; i < link.length; i++) { System.out.println(arr[i]+" "+link[i]); } print(arr,maxI,link); } void print(int arr[],int index,int link[]){ if(link[index]==index){ System.out.println(arr[index]+" "); return; }else{ print(arr, link[index], link); System.out.println(arr[index]+" "); } }
def longestincrsub(arr1): n=len(arr1) l=[1]*n for i in range(0,n): for j in range(0,i) : if arr1[j]<arr1[i] and l[i]<l[j] + 1: l[i] =l[j] + 1 l.sort() return l[-1] arr1=[10,22,9,33,21,50,41,60] a=longestincrsub(arr1) print(a)
хотя есть способ, с помощью которого вы можете решить это в O(nlogn) времени(это решает в O(n^2) времени), но все же этот способ дает подход динамического программирования, который также хорош .