实在愚钝,虽然是以前看过的算法,今天也是折腾了一天才稍微弄懂了一些。特此记下笔记

  第一次遇到这个问题的场景是猴子摘桃问题,原题如下:

  小猴子下山,沿着下山的路有一排桃树,每棵树都结了一些桃子。小猴子想摘桃子,但是有一些条件需要遵守,小猴子只能沿着下山的方向走,
不能回头,每颗树最多摘一个,而且一旦摘了一棵树的桃子,就不能再摘比这棵树结的桃子少的树上的桃子。那么小猴子最多能摘到几颗桃子呢?
举例说明,比如有5棵树,分别结了10,4,5,12,8颗桃子,那么小猴子最多能摘3颗桃子,来自于结了4,5,8颗桃子的桃树

  首先讲第一种解法:

  要明白这题最关键的一步就是要清楚,不要直接拿数组中的元素一个个去比较,只需要知道,前 i 个元素中最长的递增子序列(LIS)的长度,或者,以第 i 个元素结尾的LIS的长度 ,第1个元素结尾的LIS的只有他自己一个,所以result[0] = 1,以第2个元素结尾的LIS则之需要和第1个元素peach[0]比较即可,若大于第1个元素,则在result[0]++ 就是他的LIS ,若小于,则同样result[1]=1 , 很明显,第i个元素结尾的LIS起码都是1。。  以后的任意i值,都可以依次类推获得到结果。 以下是该方法的算法代码:

