image-20240120092756517

image-20240120092756517

image-20240120101629096image-20240120101630012

API

Arrays

排序:

当我们使用 Scanner 类的 nextInt() 方法读取整数时,会在输入缓冲区留下换行符。这会导致接下来使用 nextLine() 方法时直接读取到这个换行符,而不是我们想要的输入。

让我用代码演示一下这种情况:

import java.util.Scanner;

public class Main {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);

        System.out.println("请输入一个整数:");
        int num = scanner.nextInt(); // 读取整数
        System.out.println("你输入的整数是:" + num);

        System.out.println("请输入一行字符串:");
        String str = scanner.nextLine(); // 本来想读取一行字符串,但实际上会读取到之前的换行符
        System.out.println("你输入的字符串是:" + str);

        scanner.close();
    }
}

如果我们输入如下内容:

123
hello

输出结果将会是:

请输入一个整数:
你输入的整数是:123
请输入一行字符串:
你输入的字符串是:

可以看到,由于之前的 nextInt() 留下了一个换行符,导致接下来的 nextLine() 直接读取到了这个换行符,而不是我们期望的输入 “hello”。

为了解决这个问题,可以在 nextInt() 后面加上一个 nextLine() 来消耗掉这个换行符,保证接下来的 nextLine() 可以正确读取到我们想要的输入。

归并排序模板

static void mergeSort(int[] a, int[] aa, int x, int y) {
    // 如果数组只有一个元素,则返回,不需要排序
    if (x == y) return;
    // 找到数组的中间位置
    int mid = (x + y) >> 1;
    // 递归地对左半部分进行排序
    mergeSort(a, aa, x, mid);
    // 递归地对右半部分进行排序
    mergeSort(a, aa, mid + 1, y);
    // 合并左右两部分并排序
    int i = x, j = mid + 1, k = x;
    while (i <= mid && j <= y) {
        // 如果左半部分当前元素小于等于右半部分当前元素,将左半部分的当前元素放入临时数组
        if (a[i] <= a[j]) {
            aa[k++] = a[i++];
        } else {
            // 如果右半部分当前元素小于左半部分当前元素,将右半部分的当前元素放入临时数组
            aa[k++] = a[j++];
            // 计算逆序对数量,即右半部分当前元素与左半部分剩余元素的数量
            ans += mid - i + 1;
            // 对结果取模,防止溢出
            ans %= mod;
        }
    }
    // 处理左半部分剩余元素
    while (i <= mid) {
        aa[k++] = a[i++];
    }
    // 处理右半部分剩余元素
    while (j <= y) {
        aa[k++] = a[j++];
    }
    // 将临时数组中排好序的元素复制回原数组
    for (int index = x; index <= y; index++) {
        a[index] = aa[index];
    }
}

归并排序是一种分治算法,其基本思想是将待排序数组分割成两部分,分别对这两部分进行排序,然后将两部分合并起来得到有序数组。

在这段代码中,mergeSort方法实现了归并排序算法。具体步骤包括:

  1. 将数组a在范围[x, y]内进行归并排序。
  2. 如果x等于y,表示当前待排序数组只有一个元素,直接返回。
  3. 计算中间位置mid,以mid为界将数组分为左右两部分,分别继续进行归并排序。
  4. 合并左右两部分:利用i、j、k三个指针分别指向左部分、右部分和辅助数组aa,比较左右两部分的元素大小,并将较小的元素放入辅助数组aa中。
  5. 在合并过程中,如果发现a[i] > a[j],则累加逆序对数量ans,并对mod取模。
  6. 将剩余未处理的元素依次放入辅助数组aa中。
  7. 最后将aa中的排序结果复制回原数组a。

冒泡排序

 //冒泡排序
        for(int i=0;i<arr.length-1;i++) {
            for(int x=0;x< arr.length-1-i;x++) {
                if(arr[x] > arr[x+1]) {
                    //当前一个大于后一个时,双方交换位置
                    int temp = arr[x];
                    arr[x] = arr[x+1];
                    arr[x+1] = temp;
                }
            }
        }

二维数组的初始化

char[][] arr = {
    {'U', 'D', 'D', 'L', 'U', 'U', 'L', 'R', 'U', 'L'},
    {'U', 'U', 'R', 'L', 'L', 'L', 'R', 'R', 'R', 'U'},
    {'R', 'R', 'U', 'U', 'R', 'L', 'D', 'L', 'R', 'D'},
    {'R', 'U', 'D', 'D', 'D', 'U', 'U', 'U', 'U', 'U'},
    {'U', 'R', 'U', 'D', 'L', 'L', 'R', 'R', 'U', 'U'},
    {'D', 'U', 'R', 'L', 'R', 'L', 'D', 'L', 'R', 'L'},
    {'U', 'L', 'L', 'U', 'R', 'L', 'L', 'R', 'D', 'U'},
    {'R', 'D', 'L', 'U', 'L', 'L', 'R', 'D', 'D', 'D'},
    {'U', 'U', 'D', 'D', 'U', 'D', 'U', 'D', 'L', 'L'},
    {'U', 'L', 'R', 'D', 'L', 'U', 'U', 'R', 'R', 'R'}
};

整形

int[][] myArray = new int[3][3];
Scanner sc = new Scanner(System.in);
for (int i = 0; i < myArray.length; i++) {
    for (int j = 0; j < myArray[i].length; j++) {
        myArray[i][j] = sc.nextInt(); // 从标准输入中读取数据并赋值给数组元素
    }
}

字符型

arr = new char[10][10];               // 创建一个 10 行 10 列的字符型二维数组 arr,并将其初始化
        for(int i = 0;i<arr.length;i++) {     // 对于 arr 中的每一行
            String s = sc.next();             // 从标准输入读入一行字符串
            arr[i] = s.toCharArray();         // 将字符串转换为字符数组,并将其赋值给 arr 中的对应行
        }
char[][] arr = new char[10][10]; // 创建一个大小为 10x10 的字符数组

String str = "UDDLUULRUL\n" +
                "UURLLLRRRU\n" +
                "RRUURLDLRD\n" +
                "RUDDDDUUUU\n" +
                "URUDLLRRUU\n" +
                "DURLRLDLRL\n" +
                "ULLURLLRDU\n" +
                "RDLULLRDDD\n" +
                "UUDDUDUDLL\n" +
                "ULRDLUURRR";
// 定义一个包含一串字符串的变量

String[] arr1 = str.split("\n");
// 使用换行符 "\n" 将字符串分割成多行,并存储到字符串数组 arr1 中

int count = 0;
// 定义一个计数器变量,初始值为 0

for (int i = 1; i <= 10; i++) {
    for (int j = 1; j <= 10; j++) {
        arr[i - 1][j - 1] = arr1[i - 1].charAt(j - 1);
        // 将每个字符数组 arr1 中的字符赋值给二维字符数组 arr
    }
}

for (char[] row : arr) {
    System.out.println(Arrays.toString(row));
}
// 打印二维字符数组 arr 的内容,每一行以字符串形式打印出来

结果

[U, D, D, L, U, U, L, R, U, L]
[U, U, R, L, L, L, R, R, R, U]
[R, R, U, U, R, L, D, L, R, D]
[R, U, D, D, D, D, U, U, U, U]
[U, R, U, D, L, L, R, R, U, U]
[D, U, R, L, R, L, D, L, R, L]
[U, L, L, U, R, L, L, R, D, U]
[R, D, L, U, L, L, R, D, D, D]
[U, U, D, D, U, D, U, D, L, L]
[U, L, R, D, L, U, U, R, R, R]

数学

模运算

模运算是大数运算中的常用操作。如果一个数太大,无法直接输出,或者不需要直接输出,可以把它取模后,缩小数值再输出。取模也是哈希的常用技术。

定义取模运算为 a 除以 m 的余数,记为:amodm=a

