因为笔者最近在准备 蓝桥杯 算法竞赛,这段时间学习了众多的算法知识,其中不乏有
快速幂
、bfs
、dfs
、全排列
等常用的算法技巧和模板。而很多的算法题目其实都是有规律可循的,有一定的模板可以套用,因此笔者在这里以笔记的形式对这些模板和技巧进行记录,以便后面的复习使用~!!(若读者对笔者列出的模板代码有更好的优化建议,也欢迎在评论区中提出)
前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家,点击跳转到网站
例题:https://www.lanqiao.cn/problems/593/learning/
// 求最大公约数:如 12 和 18 的最大公约数是 6;
public int gcd(int a, int b) {
return b == 0 ? a : gcd(b, a % b);
}
// 求最小公倍数:如 2 和 3 的最小公倍数为 6;
public int lcm(int a, int b) {
return a * b / gcd(a, b);
}
用于解二元一次不定方程:ax + by = c
private static int x, y;
public static int exgcd(int a, int b) {
if(b == 0) {
x = 1;
y = 0;
return a;
}
int c = exgcd(b, a % b);
int z = x;
x = y;
y = z - a / b * y;
return c;
}
以上公式皆摘抄自博主执梗的原创文章:【蓝桥真题3】蓝桥改革变难,想进国赛这些能力你可缺一不可
我们虽然可以使用Java数学包下的Math.pow
方法来求出x的n次幂的值,但这个函数本身运行起来是非常耗时的,对于我们需要参加竞赛的读者来说,无疑会让我们在超时的边缘徘徊,而快速幂可以通过将指数拆分成多个因数相乘的形式来简化幂运算,大大调高运算效率,可谓是我们的一大得力帮手!
例题:https://leetcode-cn.com/problems/powx-n/
// x为底数,n为幂数
public double myPow(double x, int n) {
double result = 1;
long v = n;
if(v < 0) {
x = 1 / x;
v = -v;
}
while(v > 0) {
if((v & 1) == 1) {
result *= x;
}
v >>= 1;
x *= x;
}
return result;
}
埃筛法是能够快速筛选出质数的方法之一,在众多质数筛选方法中,埃筛法虽然不是最快的方法,但绝对是最容易理解的方法。
原理: 从小到大开始筛选质数,当某个数被认定为质数时,那么后面能够被该数整除的数就一定不是质数,为这些数增加一个标记,当遍历到这些被标记的数时,直接跳过即可,无需再为其执行检验是否为质数的操作,因为判断一个数是否为质数是筛选质数过程中最耗时的操作,而埃筛法则能大幅度的减少该操作的执行频率,从而提高素数筛选的效率。
// 求n下有多少个质数
public static void main(String[] args) {
int n = 100;
boolean[] flag = new boolean[n + 1];
for(int i = 2; i <= n; i++) {
if(flag[i]) continue;
System.out.println(i);
for(int j = i; i * j <= n; j++) {
flag[i * j] = true;
}
}
}
丑数即因数只有2, 3, 5
的数,那么我们对一个数n一直除以2,3,5后,若最后n = 1
,很明显这个数n就属于丑数,否则就不是丑数。
例题:https://leetcode-cn.com/problems/ugly-number/
public boolean isUgly(int n) {
if(n < 1) {
return false;
}
while (n % 5 == 0) {
n /= 5;
}
while (n % 3 == 0) {
n /= 3;
}
while (n % 2 == 0) {
n /= 2;
}
return n == 1;
}
// 保留两位小数
String.format("%.2f", n);
int x = 1040063, n = 36;
// n进制转为十进制
Integer.valueOf("MAIN", n); // 1040063
// 十进制转为n进制
Integer.toString(x, n); // main
对于日期问题,最让我们头疼的应该就是要考虑年份是否为闰年,即2月份应该是28天还是29天,其他月份的天数是30天还是31天等问题。
经典的问题还有:从某个日期开始到另一个日期相差了多少天;从某个日期开始到另一个日期开始有多少个星期一等,需要做许多的判断。但如果你熟练的掌握了Java内置的与时间处理相关的类,那么这些问题都将不再会是你的烦恼~!
// 求2022-2-1到2022-3-1相差的天数
public static void main(String[] args) {
LocalDateTime time1 = LocalDateTime.of(2022, 2, 1, 0, 0);
LocalDateTime time2 = LocalDateTime.of(2022, 3, 1, 0, 0);
long result = Duration.between(time1, time2).toDays(); // 28
}
例题:https://www.lanqiao.cn/problems/597/learning/
// 遍历2022-01-01到2022-03-01这段日期中的每一天
public static void main(String[] args) {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
Calendar calendar = Calendar.getInstance();
// 2022-01-01(注意:这里月份需要减一)
calendar.set(2022, 2 - 1, 1);
// 2022-03-01
while("2022-03-01".equals(sdf.format(calendar.getTime()))) {
// ...
calendar.add(Calendar.DAY_OF_MONTH, 1);
}
}
class Solution {
public int search(int[] nums, int target) {
int left = 0;
int right = nums.length - 1;
while(left <= right) {
int mid = left + (right - left)/2;
if(target == nums[mid]) {
return mid;
}
if(target > nums[mid]) {
left = mid + 1;
} else {
right = mid - 1;
}
}
return -1;
}
}
('a' | ' ') = 'a'
('A' | ' ') = 'a'
('b' & '_') = 'B'
('B' & '_') = 'B'
('d' ^ ' ') = 'D'
('D' ^ ' ') = 'd'
int x = -1, y = 2;
// 同号为true,异号为false;
boolean flag = ((x ^ y) < 0);
原理: a ^ b ^ a = b
public static int missingNumber(int[] nums) {
int result = nums.length;
for(int i = 0; i < nums.length; i++) {
result ^= nums[i];
result ^= i;
}
return result;
}
例题:https://leetcode-cn.com/problems/sudoku-solver/
// 访问标记,防止走回头路
private boolean[][] visited;
public void dfs(char[][] grid, int i, int j) {
int n = grid.length;
// 边界判断,以及判断是否已经被访问过了
if(i < 0 || j < 0 || i >= n || j >= n || visited[i][j]) {
return;
}
// 开始对上、下、左、右四个方向进行选择
visited[i][j] = true;
// ......
dfs(grid, i + 1, j);
dfs(grid, i - 1, j);
dfs(grid, i, j + 1);
dfs(grid, i, j - 1);
// 撤销选择
visited[i][j] = false;
}
BFS主要用于解决最短路径的问题,比如最经典的迷宫最优路径问题,使用BFS就再合适不过了。
例题:https://www.lanqiao.cn/problems/1216/learning/
public class Main {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
int n = scanner.nextInt();
int m = scanner.nextInt();
int[][] grid = new int[n][m];
for(int i = 0; i < n; i++) {
for(int j = 0; j < m; j++) {
grid[i][j] = scanner.nextInt();
}
}
// 起点坐标
int si = scanner.nextInt() - 1;
int sj = scanner.nextInt() - 1;
// 终点坐标
int ei = scanner.nextInt() - 1;
int ej = scanner.nextInt() - 1;
scanner.close();
// 对应下、上、右、左的移动方向
int[][] moves = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
Queue<Node> queue = new LinkedList<>();
// 将起点压入队列中
queue.offer(new Node(si, sj, 0));
// 将起点标记为已访问
grid[si][sj] = 0;
while(!queue.isEmpty()) {
Node node = queue.poll();
// 如果当前节点是终点,则输出在迷宫走的步数
if(node.i == ei && node.j == ej) {
System.out.println(node.count);
return;
}
// 开始遍历上下左右
for(int[] move : moves) {
int k = node.i + move[0];
int v = node.j + move[1];
// 如果当前方向已经被访问过,或者存在墙,则跳过
if(k < 0 || v < 0 || k >= n || v >= m || grid[k][v] == 0) {
continue;
}
// 将该方向下的节点存入队列中
queue.offer(new Node(k, v, node.count + 1));
// 并将该节点设置为已访问
grid[k][v] = 0;
}
}
System.out.println(-1);
}
}
class Node {
int i, j, count;
public Node(int i, int j, int count) {
this.i = i;
this.j = j;
this.count = count;
}
}
例题:https://www.lanqiao.cn/problems/731/learning/
// 需要进行排列的数字
private static int[] nums = {1, 2, 3, 4, 5, 6, 7, 8, 9};
// 符合排列要求的结果数
private static int count = 0;
public static void main(String[] args) {
dfs(0); // 从第一个数开始
System.out.println(count);
}
public static void dfs(int i) {
if(i == nums.length) {
check();
return;
}
for(int j = i; j < nums.length; j++) {
swap(i, j); // 置换数字位置
dfs(i + 1); // 排列下一位
swap(i, j); // 还原数字位置
}
}
// 检查排列后是否符合题目要求
public static void check() {
int a = nums[0] * 100 + nums[1] * 10 + nums[2];
int b = nums[3] * 100 + nums[4] * 10 + nums[5];
int c = nums[6] * 100 + nums[7] * 10 + nums[8];
if(a + b == c) {
count++;
}
}
// 对数组中的数进行位置的替换,可以保证排列后的数都是不相同的数
public static void swap(int i, int j) {
int temp = nums[i];
nums[i] = nums[j];
nums[j] = temp;
}
例题:https://www.lanqiao.cn/problems/1135/learning/
private static int[] set;
public static void main(String[] args) {
Scanner scan = new Scanner(System.in);
int n = scan.nextInt();
int m = scan.nextInt();
set = new int[n + 1];
init();
int[][] nums = new int[m][3];
for(int i = 0; i < m; i++) {
nums[i][0] = scan.nextInt();
nums[i][1] = scan.nextInt();
nums[i][2] = scan.nextInt();
}
for(int[] num : nums) {
if(num[0] == 1) {
join(num[1], num[2]);
} else {
int k = find(num[1]);
int v = find(num[2]);
System.out.println(k == v ? "YES" : "NO");
}
}
scan.close();
}
// 初始化
public static void init() {
for(int i = 0; i < set.length; i++) {
set[i] = i;
}
}
// 合并
public static void join(int i, int j) {
int k = find(i);
int v = find(j);
if(k != v) set[k] = v;
}
// 查根
public static int find(int i) {
return set[i] == i ? i : (set[i] = find(set[i]));
}
例题:https://leetcode-cn.com/problems/range-sum-query-immutable/
private int[] preSum;
public NumArray(int[] nums) {
preSum = new int[nums.length + 1];
for(int i = 1; i <= nums.length; i++) {
preSum[i] = preSum[i - 1] + nums[i - 1];
}
}
public int sumRange(int left, int right) {
return preSum[right + 1] - preSum[left];
}
例题:https://leetcode-cn.com/problems/range-sum-query-2d-immutable/
private int[][] preSum;
public NumMatrix(int[][] matrix) {
int n = matrix.length, m = matrix[0].length;
if(n == 0 || m == 0) {
return;
}
preSum = new int[n + 1][m + 1];
for(int i = 1; i <= n; i++) {
for(int j = 1; j <= m; j++) {
preSum[i][j] = preSum[i - 1][j] + preSum[i][j - 1] + matrix[i - 1][j - 1] - preSum[i - 1][j - 1];
}
}
}
public int sumRegion(int row1, int col1, int row2, int col2) {
return preSum[row2 + 1][col2 + 1] - preSum[row1][col2 + 1] - preSum[row2 + 1][col1] + preSum[row1][col1];
}
背包问题是最经典的动态规划问题,这里笔者给出01背包
和完全背包
的模板,这两个背包问题的区别只在于前者每个物体只可以选择一次,而后者则可以无限重复选择,因此在01背包
不考虑空间优化的前提下,它们的实现代码仅有一行只差~!
0 - 1背包:https://www.acwing.com/problem/content/2/
完全背包:https://www.acwing.com/problem/content/3/
多重背包:https://www.acwing.com/problem/content/4/
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
int n = scanner.nextInt();
int m = scanner.nextInt();
int[] vs = new int[n + 1]; // 每个物体的体积
int[] ws = new int[n + 1]; // 每个物体的重量
for(int i = 1; i <= n; i++) {
vs[i] = scanner.nextInt();
ws[i] = scanner.nextInt();
}
int[][] dp = new int[n + 1][m + 1];
for(int i = 1; i <= n; i++) {
for(int j = 1; j <= m; j++) {
// 对当前物体不做选择,那么当前背包的重量应与上一次选择时的重量相同
dp[i][j] = dp[i - 1][j];
// 如果背包剩余的体积大于当前物体的体积,则做出选择
if(j >= vs[i]) {
// 01背包
dp[i][j] = Math.max(dp[i][j], dp[i - 1][j - vs[i]] + ws[i]);
// 完全背包
// dp[i][j] = Math.max(dp[i][j], dp[i][j - vs[i]] + ws[i]);
}
}
}
System.out.println(dp[n][m]);
}
打家劫舍1:https://leetcode-cn.com/problems/house-robber/
打家劫舍2:https://leetcode-cn.com/problems/house-robber-ii/
打家劫舍3:https://leetcode-cn.com/problems/house-robber-iii/
/*
* “打家劫舍1”的实现代码如下
*/
private int[] memo;
public int rob(int[] nums) {
memo = new int[nums.length];
Arrays.fill(memo, -1);
return dp(nums, 0);
}
public int dp(int[] nums, int i) {
if(i >= nums.length) {
return 0;
}
if(memo[i] != -1) {
return memo[i];
}
memo[i] = Math.max(
dp(nums, i + 1),
dp(nums, i + 2) + nums[i]
);
return memo[i];
}
例题:https://www.lanqiao.cn/problems/545/learning/
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
int n = scanner.nextInt();
int[][] dp = new int[n + 1][n + 1];
int[] nums = new int[n + 1];
for(int i = 1; i <= n; i++) {
nums[i] = nums[i - 1] + scanner.nextInt();
}
for(int[] d : dp) {
Arrays.fill(d, Integer.MAX_VALUE);
}
for(int len = 1; len <= n; len++) {
for(int i = 1; i + len - 1 <= n; i++) {
int j = i + len - 1;
if(len == 1) {
dp[i][j] = 0;
}
for(int k = i; k <= j - 1; k++) {
dp[i][j] = Math.min(dp[i][j], dp[i][k] + dp[k + 1][j] + nums[j] - nums[i - 1]);
}
}
}
System.out.println(dp[1][n]);
}
最长公共子序列:https://leetcode-cn.com/problems/longest-common-subsequence/
最长连续序列:https://leetcode-cn.com/problems/longest-consecutive-sequence/
最长递增子序列:https://leetcode-cn.com/problems/longest-increasing-subsequence/
最长连续递增序列:https://leetcode-cn.com/problems/longest-continuous-increasing-subsequence/
最长回文子串:https://leetcode-cn.com/problems/longest-palindromic-substring/
最长回文子序列:https://leetcode-cn.com/problems/longest-palindromic-subsequence/
编辑距离:https://leetcode-cn.com/problems/edit-distance/
/*
* “最长公共子序列”的实现代码如下
*/
public int longestCommonSubsequence(String text1, String text2) {
int m = text1.length(), n = text2.length();
int[][] dp = new int[m + 1][n + 1];
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
if (text1.charAt(i - 1) == text2.charAt(j - 1)) {
dp[i][j] = dp[i - 1][j - 1] + 1;
} else {
dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
}
}
}
return dp[m][n];
}
例题: 求将正整数n无序拆分成最大数为k(称为n的k拆分)的拆分方案个数,要求所有的拆分方案不重复。
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
int n = scanner.nextInt();
int k = scanner.nextInt();
int[][] dp = new int[n + 1][k + 1];
for(int i = 1; i <= n; i++) {
for(int j = 1; j <= k; j++) {
if(i == 1 && j == 1) {
dp[i][j] = 1;
} else if(i < j) {
dp[i][j] = dp[i][i];
} else if(i == j) {
dp[i][j] = dp[i][j - 1] + 1;
} else {
dp[i][j] = dp[i][j - 1] + dp[i - j][j];
}
}
}
System.out.println(dp[n][k]);
}
面对大输入量的题目,使用常规的Scanner类来完成输入操作,就会出现超时或者超内存的情况,此时可以使用如下快读模板避免这一情况的发生。
public class Main {
private static BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
private static StreamTokenizer st = new StreamTokenizer(reader);
private static int nextInt() throws IOException {
st.nextToken();
return (int) st.nval;
}
private static long nextLong() throws IOException {
st.nextToken();
return (long) st.nval;
}
private static double nextDouble() throws IOException {
st.nextToken();
return (double) st.nval;
}
private static String next() throws IOException {
st.nextToken();
return st.sval;
}
}
对于常用的算法模板和技巧就分享到这里了,若上述模板存在任何问题或有所补充,欢迎各位读者在下方提出 ~ !!此篇文章将持续更新 ~ !!