2024年6月2日,顺利拿下国赛二等奖
注意事项
1、数据类型的选择
- 数据范围:
- int : 取值范围-2,147,483,648 ~ 2,147,483,647
- long : 取值范围 -9,223,372,036,854,775,808 ~ 9,223,372,036,854,775,807
- 数据类型的选择:
- 在解题过程中,在初始化数据时需要注意它的数据范围
- 输入时的数据一般可以根据题目给出的运行限制确定
- 输出时的结果需要根据题意大概估算出最大所需的数据
- 如果上述数据范围不满足要求,则需要考虑使用大数(BigInteger)进行保存
2、数组范围:初始化问题
- 数组最大可以存储 2,147,483,645 个元素,十位数,与 int 类型的取值范围差不多
- 在使用多维数组时,需要注意它的存储大小!!
- 比如:
- 二维数组(len * len):len 的最大取值大概在 10000 左右
- 三位数组(len * len * len):len 的最大取值大概在 1000 左右
3、其他
- 当输入有多组数据,但没有明确给出组数时,可以使用
sc.hasNext()
解决。 - 只要思想不滑坡,办法总比困难多!!
- 要学会寻找解题的关键,合理运用算法与数据结构!!
对拍技巧
- 对拍的含义是,你可以写个暴力程序,时间复杂度很高,但正确性一定能保证的算法,与你编写复杂度足以通过题目的代码,用相同的数据比对输出结果是否相同。
- 一般情况下,建议写一题拍一题,如果手速不快不建议,因为对拍同样浪费时间,但如果你最后几题写不出来,又想拿好的奖项,那么你一定要确保前面的题目正确性。
- 按照以往的经验,b组,满分150,填空10,大题140。
- 20分应该能省三,
- 30+运气好能省二,
- 70+基本上能省一,如果难点可能60多甚至50多也可以。
- 因此可能你离获奖就差那5分,同时我们大题目也可以拿部分分,努力将自己总分提上去
数学技巧
- 小数转整数(向上取整):
Math.ceil(x)
- 小数转整数(向下取整):
Math.floor(x)
- 小数转整数(四舍五入):
Math.round(x)
java 日期函数 LocalDate 的使用
1、LocalDate now()
从默认时区的系统时钟获取当前日期。
LocalDate a = LocalDate.now();
System.out.println(a); // 2024-04-11
2、 LocalDate of(int year, int month, int dayOfMonth)
从年、月和日获取实例 LocalDate
year – 代表年份,从MIN_YEAR年到MAX_YEAR年
month – 代表的月份,从1月1日(1月)到12日(12月)
dayOfMonth – 表示从 1 到 31 的月份中的某天
LocalDate localDate = LocalDate.of(2024, 04, 11);
System.out.println(localDate);//2024-04-11
3、LocalDate ofYearDay(int year, int dayOfYear)
从一年和一年中的某天获取实例 LocalDate
year – 代表年份,从MIN_YEAR年到MAX_YEAR年
dayOfYear – 代表一年中的一天,从 1 到 366
注意:如果任何字段的值超出范围,或者如果一年中的某一天对年份无效
LocalDate localDate = LocalDate.ofYearDay(2024 ,32);
System.out.println(localDate);// 2024-02-01
4、int getYear()
获取年份字段
LocalDate a = LocalDate.of(2024,04,11);
int year = a.getYear();
System.out.println(year); // 2024
5、int getMonthValue()
获取从 1 到 12 的月份字段
LocalDate a = LocalDate.of(2024,04,11);
int month = a.getMonthValue();
System.out.println(month); // 4
6、int getDayOfMonth()
获取月份中的某天字段
LocalDate a = LocalDate.of(2024,04,11);
int day = a.getDayOfMonth();
System.out.println(day); // 11x
7、int getDayOfYear()
获取一年中的某天字段,
返回:一年中的某一天,从 1 到 365,或闰年为 366,也就是当年当月当日的天数
LocalDate now = LocalDate.of(2024,4,11);
int localDate = now.getDayOfYear();
System.out.println(localDate); // 102
8、 boolean isLeapYear()
检查年份是否为闰年
LocalDate now = LocalDate.of(1904,6,5);
Boolean localDate = now.isLeapYear();
System.out.println(localDate); // true
9、int lengthOfMonth()
返回月份有多少天
LocalDate now = LocalDate.of(2024,4,11);
int localDate = now.lengthOfMonth();
System.out.println(localDate); // 30
10、int lengthOfYear()
返回年份有多少天
LocalDate now = LocalDate.of(2024,4,11);
int localDate = now.lengthOfYear();
System.out.println(localDate); // 366
11、LocalDate plusYears(long yearsToAdd)
返回添加 LocalDate 指定年数
参数:
yearsToAdd – 要添加的年份,可能是负数(如果是正数则增加年份,是负数则减年份)
LocalDate now = LocalDate.of(2024,4,11);
LocalDate localDate = now.plusYears(2);
System.out.println(localDate); // 2026-04-11
12、LocalDate plusMonths(long monthsToAdd)
返回添加 LocalDate 指定月数
参数:
monthsToAdd – 要添加的月份,可能是负数(如果是正数则增加月数,是负数则减月数)
LocalDate now = LocalDate.of(2024,4,11);
LocalDate localDate = now.plusMonths(-2);
System.out.println(localDate); // 2024-12-28
13、LocalDate plusWeeks(long weeksToAdd)
返回添加了 LocalDate 指定周数,可能是负数(如果是正数则增加周数,是负数则减周数)
LocalDate now = LocalDate.of(2023,2,28);
LocalDate localDate = now.plusWeeks(1);
System.out.println(localDate); // 2023-03-07
14、LocalDate plusDays(long daysToAdd)
返回添加 LocalDate 指定天数
参数:
daysToAdd – 添加的天数,可能是负数(如果是正数则增加天数,是负数则减天数)
LocalDate now = LocalDate.of(2023,2,28);
LocalDate localDate = now.plusDays(10);
System.out.println(localDate); // 2023-03-10
15、int compareTo(ChronoLocalDate other)
将此日期与另一个日期进行比较。
返回:
比较器值,如果较小则为负值,如果较大则为正值
LocalDate now = LocalDate.of(2023,1,28);
LocalDate now1 = LocalDate.of(2023,2,28);
int localDate = now.compareTo(now1);
System.out.println(localDate); // -1
Eclipse 调试技巧
- F5:Step into/跳入方法/进入该行的函数内部
- F6:Step over/向下逐行调试/一行一行执行
- F7:Step return/跳出方法/退出当前的函数
- F8:直接跳转到下一个断点
常用算法(★★★★★)
一、使用前缀和求区间值
1. 一维前缀和
-
构造一维前缀和数组
- 假设有一个二维数组 a
- 需要构造一个前缀和数组 b,则可以按照如下公式进行计算:
b[i] = b[i - 1] + a[i]
- 注意:在初始化数组时,i, j 需要从 1 开始,否则会出现数组越界情况!!!
-
利用一维前缀和数组计算某一区间的和
- 假设已知二维数组 a , 前缀和数组 b,求数组 a 中某一区间的和,该区间为
[L, R]
- 解题公式:
sum = b[R] - b[L - 1]
- 假设已知二维数组 a , 前缀和数组 b,求数组 a 中某一区间的和,该区间为
2. 二维前缀和
-
构造二维前缀和数组
- 假设有一个二维数组 a
- 需要构造一个前缀和数组 b,则可以按照如下公式进行计算:
b[i][j] = b[i - 1][j] + b[i][j - 1] - b[i - 1][j - 1] + a[i][j]
- 注意:在初始化数组时,i, j 需要从 1 开始,否则会出现数组越界情况!!!
-
利用二维前缀和数组计算某一区间的和
- 假设已知二维数组 a , 前缀和数组 b,求数组 a 中某一区间的和,该区间左上角坐标为
[x1, y1]
, 右下角坐标为[x2, y2]
- 解题公式:
sum = b[x2][y2] - b[x1 - 1][y1] - b[x1][y1 - 1] + b[x1 - 1][y1 - 1]
- 假设已知二维数组 a , 前缀和数组 b,求数组 a 中某一区间的和,该区间左上角坐标为
二、差分算法
- 已知原数组 a ,数组 b 是 a 的差分数组,则:
- 构造差分数组 b:
- 当 i = 1 时,b[1] = a[1]
- 当 i > 1时, b[i] = a[i] + a[i - 1]
- 差分。通过差分数组,计算数值 a 某一区间加上常数 k 后的数组
- b[L] + K
- b[R + 1] - K (注意在计算过程中确保不超出数组范围)
- 还原原数组 b:
- 当 i = 1 时,a[1] = b[1]
- 当 i > 1时, a[i] = b[i] + a[i - 1]
- 构造差分数组 b:
- 注意:
- 前缀和与差分都是离线的
- 即:原数组 a 与 差分数组 b 在修改数据时,不同步,需要不断更新实现同步。
- 前缀和与差分都是离线的
三、欧几里得 – 求最大公约数与最小公倍数
1. 最大公约数
- 原理:
- 如果我们需要找到两个正整数1997和615的最大公约数,我们使用欧几里德算法如下所示:
- 1997/615=3(余数152)
- 615/152=4(余数7)
- 152/7=21(余数5)
- 7/5=1(余数2)
- 5/2=2(余数1)
- 2/1=2(余数0)
- 用除数和余数重复除法运算,当余数为0时,得到1997年和615年的最大公约数1。
- 如果我们需要找到两个正整数1997和615的最大公约数,我们使用欧几里德算法如下所示:
public static int getGYS(int a, int b) { // 求最大公约数
if(b == 0){
return a;
}
return getGYS(b, a % b);
}
2. 最小公倍数
- 原理:两个数的最小公倍数等于两个数的乘积除以它们的最大公约数
public static int getGBS(int a, int b, int c) { // 求最小公倍数
int gbs = a * b / getGYS(a, b);
gbs = gbs * c / getGYS(gbs, c);
return gbs;
}
四、求一个数的所有因子
public static void fun(long num, ArrayList<Long> arr){
//从1遍历到num的平方跟即可。
//这时你可能会说,那后面的数呢,比如num本身,不是计算不到了嘛?
//别急,看for循环里面的处理情况
for ( long i = 1 ; i <= Math.sqrt(num) ; i++ ){
//如果能被num整除,那肯定是num的因子,毫无疑问
if ( num % i == 0 ){
arr.add(i);
//重点的部分在这里!!!
//当i能被num整除的情况下,此时i是相对较小的因子,用i求出num另一个较大的因子n
//因为当i能被num整除时,那么数"num/i"也一定能被num整除
//不需要再进行重复的计算,这样算法的运行时间大大降低
long n = num / i;
//但用i算出另一个较大的因子时,会出现重复的情况
//例如num = 4,当遍历到2时,算出另一个较大的因子也是2,这就重复了,要判断一下
if ( n != i ){
arr.add(n);
}
}
}
}
五、广度优先搜索算法(BFS)
1. 原理
-
算法描述(遍历过程):
- 访问根节点,设置标记,入队列
- 队列顶点出队列,同时将该顶点下满足条件的所有孩子顶点进行如下操作:
- 判断孩子顶点或构成的路径是否满足场景所需要的条件,比如:在迷宫场景中,寻找到达终点的路线,此场景孩子结点需要满足的条件就是:孩子结点与迷宫终点是同一结点。
- 如果满足,则返回结果,并退出搜索
- 否则,继续搜索
- 将孩子结点设置对应标记(防止重复遍历,成为死循环)
- 将孩子结点入队列
- 判断孩子顶点或构成的路径是否满足场景所需要的条件,比如:在迷宫场景中,寻找到达终点的路线,此场景孩子结点需要满足的条件就是:孩子结点与迷宫终点是同一结点。
- 重复执行第二步操作,直到队列为空,才停止搜索
-
创建广度优先算法一般所需的数据结构和方法:
- 队列:用于保存入队列的结点(结点为等待操作的结点)
- 标志位:用于记录结点访问情况,已访问为 true,未访问为 false
- void getChildNode(Queue queue, Node ParentNode):
- 根据提供的父结点 ParentNode,将所有满足条件的孩子结点 ChildNode,入队列 queue,同时将结点的访问情况(标志位)设置为 ture
- 其中,满足的条件根据场景的不同而不同
2. 算法模板
void bfs(Node node) {
Queue queue = new LinkedList<>();
bj = new boolean[n];
queue.add(node);
bj[node.index] = true;
while(!queue.isEmpty()) {
Node nowNode = queue.poll();
LinkedList<Node> nextNodes = getNodes(node);
while(!nextNodes.isEmpty()) {
Node nextNode = nextNodes.poll();
if(满足条件) {
相关操作
}
bj[nextNode.index] = true;
queue.add(nextNode);
}
}
}
3. 注意事项
- 如果场景需要寻找路径,则在生成子结点时,需要保存指向其结点的父结点;如果不需要,则不用这一步操作。
- 在必要的情况下,一定要设置标记位,这样可以节省搜索时间,也可以避免陷入死循环。
六、深度优先搜索算法(DFS)
1. 原理
- 特点:
- 每个结点只能访问一次
- 一步一步向前搜索,无法向前时便回退。
- 本质:
- 深搜优先搜索的本质上就是持续搜索,遍历了所有可能的情况,必然能得到解。
- 搜索过程
- 从图中某顶点v出发,访问顶点v;
- 依次从v的未被访问的邻接点出发,对图进行深度优先遍历;直至图中和v有路径相通的顶点都被访问;
- 若此时图中尚有顶点未被访问,则从一个未被访问的顶点出发,重新进行深度优先遍历,直到图中所有顶点均被访问过为止。
2. 算法模板
void dfs(Node node) {
if(满足题目条件) {
相关操作
}
// 通过当前结点获取下一结点
LinkedList<Node> nextNodes = getNodes(node);
while(!nextNodes.isEmpty()) {
Node node = nextNodes.poll();
// 设置标记
bj[index] = true;
dfs(node);
//释放标记
bj[index] = false;
}
}
LinkedList<Node> getNodes(Node node) {
// 获取邻近结点
// 并进行剪枝操作
}
七、Java 排序 API 的使用
- 对数组(整数,字符,字符串,浮点数,二进制数)进行排序(默认升序):
Arrays.sort(ints);
- 对数组的指定范围进行排序:
Arrays.sort(ints, 0, 4);
// 注意:
// 指定的排序范围是左闭右开区间:[0, 4)
- 对数组进行排序(指定是升序还是降序):
// 需要通过 Comparator 接口实现
Comparator comparator = (Comparator<Integer>) (o1, o2) -> o2 - o1; // 降序
Comparator comparator = (Comparator<Integer>) (o1, o2) -> o1 - o2; // 升序
// 记忆口诀:
// 降序:o2 - o1
// 升序:o1 - o2
// 调用排序API
Arrays.sort(ints, comparator);
Arrays.sort(ints, 0, t, comparator);
// 简便写法:
Arrays.sort(ints, (o1, o2) -> o2 - o1); //降序
Arrays.sort(ints, 0, t, (o1, o2) -> o1 - o2); // 升序
- 对哈希表进行排序(键值对)
// 已知一个哈希表,如下所示:
HashMap<Integer, Integer> hashMap = new HashMap<>();
// 对哈希表进行排序
// 1. 将哈希表转换为ArrayList集合
ArrayList<Map.Entry<Integer, Integer>> arrayList = new ArrayList<>(hashMap.entrySet());
// 2. 对ArrayList进行排序
arrayList.sort(Map.Entry.comparingByKey()); // 通过比较键进行排序
arrayList.sort(Map.Entry.comparingByValue()); // 通过比较值,进行排序
八、 双指针
- 定义:
- 双指针指的是在遍历对象的过程中,不是普通的使用单个指针进行访问,而是使用两个相同方向(快慢指针)或者相反方向(对撞指针)的指针进行扫描,从而达到相应的目的。
- 严格的来说,双指针只能说是是算法中的一种技巧。
- 最常见的双指针算法有两种:
- 在一个序列里边,用两个指针维护一段区间。
- 在两个序列里边,用两个指针分别指向不同序列,来维护某种次序。
- 双指针算法的核心思想(作用):
- 优化:在利用双指针算法解题时,考虑原问题如何用暴力算法解出,观察是否可构成单调性,若可以,就可采用双指针算法对暴力算法进行优化.
- 当我们采用朴素的方法即暴力枚举每一种可能的情况,时间复杂度为
O(n*n)
- 当我们使用双指针算法时通过某种性质就可以将上述
O(n*n)
的操作优化到O(n)
- 当我们采用朴素的方法即暴力枚举每一种可能的情况,时间复杂度为
- 优化:在利用双指针算法解题时,考虑原问题如何用暴力算法解出,观察是否可构成单调性,若可以,就可采用双指针算法对暴力算法进行优化.