对于 a mod m = a,必须要求 a 和 m 的正负号一致,都为正数或都为负数;如果正负不同,取模和求余的结果是不同的。另外取模的结果满足 0 ≤ a mod m < m−1,题目会用给定的 m,限制计算结果的范围。

取模运算的加减乘除

image-20240303213526199

image-20240303213526199

java里有快速幂库函数

import java.math.BigInteger;
import java.util.Scanner;
 
public class Main {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        BigInteger bigInteger1 = scanner.nextBigInteger();
        BigInteger bigInteger2 = scanner.nextBigInteger();
        BigInteger bigInteger3 = scanner.nextBigInteger();
        System.out.println(bigInteger1.modPow(bigInteger2,bigInteger3));
    }
}
计算第一个大整数的第二个大整数次方对第三个大整数取模的结果,并将结果打印到标准输出。

不用函数

package 模板; // 定义一个包名为"模板"

import java.util.Scanner; // 导入 Scanner 类,用于从控制台读取输入

public class 快速幂模运算 {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in); // 创建 Scanner 对象,用于从控制台读取输入

        // 输入三个整数
        int b = scanner.nextInt(); // 读取第一个整数
        int p = scanner.nextInt(); // 读取第二个整数
        int k = scanner.nextInt(); // 读取第三个整数

        int ans = 1; // 初始化结果变量为 1

        // 使用快速幂算法计算 b^p % k 的结果
        while (p != 0) {
            if (p % 2 == 1) { // 如果 p 是奇数
                ans = (ans * b) % k; // 更新结果变量
            }
            p >>= 1; // 将 p 右移一位,相当于 p 除以 2
            b = (b * b) % k; // 更新 b 的值为 b 的平方取模 k
        }

        System.out.println(ans); // 输出结果
    }
}

单纯取模

BigInteger result = a.mod(b);

计算大整数的幂运算后对另一个大整数取模的结果

 System.out.println(bigInteger1.modPow(bigInteger2,bigInteger3));

N = q*p 求qp

import java.math.BigInteger;

public class Main {
    public static void main(String[] args) {
        // 给定的大整数字符串
        String s = "1001733993063167141";
        
        // 将字符串转换为 BigInteger 对象
        BigInteger temp = new BigInteger(s);
        BigInteger bigInteger = new BigInteger(s);
        
        // 计算给定大整数的平方根
        bigInteger = bigInteger.sqrt();
        
        // 初始化计数器和常量
        BigInteger i = new BigInteger("2");
        BigInteger one = BigInteger.ONE;
        BigInteger zero = BigInteger.ZERO;
        
        // 从 2 开始迭代直到平方根
        for (; i.compareTo(bigInteger) < 0; ) {
            // 检查是否能整除给定的大整数
            if (temp.remainder(i).equals(zero)) {
                // 如果能整除,则打印出因子
                System.out.println(i);
            }
            // 增加计数器
            i = i.add(BigInteger.ONE);
        }
        
        // 打印结束标志
        System.out.println("END!");
    }
}

image-20240316201112511

image-20240316201112511

解决方法

  i.compareTo(sqrtN)<=0

大整数的加减乘除

// 使用 BigInteger.TWO 进行大整数除法,实现对两个 BigInteger 对象相除
// 返回商,这里是对 left 和 right 之和进行除以 2 的操作,用于更新二分查找的中间值
BigInteger mid = left.add(right).divide(BigInteger.TWO);

// 使用 BigInteger.TWO 进行大整数加法,实现对两个 BigInteger 对象的加法操作
// 将 left 和 right 相加后除以 2,得到新的中间值,用于更新二分查找的中间值
BigInteger mid = left.add(right).divide(BigInteger.TWO);

// 使用 BigInteger.TWO 进行大整数减法,实现对两个 BigInteger 对象的减法操作
// 将当前中间值 mid 减去 1,向左移动一个单位,用于更新二分查找的右边界
BigInteger right = mid.subtract(BigInteger.ONE);

// 使用 BigInteger.TWO 进行大整数乘法,实现对两个 BigInteger 对象的乘法操作
// 将当前中间值 mid 乘以 2,用于更新二分查找的右边界
BigInteger square = mid.multiply(mid);

// 使用 BigInteger.TWO 进行大整数除法,实现对两个 BigInteger 对象相除
// 将 left 和 right 之和除以 2,用于更新二分查找的中间值
BigInteger mid = left.add(right).divide(BigInteger.TWO);
//取余和0
if (temp.remainder(i).equals(zero)) {

矩阵乘法

img

样例输入

2 1 3
1
2
1 2 3

样例输出

1 2 3



2 4 6
package 模板;

import java.util.Scanner;

public class 矩阵乘法 {
    public static void main(String[] args) {
        //矩阵乘法
        //输入数据 N,M,K;
        Scanner scanner = new Scanner(System.in);
        int N = scanner.nextInt();
        int M = scanner.nextInt();
        int K = scanner.nextInt();
        //定义二维数组存放
        int [][] k1 = new int[N][M];
        int [][] k2 = new int[M][K];
        int [][] k3 = new int[N][K];
        for (int i = 0;i < N ; i++){
            for(int j = 0;j< M;j++) {
                k1[i][j] = scanner.nextInt();
            }
        }
        for (int i = 0; i < M; i++) {
            for (int j = 0; j < K; j++) {
                k2[i][j] = scanner.nextInt();
            }
        }
        for (int i = 0; i < N; i++) {
            for (int j = 0; j < K; j++) {
                int sum = 0;
                for (int k = 0; k < M; k++) {
                    sum = sum + k1[i][k] * k2[k][j];
                }
                k3[i][j] = sum;
            }
        }
        for (int i = 0; i < N; i++) {
            for (int j = 0; j < K; j++) {
                System.out.print(k3[i][j] + " ");
            }
            System.out.println();
        }
    }
}

矩阵乘法模板

 // 矩阵乘法
    private static int[][] solve(int[][] a1, int[][] a2) {
        int[][] a3 = new int[N + 1][N + 1]; // 存储计算结果
        for (int i = 1; i <= N; i++) {
            for (int j = 1; j <= N; j++) {
                int sum = 0;
                for (int k = 1; k <= N; k++) {
                    sum = sum + a1[i][k] * a2[k][j]; // 计算矩阵元素乘积的和
                }
                a3[i][j] = sum; // 存储结果
            }
        }
        return a3; // 返回计算结果
    }

计算矩阵幂次

 // 计算矩阵的幂次
    private static int[][] modPow() {
        int[][] ans = new int[N + 1][N + 1]; // 存储计算结果
        for (int i = 1; i <= N; i++) {
            ans[i][i] = 1; // 单位矩阵
        }
        while (M > 0) {
            if (M % 2 == 1) { // 若幂次是奇数
                ans = solve(ans, a); // 计算结果乘以原始矩阵
            }
            M = M / 2; // 幂次除以 2
            a = solve(a, a); // 计算原始矩阵的平方
        }
        return ans; // 返回计算结果
    }

最大公约数

整数 a 和 b 的最大公约数是指能同时整除 a 和 b 的最大整数 编码时只需要考虑正整数的最大公约数

java 大数有gcd函数

img

import java.util.Scanner;
public class Main {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        // 从标准输入读取两个整数
        int a = scanner.nextInt();
        int b = scanner.nextInt();
        // 调用 gcd 方法计算最大公约数并打印结果
        System.out.println(gcd(a, b));
    }
    // 计算两个整数的最大公约数
    private static int gcd(int a, int b) {
        // 如果 b 为 0,则返回 a,a 即为最大公约数
        if (b == 0) {
            return a;
        } else {
            // 否则递归调用 gcd 方法,传入 b 和 a%b,并返回结果
            return gcd(b, a % b);
        }
    }
}

最小公倍数

  1. 是指几个自然数共有的倍数中除0以外最小的一个数。
  2. 有结论:lcm(a,b)gcd(a,b)= ab;
