考点总结

填空题 1:劲舞团

  • 考点:字符串处理、时间戳计算、连续匹配计数。
  • 解题思路:通过比较输入的字符串和时间戳,判断连续匹配的次数,并更新最大匹配次数。

填空题 2:召唤数学精灵

  • 考点:数学规律、因子分解、循环逻辑。
  • 解题思路:通过去除数字中的所有2因子,判断最终是否为1来统计满足条件的数字数量。

大题 1:封闭图形个数(快速排序)

  • 考点:快速排序算法、数组操作、权重计算。
  • 解题思路:计算每个数字的权重,然后通过快速排序对数组进行排序。

大题 2:吊坠(最小生成树)

  • 考点:最小生成树(贪心算法)、字符串处理、权重计算。
  • 解题思路:将问题转化为最小生成树问题,通过贪心算法构建最小生成树,并计算总权重。

大题 3:挖矿(数轴问题)

  • 考点:数轴上的动态规划、前缀和、贪心算法。
  • 解题思路:通过维护正负轴的矿洞位置,利用前缀和快速计算矿石数量,并通过贪心算法枚举四种情况求解最大值。

大题 6:智力测试(组合数学)

  • 考点:组合数学、前缀积、模逆元、二分查找。
  • 解题思路:通过组合数学公式和模逆元优化计算,利用前缀积快速计算区间贡献,并通过二分查找定位权值。

最大异或节点(字典树 + 图论)

  • 考点:字典树(Trie)、图论、异或操作。
  • 解题思路:利用字典树存储权值,通过图的遍历动态维护邻接节点的权值,计算最大异或值。

植物生命力(树状数组 + DFS)

  • 考点:树状数组、深度优先搜索(DFS)、区间统计。
  • 解题思路:通过树状数组高效统计区间内的节点数量,结合DFS遍历树结构,统计满足条件的节点对数量。

填空 1 劲舞团

import java.util.Scanner; // 导入 Scanner 类,用于从控制台读取输入
import java.io.File; // 导入 File 类,虽然在这段代码中没有使用到,可能是多余的导入
import java.io.FileNotFoundException; // 导入 FileNotFoundException 类,用于处理文件未找到的异常,但在这段代码中未使用

public class Main {
    public static void main(String[] args) {
        String a1, a2; // 用于存储前一次和当前次输入的字符串 a
        String b1, b2; // 用于存储前一次和当前次输入的字符串 b
        long t1, t2; // 用于存储前一次和当前次输入的时间戳
        int k = 0, max = 0; // k 用于记录当前连续匹配的次数,max 用于记录最大连续匹配次数
        Scanner sc = new Scanner(System.in); // 创建 Scanner 对象,从控制台读取输入

        // 先处理第一行输入
        a1 = sc.next(); // 读取第一个字符串 a1
        b1 = sc.next(); // 读取第一个字符串 b1
        t1 = sc.nextLong(); // 读取第一个时间戳 t1
        if (a1.equals(b1)) // 如果 a1 和 b1 相等
            k++; // 增加连续匹配次数

        // 如果还有输入,继续处理后续行
        while (sc.hasNext()) { // 判断是否还有输入
            a2 = sc.next(); // 读取当前行的字符串 a2
            b2 = sc.next(); // 读取当前行的字符串 b2
            t2 = sc.nextLong(); // 读取当前行的时间戳 t2

            // 判断当前行是否满足条件
            if (a2.equals(b2) && t2 - t1 < 1000) { // 如果 a2 和 b2 相等,并且时间差小于 1000 毫秒
                k++; // 增加连续匹配次数
            } else {
                max = Math.max(max, k); // 更新最大连续匹配次数
                k = a2.equals(b2) ? 1 : 0; // 如果 a2 和 b2 相等,重新开始计数,否则重置为 0
            }

            // 更新前一次的值为当前次的值,为下一次迭代做准备
            a1 = a2;
            b1 = b2;
            t1 = t2;
        }

        // 输出最大连续匹配次数
        System.out.println(max);
    }
}

填空 2 召唤数学精灵

找规律

import java.util.Scanner;
// 1:无需package
// 2: 类名必须Main, 不可修改

