首页 文章 精选 留言 我的

精选列表

搜索[计算机基础],共10000篇文章
优秀的个人博客,低调大师

java基础学习_基础语法(下)02_day06总结

========================================================================================================================================================== 涉及到的知识点有:1:二维数组(理解) (1)二维数组的定义 (2)二维数组的格式 格式一:(自动动态初始化) 格式二:(半自动动态初始化) 格式三:(静态初始化) 面试题: (3)二维数组的案例(掌握) A:二维数组的遍历 B:二维数组的求和 C:打印杨辉三角形(行数可以键盘录入)2:两个思考题(理解) (1)Java中的参数传递问题及图解。 (2)数据加密问题。 ==========================================================================================================================================================1:二维数组(理解) (1)二维数组的定义:元素是一维数组的数组。 (2)二维数组的格式: 格式一:(自动动态初始化) 数据类型[][] 数组名 = new 数据类型[m][n]; //常用这个格式。 数据类型 数组名[][] = new 数据类型[m][n]; //该格式可以,但是很少用了。 数据类型[] 数组名[] = new 数据类型[m][n]; //该格式也可以,但是很少用了。 m表示这个二维数组有多少个一维数组。 n表示每一个一维数组的元素个数。 举例: int[][] arr = new int[3][2]; 定义了一个二维数组arr。 这个二维数组有3个一维数组,名称是arr[0],arr[1],arr[2]。 每个一维数组有2个元素,可以通过arr[m][n]来获取。 即: arr[m][n] 表示获取第m+1个一维数组的第n+1个元素。 例如:arr[1][2] 表示获取第2个一维数组的第3个元素。 如下如图所示01: --------------------------------------- 格式二:(半自动动态初始化) 数据类型[][] 数组名 = new 数据类型[m][]; m表示这个二维数组有多少个一维数组。 这一次没有直接给出一维数组的元素个数,可以动态的给出。 举例: int[][] arr = new int[3][]; arr[0] = new int[2]; arr[1] = new int[3]; arr[2] = new int[1]; 如下如图所示02: --------------------------------------- 格式三:(静态初始化) 数据类型[][] 数组名 = new 数据类型[][]{ {...}, {...}, {...} }; 数据类型[][] 数组名 = { {...}, {...}, {...} }; 格式三的简化版格式 举例: int[][] arr = { { 1, 2, 3 }, { 4, 5, 6 }, { 7, 8, 9 } }; int[][] arr = { { 1, 2, 3 }, { 4, 5 }, { 6 } }; 如下如图所示03: 面试题: 下面定义的区别: int x, y; //定义了1个int类型的变量x,同时也定义了1个int类型的变量y。 //等价于 int x; int y; --------------------------------------- int[] x, y[]; //定义了1个int类型的一维数组x,同时也定义了1个int类型的二维数组y。 //等价于 int[] x; int[] y[]; (3)二维数组的案例(掌握): A:二维数组的遍历 外循环控制的是二维数组的长度,其实就是一维数组的个数。 内循环控制的是一维数组的长度。 public static void printArray2(int[][] arr) { for(int x = 0; x < arr.length; x++) { for(int y = 0; y < arr[x].length; y++) { System.out.print(arr[x][y]+" "); } System.out.println(); } } B:二维数组的求和 int sum = 0; for(int x = 0; x < arr.length; x++) { for(int y = 0; y < arr[x].length; y++) { sum += arr[x][y]; } } C:打印杨辉三角形(行数可以键盘录入) 1 /* 2 需求:打印杨辉三角形(行数可以键盘录入) 3 4 1 5 1 1 6 1 2 1 7 1 3 3 1 8 1 4 6 4 1 9 1 5 10 10 5 1 10 11 分析:看这种图像的规律: 12 A:任何一行的第一列和最后一列都是1。 13 B:从第三行开始,除去第一列和最后一列,剩余的每一列的数据是它上一行的前一列和它上一行的本列之和。 14 15 步骤: 16 A:首先定义一个二维数组。行数如果是n,我们把列数也先定义为n。 17 这个n的数据来自于键盘录入。 18 B:给这个二维数组任何一行的第一列和最后一列赋值为1。 19 C:按照规律给其他元素赋值: 20 从第三行开始,除去第一列和最后一列,剩余的每一列的数据是它上一行的前一列和它上一行的本列之和。 21 D:遍历这个二维数组。 22 */ 23 import java.util.Scanner; 24 25 class Array2Test3 { 26 public static void main(String[] args) { 27 //创建键盘录入对象。 28 Scanner sc = new Scanner(System.in); 29 30 //这个n的数据来自于键盘录入。 31 System.out.println("请输入一个数据:"); 32 int n = sc.nextInt(); 33 34 //定义二维数组 35 int[][] arr = new int[n][n]; 36 37 //给这个二维数组任何一行的第一列和最后一列赋值为1 38 for(int x = 0; x < arr.length; x++) { 39 arr[x][0] = 1; //任何一行第一列 40 arr[x][x] = 1; //任何一行的最后一列 41 } 42 43 //按照规律给其他元素赋值 44 //从第三行开始,除去第一列和最后一列,剩余的每一列的数据是它上一行的前一列和它上一行的本列之和。 45 for(int x = 2; x < arr.length; x++) { 46 //这里如果 y <= x 是有个小问题的,就是最后一列的问题,因为最后一列已经给过值了。 47 //所以这里要减去1 48 //并且y也应该从1开始,因为第一列也给过值了。 49 for(int y = 1; y <= x - 1; y++) { 50 //除去第一列和最后一列,剩余的每一列的数据是它上一行的前一列和它上一行的本列之和。 51 arr[x][y] = arr[x - 1][y - 1] + arr[x - 1][y]; 52 } 53 } 54 55 //遍历这个二维数组。 56 /* 57 for(int x = 0; x < arr.length; x++) { 58 for(int y = 0; y < arr[x].length; y++) { 59 System.out.print(arr[x][y]+"\t"); 60 } 61 System.out.println(); 62 } 63 */ 64 //这个时候,要注意了,内循环的变化必须和曾经讲过的九九乘法表类似。 65 for(int x = 0; x < arr.length; x++) { 66 for(int y = 0; y <= x; y++) { 67 System.out.print(arr[x][y]+"\t"); 68 } 69 System.out.println(); 70 } 71 } 72 } -----------------------------------------------------------------------------2:两个思考题(理解) (1)Java中的参数传递问题及图解。 基本类型:形式参数的改变对实际参数没有影响。 引用类型:形式参数的改变直接影响实际参数。 基本类型:传递的是基本类型的数据值。 引用类型:传递的是地址值。 小结:不管怎么说,都是值,即在Java中,只有值传递。 如下图所示04: (2)数据加密问题。 综合的小案例。 int index = 0; arr[index] = number % 10 = number / 1 % 10; index++; arr[index] = number / 10 % 10 = number / 10 % 10; index++; arr[index] = number / 10 / 10 % 10 = number /100 % 10; ...... --------------------------------------- int index = 0; while (number > 0) { arr[index] = number % 10; number /= 10; } 示例代码如下: 1 /* 2 把刚才的代码改进一下: 3 A:把数据改进为键盘录入 4 B:把代码改进为方法实现 5 6 7 另一个数据的测试: 8 number:1234567 9 第一步:7654321 10 第二步:2109876 11 第三步:6109872 12 13 知识点: 14 变量 15 数据类型 16 运算符 17 键盘录入 18 语句 19 方法 20 数组 21 */ 22 import java.util.Scanner; 23 24 class JiaMiDemo2 { 25 public static void main(String[] args) { 26 //创建键盘录入对象 27 Scanner sc = new Scanner(System.in); 28 29 //请输入一个数据 30 System.out.println("请输入一个数据(小于8位):"); 31 int number = sc.nextInt(); 32 33 //写功能实现把number进行加密 34 //调用 35 String result = jiaMi(number); 36 System.out.println("加密后的结果是:"+result); 37 } 38 39 /* 40 需求:写一个功能,把数据number实现加密。 41 两个明确: 42 返回值类型:String 为了做一个字符串的拼接。 43 参数列表:int number 44 */ 45 public static String jiaMi(int number) { 46 //定义数组 47 int[] arr = new int[8]; 48 49 //定义索引 50 int index = 0; 51 52 //把number中的数据想办法放到数组中 53 while(number > 0) { 54 arr[index] = number % 10; 55 index++; 56 number /= 10; 57 } 58 59 //把每个数据加5,然后对10取得余数 60 for(int x = 0; x < index; x++) { 61 arr[x] += 5; 62 arr[x] %= 10; 63 } 64 65 //把第一位和最后一位交换 66 int temp = arr[0]; 67 arr[0] = arr[index - 1]; 68 arr[index - 1] = temp; 69 70 //把数组的元素拼接成一个字符串返回 71 //定义一个空内容字符串 72 String s = ""; 73 74 for(int x = 0; x < index; x++) { 75 s += arr[x]; 76 } 77 78 return s; 79 } 80 } =============================================================================我的GitHub地址: https://github.com/heizemingjun 我的博客园地址: http://www.cnblogs.com/chenmingjun 我的蚂蚁笔记博客地址: http://blog.leanote.com/chenmingjun Copyright ©2018 黑泽明军 【转载文章务必保留出处和署名,谢谢!】

