编程之美学习之最长子序列的解法
实在愚钝,虽然是以前看过的算法,今天也是折腾了一天才稍微弄懂了一些。特此记下笔记
第一次遇到这个问题的场景是猴子摘桃问题,原题如下:
小猴子下山,沿着下山的路有一排桃树,每棵树都结了一些桃子。小猴子想摘桃子,但是有一些条件需要遵守,小猴子只能沿着下山的方向走,
不能回头,每颗树最多摘一个,而且一旦摘了一棵树的桃子,就不能再摘比这棵树结的桃子少的树上的桃子。那么小猴子最多能摘到几颗桃子呢?
举例说明,比如有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)。依据单调递增,将上面便利部分,做以下改动
最后将上面的代码换成二分查找去匹配,效率应该会更有改善,有兴趣的同学可以尝试
以前纯属个人观点,如有错误还请大佬们海涵,还请大佬们指点
以下来自知乎,拿最长子序列做例子学习动态规划的。
链接: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
编程之美学习之最长子序列的解法相关推荐
- 24-单调递增最长子序列(多种解法总结)
单调递增最长子序列 http://acm.nyist.edu.cn/JudgeOnline/problem.php?pid=17 时间限制:3000 ms | 内存限制:65535 KB 难度:4 ...
- 编程之美学习笔记--一摞烙饼的排序
问题:假设有n块大小不一的烙饼,翻烙饼时只能从最上面的烙饼开始,一次抓住最上面的几块饼,把它们上下颠倒个儿,那么最少要翻多少次,才能够达到最后的大小有序? 思路 先上一张图,可以很好的说明思路: 假设 ...
- 编程之美学习笔记(三):一摞烙饼的排序
问题描述 星期五的晚上,一帮同事在希格玛大厦附近的"硬盘酒吧"多喝了几杯,程序员多喝了几杯之后谈什么呢?自然是算法 问题.有个同事说: "我以前在餐厅打工,顾客经常点非常 ...
- 编程之美(六)饮料供货
在微软亚洲研究院上班,大家早上来的第一件事是干啥呢?查看邮件?NoNoNo,是去水房拿饮料:酸奶,豆浆,绿茶.王老吉.咖啡.可口可乐--(当然,还是有很多同事把拿饮料当做第二件事). 管理水房的阿姨们 ...
- 《编程之美》读书笔记08:2.9 Fibonacci序列
<编程之美>读书笔记08:2.9 Fibonacci序列 计算Fibonacci序列最直接的方法就是利用递推公式 F(n+2)=F(n+1)+F(n).而用通项公式来求解是错误的,用浮点数 ...
- 学习思考之《编程之美》.
一.智者说:无聊的时候来几道算法题,可以训练训练自己的思维嘛!难怪之前人家说数学好的人编程起来事半功倍,写算法的过程中真是深有体会啊!感觉就像是在做大学的高数题......本博文仅用来记录自己学习算法 ...
- 让多核CPU占用率曲线听你指挥(Windows实现)——《编程之美》1.1学习笔记
让多核CPU占用率曲线听你指挥--<编程之美>1.1学习笔记 Problem: 写一个程序,让用户来决定Windows任务管理器(Task Manager)的CPU占用率.有以下几种情况: ...
- 《编程之美》学习笔记
师兄留下了<编程之美>,今天翻开之后,打算开始学习,为下步工作准备: 先记下几句话吧: 1.题目关键不在于答案,在于思考问题的方法. 2.微软职位:1AR 协助研究员2,DEV 软件开发工 ...
- 从《编程之美》买票找零问题说起,娓娓道来卡特兰数——兼爬坑指南
转自:从<编程之美>买票找零问题说起,娓娓道来卡特兰数--兼爬坑指南 引子: 大约两个月前,我在练习一些招聘的笔试题中,有一道和卡特兰数相关.那时还没来得及开始仔细看<编程之美> ...
- [编程之美]买票找零(卡特兰数)
第一次看这题的时候没有好好注意,后来发现这是一类大问题,学习了卡特兰数这个概念,顺便又复习了高中的排列组合知识... 一.书中问题 先看一下书中引入卡特兰数的例子: <编程之美>4.3买票 ...
最新文章
- 关于loader加载的东西必须是继承sprite
- db2 如何导出insert语句_《MySQL 入门教程》第 23 篇 DML 语句之插入数据
- windows ping 不通虚拟机
- Python10分钟入门
- 七天来学习ASP.NET MVC (两)——ASP.NET MVC 数据传输
- BeginnersBook JSP、JSTL、Servlet 教程
- Linux htop工具使用详解
- 进程部分(IPC机制及生产者消费者模型)和线程部分
- 上海计算机三级网络,上海市计算机三级网络技术
- 打开visio后屏幕会不停的抖动是怎么回事
- php json输出后 u6563,肉肉's Blog
- 程序员都需要会的JVM调优总结 -Xms -Xmx -Xmn -Xss,附idea配置实战(程序员必学)
- 2019-3-5 梦
- java 四舍六入五成双_【数据小常识】“四舍六入五成双”
- Fresh gizmo
- CentOS下配置apache虚拟主机
- 使用伪类(before,after)给元素添加分割线(|)
- java 日期 第几周-java 获取给定日期属于当年第几周
- TrueLicense实现产品License验证
- XML 解析错误:找不到根元素
热门文章
- (1)信息熵,条件熵,信息增益,信息增益率
- spring教程笔记2
- python切片为列表增加元素_python – 使用切片语法来加入列表的一部分列表元素...
- linux安装opencv
- libSM.so.6: cannot open shared object file: No such file or directoryapt-file search libSM.so.6
- 2020.06.25 端午节快乐
- MySQL索引详细介绍
- windows查看8080端口并杀死进程
- python安装nodejs_linux上nodejs安装
- python3扫描_Python3实现TCP端口扫描器