public class Main {
 public static void main(String[] args) {
    /*规律题思路:
    1.除了1以外的奇数都符合 1=0+1但0不是正整数 3=1+2 5=2+3 规律明显
    2.6=1+2+3 规律 6//2=3(奇数) 那么对于一个偶数除以2以后是奇数的也可以成立
    (对除后的奇数加减一,得到连续的正整数相加 8//2=4就不行)
    对于8:8//2=4 4//2=2 2//2=1;对于6:6//2=3 3%2!=0*/
    Scanner scan=new Scanner(System.in);
    int n=scan.nextInt(); // 读取测试用例的数量
    int ans=0; // 初始化答案计数器
    for(int i=1;i<=n;i++) {
        Long j=scan.nextLong(); // 读取每个测试用例的数字
        while(j%2==0&&j>1) { // 去除数字中的所有2因子
            j/=2;
        }
        if(j==1) { // 如果去除所有2因子后,数字为1,则计数器加1
            ans++;
        }
    }
    System.out.println(ans); // 输出满足条件的数字的数量
}
}

大题 1 封闭图形个数 快速排序

import java.util.Scanner;

public class Main {

    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        int a = sc.nextInt(); // 输入数组的大小
        int[] sz1 = new int[a]; // 存储输入的整数
        int[] sz2 = new int[a]; // 存储计算后的值,用于排序依据
        int[] sz3 = {1, 0, 0, 0, 1, 0, 1, 0, 2, 1}; // 固定数组,用于计算每个数字的权重

        // 读取输入的整数并存入 sz1
        for (int i = 0; i < a; i++) {
            sz1[i] = sc.nextInt();
        }

        // 计算 sz2 的值
        for (int i = 0; i < a; i++) {
            int c = 0; // 用于累加权重
            // 遍历数字的每一位
            for (int b = 1; b <= sz1[i]; b *= 10) {
                // 根据 sz3 数组计算当前位的权重,并累加到 c
                c = c + sz3[sz1[i] / b % 10];
            }
            sz2[i] = c; // 将计算结果存入 sz2
        }

        // 调用快速排序函数
        dfs(0, a - 1, sz1, sz2);

        // 输出排序后的 sz1 数组
        for (int j = 0; j < a; j++) {
            System.out.print(sz1[j] + " ");
        }
    }

    // 快速排序函数
    public static void dfs(int low, int high, int[] sz1, int[] sz2) {
        if (low < high) { // 如果区间内有多个元素
            int d = 0; // 用于记录分区点的位置
            int a = low, b = high - 1; // 初始化左右指针

            // 快速排序的分区过程
            while (a < b) {
                // 如果左指针的值大于基准值(sz2[high]),需要交换
                if (sz2[a] > sz2[high] || (sz2[a] == sz2[high] && sz1[a] > sz1[high])) {
                    while (a < b) { // 在右指针找到合适的值进行交换
                        if (sz2[b] < sz2[high] || (sz2[b] == sz2[high] && sz1[b] < sz1[high])) {
                            // 交换 sz2 和 sz1 中的值
                            int c = sz2[b];
                            sz2[b] = sz2[a];
                            sz2[a] = c;
                            c = sz1[b];
                            sz1[b] = sz1[a];
                            sz1[a] = c;
                            a++; // 左指针右移
                            b--; // 右指针左移
                            break;
                        } else {
                            b--; // 右指针左移
                        }
                    }
                } else {
                    a++; // 左指针右移
                }
            }

            // 处理最后一个分区点
            if (sz2[a] < sz2[high] || (sz2[a] == sz2[high] && sz1[a] < sz1[high])) {
                // 如果左指针的值小于基准值,交换 a+1 和 high
                int w = sz2[a + 1];
                sz2[a + 1] = sz2[high];
                sz2[high] = w;
                w = sz1[a + 1];
                sz1[a + 1] = sz1[high];
                sz1[high] = w;
                d = a + 1; // 分区点为 a+1
            } else if (sz2[a] > sz2[high] || (sz2[a] == sz2[high] && sz1[a] > sz1[high])) {
                // 如果左指针的值大于基准值,交换 a 和 high
                int w = sz2[a];
                sz2[a] = sz2[high];
                sz2[high] = w;
                w = sz1[a];
                sz1[a] = sz1[high];
                sz1[high] = w;
                d = a; // 分区点为 a
            } else {
                d = a; // 如果相等,分区点为 a
            }

            // 递归处理左右子区间
            dfs(low, d - 1, sz1, sz2);
            dfs(d + 1, high, sz1, sz2);
        }
    }
}