优秀的个人博客,低调大师

java基础学习_基础语法(下)01_day05总结

========================================================================================================================================================== 涉及到的知识点有:1:方法(掌握) (1)方法的定义 (2)方法的格式 (3)如何写一个方法呢?两个明确 (4)如何进行方法调用呢? A:有明确返回值的方法调用的方式 B:没有明确返回值的方法调用的方式:(即用void类型修饰的方法调用) (5)方法的案例 (6)方法的注意事项 (7)方法的重载 (8)方法重载的案例2:数组(一维数组)(掌握) (1)数组的定义 (2)数组的特点 (3)数组的定义格式 (4)数组的初始化方式 A:动态初始化 B:静态初始化(常用) (5)Java语言的内存分配 (6)数组的内存图解 (7)数组操作时的两个常见小问题 (8)数组的常见操作 A:数组的遍历 方式1 方式2 B:数组的最值 最大值 最小值 C:数组的逆序(逆置) 方式1://使用一个索引,需要考虑到变量的变化。 方式2://使用两个索引,不用考虑变量变化。 D:数组的查表(根据键盘录入索引,查找对应星期) E:数组的元素查找(查找指定元素第一次在数组中出现的索引) ==========================================================================================================================================================1:方法(掌握) (1)方法的定义:就是完成特定功能的代码块。 注意:在很多语言里面有函数的定义,而在Java中,函数被称为方法。 (2)方法的格式: 修饰符 返回值类型 方法名(参数类型 参数名1, 参数类型 参数名2, 参数类型 参数名3...) { 方法体语句; ---------------------------------------------------- return 返回值; 参数列表 } --------------------------------------- 修饰符:目前就用 public static。后面再详细讲解其他修饰符。 返回值类型:就是功能结果的数据类型。 方法名:就是起了一个名字,符合命名规则即可,方便我们调用该方法。 参数类型:就是参数的数据类型。限定调用方法时传入参数的数据类型。 参数名:就是变量名,接收调用方法时传入的参数。 参数分类: 实际参数(实参):实际参与运算的数据。 形式参数(形参):方法上定义的,用于接收实际参数的变量。 方法体语句:就是完成功能的代码。 return:结束方法以及返回方法指定类型的值。 返回值:就是功能的结果,由return带给调用者。 (3)如何写一个方法呢?两个明确: a:返回值类型:明确功能结果的数据类型。 b:参数列表:明确参数的个数以及参数的数据类型。 (4)如何进行方法调用呢? A:有明确返回值的方法调用的方式: a:单独调用,没有意义。 sum(x, y); b:输出调用,但是不够好,因为我不一定非要把结果输出,可能针对结果进行进一步操作。但是讲课一般我就用了。 System.out.println(sum(x, y)); c:赋值调用,推荐方式。 int z = sum(x, y); 如下图所示01: B:没有明确返回值的方法调用的方式:(即用void类型修饰的方法调用) a:只能单独调用。 (5)方法的案例: A:求和方案。 B:获取两个数中的较大值。(返回值是int类型,用三元改进。) C:比较两个数据是否相同。(返回值是boolean类型,用三元改进。) D:获取三个数中的最大值。(返回值是int类型,用if else嵌套,用三元改进。) E:输出m行n列的星形。(返回值是void类型。) F:键盘录入一个数据n(1<=n<=9),输出对应的nn乘法表。 (6)方法的注意事项: A:方法不调用不执行。 B:方法与方法是平级关系,不能嵌套定义。 C:方法在定义的时候,参数是用逗号,隔开的 D:方法在调用的时候,不用在传递数据的类型。 E:如果方法有明确的返回值类型,就必须有return语句返回。 (7)方法的重载 在同一个类中,方法名相同,参数列表不同。与返回值无关。 参数列表不同: 参数的个数不同。 参数的对应的数据类型不同。 (8)方法重载的案例 不同的类型的多个同名方法的比较。----------------------------------------------------------------------------- 2:数组(一维数组)(掌握) (1)数组的定义:存储同一种数据类型的多个元素(变量)的容器(集合)。 (2)数组的特点:每一个元素都有编号,从0开始,最大编号是长度-1。 编号的专业叫法:索引(角标)。 数组既可以存储基本数据类型,也可以存储引用数据类型。 (3)数组的定义格式: A:数据类型[] 数组名; int[] a; //定义了一个int类型的数组a变量。 B:数据类型 数组名[]; int a[]; //定义了一个int类型的a数组变量。 --------------------------------------- 推荐是用A方式,A方式的可读性更强,B方法就忘了吧。在Java中均可。 B方式早期的时候确实有很多人这样用。不过,现在这样用的人越来越少了。 作为Java的粉丝C#(Java的模仿者)就不再支持第二种语法格式了。越来越多的语言可能会抛弃第二种格式。 但是看源码的时候要能看懂。 (4)数组的初始化方式: Java中的数组必须先初始化,然后才能使用。 所谓初始化:就是为数组中的数组元素分配内存空间,并为每个数组元素赋值。 A:动态初始化 只指定数组长度,由系统分配初始值。(数组长度其实就是数组中元素的个数。) 举例: int[] arr = new int[3]; System.out.println(arr); //[I@6d06d69c 地址值,数组名其实就是该数组首元素的地址。 B:静态初始化(常用) 给初值,由系统决定数组长度。 举例: int[] arr = new int[]{ 1, 2, 3 }; 简化版:int[] arr = { 1, 2 ,3 }; 如下图所示06: (5)Java语言的内存分配: Java程序在运行时,需要在内存中的分配空间。 为了提高运算效率,就对空间进行了不同区域的划分,因为每一片区域都有特定的处理数据方式和内存管理方式。 A:栈:存储局部变量。 B:堆:存储所有new出来的东西。 C:方法区(面向对象部分详细讲解) D:本地方法区(系统相关) E:寄存器(CPU使用) --------------------------------------- 注意: a:局部变量:在方法定义中或者方法声明上定义的变量。使用完毕,立即消失。 b:栈内存和堆内存的区别: 栈:数据使用完毕,就消失。 堆:每一个new出来的东西都有地址。 堆中的每一个变量都有默认值。 byte,short,int,long --> 0 float,double --> 0.0 char --> '\u0000' //因为Java语言采用的是Unicode编码。 boolean --> false 引用类型 --> null 在Java语言中,数据使用完毕后,就变成垃圾了,但并没有立即回收,会在垃圾回收器空闲的时候回收。 在C++语言中,有构造函数和析构函数,调用析构函数用来释放空间。 如下图所示02: (6)数组的内存图解: A:一个数组 B:二个数组 C:三个数组(两个栈变量指向同一个堆内存) 如下图所示03/04/05: (7)数组操作时的两个常见小问题: a:ArrayIndexOutOfBoundsException:数组索引越界异常。 原因:你访问了不存在的索引。 b:NullPointerException:空指针异常。 原因:数组已经不再指向堆内存了。而你还用数组名去访问元素。(即:数组引用没有指向实体,却在操作实体中的元素。) (8)数组的常见操作:--------------------------------------- A:数组的遍历 数组的一个属性:获取数值长度:数值名.length 方式1: public static void printArray(int[] arr) { for(int x = 0; x < arr.length; x++) { System.out.println(arr[x]); } } 方式2:通过字符串的拼接,让元素在一行上输出。 public static void printArray(int[] arr) { System.out.print("[ "); for(int x = 0; x < arr.length; x++) { if(x == arr.length - 1) { System.out.println(arr[x]+" ]"); }else { System.out.print(arr[x]+", "); } } }--------------------------------------- B:数组的最值 最大值: public static int getMax(int[] arr) { int max = arr[0]; for(int x = 1; x < arr.length; x++) { if(arr[x] > max) { max = arr[x]; } } return max; } 最小值: public static int getMin(int[] arr) { int min = arr[0]; for(int x = 1; x < arr.length; x++) { if(arr[x] < min) { min = arr[x]; } } return min; }--------------------------------------- C:数组的逆序(逆置) 方式1://使用一个索引,需要考虑到变量的变化。 public static void reverse(int[] arr) { for(int x = 0; x < arr.length / 2; x++) { int temp = arr[x]; arr[x] = arr[arr.length - 1 - x]; arr[arr.length - 1 - x] = temp; } } 方式2://使用两个索引,不用考虑变量变化。 public static void reverse(int[] arr) { for(int start = 0, end = arr.length - 1; start <= end; start++, end--) { int temp = arr[start]; arr[start] = arr[end]; arr[end] = temp; } }--------------------------------------- D:数组的查表(根据键盘录入索引,查找对应星期) public static String getString(String[] strArray, int index) { return strArray[index]; } E:数组的元素查找(查找指定元素第一次在数组中出现的索引) 方式1: public static int getIndex(int[] arr, int value) { for(int x = 0; x < arr.length; x++) { if(arr[x] == value) { return x; } } return -1; //找不到的时候对应的返回的值。或者是for循环的判断条件语句为false时候对应的方茴的值。 } 特别注意:只要是判断条件语句,就有可能是false,所以要细心!!! 方式2: public static int getIndex(int[] arr, int value) { int index = -1; for(int x = 0; x < arr.length; x++) { if(arr[x] == value) { index = x; break; } } return index; }=============================================================================我的GitHub地址: https://github.com/heizemingjun 我的博客园地址: http://www.cnblogs.com/chenmingjun 我的蚂蚁笔记博客地址: http://blog.leanote.com/chenmingjun Copyright ©2018 黑泽明军 【转载文章务必保留出处和署名,谢谢!】

