备战蓝桥杯:常用的算法模板和技巧摘录

前言

因为笔者最近在准备 蓝桥杯 算法竞赛,这段时间学习了众多的算法知识,其中不乏有快速幂bfsdfs全排列等常用的算法技巧和模板。而很多的算法题目其实都是有规律可循的,有一定的模板可以套用,因此笔者在这里以笔记的形式对这些模板和技巧进行记录,以便后面的复习使用~!!(若读者对笔者列出的模板代码有更好的优化建议,也欢迎在评论区中提出)

前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家,点击跳转到网站

备战蓝桥杯:常用的算法模板和技巧摘录_第1张图片

文章目录

  • 前言
  • 1.数学
      • 1.1.最大公约数
      • 1.2.最小公倍数
      • 1.3.拓展欧几里得
      • 1.4.快速幂
      • 1.5.埃筛法
      • 1.6.判断丑数
      • 1.7.保留小数
      • 1.8.进制转换
  • 2.日期
      • 2.1.两个日期相差的天数
      • 2.2.遍历某段日期的每一天
  • 3.二分
  • 4.位运算技巧
      • 4.1.英文字符转换为小写
      • 4.2.英文字符转换为大写
      • 4.3.英文字符大小写互换
      • 4.4.判断两个数是否异号
      • 4.5.找出在0~n中缺少的一个数
  • 5.深搜广搜
      • 5.1.深度优先搜索(dfs)
      • 5.2.广度优先搜索(bfs)
  • 6.全排列
  • 7.并查集
  • 8.前缀和
      • 8.1.一维前缀和
      • 8.2.二维前缀和
  • 9.动态规划
      • 9.1.背包问题
      • 9.2.打家劫舍
      • 9.3.石子合并
      • 9.4.最长序列
      • 9.5.n的k拆分方案
  • 10.快读模板


1.数学

1.1.最大公约数

例题: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);
}

1.2.最小公倍数

// 求最小公倍数:如 2 和 3 的最小公倍数为 6;
public int lcm(int a, int b) {
	return a * b / gcd(a, b);
}

1.3.拓展欧几里得

用于解二元一次不定方程: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】蓝桥改革变难,想进国赛这些能力你可缺一不可

1.4.快速幂

我们虽然可以使用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;
}

1.5.埃筛法

埃筛法是能够快速筛选出质数的方法之一,在众多质数筛选方法中,埃筛法虽然不是最快的方法,但绝对是最容易理解的方法。

原理: 从小到大开始筛选质数,当某个数被认定为质数时,那么后面能够被该数整除的数就一定不是质数,为这些数增加一个标记,当遍历到这些被标记的数时,直接跳过即可,无需再为其执行检验是否为质数的操作,因为判断一个数是否为质数是筛选质数过程中最耗时的操作,而埃筛法则能大幅度的减少该操作的执行频率,从而提高素数筛选的效率。

// 求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;
    	}
    }
}	

1.6.判断丑数

丑数即因数只有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;
}

1.7.保留小数

// 保留两位小数
String.format("%.2f", n);

1.8.进制转换

int x = 1040063, n = 36;
// n进制转为十进制
Integer.valueOf("MAIN", n);	// 1040063
// 十进制转为n进制
Integer.toString(x, n);		// main

2.日期

对于日期问题,最让我们头疼的应该就是要考虑年份是否为闰年,即2月份应该是28天还是29天,其他月份的天数是30天还是31天等问题。

经典的问题还有:从某个日期开始到另一个日期相差了多少天;从某个日期开始到另一个日期开始有多少个星期一等,需要做许多的判断。但如果你熟练的掌握了Java内置的与时间处理相关的类,那么这些问题都将不再会是你的烦恼~!

2.1.两个日期相差的天数

// 求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
}

2.2.遍历某段日期的每一天

例题: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);
	}
}

3.二分

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;
    }
}

4.位运算技巧

4.1.英文字符转换为小写

('a' | ' ') = 'a'
('A' | ' ') = 'a'

4.2.英文字符转换为大写

('b' & '_') = 'B'
('B' & '_') = 'B'

4.3.英文字符大小写互换

('d' ^ ' ') = 'D'
('D' ^ ' ') = 'd'

4.4.判断两个数是否异号

int x = -1, y = 2;
// 同号为true,异号为false;
boolean flag = ((x ^ y) < 0); 

4.5.找出在0~n中缺少的一个数

原理: 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;
} 

5.深搜广搜

5.1.深度优先搜索(dfs)

例题: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;
}

5.2.广度优先搜索(bfs)

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;
    }
}

6.全排列

例题: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;
}

7.并查集

例题: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]));
}

8.前缀和

8.1.一维前缀和

例题: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];
}

8.2.二维前缀和

例题: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];
}

9.动态规划

9.1.背包问题

背包问题是最经典的动态规划问题,这里笔者给出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]);
}

9.2.打家劫舍

打家劫舍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];
}

9.3.石子合并

例题: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]);
}

9.4.最长序列

最长公共子序列: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];
}

9.5.n的k拆分方案

例题: 求将正整数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]);
}	

10.快读模板

面对大输入量的题目,使用常规的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;
	}
}

备战蓝桥杯:常用的算法模板和技巧摘录_第2张图片

对于常用的算法模板和技巧就分享到这里了,若上述模板存在任何问题或有所补充,欢迎各位读者在下方提出 ~ !!此篇文章将持续更新 ~ !!

你可能感兴趣的:(数据结构与算法,java,蓝桥杯,算法)