大题 2 吊坠

转换为最小生成树问题

import java.util.ArrayList;  // 导入ArrayList类,用于存储动态数组
import java.util.Scanner;    // 导入Scanner类,用于从标准输入读取数据

// 1: 无需使用package声明包名
// 2: 类名必须为Main,不可修改
public class Main {
    public static void main(String[] args) {
        Scanner scan = new Scanner(System.in);  // 创建Scanner对象,用于读取用户输入

        // 读取输入数据
        int n = scan.nextInt();  // 读取字符串的数量n
        int m = scan.nextInt();  // 读取另一个参数m(未在代码中使用)
        scan.nextLine();         // 跳过当前行的换行符

        // 创建一个字符串数组,用于存储输入的n个字符串
        String[] a = new String[n];
        for (int i = 0; i < n; i++) {
            String s = scan.nextLine();  // 逐行读取字符串
            a[i] = s;                    // 将读取的字符串存入数组
        }

        // 创建一个二维数组,用于记录每对字符串之间的权重
        int[][] weight = new int[n][n];
        // 计算每对字符串之间的权重
        for (int i = 0; i < n - 1; i++) {
            for (int j = i + 1; j < n; j++) {
                int k = weightCount(a[i], a[j]);  // 调用weightCount函数计算权重
                weight[i][j] = k;                // 存储权重
                weight[j][i] = k;                // 由于权重是双向的,同时存储对称位置的权重
            }
        }

        // 使用贪心算法构建最小生成树
        int result = 0;  // 初始化最小生成树的总权重为0
        ArrayList<Integer> tree = new ArrayList<>();  // 用于存储已经加入树的节点
        tree.add(0);  // 将第一个节点(索引为0)加入树
        ArrayList<Integer> node = new ArrayList<>();  // 用于存储尚未加入树的节点
        // 将所有其他节点加入node列表
        for (int i = 1; i < n; i++) {
            node.add(i);
        }

        // 构建最小生成树的过程
        while (node.size() != 0) {  // 当还有未加入树的节点时,继续循环
            int maxNode = 0;  // 用于记录当前找到的最大权重边的节点
            int maxEdge = 0;  // 用于记录当前找到的最大权重边的权重
            // 遍历未加入树的节点
            for (int i : node) {
                // 遍历已加入树的节点
                for (int j : tree) {
                    if (weight[i][j] >= maxEdge) {  // 如果找到更大的权重边
                        maxEdge = weight[i][j];  // 更新最大权重
                        maxNode = i;             // 更新最大权重边对应的节点
                    }
                }
            }
            // 将找到的最大权重边的节点加入树
            node.remove((Integer) maxNode);  // 从未加入树的节点列表中移除该节点
            tree.add(maxNode);               // 将该节点加入树
            result = result + maxEdge;       // 累加最大权重到总权重
        }

        // 输出最小生成树的总权重
        System.out.print(result);

        // 关闭Scanner对象
        scan.close();
    }

    // 定义权重计算函数
    public static int weightCount(String a, String b) {
        // 将两个字符串各自拼接一次,用于后续查找子串
        StringBuilder A = new StringBuilder(a + a);
        StringBuilder B = new StringBuilder(b + b);
        int k = a.length();  // 初始化最长公共子串长度为字符串长度
        int len = k;         // 用于记录当前查找的子串长度
        while (k > 0) {  // 从最长可能的子串长度开始递减查找
            boolean flag = false;  // 标志位,用于标记是否找到公共子串
            for (int i = 0; i < len; i++) {  // 遍历拼接后的字符串A
                String s = A.substring(i, i + k);  // 截取长度为k的子串
                if (B.indexOf(s) != -1) {  // 如果子串在B中存在
                    flag = true;           // 设置标志位为true
                    break;                 // 跳出循环
                }
            }
            if (flag) {  // 如果找到公共子串
                break;   // 停止查找,返回当前子串长度
            }
            k--;  // 如果未找到公共子串,递减子串长度
        }
        return k;  // 返回最长公共子串的长度作为权重
    }
}

大题 3 挖矿