/*小猴子下山,沿着下山的路有一排桃树,每棵树都结了一些桃子。小猴子想摘桃子,但是有一些条件需要遵守,小猴子只能沿着下山的方向走,不能回头,每颗树最多摘一个,而且一旦摘了一棵树的桃子,就不能再摘比这棵树结的桃子少的树上的桃子。那么小猴子最多能摘到几颗桃子呢?举例说明,比如有5棵树,分别结了10,4,5,12,8颗桃子,那么小猴子最多能摘3颗桃子,来自于结了4,5,8颗桃子的桃树。*/int peaches[] = new int[]{4,4,5,2,3,156,15,6156,156,165,15,6};@Testpublic void test222(){/*Scanner in = new Scanner(System.in );System.out.print("请输入数的颗树:");int trees = Integer.parseInt(in.nextLine().trim());peaches = new int[trees];for (int i = 0; i < peaches.length; i++) {peaches[i] = Integer.parseInt(in.nextLine().trim());}*/printArray(peaches);System.out.println(pick(peaches));System.out.println(findMax2(peaches));}/*    523156156156156165156*/// 思路:求出以位置所有 以 peach[i](存入result[i]中)结尾的最长递增子序列的长度--->根据每个 result[0~j],// 拿peach[i]和peach[j]比较,如果peach[i]>peach[j],且result[j]+1>result[i],很明显,就该把result[i]值加1// 所以,这种方法起码能找出一个最长序列,有时候可以找出多条,但有个条件,结尾的peach[i] 一定不相同int pick(int[] peaches) {int max = 1;int length = peaches.length;// sequences存储桃树peach[i]每个位置,最长序列的每个元素。List<ArrayList<Integer>> sequences = new ArrayList<ArrayList<Integer>>();// 记录每个位置的最长递增子序列的长度int result[] = new int[length];for (int i = 0; i < length; i++) {result[i] = 1;ArrayList<Integer> list= new ArrayList<Integer>();// 相当于每个位置初始化设置result[i]=1并且把自己放进去  ---->为了存好没进入循环的以及进入循环没进入关键判断的元素
            list.add(peaches[i]);for (int j = 0; j < i; j++) {//必須新new一個,每一次循环,都是一次新的比较和 , 即和result[0 ~ i-1]的比较// 不能是clear,因为这个newL是被引用的,所以,不能修改原对象的值ArrayList<Integer> newL = new ArrayList<Integer>();//这里也要初始化一次 ----> 为了进入判断后失去了本身元素,导致后面连锁的错误
                newL.add(peaches[i]);//关键判断: 如果是i位置大于j位置,且j位置的最长递增子序列的长度+1长于目前i位置的最长递增子序列的长度,则更新i位置的最长递增子序列if (peaches[j] <= peaches[i] && result[j] + 1 > result[i]){result[i] = result[j] + 1;//j为此时比较的位置上最大的序列集合存储的索引
                    newL.addAll(sequences.get(j));list = newL;}}sequences.add(list);System.out.println(sequences);}for (int i : result)max = i > max ? i : max;System.out.println(sequences);System.out.println(Arrays.toString(result));/* for(ArrayList<Integer> l : sequences){// if(l.size() >= max){System.out.println(l);}}   */return max;}/** 第二种方法的思路很简单,即开辟一段新的空间存储相应长度LCS中的最大元素的最小元素,举个例子:1,2,5,4 ...  当扫描到5(i=2)的时候,maxV[3] = 5 , result[2] = 3 * ,maxV[3] 即指的是 长度为3的LCS中最大元素的最小元素,所以当扫描到3的时候,maxV[3]就更新为4了 , 因为 5>4 ,且他们的result的长度都为3 这样子,就可以直接比较当前的peach[i]和* nMaxResult就可以算出当前i的result*/// 方法二private int findMax2(int [] peaches){int [] maxV = new int[peaches.length];maxV[1] = peaches[0];maxV[0] = -1;int [] result = new int[peaches.length];for(int i=0 ; i<result.length ; i++){result[i] = 1;}// 最长子序列变量int nMaxResult = 1;for(int i=1 ; i<peaches.length ; i++){int j;//只要中一个小于peaches[i],更新i的最长子序列的长度,退出遍历//这里可以换成二分查找for(j = nMaxResult ; j>=1 ; j--){//for(j = result[i-1] ; j>=1 ; j--){if(peaches[i] >= maxV[j] && result[i]<(j+1)){result[i] = j+1;break;}}if(result[i] > nMaxResult){nMaxResult = result[i];maxV[result[i]] = peaches[i];}else if( maxV[j]<peaches[i] &&peaches[i]<maxV[j+1] ){// 持续更新LCS中最大元素的最小元素 , 保证后面的比较是正确的// 比如 8 9 1 2 3 走完前两步后maxV[1] = 8 , maxV[2] = 9 再走完两部后maxV[1]=1 maxV[2]=2,// 这样就保证了当扫描第3的时候能够匹配成功,这就能够是LIS尽可能长了。maxV[result[i]] = peaches[i];}}System.out.println(Arrays.toString(result));return nMaxResult;}

  

  以上多了我自己添加了打印序列的代码,同学们也可以自己屏蔽掉(想看的话,是从后往前看哦,我懒得改了)。同时我也偷了个懒,顺便把第二种发放贴了上去了。在这里继续介绍下去...

  第二种方法的思路很简单,即开辟一段新的空间存储相应长度LCS中的最大元素的最小元素,举个例子:1,2,5,4 ...  当扫描到5(i=2)的时候,maxV[3] = 5 , LIS[2] = 3 ,maxV[3] 即指的是 长度为3的LCS中最大元素的最小元素,所以当扫描到3的时候,maxV[3]就更新为4了 , 因为 5>4 ,且他们的LIS的长度都为3

  这样子,就可以直接比较当前的peach[i]和nMaxLIS就可以算出当前i的LIS

  这样做是有原因的! 其实现在这么做的话,效率还是n2 , 其实还有第三种代码,因为maxV中的记录肯定是满足maxV[1] < maxV[2] <maxV[3] ....细心的同学可以已经发现,这种规律可以用二分查找方法取代遍历 这样就可以把效率提高到 nlog2n (二分查找的效率是log2n)。依据单调递增,将上面便利部分,做以下改动

  最后将上面的代码换成二分查找去匹配,效率应该会更有改善,有兴趣的同学可以尝试

  以前纯属个人观点,如有错误还请大佬们海涵,还请大佬们指点