import java.util.Scanner;
public class Main {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        int a = scanner.nextInt();
        int b = scanner.nextInt();
        System.out.println((a*b)/gcd(a,b));
    }
    private static int gcd(int a, int b) {
        if (b == 0) {
            return a;
        } else {
            return gcd(b, a % b);
        }
    }
}

等差数列

  1. 最小数量 = (最大值-最小值)/公差 +1
  2. 公差 = 最小公倍数
  3. 思路:首先排序数组,然后找到数组中相邻元素的最大公约数作为等差数列的公差,最后通过等差数列的性质计算出最长等差子序列的长度。
import java.util.Arrays;
import java.util.Scanner;
public class Main {
    private static int[] a;
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        // 读取数组长度
        int N = scanner.nextInt();
        // 初始化数组
        a = new int[N];
        // 读取数组元素
        for (int i = 0; i < N; i++) {
            a[i] = scanner.nextInt();
        }   
        // 对数组元素进行排序
        Arrays.sort(a);
        // 初始化公共差
        int temp = a[1] - a[0];
        // 计算数组中所有相邻元素的最大公约数
        for (int i = 1; i < N; i++) {
            temp = gcd(temp, a[i] - a[i - 1]);
        }
        // 输出结果:数组中所有元素按照公共差排列的长度
        System.out.println((a[N-1]-a[0])/temp+1);
    }
    // 计算两个数的最大公约数
    private static int gcd(int a, int b) {
        if (b == 0) {
            return a;
        } else {
            return gcd(b, a % b);
        }
    }
}

素数


最大最小公倍数

一定要挑出三个互质的数才能使最小公倍数 最大

import java.math.BigInteger;
import java.util.Scanner;
public class Main {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        int N = scanner.nextInt();
        BigInteger n = new BigInteger(String.valueOf(N));
        BigInteger n1 = new BigInteger(String.valueOf(N-1));
        BigInteger n2 = new BigInteger(String.valueOf(N-2));
        BigInteger n3 = new BigInteger(String.valueOf(N-3));
        if (N % 2 == 1) {
            System.out.println(n.multiply(n1).multiply(n2));
        } else {
            if (N % 3 == 0) {
                System.out.println(n1.multiply(n2).multiply(n3));
            } else {
                System.out.println(n.multiply(n1).multiply(n3));
            }
        }
    }
}

贪心

贪心(Greedy)可以说是最容易理解的算法思想:把整个问题分解成多个步骤,在每个步骤,都选取当前步骤的最优方案,直到所有步骤结束;在每一步,都不考虑对后续步骤的影响,在后续步骤中也不再回头改变前面的选择。简单地说,其思想就是“走一步看一步”、“目光短浅”。

虽然贪心法不一定能得到最优解,但是它思路简单、编程容易。因此,如果一个问题确定用贪心法能得到最优解,那么应该使用它。

贪心法求解的问题,需要满足以下特征:
最优子结构性质。当一个问题的最优解包含其子问题的最优解时,称此问题具有最优子结构性质,也称此问题满足最优性原理。也就是说,从局部最优能扩展到全局最优。
贪心选择性质。问题的整体最优解可以通过一系列局部最优的选择来得到。
贪心算法没有固定的算法框架,关键是如何选择贪心策略。

所谓贪心策略,必须具备无后效性,即某个状态以后的过程不会影响以前的状态,只与当前状态有关。另外,把贪心法用在“虽然不是最好,但是还不错,我表示满意!”的某些难解问题上,例如旅行商问题,是很难得到最优解的。但是用贪心法常常能得到不错的近似解。如果不一定非要求得最优解,那么贪心的结果,也是很不错的方案。

折半查找模板

 public static int binarySearch(String [] a, String  x) {
        int low = 0,high = a.length-1;
        while(low <= high){
            int mid = (low + high)/2;
            if(a[mid].compareTo(x) <0){
                low = mid +1;
            }
            else if(a[mid].compareTo(x)>0){
                high = mid-1;
            }
            else
                return mid;
        }
        return -1;
    }

判断质数

 // 检查一个数是否为质数的方法
    static boolean check(int sum) {
        if (sum <= 1) {
            return false; // 如果 sum 小于等于 1,则返回 false
        }
        for (int i = 2; i * i <= sum; ++i) { // 循环检查 sum 是否可以被 i 整除
            if (sum % i == 0) return false; // 如果可以被整除,则不是质数,返回 false
        }
        return true; // 否则是质数,返回 true
    }

dfs

深度优先搜索算法(Depth First Search,简称DFS):一种用于遍历或搜索树或图的算法。 沿着树的深度遍历树的节点,尽可能深的搜索树的分支。当节点v的所在边都己被探寻过或者在搜寻时结点不满足条件,搜索将回溯到发现节点v的那条边的起始节点。整个进程反复进行直到所有节点都被访问为止。属于盲目搜索,最糟糕的情况算法时间复杂度为O(!n)。

算法思想

沿着树的深度来遍历树的节点,尽可能深的搜索树的分支。当节点v的所有边都己被探寻过,搜索将回溯到发现节点v的那条边的起始节点。这一过程一直进行到已发现从源节点可达的所有节点为止。如果还存在未被发现的节点,则选择其中一个作为源节点并重复以上过程,整个进程反复进行直到所有节点都被访问为止。

int check(参数)
{
    if(满足条件)
        return 1;
    return 0;
}
void dfs(int step)
{
        判断边界
        {
            相应操作
        }
        尝试每一种可能
        {
               满足check条件
               标记
               继续下一步dfs(step+1)
               恢复初始状态(回溯的时候要用到)
        }
}   
static ans;//全局变量
static void dfs(层数,其他参数){
    if(出局判断){
        更新答案;//答案用全局变量表示
        return;//返回上一层
    }
    (剪枝)
    for(枚举下一层可能的情况){//每一种情况
        if (used[i] == 0){//判断有没有使用过
            used[i] = 1;//标记已经使用过
            dfs(层数+1,其他参数);//下一层
            used[i] = 0; //恢复状态
        }
        return;
    }
}
static Set<Integer> st; // 用于存储结果集,其中不会包含重复元素
static void dfs(int pos, int sum) {
        if (pos == len) { // 如果当前位置等于字符串的长度
            if (check(sum)) { // 检查当前生成的数是否为质数
                st.add(sum); // 如果是质数,则加入结果集
                return;
            }
            return;
        }
        for (int i = 0; i < len; ++i) { // 遍历字符串中的每一个字符
            if (!vis[i]) { // 如果当前字符未被访问过
                sum = sum * 10 + (s.charAt(i) - '0'); // 将当前字符转换为数字并加到 sum 中
                vis[i] = true; // 将当前位置标记为已访问
                dfs(pos + 1, sum); // 递归调用,继续生成下一个数字
                sum /= 10; // 回溯,恢复 sum 的值
                vis[i] = false; // 恢复当前位置的访问状态
            }
        }
    }

排列

全排列

import java.util.Scanner;

public class Main {
    // 创建一个数组用于存储输入的数字
    public static int[] a = new int[3];

    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        // 循环读取输入的数字并存储到数组中
        for (int i = 0; i <= 2; i++) {
            a[i] = scanner.nextInt();
        }
        // 调用深度优先搜索函数,从索引 0 到索引 2 进行全排列
        dfs(0, 2);
    }

    // 深度优先搜索函数,用于生成从第 s 到第 t 的全排列
    public static void dfs(int s, int t) {
        // 如果 s 等于 t,表示已经生成一个完整的排列,可以打印结果并返回
        if (s == t) {
            for (int i = 0; i <= 2; i++) {
                System.out.print(a[i] + " ");
            }
            System.out.println();
            return;
        }
        // 使用循环进行交换操作,生成全排列
        for (int i = s; i <= t; i++) {
            // 交换位置,将第 s 个元素与第 i 个元素交换
            swap(s, i);
            // 递归调用,继续生成剩余部分的排列
            dfs(s + 1, t);
            // 恢复原来的顺序,用于下一次循环
            swap(i, s);
        }
    }

    // 交换数组中两个位置的元素
    public static void swap(int i, int j) {
        int temp;
        temp = a[i];
        a[i] = a[j];
        a[j] = temp;
    }
}