矿洞位置是在数轴上且有正负之分,所以就要维护两个数组,下标表示位置,一个存储正轴位置的矿洞,一个存储负轴位置的矿洞,大小为10的6次方+1。 然后就是获取走过位置的矿石数,可以用前缀和进行求解,sum数组存放的就是从0走到该位置获取的矿石总数 知道走到哪个位置能获取的矿石总数后,就要考虑如何使挖矿数量最大化(是本题的难点),这题给了一个限定就是走m步能获取的最大矿石数量,同时由于每个矿洞只能挖一次,比如当你到-5的位置并且要返回到0点时消耗的步数为5 * 2 = 10得到的矿石数为sum2[5] 剩余的步数为m - 10。所以后面只需枚举四种情况并取最大值就行了。 1.一直往正轴方向走不回头 2,一直往负轴方向走不回头 3..先往正轴方向走然后回头 4.先往负轴方向走然后回头

import java.util.Scanner;

// 1: 无需package声明
// 2: 类名必须为Main,不可修改
public class Main {
    public static void main(String[] args) {
        Scanner scan = new Scanner(System.in);

        // 定义数轴的长度为10^6(正方向和负方向的最大范围)
        double length = Math.pow(10, 6);
        // 创建两个数组,分别记录正方向和负方向的矿洞位置的矿石数量
        int[] arr1 = new int[(int) (length + 1)]; // 正轴方向矿洞位置
        int[] arr2 = new int[(int) (length + 1)]; // 负轴方向矿洞位置

        // 读取输入
        int n = scan.nextInt(); // 矿洞的数量
        int m = scan.nextInt(); // 行走的最大距离

        // 创建两个前缀和数组,分别用于记录正方向和负方向的矿石数量的前缀和
        int[] sum1 = new int[m + 1]; // 正轴方向矿数前缀和数组
        int[] sum2 = new int[m + 2]; // 负轴方向矿数前缀和数组

        // 记录在位置0处的矿石数量
        int place0 = 0;

        // 遍历所有矿洞,根据位置更新矿石数量
        for (int i = 0; i < n; i++) {
            int place = scan.nextInt(); // 读取矿洞的位置
            if (place > 0) { // 如果位置为正数
                arr1[place]++; // 在正方向数组中对应的索引位置加1
            } else if (place < 0) { // 如果位置为负数
                arr2[-place]++; // 在负方向数组中对应的索引位置加1(取绝对值)
            } else {
                place0++; // 如果位置为0,则增加位置0的矿石数量
            }
        }

        // 计算正方向和负方向的矿石数量的前缀和
        for (int j = 1; j <= m; j++) {
            sum1[j] = arr1[j] + sum1[j - 1]; // 正方向前缀和
            sum2[j] = arr2[j] + sum2[j - 1]; // 负方向前缀和
        }

        // 初始化最大矿石数量
        int maxNum = Math.max(sum1[m], sum2[m]); // 先求单向行走的最大矿石数量

        // 枚举往返行走的情况
        for (int k = 1; k * 2 <= m && k <= m; k++) {
            // 枚举先向正方向走k步,再返回并走负方向m-2k步的情况
            maxNum = Math.max(sum1[k] + sum2[m - k * 2], maxNum);
            // 枚举先向负方向走k步,再返回并走正方向m-2k步的情况
            maxNum = Math.max(sum2[k] + sum1[m - k * 2], maxNum);
        }

        // 输出最终的最大矿石数量,加上位置0的矿石数量
        System.out.println(maxNum + place0);

        // 关闭Scanner对象
        scan.close();
    }
}

大题 6 智力测试

解题思路

题目要求计算在给定的行和列的权值序列中,满足特定条件的路径数量。具体思路如下:

  1. 组合数学的应用:
    • 行和列的权值序列是独立的,因此可以分别处理行和列的贡献,最后通过乘法原理合并结果。
    • 对于每个查询,需要计算在行和列的权值序列中满足条件的路径数量。
  1. 处理相同权值的情况:
    • 如果行或列的权值相同,但索引不同,说明路径无效,直接返回0。
    • 如果权值相同且索引相同,则路径有效,但需要特殊处理。
  1. 利用前缀积快速计算区间贡献:
    • 使用前缀积数组快速计算某个区间内权值的乘积。
    • 通过模逆元计算区间乘积的逆元,从而得到区间内权值的组合贡献。
  1. 组合数的计算:
    • 使用组合数公式 ( C(n, m) = \frac{n!}{m! \times (n - m)!} )。
    • 通过模逆元优化计算,避免大数除法。
  1. 二分查找定位权值:
    • 使用二分查找快速定位权值在有序序列中的位置。
    • 计算行和列的区间长度,进而计算组合数。