以下来自知乎,拿最长子序列做例子学习动态规划的。

作者:徐凯强 Andy
链接:https://www.zhihu.com/question/23995189/answer/35324479
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

动态规划中递推式的求解方法不是动态规划的本质。

我曾经作为省队成员参加过NOI,保送之后也给学校参加NOIP的同学多次讲过动态规划,我试着讲一下我理解的动态规划,争取深入浅出。希望你看了我的答案,能够喜欢上动态规划。

0. 动态规划的本质,是对问题状态的定义状态转移方程的定义
引自维基百科

dynamic programming is a method for solving a complex problem by breaking it down into a collection of simpler subproblems.

动态规划是通过拆分问题,定义问题状态和状态之间的关系,使得问题能够以递推(或者说分治)的方式去解决。
本题下的其他答案,大多都是在说递推的求解方法,但如何拆分问题,才是动态规划的核心。
拆分问题,靠的就是状态的定义状态转移方程的定义

1. 什么是状态的定义?

首先想说大家千万不要被下面的数学式吓到,这里只涉及到了函数相关的知识。

我们先来看一个动态规划的教学必备题:

给定一个数列,长度为N,
求这个数列的最长上升(递增)子数列(LIS)的长度.

1 7 2 8 3 4
为例。
这个数列的最长递增子数列是 1 2 3 4,长度为4;
次长的长度为3, 包括 1 7 8; 1 2 3 等.

要解决这个问题,我们首先要定义这个问题和这个问题的子问题。
有人可能会问了,题目都已经在这了,我们还需定义这个问题吗?需要,原因就是这个问题在字面上看,找不出子问题,而没有子问题,这个题目就没办法解决。

所以我们来重新定义这个问题:

给定一个数列,长度为N,
为:以数列中第k项结尾的最长递增子序列的长度.
中的最大值.

显然,这个新问题与原问题等价。
而对于来讲,都是的子问题:因为以第k项结尾的最长递增子序列(下称LIS),包含着以第中某项结尾的LIS。

上述的新问题也可以叫做状态,定义中的“为数列中第k项结尾的LIS的长度”,就叫做对状态的定义。
之所以把做“状态”而不是“问题” ,一是因为避免跟原问题中“问题”混淆,二是因为这个新问题是数学化定义的。

对状态的定义只有一种吗?当然不是
我们甚至可以二维的,以完全不同的视角定义这个问题:

给定一个数列,长度为N,
为:
在前i项中的,长度为k的最长递增子序列中,最后一位的最小值. .
若在前i项中,不存在长度为k的最长递增子序列,则为正无穷.
求最大的x,使得不为正无穷。

这个新定义与原问题的等价性也不难证明,请读者体会一下。
上述的就是状态,定义中的“为:在前i项中,长度为k的最长递增子序列中,最后一位的最小值”就是对状态的定义。

2. 什么是状态转移方程
上述状态定义好之后,状态和状态之间的关系式,就叫做状态转移方程。

比如,对于LIS问题,我们的第一种定义:

为:以数列中第k项结尾的最长递增子序列的长度.

设A为题中数列,状态转移方程为:

(根据状态定义导出边界情况)

用文字解释一下是:
以第k项结尾的LIS的长度是:保证第i项比第k项小的情况下,以第i项结尾的LIS长度加一的最大值,取遍i的所有值(i小于k)。

第二种定义:

为:在数列前i项中,长度为k的递增子序列中,最后一位的最小值

设A为题中数列,状态转移方程为:


否则:

(边界情况需要分类讨论较多,在此不列出,需要根据状态定义导出边界情况。)
大家套着定义读一下公式就可以了,应该不难理解,就是有点绕。