迷宫

img

import java.util.Scanner;

public class Main {
    static final int n = 10;
    static char[][] mp = new char[n + 2][n + 2];
    static boolean[][] vis = new boolean[n + 2][n + 2];
    static int[][] solve = new int[n + 2][n + 2]; // solve[i][j]=1表示这个点能走出去;=2表示出不去
    static int ans = 0;
    static int cnt = 0;

    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        for (int i = 0; i < n; i++)
            for (int j = 0; j < n; j++)
                mp[i][j] = scanner.next().charAt(0);
        for (int i = 0; i < n; i++)
            for (int j = 0; j < n; j++) {
                vis = new boolean[n + 2][n + 2];
                if (dfs(i, j)) ans++;
            }
        System.out.println("ans=" + ans + ",cnt=" + cnt);
    }

    static boolean dfs(int i, int j) {
        if (i < 0 || i > n - 1 || j < 0 || j > n - 1) return true;
        if (solve[i][j] == 1) return true;  // 点(i,j)已经算过了,能出去
        if (solve[i][j] == 2) return false; // 点(i,j)已经算过了,出不去
        if (vis[i][j]) return false;
        cnt++;  // 统计 dfs() 了多少次
        vis[i][j] = true;
        if (mp[i][j] == 'L') {
            if (dfs(i, j - 1)) {
                solve[i][j] = 1;
                return true;
            } else {
                solve[i][j] = 2;
                return false;
            }
        }
        if (mp[i][j] == 'R') {
            if (dfs(i, j + 1)) {
                solve[i][j] = 1;
                return true;
            } else {
                solve[i][j] = 2;
                return false;
            }
        }
        if (mp[i][j] == 'U') {
            if (dfs(i - 1, j)) {
                solve[i][j] = 1;
                return true;
            } else {
                solve[i][j] = 2;
                return false;
            }
        }
        if (mp[i][j] == 'D') {
            if (dfs(i + 1, j)) {
                solve[i][j] = 1;
                return true;
            } else {
                solve[i][j] = 2;
                return false;
            }
        }
        return false;
    }
}

哈希表的使用

import java.util.HashMap;
import java.util.Scanner;

public class Main {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        int n = scanner.nextInt(); // 读取输入的第一个整数,表示第一个数组的长度
        int m = scanner.nextInt(); // 读取输入的第二个整数,表示第二个数组的长度

        HashMap<Integer, Integer> mp = new HashMap<>(); // 创建一个 HashMap,用于存储第一个数组中每个元素出现的次数
        for (int i = 0; i < n; i++) {
            int x = scanner.nextInt(); // 读取第一个数组中的每个元素
            mp.put(x, mp.getOrDefault(x, 0) + 1); // 将元素加入 HashMap,并更新其出现次数
        }

        int cnt = 0; // 用于统计第二个数组中与第一个数组中元素相同的个数
        for (int i = 0; i < m; i++) {
            int x = scanner.nextInt(); // 读取第二个数组中的每个元素
            if (mp.containsKey(x) && mp.get(x) > 0) { // 如果该元素在第一个数组中出现,并且出现次数大于 0
                mp.put(x, mp.get(x) - 1); // 将其出现次数减 1
                cnt++; // 统计个数加 1
            }
        }
        System.out.println(cnt); // 输出统计结果
    }
}
import java.util.HashMap;

public class Main {
    public static void main(String[] args) {
        // 创建一个 HashMap 对象
        HashMap<Integer, String> map = new HashMap<>();

        // 添加键值对
        map.put(1, "Java");
        map.put(2, "Python");
        map.put(3, "C++");

        // 获取值
        String value = map.get(2);
        System.out.println("Value at key 2: " + value);

        // 检查键是否存在
        boolean containsKey = map.containsKey(3);
        System.out.println("Key 3 exists: " + containsKey);

        // 删除键值对
        map.remove(1);
        System.out.println("After removing key 1: " + map);
    }
}

双指针找相同元素

import java.util.Scanner;
import java.util.Arrays;
public class Main {
      public static void main(String[] args) {
        // 创建 Scanner 对象,用于从标准输入读取数据
        Scanner sc = new Scanner(System.in);
        // 读取两个整数,分别表示两个数组的长度
        int n = sc.nextInt();
        int m = sc.nextInt();
        // 创建两个整型数组,分别用于存储两个数组的元素
        int a[] = new int[n];
        int b[] = new int[m];
        // 初始化变量 sum,用于统计相同元素的个数
        int sum = 0;
        // 读取第一个数组的元素,并存储到数组 a 中
        for (int i = 0; i < n; i++) {
            a[i] = sc.nextInt();
        }
        // 读取第二个数组的元素,并存储到数组 b 中
        for (int i = 0; i < m; i++) {
            b[i] = sc.nextInt();
        }
        // 使用 Arrays.sort 对数组 a 和 b 进行排序,使其从小到大排列
        Arrays.sort(a);
        Arrays.sort(b);
        // 关闭 Scanner 对象,释放资源
        sc.close();
        // 始化两个指针 x 和 y,分别指向数组 a 和 b 的起始位置
        int x = 0;
        int y = 0;
        
        // 遍历数组 a 和 b,统计相同元素的个数
        while (x < n && y < m) {
            if (a[x] == b[y]) { // 如果两个元素相等,则相同元素个数加一,并移动两个指针
                sum++;
                x++;
                y++;
            } else if (a[x] < b[y]) { // 如果数组 a 的元素小于数组 b 的元素,则将指针 x 向后移动一位
                x++;
            } else { // 如果数组 a 的元素大于数组 b 的元素,则将指针 y 向后移动一位
                y++;
            }
        }
        
        // 输出相同元素的个数
        System.out.println(sum);
    }
}

BFS

计算斐波那契数列的第 n 项

 // 计算斐波那契数列的第 n 项
    private static BigInteger fb(int n) {
        BigInteger[] fib = new BigInteger[n + 1];
        fib[1] = BigInteger.ONE;
        fib[2] = BigInteger.ONE;
        for (int i = 3; i <= n; i++) {
            fib[i] = fib[i - 1].add(fib[i - 2]);
        }
        return fib[n];
    }

加入记忆化更高效 DP

public class 斐波那契数列 {
    //求第20个数
    static int cnt;
    static long[] memo; // 使用静态数组作为记忆化存储
    static long fib(int n) {
        cnt++;
        if (n == 1 || n == 2) {
            return 1;
        }
        // 如果已经计算过第 n 个斐波那契数,则直接返回结果
        if (memo[n] != 0) {
            return memo[n];
        }
        // 否则进行递归计算,并将结果存储到 memo 数组中
        memo[n] = fib(n - 1) + fib(n - 2);
        return memo[n];
    }

    public static void main(String[] args) {
        int n = 20;
        memo = new long[n + 1]; // 初始化 memo 数组
        System.out.println(fib(n));
        System.out.println(cnt);
    }
}

排列与组合

字典序模板

private static boolean next(int[] nums, int[] prev) {
        // 从右往左找到第一个递增的数 nums[i]
        int i = nums.length - 2;
        while (i >= 0 && nums[i] >= nums[i + 1]) {
            i--;
        }
        // 如果找不到递增的数,则说明当前排列已经是最大排列,无法生成下一个排列
        if (i < 0) {
            return false;
        }
        // 从右往左找到第一个比 nums[i] 大的数 nums[j]
        int j = nums.length - 1;
        while (nums[j] <= nums[i]) {
            j--;
        }
        // 交换 nums[i] 和 nums[j]
        swap(nums, i, j);
        // 对 i 后面的数字进行逆序排列,保证新排列是下一个字典序的排列
        reverse(nums, i + 1);
        // 检查新生成的排列是否与上一个排列相同,如果相同则继续生成下一个排列
        if (prev != null && Arrays.equals(nums, prev)) {
            return next(nums, prev);
        }
        return true;
    }

    public static void reverse(int[] nums, int start) {
        int i = start;
        int j = nums.length - 1;
        while (i < j) {
            swap(nums, i, j);
            i++;
            j--;
        }
    }

    private static void swap(int[] nums, int i, int j) {
        int temp = nums[i];
        nums[i] = nums[j];
        nums[j] = temp;
    }

