引出:
首先我们拿北京大学ACM在线测试系统题库中的一道题来做为例子,然后我们在解题的过程中初步接触下动态规划(DP)算法,原题地址:
http://poj.org/problem?id=2533
原题是英文的,简单翻译一下:
给出一个序列a1,a2,a3,a4,a5,a6,a7….an,求它的一个子序列(设为i1,i2,…in),使得这个子序列满足这样的性质,i1<i2<i3<…<in并且这个子序列的长度最长,输出这个最长的长度。比如一个序列(1, 7, 3, 5, 9, 4, 8)有很多个子序列比如(1, 7), (3, 4, 8),但是它的最长子序列是(1, 3, 5, 8)或(1, 3, 5, 9),长度为4(这是递增情况,同理递减也为同一类问题,不多做赘述)。
动态规划基本思想:将待求解问题分解成若干个子问题,先求解子问题,然后从这些子问题的解得到原问题的解。经分解得到子问题往往不是互相独立的,有些子问题被重复计算了很多次。如果我们能够保存已解决的子问题的答案,而在需要时再找出已求得的答案,这样就可以避免大量的重复计算,节省时间。我们可以用一个表来记录所有已解的子问题的答案。不管该子问题以后是否被用到,只要它被计算过,就将其结果填入表中。这就是动态规划法的基本思路。
分析:
分析上述这个问题,使用动态规划是有效的方法,我们可以把求最长子序列串的问题,分解为求长度为n-1序列最长子序列串的子问题,依次分解成多个子问题,接下里我们详细来看一下算法步骤,首先我们用一个数组num[]来存放序列:
num[] = {1,7,3,5,9,4,8}
同时用另一个同样长度的数组len[]来存放,以num中对位数为结尾的最长子序列长度,初始值为1(因为自身长度即为1)
len[] = {1,1,1,1,1,1,1}
然后我们使用双重循环从低位开始遍历,第一轮拿num[0]与num[1]比较,因为1<7,并且len[0]<=len[1],说明已num[1]为尾的序列长度比以num[0]的序列长1,所以len[1]=len[0]+1,第一轮遍历后len数组变为
len[] = {1,2,1,1,1,1,1}
第二轮我们依次拿num[0]和num[1],与num[2]比较,遍历完后len变为
len[] = {1,2,2,1,1,1,1}
依次循环,直至序列结尾,我们把所有轮次len的变化列出来,方便大家查看变化,如下:
1,2,1,1,1,1,1————第1轮遍历
1,2,2,1,1,1,1————第2轮遍历
1,2,2,3,1,1,1————第3轮遍历
1,2,2,3,4,1,1————第4轮遍历
1,2,2,3,4,3,1————第5轮遍历
1,2,2,3,4,3,4————第6轮遍历
由上表格,我们可以清晰的看出len数组体现的动态规划思想,将前边计算得出的结论存放,在后续问题中用到的时候直接从存放的数据中读取,而不需要做重复的运算,大大降低了冗余运算量。
至此,希望这篇文章能够让大家对动态规划有一个初步的认识,余后可以去了解下更复杂的场景,最后附上我解答那道题的java源码(源码是基于北京大学在线评测系统Problem2533输入格式所写,大家可将输入的部分替换为直接赋值)。
源码:
- import java.io.PrintWriter;
- import java.util.Scanner;
- public class Test2533_DP {
- static Scanner cin = new Scanner(System.in);
- static PrintWriter cout = new PrintWriter(System.out, true);
- public static void main(String[] args)
- {
- int n = cin.nextInt();//获取序列长度
- int[] num = new int[n];
- int[] len = new int[n];
- int max = 1;
- for(int i=0;i<n;i++){
- num[i] = cin.nextInt();//依次输入序列数字,空格相隔
- len[i] = 1;
- }
- for(int i=1;i<n;i++){
- for(int j=0;j<i;j++){
- if(num[i]>num[j] && len[i]<len[j]+1)
- len[i] = len[j] + 1;
- }
- if(max<len[i])
- max = len[i];
- }
- cout.println(max);
- }
- }