这里可以看出,这里的状态转移方程,就是定义了问题和子问题之间的关系。
可以看出,状态转移方程就是带有条件的递推式。

3. 动态规划迷思
本题下其他用户的回答跟动态规划都有或多或少的联系,我也讲一下与本答案的联系。

a. “缓存”,“重叠子问题”,“记忆化”:
这三个名词,都是在阐述递推式求解的技巧。以Fibonacci数列为例,计算第100项的时候,需要计算第99项和98项;在计算第101项的时候,需要第100项和第99项,这时候你还需要重新计算第99项吗?不需要,你只需要在第一次计算的时候把它记下来就可以了。
上述的需要再次计算的“第99项”,就叫“重叠子问题”。如果没有计算过,就按照递推式计算,如果计算过,直接使用,就像“缓存”一样,这种方法,叫做“记忆化”,这是递推式求解的技巧。这种技巧,通俗的说叫“花费空间来节省时间”。都不是动态规划的本质,不是动态规划的核心。

b. “递归”:
递归是递推式求解的方法,连技巧都算不上。

c. "无后效性",“最优子结构”:
上述的状态转移方程中,等式右边不会用到下标大于左边i或者k的值,这是"无后效性"的通俗上的数学定义,符合这种定义的状态定义,我们可以说它具有“最优子结构”的性质,在动态规划中我们要做的,就是找到这种“最优子结构”。
在对状态和状态转移方程的定义过程中,满足“最优子结构”是一个隐含的条件(否则根本定义不出来)。对状态和“最优子结构”的关系的进一步解释,什么是动态规划?动态规划的意义是什么? - 王勐的回答 写的很好,大家可以去读一下。

需要注意的是,一个问题可能有多种不同的状态定义和状态转移方程定义,存在一个有后效性的定义,不代表该问题不适用动态规划。这也是其他几个答案中出现的逻辑误区:
动态规划方法要寻找符合“最优子结构“的状态和状态转移方程的定义在找到之后,这个问题就可以以“记忆化地求解递推式”的方法来解决。而寻找到的定义,才是动态规划的本质。

有位答主说:

分治在求解每个子问题的时候,都要进行一遍计算
动态规划则存储了子问题的结果,查表时间为常数

这就像说多加辣椒的菜就叫川菜,多加酱油的菜就叫鲁菜一样,是存在误解的。

文艺的说,动态规划是寻找一种对问题的观察角度,让问题能够以递推(或者说分治)的方式去解决。寻找看问题的角度,才是动态规划中最耀眼的宝石!(大雾)

转载于:https://www.cnblogs.com/liujiaa/p/7757827.html