完整代码

import java.util.*;

public class Main {
    public static final int M = 1_000_000_007;
    public static long[] f = new long[200005];

    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        int n = scanner.nextInt(); // 行的数量
        int m = scanner.nextInt(); // 列的数量
        int t = scanner.nextInt(); // 查询次数

        // 初始化阶乘数组
        f[0] = 1;
        for (int i = 1; i <= n + m; i++) {
            f[i] = f[i - 1] * i % M;
        }

        // 读取输入并统计频率
        Map<Integer, Integer> mpa = new TreeMap<>(); // 行权值频率
        Map<Integer, Integer> mpb = new TreeMap<>(); // 列权值频率
        int[] a = new int[n + 1]; // 行权值数组
        int[] b = new int[m + 1]; // 列权值数组

        for (int i = 1; i <= n; i++) {
            a[i] = scanner.nextInt();
            mpa.put(a[i], mpa.getOrDefault(a[i], 0) + 1);
        }

        for (int i = 1; i <= m; i++) {
            b[i] = scanner.nextInt();
            mpb.put(b[i], mpb.getOrDefault(b[i], 0) + 1);
        }

        // 添加0作为边界
        mpa.put(0, 0);
        mpb.put(0, 0);

        // 将map转换为列表
        List<Map.Entry<Integer, Integer>> va = new ArrayList<>(mpa.entrySet());
        List<Map.Entry<Integer, Integer>> vb = new ArrayList<>(mpb.entrySet());

        // 计算前缀积
        long[] sa = new long[va.size()];
        long[] sb = new long[vb.size()];
        sa[0] = 1;
        sb[0] = 1;

        for (int i = 1; i < va.size(); i++) {
            sa[i] = sa[i - 1] * va.get(i).getValue() % M;
        }

        for (int i = 1; i < vb.size(); i++) {
            sb[i] = sb[i - 1] * vb.get(i).getValue() % M;
        }

        // 处理查询
        while (t-- > 0) {
            int x1 = scanner.nextInt();
            int y1 = scanner.nextInt();
            int x2 = scanner.nextInt();
            int y2 = scanner.nextInt();

            // 检查无效路径
            if (a[x2] < a[x1] || b[y2] < b[y1] ||
                (a[x2] == a[x1] && x2 != x1) ||
                (b[y2] == b[y1] && y2 != y1)) {
                System.out.println(0);
                continue;
            }

            // 使用二分查找找到区间
            int p = lowerBound(va, a[x2]);
            int q = lowerBound(va, a[x1]);
            int r = lowerBound(vb, b[y2]);
            int s = lowerBound(vb, b[y1]);

            // 计算区间乘积
            long rowProduct = (p == q) ? 1 : sa[p - 1] * pow(sa[q], M - 2) % M;
            long colProduct = (r == s) ? 1 : sb[r - 1] * pow(sb[s], M - 2) % M;

            // 计算组合数
            long comb = C(p - q + r - s, p - q);

            // 最终结果
            long result = rowProduct * colProduct % M * comb % M;
            System.out.println(result);
        }

        scanner.close();
    }

    // 快速幂
    public static long pow(long a, int k) {
        if (k == 0) return 1;
        return pow(a * a % M, k / 2) * (k % 2 == 1 ? a : 1) % M;
    }

    // 组合数计算
    public static long C(int n, int m) {
        if (m > n) return 0;
        return f[n] * pow(f[m] * f[n - m] % M, M - 2) % M;
    }

    // 二分查找
    public static int lowerBound(List<Map.Entry<Integer, Integer>> list, int key) {
        int low = 0, high = list.size();
        while (low < high) {
            int mid = (low + high) / 2;
            if (list.get(mid).getKey() < key) {
                low = mid + 1;
            } else {
                high = mid;
            }
        }
        return low;
    }
}

代码补充说明

  1. 二分查找:
    • 使用 lowerBound 方法找到目标权值在有序列表中的位置。
    • 如果目标权值不存在,则返回其插入位置。
  1. 前缀积与逆元:
    • 前缀积数组 sa 和 sb 用于快速计算区间乘积。
    • 使用模逆元计算区间乘积的逆元,从而得到区间内权值的组合贡献。
  1. 组合数计算:
    • 使用预处理的阶乘数组 f 和模逆元计算组合数。
    • 公式为 ( C(n, m) = \frac{n!}{m! \times (n - m)!} ),通过模逆元优化计算。
  1. 查询处理:
    • 对每个查询,检查路径是否有效。
    • 如果路径有效,计算行和列的区间贡献,并通过组合数公式计算最终结果。

