参考链接:https://blog.csdn.net/lxt_Lucia/article/details/81206439
我们之前探讨过的很多算法都是利用问题的可划分性以及子问题之间的相似性来进行归纳,降低求解的复杂度,动态规划也不例外。每个子问题的求解过程都构成一个“阶段”。在完成前个阶段的计算后,动态规划才会执行下一阶段的计算。
为了保证这些计算能够按顺序、不重复地进行,动态规划要求已经求解的子问题不受后续阶段的影响。这个条件也被叫做“无后效性”。换言之,动态规划对状态空间的遍历构成张有向无环图(DAG), 遍历顺序就是该有向无环图的一个拓扑序。 有向无环图中的节点对应问题中的“状态”,图中的边则对应状态之间的“转移”,转移的选取就是动态规划中的“决策”。
在很多情况下,动态规划用于求解最优化问题,此时,下阶段的最优解应该能够由前面各阶段子问题的最优解导出。这个条件被称为“最优子结构性质”,当然,这儿一种比较片面的说法,它其实告诉我们,动态规划在阶段计算完成时,只会在每个状态上保留与最终解集相关的部分代表信息,这些代表信息应该具有可重复的求解过程,并能够导出后续阶段的代表信息。这样一来, 动态规划对状态的抽象和子问题的重叠递进才能够起到优化作用。
“状态”“阶段”和“决策”是构成动态规划算法的三要素,而“子问题重叠性”“无后效性”和“最优子结构性质”是问题能用动态规划求解的三个基本条件。动态规划算法把相同的计算过程作用于各阶段的同类子问题,就好像把个固定的公式在格式相同的若干输入数据上运行。因此,我们一般只需要定 义出DP的计算过程,就可以编程实现了。这个计算过程就被称为“状态转移方程”。
线性DP
- 最长上升子序列
给定一个长度为N的数列,求数值严格单调递增的子序列的长度最长是多少。
输入格式
第一行包含整数N。
第二行包含N个整数,表示完整序列。
输出格式
输出一个整数,表示最大长度。
数据范围
1≤N≤10001≤N≤1000,
−109≤数列中的数≤109−109≤数列中的数≤109输入样例:
7 3 1 2 1 8 5 6
输出样例:
4
解法1:动态规划:
我们都知道,动态规划的一个特点就是当前解可以由上一个阶段的解推出, 由此,把我们要求的问题简化成一个更小的子问题。子问题具有相同的求解方式,只不过是规模小了而已。最长上升子序列就符合这一特性。我们要求n个数的最长上升子序列,可以求前n-1个数的最长上升子序列,再跟第n个数进行判断。求前n-1个数的最长上升子序列,可以通过求前n-2个数的最长上升子序列……直到求前1个数的最长上升子序列,此时LIS当然为1。
让我们举个例子:求 2 7 1 5 6 4 3 8 9 的最长上升子序列。我们定义d(i) (i∈[1,n])来表示前i个数以A[i]结尾的最长上升子序列长度。
前1个数 d(1)=1 子序列为2;
前2个数 7前面有2小于7 d(2)=d(1)+1=2 子序列为2 7
前3个数 在1前面没有比1更小的,1自身组成长度为1的子序列 d(3)=1 子序列为1
前4个数 5前面有2小于5 d(4)=d(1)+1=2 子序列为2 5
前5个数 6前面有2 5小于6 d(5)=d(4)+1=3 子序列为2 5 6
前6个数 4前面有2小于4 d(6)=d(1)+1=2 子序列为2 4
前7个数 3前面有2小于3 d(3)=d(1)+1=2 子序列为2 3
前8个数 8前面有2 5 6小于8 d(8)=d(5)+1=4 子序列为2 5 6 8
前9个数 9前面有2 5 6 8小于9 d(9)=d(8)+1=5 子序列为2 5 6 8 9
d(i)=max{d(1),d(2),……,d(i)} 我们可以看出这9个数的LIS为d(9)=5
总结一下,d(i)就是找以A[i]结尾的,在A[i]之前的最长上升子序列+1,当A[i]之前没有比A[i]更小的数时,d(i)=1。所有的d(i)里面最大的那个就是最长上升子序列。其实说的通俗点,就是每次都向前找比它小的数和比它大的数的位置,将第一个比它大的替换掉,这样操作虽然LIS序列的具体数字可能会变,但是很明显LIS长度还是不变的,因为只是把数替换掉了,并没有改变增加或者减少长度。但是我们通过这种方式是无法求出最长上升子序列具体是什么的,这点和最长公共子序列不同。
状态设计:F [ i ] 代表以 A [ i ] 结尾的 LIS 的长度
状态转移:F [ i ] = max { F [ j ] + 1 ,F [ i ] } (1 <= j < i,A[ j ] < A[ i ])
边界处理:F [ i ] = 1 (1 <= i <= n)
时间复杂度:O (n^2)
import java.util.*; public class Main { public static void main(String[] args) { Scanner scanner=new Scanner(System.in); int num=scanner.nextInt(); int arr[]=new int[num]; for (int i = 0; i < num; i++) { arr[i]=scanner.nextInt(); } int dp[]=new int[num]; Arrays.fill(dp,1); for (int i = 0; i < num; i++) { for (int j = 0; j <i ; j++) { if (arr[i]>arr[j]) dp[i]=Math.max(dp[i],dp[j]+1); } } int max=0; for (int i = 1; i <num ; i++) { max=Math.max(max,dp[i]); } System.out.println(max); } }
解法2:贪心+二分:
思路:
新建一个 low 数组,low [ i ]表示长度为i的LIS结尾元素的最小值。对于一个上升子序列,显然其结尾元素越小,越有利于在后面接其他的元素,也就越可能变得更长。因此,我们只需要维护 low 数组,对于每一个a[ i ],如果a[ i ] > low [当前最长的LIS长度],就把 a [ i ]接到当前最长的LIS后面,即low [++当前最长的LIS长度] = a [ i ]。
那么,怎么维护 low 数组呢?
对于每一个a [ i ],如果a [ i ]能接到 LIS 后面,就接上去;否则,就用 a [ i ] 取更新 low 数组。具体方法是,在low数组中找到第一个大于等于a [ i ]的元素low [ j ],用a [ i ]去更新 low [ j ]。如果从头到尾扫一遍 low 数组的话,时间复杂度仍是O(n^2)。我们注意到 low 数组内部一定是单调不降的,所有我们可以二分 low 数组,找出第一个大于等于a[ i ]的元素。二分一次 low 数组的时间复杂度的O(lgn),所以总的时间复杂度是O(nlogn)。
我们再举一个例子:有以下序列A[ ] = 3 1 2 6 4 5 10 7,求LIS长度。
我们定义一个B[ i ]来储存可能的排序序列,len 为LIS长度。我们依次把A[ i ]有序地放进B[ i ]里。
(为了方便,i的范围就从1~n表示第i个数)
A[1] = 3,把3放进B[1],此时B[1] = 3,此时len = 1,最小末尾是3
A[2] = 1,因为1比3小,所以可以把B[1]中的3替换为1,此时B[1] = 1,此时len = 1,最小末尾是1
A[3] = 2,2大于1,就把2放进B[2] = 2,此时B[ ]={1,2},len = 2
同理,A[4]=6,把6放进B[3] = 6,B[ ]={1,2,6},len = 3
A[5]=4,4在2和6之间,比6小,可以把B[3]替换为4,B[ ] = {1,2,4},len = 3
A[6] = 5,B[4] = 5,B[ ] = {1,2,4,5},len = 4
A[7] = 10,B[5] = 10,B[ ] = {1,2,4,5,10},len = 5
A[8] = 7,7在5和10之间,比10小,可以把B[5]替换为7,B[ ] = {1,2,4,5,7},len = 5
最终我们得出LIS长度为5。但是,但是!!这里的1 2 4 5 7(这只是其中一种情况,1 2 4 5 10也是一种情况)很明显并不是正确的最长上升子序列。是的,B序列并不表示最长上升子序列,它只表示相应最长子序列长度的排好序的最小序列。这有什么用呢?我们最后一步7替换10并没有增加最长子序列的长度,而这一步的意义,在于记录最小序列,代表了一种“最可能性”。假如后面还有两个数据8和9,那么B[6]将更新为8,B[7]将更新为9,len就变为7,可以自行体会它的作用。
因为在B中插入的数据是有序的,不需要移动,只需要替换,所以可以用二分查找插入的位置,那么插入n个数的时间复杂度为〇(logn),这样我们会把这个求LIS长度的算法复杂度降为了〇(nlogn)。话不多说了,show me the code!
代码实现:
#include <bits/stdc++.h> using namespace std; const int maxn =100000+5, INF =INT_MAX; int low[maxn], a[maxn]; int n, ans; int binary_search(int *a, int r, int x) //二分查找,返回a数组中第一个>=x的位置 { int l = 1, mid; while(l <r) { mid = (l+r) >> 1; if(a[mid] >= x) r=mid; else l = mid + 1; } return l; } int main() { scanf("%d", &n); for(int i=1; i<=n; i++) { scanf("%d", &a[i]); low[i] = INF; //由于low中存的是最小值,所以low初始化为INF } low[1] = a[1]; ans = 1; //初始时LIS长度为1 for(int i=2; i<=n; i++) { if(a[i] > low[ans]) //若a[i]>=low[ans],直接把a[i]接到后面 low[++ans] = a[i]; else //否则,找到low中第一个>=a[i]的位置low[j],用a[i]更新low[j] low[binary_search(low, ans, a[i])] = a[i]; } printf("%d ", ans); //输出答案 return 0; }