递归实现全排列

public static void fullsort(int []num,int st,int m,int [] cnt,boolean[] used) {
        if (st == num.length -1){
            cnt[0]++;
            if (cnt[0] == m){
                printArray(num);
            }

               return;
            }
        for (int i = st; i<num.length;i++)
        {
            if (!used[i]) {
                used[i] = true;
                swap(num, st, i);
                fullsort(num, st + 1, m, cnt, used);
                swap(num, st, i);
                used[i] = false;
            }
        }
    }
    public static void swap(int []a,int b,int c) {
        int temp = a[b];
        a[b] = a[c];
        a[c] = temp;
    }
    public static void printArray(int[] arr) {
        for (int i : arr) {
            System.out.print(i + " ");
        }
        System.out.println();
    }

手写排列

public static void main(String[] args) {
        //定义数组
//        4选3 循环3次
        int cnt = 0;
        int []m = {1,2,3,4};
        for (int i = 0; i<4;i++){
            for(int j = 0; j<4;j++){
                if (j != i){
                    for (int k = 0; k<4;k++){
                        if (k != j && k != i){
                            // 输出排列结果
                            cnt++;
                            System.out.printf("%d%d%d,",  m[i],m[j],m[k]);
                        }
                    }
                }
            }
        }
        System.out.println();
        System.out.println(cnt);
//        123,124,132,134,142,143,213,214,231,234,241,243,312,314,321,324,341,342,412,413,421,423,431,432,
//24
        //字典序 从小到大排序

手写组合

public static void main(String[] args) {
        //组合 n个数选m个
        //n!/m!(n-m!)
        //4选3
        //循环3次不用判断
        int []m = {1,2,3,4};
        int cnt = 0;
        for (int i = 0; i<4;i++){
            for(int j = i+1; j<4;j++){
                    for (int k = j+1; k<4;k++){

                        cnt++;
                        System.out.printf("%d%d%d,",  m[i],m[j],m[k]);

                    }
                }
            }
        System.out.println();
        System.out.println(cnt);
//        123,124,134,234,
//          4
//        字典序 从小到大排序
        }

DFS实现全排列

public class 全排列 {
    static int[] sequences = {1,2,3,4};//待全排序的序列

    public static void main(String[] args) {
        dfs(0,sequences.length-1);
    }

    public static void dfs(int start,int end) {
        //递归结束条件
        if(start == end){
            System.out.println(Arrays.toString(sequences));//输出一个全排列序列
            return;
        }
        for(int i = start;i<=end;i++){
            swap(sequences, start, i);//把当前第1个数与后面所有数交换位置,注意所以i是从start开始
            dfs(start+1, end);
            swap(sequences, start, i);//恢复,用于下一次交换
        }
    }

    public static void swap(int[] a,int i,int j){
        int temp = a[i];
        a[i] = a[j];
        a[j] = temp;
    }
}

拼接字符串大小再进行输出

”231“”342“”1“输出最大
”342“”231“”1“
package 排列和组合;

import java.util.Arrays;
import java.util.Scanner;

public class 拼数字符串比较大小再拼接输出 {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        int inputCount = Integer.parseInt(scanner.nextLine());

        String[] nums = new String[inputCount];
        for (int i = 0; i < inputCount; i++) {
            nums[i] = scanner.next();
        }


        // 记录最大的排列
        StringBuilder maxPermutation = new StringBuilder();

        // 求全排列
        generatePermutations(nums, 0, inputCount - 1, maxPermutation);

        // 输出最大的排列
        System.out.println(maxPermutation);
    }

    private static void generatePermutations(String[] nums, int start, int end, StringBuilder maxPermutation) {
        if (start == end) {
            // 当只有一个元素时,输出排列结果
            StringBuilder currentPermutation = new StringBuilder();
            for (String num : nums) {
                currentPermutation.append(num);
            }
            // 如果当前排列比记录的最大排列大,则更新最大排列
            if (currentPermutation.toString().compareTo(maxPermutation.toString()) > 0) {
                maxPermutation.setLength(0);
                maxPermutation.append(currentPermutation);
            }
            return;
        }

        // 递归地生成全排列
        for (int i = start; i <= end; i++) {
            // 将第i个元素与第start个元素交换位置
            swap(nums, start, i);
            // 递归生成以第start+1个元素开头的子数组的全排列
            generatePermutations(nums, start + 1, end, maxPermutation);
            // 恢复原始顺序,以便下一轮交换
            swap(nums, start, i);
        }
    }

    private static void swap(String[] nums, int i, int j) {
        String temp = nums[i];
        nums[i] = nums[j];
        nums[j] = temp;
    }

}

二分法

         二分法有「整数二分」和「实数二分」两种情况。实数二分的代码好写不易出错;整数二分的代码因为要考虑整除的问题,代码容易出错。

        二分法的效率极高。比如在有序的n个数中找某个数,只需要二分log2​n ​次,也就是说它的时间复杂度是 O(log2​n) 的。例如有 n = 10^7个数,只需要24 次就能找到答案。

        总结一下,二分法把一个长度为 n 的有序序列上 O(n) 的查找时间,优化到了O(log2​n)​。注意,这个序列的数字一定要是「有序」的,二分才有意义。在乱序的一堆数字上搞二分,没有任何用处。所以这个数字序列如果是乱序的,应该先排序再二分。

        只搜索一个数的话二分法不如直接暴力搜索,但是搜m个数。那么排序再二分就是 O(nlogn+mlogn),而你的暴力法是 O(mn)的,显然就快多了。

整数二分

假设目标值在闭区间[l,r]

int binarySearch(int[] nums, int target){
  if(nums == null || nums.length == 0){ //数组为空
    return -1;
  }
  int l = 0, r = nums.length - 1; //设置左右边界
  while(l <= r){ 
    int mid = l + (r-l) / 2; // 等同于mid=(l+r)/2,这种写法是为了防止数组越界,也可以写为(l+r) >>> 1
    if(nums[mid] == target){ //最终target=mid,输出mid
        return mid; 
    }else if(nums[mid] < target) { //目标值在(mid,r]之间
        l = mid + 1; 
    }else {  //目标值在[l,mid)之间
        r = mid - 1; 
    }
  }
  // 最后判断: l>r 即数组不存在
  return -1;
}

关于开闭区间问题

static int bin_search(int[] a, int n, int x) { // a[0]~a[n-1]是单调递增的
        int left = 0, right = n ;//注意:不是 n-1,此时是左闭右开的[0,n)
        while (left < right) {
            int mid = left + (right - left) / 2; // int mid = (left + right) >> 1;
            if (a[mid] >= x)
                right = mid;
            else
                left = mid + 1;
        } // 终止于left = right
        return left; // 返回x的位置,如果不存在则返回x应该插入的位置
    }
static int search_2(int[] arr,int l,int r,int x){
        Arrays.sort(arr);
        while (l<r){
            int mid=(l+r+1)/2;
            if (check(mid)){
                l=mid;
            }else{
                r=mid-1;
            }
        }
        return l;
    }

二分查找 (left, right)

 // 二分查找 (left, right)
    int binarySearch3(int[] nums, int target) {
        if (nums == null || nums.length == 0)
            return -1;
 
        int left = 0, right = nums.length - 1;
        while (left + 1 < right){
            int mid = left + (right - left) / 2;
            if (nums[mid] == target) {
                return mid;
            } else if (nums[mid] < target) {
                //  target 在右区间,在(middle, right)中
                left = mid;
            } else {
                // target 在左区间,在(left, middle)中
                right = mid;
            }
        }
        // 当: left + 1 == right
        if(nums[left] == target) return left;
        if(nums[right] == target) return right;
        return -1;
    }