输入输出示例

输入:
4 3 2
1 2 3 4
1 2 3
1 1 2 2
2 2 3 3
输出:
1
6

最大异或节点 字典树(Trie)和图论的算法问题

字典树(Trie)和图论的算法问题

解题思路

1. 问题背景

题目要求在一个图中,通过某种方式最大化某个值。具体来说:

  • 输入一个包含 n 个节点的图,每个节点有一个权值 a[i]。
  • 图是通过邻接表 g 表示的,节点之间有边连接。
  • 目标是通过某种策略最大化某个节点的权值。
2. 核心思想
  1. 字典树(Trie):
    • 使用字典树存储所有节点的权值,以便快速查找和更新。
    • 字典树的每个节点表示一个二进制位,通过遍历字典树可以找到与某个权值异或(XOR)最大的值。
  1. 图的遍历:
    • 遍历图的每个节点,对于每个节点,暂时移除其邻接节点的权值,计算当前节点的最大异或值,然后再恢复这些权值。
    • 这种策略类似于“动态维护”,通过局部调整来寻找全局最优解。
  1. 异或操作:
    • 异或操作有一个重要性质:a ^ b 的结果最大时,a 和 b 的二进制表示在每一位上尽量不同。
    • 通过字典树,可以高效地找到与某个权值异或最大的值。
3. 算法步骤
  1. 初始化:
    • 读取每个节点的权值 a[i],并将其插入字典树。
    • 构建图的邻接表 g。
  1. 遍历图:
    • 对于每个节点 i,暂时移除其邻接节点的权值(通过字典树的 del 方法)。
    • 使用字典树的 find 方法找到与当前节点权值异或最大的值。
    • 更新全局最大值 ans。
    • 恢复邻接节点的权值(通过字典树的 add 方法)。
  1. 输出结果:
    • 输出全局最大值 ans。

代码详细注释

import java.util.*;
import java.io.*;
import java.math.*;

public class Main {
    static PrintWriter pw = new PrintWriter(new OutputStreamWriter(System.out));
    static int n, m, maxn = 200005, inf = 0x3f3f3f3f; // 常量定义
    static long ans = 0, mod = (int) 1e9 + 7, INF = (long) 2e18; // 常量定义

    public static void main(String[] args) throws IOException {
        new Task().main(); // 调用主逻辑
        pw.flush(); // 刷新输出流
    }

    static class Task {
        void main() throws IOException {
            int t = 1; // 测试用例数量(这里固定为1)
            while (t-- > 0) {
                solve(); // 解决每个测试用例
            }
        }

        int[] a = new int[maxn]; // 存储每个节点的权值
        List<Integer>[] g = new List[maxn]; // 图的邻接表
        int[][] ch = new int[maxn << 4][2]; // 字典树的子节点数组
        int[] cnt = new int[maxn << 4]; // 字典树每个节点的计数
        int tot = 0; // 字典树的节点总数