编程之美学习之最长子序列的解法相关推荐

  1. 24-单调递增最长子序列(多种解法总结)

    单调递增最长子序列 http://acm.nyist.edu.cn/JudgeOnline/problem.php?pid=17 时间限制:3000 ms  |  内存限制:65535 KB 难度:4 ...

  2. 编程之美学习笔记--一摞烙饼的排序

    问题:假设有n块大小不一的烙饼,翻烙饼时只能从最上面的烙饼开始,一次抓住最上面的几块饼,把它们上下颠倒个儿,那么最少要翻多少次,才能够达到最后的大小有序? 思路 先上一张图,可以很好的说明思路: 假设 ...

  3. 编程之美学习笔记(三):一摞烙饼的排序

    问题描述 星期五的晚上,一帮同事在希格玛大厦附近的"硬盘酒吧"多喝了几杯,程序员多喝了几杯之后谈什么呢?自然是算法 问题.有个同事说: "我以前在餐厅打工,顾客经常点非常 ...

  4. 编程之美(六)饮料供货

    在微软亚洲研究院上班,大家早上来的第一件事是干啥呢?查看邮件?NoNoNo,是去水房拿饮料:酸奶,豆浆,绿茶.王老吉.咖啡.可口可乐--(当然,还是有很多同事把拿饮料当做第二件事). 管理水房的阿姨们 ...

  5. 《编程之美》读书笔记08:2.9 Fibonacci序列

    <编程之美>读书笔记08:2.9 Fibonacci序列 计算Fibonacci序列最直接的方法就是利用递推公式 F(n+2)=F(n+1)+F(n).而用通项公式来求解是错误的,用浮点数 ...

  6. 学习思考之《编程之美》.

    一.智者说:无聊的时候来几道算法题,可以训练训练自己的思维嘛!难怪之前人家说数学好的人编程起来事半功倍,写算法的过程中真是深有体会啊!感觉就像是在做大学的高数题......本博文仅用来记录自己学习算法 ...

  7. 让多核CPU占用率曲线听你指挥(Windows实现)——《编程之美》1.1学习笔记

    让多核CPU占用率曲线听你指挥--<编程之美>1.1学习笔记 Problem: 写一个程序,让用户来决定Windows任务管理器(Task Manager)的CPU占用率.有以下几种情况: ...

  8. 《编程之美》学习笔记

    师兄留下了<编程之美>,今天翻开之后,打算开始学习,为下步工作准备: 先记下几句话吧: 1.题目关键不在于答案,在于思考问题的方法. 2.微软职位:1AR 协助研究员2,DEV 软件开发工 ...

  9. 从《编程之美》买票找零问题说起,娓娓道来卡特兰数——兼爬坑指南

    转自:从<编程之美>买票找零问题说起,娓娓道来卡特兰数--兼爬坑指南 引子: 大约两个月前,我在练习一些招聘的笔试题中,有一道和卡特兰数相关.那时还没来得及开始仔细看<编程之美> ...

  10. [编程之美]买票找零(卡特兰数)

    第一次看这题的时候没有好好注意,后来发现这是一类大问题,学习了卡特兰数这个概念,顺便又复习了高中的排列组合知识... 一.书中问题 先看一下书中引入卡特兰数的例子: <编程之美>4.3买票 ...

最新文章

  1. 关于loader加载的东西必须是继承sprite
  2. db2 如何导出insert语句_《MySQL 入门教程》第 23 篇 DML 语句之插入数据
  3. windows ping 不通虚拟机
  4. Python10分钟入门
  5. 七天来学习ASP.NET MVC (两)——ASP.NET MVC 数据传输
  6. BeginnersBook JSP、JSTL、Servlet 教程
  7. Linux htop工具使用详解
  8. 进程部分(IPC机制及生产者消费者模型)和线程部分
  9. 上海计算机三级网络,上海市计算机三级网络技术
  10. 打开visio后屏幕会不停的抖动是怎么回事
  11. php json输出后 u6563,肉肉's Blog
  12. 程序员都需要会的JVM调优总结 -Xms -Xmx -Xmn -Xss,附idea配置实战(程序员必学)
  13. 2019-3-5 梦
  14. java 四舍六入五成双_【数据小常识】“四舍六入五成双”
  15. Fresh gizmo
  16. CentOS下配置apache虚拟主机
  17. 使用伪类(before,after)给元素添加分割线(|)
  18. java 日期 第几周-java 获取给定日期属于当年第几周
  19. TrueLicense实现产品License验证
  20. XML 解析错误:找不到根元素

热门文章

  1. (1)信息熵,条件熵,信息增益,信息增益率
  2. spring教程笔记2
  3. python切片为列表增加元素_python – 使用切片语法来加入列表的一部分列表元素...
  4. linux安装opencv
  5. libSM.so.6: cannot open shared object file: No such file or directoryapt-file search libSM.so.6
  6. 2020.06.25 端午节快乐
  7. MySQL索引详细介绍
  8. windows查看8080端口并杀死进程
  9. python安装nodejs_linux上nodejs安装
  10. python3扫描_Python3实现TCP端口扫描器