浮点数二分

double binarySearch(int[] nums,int target){
    // eps 表示精度,取决于题目对精度的要求
    double l = 0; double r = nums.length-1;
    const double eps = 1e-6; //科学计数法1*10^-6 当区间长度小于eps时退出循环 
    while (r - l > eps){ //当区间长度大于eps时继续二分
        double mid = (l + r) / 2;
        if (nums[mid] > target ) r = mid;
        else l = mid;
    }
    return l; //最后while循环退出的时候l和r近似相等
}

分巧克力

import java.util.Scanner;
 
public class Main {
    private static int max = 100000;
 
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        int N = scanner.nextInt();
        int K = scanner.nextInt();
        int[] H = new int[max];
        int[] W = new int[max];
        for (int i = 0; i < N; i++) {
            H[i] = scanner.nextInt();
            W[i] = scanner.nextInt();
        }
        int left = 0, right = max;
        while (left < right) {
            int mid = left + (right - left + 1) / 2;
            int temp = 0;
            for (int i = 0; i < N; i++) {
                temp = temp + (H[i] / mid) * (W[i] / mid);
            }
            if (temp >= K) {
                left = mid;
            } else {
                right = mid - 1;
            }
        }
        System.out.println(left);
    }
}

实数二分

double bsearch_3(double left, double right)
{
    for (int i = 0; i < 100; i++)
    {
        double mid = (left + right) / 2;
        if (check(mid))
            left = mid;
        else
            right = mid;
    }
    return left;
}
double bsearch_4(double left, double right)
{
    while(right-left>0.0000001){
        double mid = (left + right) / 2;
        if (check(mid))
            left = mid;
        else
            right = mid;
    }
    return left;
}

前缀和

对于一个长度为n的数组 a[0]∼a[n−1],它的前缀和 sum[i] 等于 a[0]∼a[i] 的和。例如:

  • sum[0] = a[0]

  • sum[1] = a[0] + a[1]

  • sum[2] = a[0] + a[1] + a[2]

  • ​ 利用递推,只要计算 n 次,就能计算出所有的前缀和:sum[i] = sum[i-1] + a[i]。当然,我们也能用 sum[] 反推计算出 a[]:a[i] = sum[i] - sum[i-1]。

如果预先算出了前缀和,这时想知道 a[i]∼a[j] 的和,可以用sum[j]−sum[i−1]。 可以利用前缀和快速计算出数组中任意一个区间的和。本来复杂度为 O(n)的区间和计算,优化到了 O(1) 的前缀和计算。

public static void main(String[] args) {
    Scanner sc = new Scanner(System.in);
    // 读取输入的行数、列数和阈值 L
    int N = sc.nextInt(); // 行数
    int M = sc.nextInt(); // 列数
    int L = sc.nextInt(); // 阈值 L

    // 创建二维数组用于存储输入的矩阵
    int[][] arr = new int[N + 1][M + 1];

    // 输入数组元素
    for (int i = 1; i <= N; i++) {
        for (int j = 1; j <= M; j++) {
            arr[i][j] = sc.nextInt();
        }
    }

    // 计算二维数组的前缀和
    for (int i = 1; i <= N; i++) {
        for (int j = 1; j <= M; j++) {
            // 使用动态规划的思想,计算每个位置的前缀和
            arr[i][j] += arr[i - 1][j] + arr[i][j - 1] - arr[i - 1][j - 1];
        }
    }

    long cnt = 0; // 计数器,用于统计符合条件的子矩阵数量

    // 暴力查询子矩阵,遍历所有可能的子矩阵
    for (int i1 = 1; i1 <= N; i1++) {
        for (int i2 = i1; i2 <= N; i2++) {
            for (int j1 = 1; j1 <= M; j1++) {
                for (int j2 = j1; j2 <= M; j2++) {
                    // 计算子矩阵中元素总和
                    int z = arr[i2][j2] - arr[i2][j1 - 1] - arr[i1 - 1][j2] + arr[i1 - 1][j1 - 1];
                    // 如果子矩阵中元素总和不超过 L,则增加计数器
                    if (z <= L) {
                        cnt++;
                    }
                }
            }
        }
    }

    // 输出符合条件的子矩阵数量
    System.out.println(cnt);
}

灵能传输

image-20240402201149341

image-20240402201209453

3
3
5 -2 3
4
0 0 0 0
3
1 2 3
import java.util.*;
 
public class Main {
    public static void main(String[] args) {
        Scanner cinScanner = new Scanner(System.in);
        int T;
        T = cinScanner.nextInt(); // 读取测试用例数量
        while (T != 0) { // 循环处理每个测试用例
            T--;
            long in0 = 0, inn = 0; // 记录起始和结束位置
            long n;
            long s0, sn;
            n = cinScanner.nextLong(); // 读取数组长度
            long[] a = new long[(int) n + 2]; // 输入数组
            long[] s = new long[(int) n + 1]; // 前缀和数组
            long[] vis = new long[(int) n + 2]; // 标记数组
            long[] ans = new long[(int) n + 2]; // 存储排序后的结果
            for (int i = 0; i < n; i++) {
                a[i] = cinScanner.nextLong(); // 读取数组元素
                s[i + 1] = a[i]; // 计算前缀和
                s[i + 1] += s[i];
            }
            s0 = s[0]; // 获取最小的前缀和
            sn = s[(int) n]; // 获取最大的前缀和
            if (s0 > sn) { // 保证 s0 是最小的前缀和
                long t = s0;
                s0 = sn;
                sn = t;
            }
            Arrays.sort(s); // 对前缀和进行排序
            // 找到最小和最大前缀和的位置
            for (int i = 0; i <= (int) n; i++)
                if (s[i] == s0) {
                    in0 = i;
                    break;
                }
            for (int i = (int) n; i >= 0; i--)
                if (s[i] == sn) {
                    inn = i;
                    break;
                }
            int l = 0;
            int r = (int) n;
            // 从最小前缀和的位置开始向左遍历
            for (int i = (int) in0; i >= 0; i -= 2) {
                ans[l++] = s[i];
                vis[i] = 1; // 标记已经使用的位置
            }
            // 从最大前缀和的位置开始向右遍历
            for (int i = (int) inn; i <= n; i += 2) {
                ans[r--] = s[i];
                vis[i] = 1; // 标记已经使用的位置
            }
            // 将未使用的位置填充到结果数组中
            for (int i = 0; i <= n; i++)
                if (vis[i] == 0)
                    ans[l++] = s[i];
            long res = 0;
            // 计算相邻元素之间的最大差值
            for (int i = 1; i <= n; i++)
                res = Math.max(Math.abs((ans[i] - ans[i - 1])), res);
            System.out.println(res); // 输出结果
        }
    }
}

并查集

判断连通性除了可以用学习过的 BFS、DFS 外,还可以用一个叫”并查集“的算法

并查集又可以理解为不相交集合上的合并查询

并查集是一种非常精巧而实用的数据结构,它主要用于处理一些不相交集合的合并和查询问题。经典的应用有:判断连通性、最小生成树 Kruskal 算法、最近公共祖先(Least Common Ancestors, LCA)等。并查集在算法竞赛中也十分常见:一是简单且高效,二是应用很直观,三是容易和其他数据结构和算法结合。

1、什么是并查集

首先并查集是一个集合一种数据结构, 主要有两个操作,分别是

合并(Union):把两个不相交的集合合并为一个集合。
查询(Find):判断两个元素是否在同一个集合中。
主要用于处理不相交集合的合并及判断连接问题。

2、并查集能做什么

判断连接问题 (社交网络、关系网)
最近公共祖先
渗滤
图像处理
有限状态自动机的等价性
Hinley-Milner 的多态类型推断
Kruskal 的最小生成树算法
游戏(围棋、十六进制)
在 Fortran 中的编译语句的等价性问题