        void solve() throws IOException {
            n = sc.nextI(); // 读取节点数量
            for (int i = 1; i <= n; i++) {
                a[i] = sc.nextI(); // 读取每个节点的权值
                add(a[i]); // 将权值插入字典树
            }

            // 初始化图的邻接表
            for (int i = 0; i <= n; i++) {
                g[i] = new ArrayList<>();
            }

            // 构建图
            int s = 0; // 特殊节点(起点)
            for (int i = 1; i <= n; i++) {
                int x = sc.nextI() + 1; // 读取邻接节点
                if (x == 0) {
                    s = i; // 特殊节点
                } else {
                    g[i].add(x); // 添加边
                    g[x].add(i); // 添加反向边(无向图)
                }
            }

            // 遍历每个节点
            for (int i = 1; i <= n; i++) {
                // 暂时移除邻接节点的权值
                for (int x : g[i]) {
                    del(a[x]);
                }

                // 计算当前节点的最大异或值
                ans = Math.max(ans, find(a[i]));

                // 恢复邻接节点的权值
                for (int x : g[i]) {
                    add(a[x]);
                }
            }

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

        // 字典树查找最大异或值
        private long find(int w) {
            int p = 0; // 当前节点
            int res = 0; // 最大异或值
            for (int i = 30; i >= 0; i--) { // 从最高位到最低位
                int x = (w >> i) % 2; // 当前位的值
                if (cnt[ch[p][0]] == 0 && cnt[ch[p][1]] == 0) break; // 如果当前节点没有子节点,退出
                if (ch[p][1 - x] != 0 && cnt[ch[p][1 - x]] > 0) { // 如果存在与当前位相反的子节点
                    res |= 1 << i; // 更新异或值
                    p = ch[p][1 - x]; // 移动到相反的子节点
                } else {
                    p = ch[p][x]; // 移动到相同的子节点
                }
            }
            return res;
        }

        // 从字典树中删除一个权值
        private void del(int w) {
            int p = 0; // 当前节点
            for (int i = 30; i >= 0; i--) { // 从最高位到最低位
                int x = (w >> i) % 2; // 当前位的值
                p = ch[p][x]; // 移动到对应的子节点
                cnt[p]--; // 减少计数
            }
        }

        // 向字典树中添加一个权值
        private void add(int w) {
            int p = 0; // 当前节点
            for (int i = 30; i >= 0; i--) { // 从最高位到最低位
                int x = (w >> i) % 2; // 当前位的值
                if (ch[p][x] == 0) ch[p][x] = ++tot; // 如果子节点不存在,创建新节点
                p = ch[p][x]; // 移动到子节点
                cnt[p]++; // 增加计数
            }
        }
    }

    // 快速输入类
    static class Input {
        Input(InputStream in) {
            this.in = in;
        }

        InputStream in;
        byte[] bb = new byte[1 << 15]; // 输入缓冲区
        int i, n;

        byte getc() {
            if (i == n) { // 如果缓冲区为空
                i = n = 0;
                try {
                    n = in.read(bb); // 重新读取数据
                } catch (IOException e) {
                }
            }
            return i < n ? bb[i++] : 0; // 返回下一个字节
        }

        int nextI() {
            byte c = 0;
            while (c <= ' ') c = getc(); // 跳过空白字符
            int f = 1; // 符号标志
            int a = 0; // 结果
            while (c > ' ') {
                if (c == '-') {
                    f = -1;
                    c = getc();
                    continue;
                }
                a = a * 10 + c - '0'; // 构造整数
                c = getc();
            }
            return a * f; // 返回结果
        }

        long nextL() {
            byte c = 0;
            while (c <= ' ') c = getc(); // 跳过空白字符
            int f = 1; // 符号标志
            long a = 0; // 结果
            while (c > ' ') {
                if (c == '-') {
                    f = -1;
                    c = getc();
                    continue;
                }
                a = a * 10 + c - '0'; // 构造长整数
                c = getc();
            }
            return a * f; // 返回结果
        }

        byte[] cc = new byte[1 << 7]; // 字符串缓冲区

        String next() {
            byte c = 0;
            while (c <= ' ') c = getc(); // 跳过空白字符
            int k = 0;
            while (c > ' ') {
                if (k == cc.length) cc = Arrays.copyOf(cc, cc.length << 1); // 扩容
                cc[k++] = c; // 保存字符
                c = getc();
            }
            return new String(cc, 0, k); // 返回字符串
        }
    }

    static Input sc = new Input(System.in); // 创建输入对象
}

代码功能模块解析

1. 字典树(Trie)

字典树用于高效地存储和查找二进制表示的权值。每个节点表示一个二进制位,通过遍历字典树可以找到与某个权值异或最大的值。

  • add 方法:
    • 将一个权值插入字典树。
    • 从最高位到最低位,逐位构建路径,并更新计数。
  • del 方法:
    • 从字典树中移除一个权值。
    • 逐位减少计数。
  • find 方法:
    • 查找与某个权值异或最大的值。
    • 从最高位到最低位,优先选择与当前位相反的子节点,从而最大化异或结果。
2. 图的遍历

图通过邻接表 g 表示,每个节点的邻接节点存储在列表中。

  • 动态维护:
    • 对于每个节点,暂时移除其邻接节点的权值,计算当前节点的最大异或值,然后再恢复这些权值。
    • 这种策略确保在计算当前节点时,邻接节点的权值不会影响结果。
3. 输入输出
  • 使用自定义的 Input 类实现快速输入。
  • 使用 PrintWriter 实现快速输出。

代码优化点