优秀的个人博客,低调大师

了解TiDB基础入门

由于目前的项目把mysql换成了TiDb,所以特意来了解下tidb。其实也不能说换,由于tidb和mysql几乎完全兼容,所以我们的程序没有任何改动就完成了数据库从mysql到TiDb的转换,TiDB 是一个分布式 NewSQL SQL 、 NoSQL 和 NewSQL 的优缺点比较 数据库。它支持水平弹性扩展、ACID 事务、标准 SQL、MySQL 语法和 MySQL 协议,具有数据强一致的高可用特性,是一个不仅适合 OLTP 场景还适合 OLAP 场景的混合数据库。下面是对有关资料的整理还有一些扩展内容以链接的方式展示,有兴趣可以点击了解一下。

优秀的个人博客,低调大师

动态规划基础思想

本页面主要介绍了动态规划的基本思想,以及动态规划中状态及状态转移方程的设计思路,帮助各位初学者对动态规划有一个初步的了解。 本部分的其他页面,将介绍各种类型问题中动态规划模型的建立方法,以及一些动态规划的优化技巧。 引入 [IOI1994] 数字三角形](https://www.luogu.com.cn/problem/P1216)" 给定一个 $r$ 行的数字三角形($r \leq 1000$),需要找到一条从最高点到底部任意处结束的路径,使路径经过数字的和最大。每一步可以走到当前点左下方的点或右下方的点。 ```plain 7 3 8 8 1 0 2 7 4 4 4 5 2 6 5 ``` 在上面这个例子中,最优路径是 $7 \to 3 \to 8 \to 7 \to 5$。 最简单粗暴的思路是尝试所有的路径。因为路径条数是 $O(2^r)$ 级别的,这样的做法无法接受。 注意到这样一个事实,一条最优的路径,它的每一步决策都是最优的。 以例题里提到的最优路径为例,只考虑前四步 $7 \to 3 \to 8 \to 7$,不存在一条从最顶端到 $4$ 行第 $2$ 个数的权值更大的路径。 而对于每一个点,它的下一步决策只有两种:往左下角或者往右下角(如果存在)。因此只需要记录当前点的最大权值,用这个最大权值执行下一步决策,来更新后续点的最大权值。 这样做还有一个好处:我们成功缩小了问题的规模,将一个问题分成了多个规模更小的问题。要想得到从顶端到第 $r$ 行的最优方案,只需要知道从顶端到第 $r-1$ 行的最优方案的信息就可以了。 这时候还存在一个问题:子问题间重叠的部分会有很多,同一个子问题可能会被重复访问多次,效率还是不高。解决这个问题的方法是把每个子问题的解存储下来,通过记忆化的方式限制访问顺序,确保每个子问题只被访问一次。 上面就是动态规划的一些基本思路。下面将会更系统地介绍动态规划的思想。 动态规划原理 能用动态规划解决的问题,需要满足三个条件:最优子结构,无后效性和子问题重叠。 最优子结构 具有最优子结构也可能是适合用贪心的方法求解。 注意要确保我们考察了最优解中用到的所有子问题。 证明问题最优解的第一个组成部分是做出一个选择; 对于一个给定问题,在其可能的第一步选择中,假定你已经知道哪种选择才会得到最优解。你现在并不关心这种选择具体是如何得到的,只是假定已经知道了这种选择; 给定可获得的最优解的选择后,确定这次选择会产生哪些子问题,以及如何最好地刻画子问题空间; 证明作为构成原问题最优解的组成部分,每个子问题的解就是它本身的最优解。方法是反证法,考虑加入某个子问题的解不是其自身的最优解,那么就可以从原问题的解中用该子问题的最优解替换掉当前的非最优解,从而得到原问题的一个更优的解,从而与原问题最优解的假设矛盾。 要保持子问题空间尽量简单,只在必要时扩展。 最优子结构的不同体现在两个方面: 原问题的最优解中涉及多少个子问题; 确定最优解使用哪些子问题时,需要考察多少种选择。 子问题图中每个定点对应一个子问题,而需要考察的选择对应关联至子问题顶点的边。 无后效性 已经求解的子问题,不会再受到后续决策的影响。 子问题重叠 如果有大量的重叠子问题,我们可以用空间将这些子问题的解存储下来,避免重复求解相同的子问题,从而提升效率。 基本思路 对于一个能用动态规划解决的问题,一般采用如下思路解决: 将原问题划分为若干 阶段,每个阶段对应若干个子问题,提取这些子问题的特征(称之为 状态); 寻找每一个状态的可能 决策,或者说是各状态间的相互转移方式(用数学的语言描述就是 状态转移方程)。 按顺序求解每一个阶段的问题。 如果用图论的思想理解,我们建立一个 有向无环图,每个状态对应图上一个节点,决策对应节点间的连边。这样问题就转变为了一个在 DAG 上寻找最长(短)路的问题(参见:DAG 上的 DP)。 最长公共子序列 ???+ note "最长公共子序列问题" 给定一个长度为 $n$ 的序列 $A$ 和一个 长度为 $m$ 的序列 $B$($n,m \leq 5000$),求出一个最长的序列,使得该序列既是 $A$ 的子序列,也是 $B$ 的子序列。 子序列的定义可以参考 子序列。一个简要的例子:字符串 abcde 与字符串 acde 的公共子序列有 a、c、d、e、ac、ad、ae、cd、ce、de、ade、ace、cde、acde,最长公共子序列的长度是 4。 设 $f(i,j)$ 表示只考虑 $A$ 的前 $i$ 个元素,$B$ 的前 $j$ 个元素时的最长公共子序列的长度,求这时的最长公共子序列的长度就是 子问题。$f(i,j)$ 就是我们所说的 状态,则 $f(n,m)$ 是最终要达到的状态,即为所求结果。 对于每个 $f(i,j)$,存在三种决策:如果 $A_i=B_j$,则可以将它接到公共子序列的末尾;另外两种决策分别是跳过 $A_i$ 或者 $B_j$。状态转移方程如下: $$ f(i,j)=\begin{cases}f(i-1,j-1)+1&A_i=B_j\\max(f(i-1,j),f(i,j-1))&A_i\ne B_j\end{cases} $$ 可参考 SourceForge 的 LCS 交互网页 来更好地理解 LCS 的实现过程。 该做法的时间复杂度为 $O(nm)$。 另外,本题存在 $O\left(\dfrac{nm}{w}\right)$ 的算法[^ref1]。有兴趣的同学可以自行探索。 int a[MAXN], b[MAXM], f[MAXN][MAXM]; int dp() { for (int i = 1; i <= n; i++) for (int j = 1; j <= m; j++) if (a[i] == b[j]) f[i][j] = f[i - 1][j - 1] + 1; else f[i][j] = std::max(f[i - 1][j], f[i][j - 1]); return f[n][m]; } 最长不下降子序列 ???+ note "最长不下降子序列问题" 给定一个长度为 $n$ 的序列 $A$($n \leq 5000$),求出一个最长的 $A$ 的子序列,满足该子序列的后一个元素不小于前一个元素。 算法一 设 $f(i)$ 表示以 $A_i$ 为结尾的最长不下降子序列的长度,则所求为 $\max_{1 \leq i \leq n} f(i)$。 计算 $f(i)$ 时,尝试将 $A_i$ 接到其他的最长不下降子序列后面,以更新答案。于是可以写出这样的状态转移方程:$f(i)=\max_{1 \leq j < i, A_j \leq A_i} (f(j)+1)$。 容易发现该算法的时间复杂度为 $O(n^2)$。 === "C++" ```cpp int a[MAXN], d[MAXN]; int dp() { d[1] = 1; int ans = 1; for (int i = 2; i <= n; i++) { d[i] = 1; for (int j = 1; j < i; j++) if (a[j] <= a[i]) { d[i] = max(d[i], d[j] + 1); ans = max(ans, d[i]); } } return ans; } ``` === "Python" python a = [0] * MAXN d = [0] * MAXN def dp(): d[1] = 1 ans = 1 for i in range(2, n + 1): for j in range(1, i): if a[j] <= a[i]: d[i] = max(d[i], d[j] + 1) ans = max(ans, d[i]) return ans 算法二[^ref2] 当 $n$ 的范围扩大到 $n \leq 10^5$ 时,第一种做法就不够快了,下面给出了一个 $O(n \log n)$ 的做法。 回顾一下之前的状态:$(i, l)$。 但这次,我们不是要按照相同的 $i$ 处理状态,而是直接判断合法的 $(i, l)$。 再看一下之前的转移:$(j, l - 1) \rightarrow (i, l)$,就可以判断某个 $(i, l)$ 是否合法。 初始时 $(1, 1)$ 肯定合法。 那么,只需要找到一个 $l$ 最大的合法的 $(i, l)$,就可以得到最终最长不下降子序列的长度了。 那么,根据上面的方法,我们就需要维护一个可能的转移列表,并逐个处理转移。 所以可以定义 $a_1 \dots a_n$ 为原始序列,$d_i$ 为所有的长度为 $i$ 的不下降子序列的末尾元素的最小值,$len$ 为子序列的长度。 初始化:$d_1=a_1,len=1$。 现在我们已知最长的不下降子序列长度为 1,那么我们让 $i$ 从 2 到 $n$ 循环,依次求出前 $i$ 个元素的最长不下降子序列的长度,循环的时候我们只需要维护好 $d$ 这个数组还有 $len$ 就可以了。关键在于如何维护。 考虑进来一个元素 $a_i$: 元素大于等于 $d_{len}$,直接将该元素插入到 $d$ 序列的末尾。 元素小于 $d_{len}$,找到 第一个 大于它的元素,用 $a_i$ 替换它。 为什么: 对于步骤 1: 由于我们是从前往后扫,所以说当元素大于等于 $d_{len}$ 时一定会有一个不下降子序列使得这个不下降子序列的末项后面可以再接这个元素。如果 $d$ 不接这个元素,可以发现既不符合定义,又不是最优解。 对于步骤 2: 同步骤 1,如果插在 $d$ 的末尾,那么由于前面的元素大于要插入的元素,所以不符合 $d$ 的定义,因此必须先找到 第一个 大于它的元素,再用 $a_i$ 替换。 步骤 2 如果采用暴力查找,则时间复杂度仍然是 $O(n^2)$ 的。但是根据 $d$ 数组的定义,又由于本题要求不下降子序列,所以 $d$ 一定是 单调不减 的,因此可以用二分查找将时间复杂度降至 $O(n\log n)$. 参考代码如下: === "C++" cpp for (int i = 0; i < n; ++i) scanf("%d", a + i); memset(dp, 0x1f, sizeof dp); mx = dp[0]; for (int i = 0; i < n; ++i) { *std::upper_bound(dp, dp + n, a[i]) = a[i]; } ans = 0; while (dp[ans] != mx) ++ans; === "Python" python dp = [0x1f1f1f1f] * MAXN mx = dp[0] for i in range(0, n): bisect.insort_left(dp, a[i], 0, len(dp)) ans = 0 while dp[ans] != mx: ans += 1 参考资料与注释 [^ref1]: 位运算求最长公共子序列 - -Wallace- - 博客园 [^ref2]: 最长不下降子序列 nlogn 算法详解 - lvmememe - 博客园 AI算法蒋同学致力于信息学奥赛教学、人工智能算法研究工作! B站 ! 淘宝 !

资源下载

更多资源
Nacos

Nacos

Nacos /nɑ:kəʊs/ 是 Dynamic Naming and Configuration Service 的首字母简称,一个易于构建 AI Agent 应用的动态服务发现、配置管理和AI智能体管理平台。Nacos 致力于帮助您发现、配置和管理微服务及AI智能体应用。Nacos 提供了一组简单易用的特性集,帮助您快速实现动态服务发现、服务配置、服务元数据、流量管理。Nacos 帮助您更敏捷和容易地构建、交付和管理微服务平台。

Spring

Spring

Spring框架(Spring Framework)是由Rod Johnson于2002年提出的开源Java企业级应用框架,旨在通过使用JavaBean替代传统EJB实现方式降低企业级编程开发的复杂性。该框架基于简单性、可测试性和松耦合性设计理念,提供核心容器、应用上下文、数据访问集成等模块,支持整合Hibernate、Struts等第三方框架,其适用范围不仅限于服务器端开发,绝大多数Java应用均可从中受益。

Rocky Linux

Rocky Linux

Rocky Linux(中文名:洛基)是由Gregory Kurtzer于2020年12月发起的企业级Linux发行版,作为CentOS稳定版停止维护后与RHEL(Red Hat Enterprise Linux)完全兼容的开源替代方案,由社区拥有并管理,支持x86_64、aarch64等架构。其通过重新编译RHEL源代码提供长期稳定性,采用模块化包装和SELinux安全架构,默认包含GNOME桌面环境及XFS文件系统,支持十年生命周期更新。

Sublime Text

Sublime Text

Sublime Text具有漂亮的用户界面和强大的功能,例如代码缩略图,Python的插件,代码段等。还可自定义键绑定,菜单和工具栏。Sublime Text 的主要功能包括:拼写检查,书签,完整的 Python API , Goto 功能,即时项目切换,多选择,多窗口等等。Sublime Text 是一个跨平台的编辑器,同时支持Windows、Linux、Mac OS X等操作系统。

用户登录
用户注册