2、并查集实现结构

并查集的结构有点类似树型结构, 不过与树不同, 指针是指向父节点。并且里面会存在多颗树, 如下图

c8ab44c8397449f7a2d7d2ac0b031db6

3、算法原理

3.1、合并(Union)

即将两个节点合并归属为同一个集合。 假设有两个节点A 和 B, 一般不是将A 和 到 B, 就是将B合并到A。 在实现上其实也就是将 A的根节点的parent指针指向节点B的根节点即可。 不过为了维持这颗树结构的平衡高度, 一般由较矮的节点树 和合并到 较高的节点树。

如下图, 如果要合并 g 和 b, 先找到g和b的根节点a和e, 然后把e的父指针指向a即可完成合并。

d75b0713e21c4ff183fb10ca24d2a6d1

3.2、查询(Find)

即判断两个节点是否有关系, 通过parent指针不断向上寻找该节点的根节点, 然后判断它们两个的根节点是否一样即可, 如果一样说明属于同一个集合, 反之不是。

虽然一般查找根节点这个算法非常简单, 但是一般为了性能优化, 减少每次向上搜索的深度, 在查找find根节点的过程中会进行路径压缩。

压缩路径过程就是, 在向上寻找的过程中如果发现父节点不是根节点, 就把父指针指向爷爷节点, 然后自己直接跳到爷爷节点进行下一次判断,依次类推。 当遍历到根节点后, 整颗路径树就会变得非常的矮壮, 大大降低的搜索的深度。

de5b90997fd24c3cac04250b5672770e

字符串

字符串函数

image-20240404205047417

package 字符串;

import java.util.Scanner;

public class 标题统计 {
    public static void main(String[] args) {
        // TODO Auto-generated method stub
        Scanner sc = new Scanner(System.in);
        String s = sc.nextLine().replace(" ", "");
        System.out.println(s.length());
    }
}

图论

1.图的基本概念

图是由顶点集合以及顶点间的关系组成的一种数据结构:G = {V,E}.(顶点:vertex , 边:edge)

V是顶点集合 , V = {x|x属于某个对象集}.

E是边集 , E = {(x,y)|x , y 属于V}或者E = {<x,y>|x , y 属于V}.

Tips: (x,y)表示x,y 之间的双向通路 , 即(x,y)是无方向的.<x,y>表示x,y之间的有向通路 . 即<x,y>是有向的.
  • 完全图
假设有n个顶点 , 对于无向图任意两个顶点之间有且仅有一条边.

完全无向图有n*(n-1)/2条边 .(因为每个顶点都重复计算了一次)
对于无向图 , 任意两个顶点之间都存在方向相反的两条弧.

完全有向图有n*(n-1)条边 .
  • 领接顶点
两个顶点 v1 , v2 之间有边相连 , 则称 v1是v2的领接顶点或v2是v1的领接顶点.
  • 顶点的度
顶点的度指的是它关联边的条数.有向图中顶点的度=入度(指出顶点的边)与出度(指入顶点的边)之和.
  • 简单路径与回路
若路径上 v1,v2...vm均不重复 , 称这样的路径为简单路径. 若路径上第一个顶点 v1与最后一个顶点 vm 重合 , 则称这样的路径为回路或者环.
  • 连通图
在无向图中 , 如果图中任意顶点都是连通的 , 则称此图为连通图.
  • 强连通图
在有向图中,若在每一对顶点 vi 和 vj 之间都存在一条从 vi 到 vj 的路径,也存在一条从 vj 到 vi 的路径,则称此图是强连通图
  • 极小连通子图
既有要保证连通 , 又要保证边数最小.
  • 生成树
一个连通图的最小连通子图称作该图的生成树。有n个顶点的连通图的生成树有n个顶点和n-1条边

2.图的存储结构

因为图中既有节点又有边(节点与节点之间的关系) , 因此在图的存储中 , 我们可以使用一段连续的数组来存储节点 , 但边的关系存储较为复杂 , 通常有以下两种方式~~

2.1 邻接矩阵

因为节点与节点之间的关系就是是否连通 , 即为0 或 1 , 因此可以使用一个二维数组(领接矩阵)来保存节点与节点之间的关系.

无向图的矩阵是对称的 , 第 i 行(列)元素之和就是顶点 i 的度. 有向图的领接矩阵不一定是对称的 , 第 i 行(列)元素之和就是顶点 i 的出度(入度).
如果边带权值 , 并且两个顶点之间是连通的 , 上图中的边的关系就用权值代替 ,  如果两个节点不通 , 则用无穷大代替.
用领结矩阵存储图的优点是能够快速知道两个节点之间是否连通 , 缺陷是如果顶点较多 , 边较少(领接矩阵较为稀疏) , 矩阵中存储了大量的0 , 比较浪费空间 , 并且要求两个顶点之间的路径不是很好求.
有向图的领接矩阵表示法中 , 顶点 i 的出度为: 第 i 行非0元素之和.顶点 i 的入度为: 第 i 列非0元素之和.
package Review;
 
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
 
public class GraphOfMatrix {
    private char[] arrayV;//节点数组
    private int[][] Matrix;//领接矩阵
    private boolean isDirect;//是否是有向图
    HashMap<Character, Integer> map;//优化版的写法 , 目的是建立节点数组与其下标之间的映射关系
 
    //构造节点数组和领接矩阵 size表示当前节点的个数
    public GraphOfMatrix(int size, boolean isDirect) {
        arrayV = new char[size];
        Matrix = new int[size][size];
        //将领接矩阵的每一位都初始化为无穷大
        for (int i = 0; i < size; i++) {
            Arrays.fill(Matrix[i], Integer.MIN_VALUE);
        }
        this.isDirect = isDirect;
    }
 
    /**
     * 初始化节点数组
     *
     * @param array
     */
    public void initArray(char[] array) {
 
        for (int i = 0; i < array.length; i++) {
            //要么初始化节点数组 , 要么建立映射关系.二选一
            map.put(array[i], i);
//            arrayV[i] = array[i];
        }
    }
 
    /**
     * 添加边
     *
     * @param src    起始节点
     * @param dest   终止节点
     * @param weight 权值
     */
    public void addEdg(char src, char dest, int weight) {
        //首先要确定起始节点和终止节点在矩阵中的位置
        int srcIndex = getIndexOfV(src);
        int destIndex = getIndexOfV(dest);
        //将节点和节点之间的关系存储在矩阵中
        Matrix[srcIndex][destIndex] = weight;
        //如果是无向图 , 矩阵对称的位置同样需要赋值
        if (!isDirect) {
            Matrix[destIndex][srcIndex] = weight;
        }
    }
 
    /**
     * 获取节点数组的下标
     *
     * @param v
     * @return
     */
    public int getIndexOfV(char v) {
        //同样两种写法二选一
        return map.get(v);
//        for (int i = 0; i < arrayV.length; i++) {
//            if (arrayV[i]==v){
//                return i;
//            }
//        }
//        return -1;
    }
 
    /**
     * 获取顶点的度
     *
     * @param v 有向图 = 入度+出度
     * @return
     */
    public int getDevOfV(char v) {
        int count = 0;
        int srcIndex = getIndexOfV(v);
        for (int i = 0; i < Matrix.length; i++) {
            if (Matrix[srcIndex][i] != Integer.MIN_VALUE) {
                count++;
            }
        }
        //计算有向图的出度
        if (isDirect) {
            for (int i = 0; i < Matrix[0].length; i++) {
                if (Matrix[i][srcIndex] != Integer.MIN_VALUE) {
                    count++;
                }
            }
        }
        return count;
    }
 
    //打印领接表
    public void printGraph() {
        for (int i = 0; i < Matrix.length; i++) {
            for (int j = 0; j < Matrix[0].length; j++) {
                if (Matrix[i][j] != Integer.MIN_VALUE) {
                    System.out.print(Matrix[i][j] + " ");
                } else {
                    System.out.print("∞ ");
                }
            }
            System.out.println();
        }
    }
 