  1. 字典树的高效性:
    • 字典树的深度为 31(31 位整数),查找和插入操作的时间复杂度为 ( O(31) )。
  1. 动态维护:
    • 通过暂时移除和恢复邻接节点的权值,避免了全局修改对后续计算的影响。
  1. 快速输入输出:
    • 使用自定义的输入输出类,避免了标准输入输出的性能瓶颈。

植物生命力

import java.util.*;
import java.io.*;

public class Main {
    static int N = 200010;  // 节点数量上限
    static int n, s;  // n是节点数量,s是根节点编号
    static int[] a = new int[N];  // 存储每个节点的生命力
    static long res = 0;  // 存储结果
    static List<Integer>[] g = new List[N];  // 存储图的邻接表
    static int[] tr = new int[N];  // 树状数组

    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        StringTokenizer st = new StringTokenizer(br.readLine());
        n = Integer.parseInt(st.nextToken());  // 读取节点数量
        s = Integer.parseInt(st.nextToken());  // 读取根节点编号

        // 初始化邻接表
        for (int i = 1; i <= n; i++) {
            g[i] = new ArrayList<>();
        }

        // 读取每个节点的生命力
        st = new StringTokenizer(br.readLine());
        for (int i = 1; i <= n; i++) {
            a[i] = Integer.parseInt(st.nextToken());
        }

        // 构建图的邻接表
        for (int i = 0; i < n - 1; i++) {
            st = new StringTokenizer(br.readLine());
            int u = Integer.parseInt(st.nextToken());
            int v = Integer.parseInt(st.nextToken());
            g[u].add(v);
            g[v].add(u);
        }

        // 从根节点开始深度优先搜索
        dfs(s, -1);

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

    // 深度优先搜索函数
    static void dfs(int u, int fa) {
        modify(a[u], 1);  // 在树状数组中标记当前节点的生命力

        // 遍历当前节点的所有子节点
        for (int v : g[u]) {
            if (v != fa) {  // 避免回溯到父节点
                dfs(v, u);  // 递归遍历子节点
            }
        }

        // 计算当前节点的子树中满足条件的节点数量
        for (int k = 2; k * a[u] <= n; k++) {
            int t = k * a[u];
            res -= query(t) - query(t - 1);  // 减去生命力为 k * a[u] 的节点数量
        }

        res += query(n) - query(a[u]);  // 加上生命力小于 a[u] 的节点数量
        modify(a[u], -1);  // 取消标记
    }

    // 修改树状数组的函数
    static void modify(int idx, int val) {
        while (idx <= n) {
            tr[idx] += val;  // 更新树状数组的值
            idx += idx & -idx;  // 跳到下一个区间
        }
    }

    // 查询树状数组的函数
    static int query(int idx) {
        int sum = 0;
        while (idx > 0) {
            sum += tr[idx];  // 累加区间值
            idx -= idx & -idx;  // 跳到上一个区间
        }
        return sum;
    }
}

代码解析

1. 数据结构
  • a 数组:存储每个节点的生命力。
  • g 邻接表:存储树的结构。
  • tr 树状数组:用于高效统计区间内的节点数量。
  • res:全局变量,用于存储最终结果。
2. 树状数组的实现
  • modify 方法:
    • 用于在树状数组中标记或取消标记某个值。
    • 时间复杂度为 (O(\log n))。
  • query 方法:
    • 用于查询某个区间内的累加值。
    • 时间复杂度为 (O(\log n))。
3. DFS 的递归逻辑
  • 在进入节点 u 时,标记其生命力值 a[u]。
  • 递归处理所有子节点。
  • 在离开节点 u 时,取消标记其生命力值。
4. 统计满足条件的节点对
  • 对于每个节点 u,统计其子树中满足条件的节点数量:
    • 生命力是当前节点的整数倍:通过树状数组查询 [k * a[u], k * a[u]] 的数量。
    • 生命力小于当前节点:通过树状数组查询 [1, a[u] - 1] 的数量。
5. 结果更新
  • 使用全局变量 res 累加满足条件的节点对数量。

复杂度分析

  1. 时间复杂度:
    • 每个节点的 DFS 遍历:(O(n))。
    • 每个节点的树状数组操作:(O(\log n))。
    • 总时间复杂度:(O(n \log n))。
  1. 空间复杂度:
    • 树状数组和邻接表:(O(n))。

更多推荐