    public static void main(String[] args) {
        char[] chars = {'A', 'B', 'C', 'D',};
        graph.GraphOfMatrix graph = new graph.GraphOfMatrix(chars.length, true);
        graph.initArray(chars);
 
        graph.addEdge('A', 'B', 1);
        graph.addEdge('A', 'D', 1);
        graph.addEdge('B', 'A', 1);
        graph.addEdge('B', 'C', 1);
        graph.addEdge('C', 'B', 1);
        graph.addEdge('C', 'D', 1);
        graph.addEdge('D', 'A', 1);
        graph.addEdge('D', 'C', 1);
 
        graph.printGraph();
        System.out.print("输入节点的度为: ");
        System.out.println(graph.getDevOfV('A'));
        System.out.println("=============");
    }
 
}

image-20240405155452502

2.2 邻接表

使用数组表示节点的集合 , 使用链表表示边的关系 , 每个链表的节点中即存放边的关系也存放权重.

1. 无向图临接表存储

A , B , C , D 分别代表 0 , 1 , 2, 3.

image-20240405155522354

若 G 为无向图 , 则所需的存储空间为 O(V + 2E)
若 G 为有向图 , 则所需的存储空间为 O(V + E)
在有向图的领接表表示中 , 求一个给定顶点的出度/入度 , 只需计算领接表中相应节点的个数.
package Review;
 
import java.util.ArrayList;
 
public class GraphByNode {
    //构造存储边的链表
    static class Node {
        public int src;//起始位置
 
        public int dest;//目标位置
 
        public int weight;//权值
 
        public Node next;
 
        public Node(int src, int dest, int weight) {
            this.src = src;
            this.dest = dest;
            this.weight = weight;
        }
    }
 
    //存储节点的数组
    public char[] arrayV;
    //存在链表的集合
    public ArrayList<Node> edgList;
    //判断是否为有向图
    public boolean isDirect;
 
    //构造领接表
    public GraphByNode(int size, boolean isDirect) {
        arrayV = new char[size];
        edgList = new ArrayList<>(size);
        this.isDirect = isDirect;
    }
 
    /**
     * 初始化顶点数组
     *
     * @param array
     */
    public void initArray(char[] array) {
        for (int i = 0; i < arrayV.length; i++) {
            arrayV[i] = array[i];
        }
    }
 
    /**
     * 添加边
     *
     * @param src    起点
     * @param dest   终点
     * @param weight 权重
     */
    public void addEdge(char src, char dest, int weight) {
        int srcIndex = getIndexOfV(src);
        int destIndex = getIndexOfV(dest);
        addEdgeChild(srcIndex, destIndex, weight);
    }
 
    public void addEdgeChild(int srcIndex, int destIndex, int weight) {
        //获取链表的头结点
        Node cur = edgList.get(srcIndex);
        //遍历整个链表查看之前是否已存在该边
        while (cur != null) {
            if (cur.dest == destIndex) {
                //之前存过这条边直接返回
                return;
            }
            cur = cur.next;
        }
        //之前没有存储过这条边 , 头插法插入链表
        Node node = new Node(srcIndex, destIndex, weight);
        node.next = edgList.get(srcIndex);
        edgList.set(srcIndex, node);
    }
 
    /**
     * 获取 顶点下标
     *
     * @param v
     * @return
     */
    public int getIndexOfV(char v) {
        for (int i = 0; i < arrayV.length; i++) {
            if (arrayV[i] == v) {
                return i;
            }
        }
        return -1;
    }
 
    /**
     * 获取顶点的度
     *
     * @param v
     * @return
     */
    public int getDevOfV(char v) {
        int count = 0;
        int srcIndex = getIndexOfV(v);
        Node cur = edgList.get(srcIndex);
        while (cur != null) {
            count++;
            cur = cur.next;
        }
        //以上仅仅计算了出度 , 还需计算入度
        //遍历除了自身外 , 每一个顶点对应的链表中节点是否有指向srcIndex的.
        if (isDirect) {
            //将srcIndex当做目的下标.
            int destIndex = srcIndex;
            for (int i = 0; i < arrayV.length; i++) {
                //出去自身
                if (destIndex == i) {
                    continue;
                }
                Node pCur = edgList.get(i);
                while (pCur != null) {
                    if (pCur.dest == destIndex) {
                        count++;
                    }
                    pCur = pCur.next;
                }
            }
        }
        return count;
    }
 
    /**
     * 打印领接表
     */
    public void printGraph() {
        for (int i = 0; i < arrayV.length; i++) {
            System.out.println(arrayV[i] + "->");
            Node cur = edgList.get(i);
            while (cur != null) {
                System.out.println(arrayV[cur.dest] + "->");
                cur = cur.next;
            }
            System.out.println();
        }
    }
 
    public static void main(String[] args) {
        graph.GraphByNode graph = new graph.GraphByNode(4, false);
        char[] array = {'A', 'B', 'C', 'D',};
        graph.initArray(array);
 
        graph.addEdge('A', 'B', 1);
        graph.addEdge('A', 'D', 1);
        graph.addEdge('B', 'A', 1);
        graph.addEdge('B', 'C', 1);
        graph.addEdge('C', 'B', 1);
        graph.addEdge('C', 'D', 1);
        graph.addEdge('D', 'A', 1);
        graph.addEdge('D', 'C', 1);
 
        graph.printGraph();
        System.out.println(graph.getDevOfV('A'));
        System.out.println("=============");
    }
}

image-20240405155555827

3. 图的遍历

3.1 图的广度优先遍历

广度优先遍历类似于二叉树的层序遍历 , 由于二叉树的层序遍历借助队列 , 那么图的广度优先遍历也要借助队列. 广度优先遍历每次都访问起始节点相邻的所有节点 , 下图中的访问顺序就是B->A->C->D.

image-20240405161452974

Tips:

注意入队和出队都要将visited数组下标置为true , 否则会出现多次打印最后一个元素的情况.

示例代码:

 /**
     * 广度优先搜索
     * @param v
     */
    public void bfs(char v) {
        //获取起始节点的下标
        int srcIndex = getIndexOfV(v);
        //调用队列
        Queue<Integer> queue = new LinkedList<>();
        //使用visited数组记录节点是否被访问
        boolean[] visited = new boolean[arrayV.length];
        queue.offer(srcIndex);
        while (!queue.isEmpty()) {
            int top = queue.poll();
            visited[top] = true;//每弹出一个元素visited数组相应下标就置为true
            System.out.println(arrayV[top] + "->");
            for (int i = 0; i < arrayV.length; i++) {//搜索领接矩阵中起始节点行的每一个元素
                if (Matrix[top][i] != Integer.MAX_VALUE && visited[i] != true) {
                    queue.offer(i);
                    visited[i] = true;//每存入一个元素visited数组相应下标就置为true
                }
            }
        }
    }

3.2 图的深度优先遍历

图的深度优先优先遍历类似于二叉树的前序遍历 , 需要递归实现.从起始位置一条路走到底 , 再返回寻找下一条路.返回时需要一个visited数组记录元素使用遍历过.

图示过程:

image-20240405161549100

代码:

/**
     * 深度优先遍历
     *
     * @param v 起始元素
     */
    public void dfs(char v) {
        int srcIndex = getIndexOfV(v);
        boolean[] visited = new boolean[arrayV.length];
        dfsChild(srcIndex, visited);
    }
 
    public void dfsChild(int srcIndex, boolean[] visited) {
        System.out.println(arrayV[srcIndex] + "->");
        visited[srcIndex] = true;
        for (int i = 0; i < Matrix[srcIndex].length; i++) {
            if (Matrix[srcIndex][i] != Integer.MAX_VALUE) {
                dfsChild(i, visited);
            }
        }
    }

execl

image-20240411192531452

image-20240411192538077