题目

找倍数(BFS + 同余剪枝)

解题思路

题目关键特征

给定正整数 n(1 ≤ n ≤ 200),要求找到一个最短的正整数 m,满足:

  • mn 的倍数;
  • m 的十进制表示仅由数字 '0' 和 '1' 构成
  • m 不能以 '0' 开头(隐含第一位为 '1')。

由于答案可能长达上百位,无法用 longBigInteger 暴力枚举倍数,必须避免显式构造大整数。

算法适用条件

问题本质是在无限状态空间中搜索满足整除条件的最短字符串。BFS 天然适用于“最短路径”类问题,而本题的状态可压缩为当前构造数字对 n 的余数,状态总数不超过 n(即 0 到 n-1),因此 BFS 在有限步内必终止。

BFS搜索步骤演示动图

BFS搜索动图

问题转化思路

将“构造只含 0/1 的数字”转化为从字符串 "1" 出发,每次在末尾追加 '0' 或 '1' 的状态扩展过程。关键洞察在于:

若两个不同数字 ab 满足 a % n == b % n,则它们后续添加相同后缀产生的新数对 n 的余数也相同。因此,每个余数只需保留首次出现的最短路径。

由此,搜索空间从指数级字符串集合压缩为最多 n 个余数状态。

有效剪枝手段

使用 visited 集合记录已出现的余数。若某余数 r 已被访问,则跳过所有后续产生 r 的更长字符串。此剪枝保证:

  • 每个余数仅入队一次;
  • BFS 首次到达余数 0 时对应的字符串即为最短解;
  • 时间复杂度为 O(n),空间复杂度为 O(n)。

若不剪枝,队列将无限增长(如 "10", "100", "1000", ... 均余 4 mod 6),导致超时或内存溢出。

余数递推机制

为避免大数运算,采用逐位取模计算余数:

remainder = (remainder * 10 + digit) % n

该公式基于模运算分配律:(a * 10 + d) % n = ((a % n) * 10 + d) % n。由此可在 O(len(s)) 时间内计算任意长度字符串的余数,且中间值始终小于 n

参考代码

import java.util.*;

public class Main {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        int n = sc.nextInt();
        String m = bfs(n);
        System.out.println(m);
    }

    private static String bfs(int n) {
        Queue<String> queue = new LinkedList<>();
        // 记录已经出现过的余数: 每个余数只需处理一次, 保证最短路径
        Set<Integer> visited = new HashSet<>();
        // 起始状态: 第一位必须是 '1', 不能以 '0' 开头
        queue.offer("1");
        // 初始余数为 1 % n, 但暂不加入 visited (在扩展时统一处理)
    
        while (!queue.isEmpty()) {
            String s = queue.poll();
        
            // 逐位计算 s 对 n 的余数, 避免大数溢出
            int remainder = 0;
            for (char c : s.toCharArray()) {
                remainder = (remainder * 10 + (c - '0')) % n;
            }
        
            // 终止条件: 余数为 0 表示 s 是 n 的倍数
            if (remainder == 0) {
                return s;
            }
        
            // 尝试在当前字符串末尾追加 '0' 和 '1'
            String s0 = s + "0";
            String s1 = s + "1";
        
            // 利用余数递推公式快速计算新余数, 无需重新遍历整个字符串
            int r0 = (remainder * 10 + 0) % n;
            int r1 = (remainder * 10 + 1) % n;
        
            // 剪枝: 若余数未出现过, 则加入队列并标记
            if (!visited.contains(r0)) {
                visited.add(r0);
                queue.offer(s0);
            }
            if (!visited.contains(r1)) {
                visited.add(r1);
                queue.offer(s1);
            }
        }
        // 根据鸽巢原理, 解必然存在, 此处不会执行
        return "";
    }
}

八数码(BFS状态搜索)

解题思路

题目关键特征

本题是经典的八数码问题(8-puzzle),目标是通过移动空格(x)与相邻数字交换,将初始 3×3 网格变换为目标状态 12345678x。每一步操作只能交换 x 与其上下左右的数字,求最少步数。

算法适用条件

  • 状态空间有限:3×3 网格共有 9! / 2 = 181440 个可达状态(因奇偶性限制),适合 BFS 枚举
  • 最短路径要求:BFS 天然保证首次到达目标状态时步数最少
  • 状态可编码:将 3×3 网格压缩为长度为 9 的字符串(如 "1234x6758"),便于哈希存储和比较

有效剪枝手段

  • 全局状态去重:使用 Set<String> visited 记录所有已访问的完整棋盘布局,避免重复搜索
    • ❌ 错误做法:仅记录 x 的位置(不同布局下 x 可在同一位置)
    • ✅ 正确做法:整个字符串作为状态唯一标识
  • 边界检查:交换前验证新坐标 (nx, ny) 是否在 [0, 2] 范围内

问题转化思路

将网格操作转化为状态图搜索

  • 节点 = 一种棋盘布局(字符串)
  • = 一次合法的 x 与邻居交换
  • 起点 = 输入字符串,终点 = "12345678x"
  • 搜索策略 = BFS(逐层扩展,首次命中即最优)

💡 类比:迷宫中状态是坐标 (x,y),而八数码中状态是整个棋盘——因为“怎么来的”会影响后续可能性。


参考代码

import java.util.*;

public class Main {
    static int n = 3;

    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        StringBuilder sb = new StringBuilder();
        // 读取9个字符(含'x'),拼接成初始状态字符串
        for (int i = 1; i <= n * n; i++) {
            sb.append(sc.next());
        }
        String start = sb.toString();
        // 目标状态固定为"12345678x"
        int result = bfs(start, "12345678x");
        System.out.println(result);
    }

    // 四个移动方向:上、下、左、右
    static int[][] dirs = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}};

    /**
     * 使用BFS搜索从start到target的最少步数
     * @param start 初始棋盘状态(长度为9的字符串)
     * @param target 目标状态 "12345678x"
     * @return 最少交换次数,若不可达返回-1
     */
    private static int bfs(String start, String target) {
        Queue<String> queue = new LinkedList<>();
        Set<String> visited = new HashSet<>();
        queue.offer(start);
        visited.add(start);
        int step = 0;

        while (!queue.isEmpty()) {
            int size = queue.size();
            // 处理当前层的所有状态(步数相同)
            for (int i = 0; i < size; i++) {
                String state = queue.poll();
                // 检查是否达到目标
                if (state.equals(target)) {
                    return step;
                }
                // 定位空格'x'的位置
                int index = state.indexOf('x');
                int x = index / n, y = index % n;
                // 尝试四个方向的移动
                for (int[] dir : dirs) {
                    int nx = x + dir[0], ny = y + dir[1];
                    // 检查新位置是否在棋盘内
                    if (nx >= 0 && nx < n && ny >= 0 && ny < n) {
                        int newIndex = nx * n + ny;
                        char[] chars = state.toCharArray();
                        // 交换空格与相邻数字
                        char temp = chars[index];
                        chars[index] = chars[newIndex];
                        chars[newIndex] = temp;
                        String newState = new String(chars);
                        // 若新状态未访问过,则加入队列
                        if (!visited.contains(newState)) {
                            queue.offer(newState);
                            visited.add(newState);
                        }
                    }
                }
            }
            // 当前层处理完毕,步数加1
            step++;
        }
        // 队列空仍未找到目标,说明不可达
        return -1;
    }
}

djwcb

解题思路(循环节发现与快速幂)

题目关键特征

给定极大指数 p(可能长达 20 万位),求 x^p mod 10。由于 p 远超任何整型范围,不能直接计算幂,必须寻找规律或数学性质。

从实验出发:观察个位数的幂次规律

结果仅依赖个位x^p mod 10 等价于 (x mod 10)^p mod 10,所以我们只需要关注个位数的 p次幂是什么样的

我们先写一个小程序,打印 0~9 每个数字的前若干次幂的个位数:

public static void main(String[] args) {
    for (int i = 0; i <= 9; i++) {
        test(i);
    }
}
private static void test(long num) {
    System.out.print(num + ":\t");
    long base = num;
    for (int i = 1; i <= 12; i++) {
        base %= 10;
        System.out.print(base + "\t");
        base *= num;
    }
    System.out.println();
}

运行结果如下:

0:	0	0	0	0	0	0	0	0	0	0	0	0
1:	1	1	1	1	1	1	1	1	1	1	1	1
2:	2	4	8	6	2	4	8	6	2	4	8	6
3:	3	9	7	1	3	9	7	1	3	9	7	1
4:	4	6	4	6	4	6	4	6	4	6	4	6
5:	5	5	5	5	5	5	5	5	5	5	5	5
6:	6	6	6	6	6	6	6	6	6	6	6	6
7:	7	9	3	1	7	9	3	1	7	9	3	1
8:	8	4	2	6	8	4	2	6	8	4	2	6
9:	9	1	9	1	9	1	9	1	9	1	9	1

关键发现

  • 所有个位数的幂次个位都会进入循环
  • 最长循环节长度为 4(如 2、3、7、8)
  • 其余要么是 1(如 0、1、5、6),要么是 2(如 4、9)

这意味着:只要知道 p 对 4 取余的结果,就能确定最终个位数

📌 注意:如果 p % 4 == 0,说明刚好走完若干个完整循环,应取第 4 项(而不是第 0 项)。

如何对超大指数 p 取模 4?

题目中 p 是一个可能长达 20 万位的字符串。但我们知道:

  • 一个数是否能被 4 整除,只看最后两位(因为 100 是 4 的倍数)
  • 例如:123456 → 看 5656 % 4 = 0 → 整个数能被 4 整除

因此,只需提取 p 的最后 1~2 位转成整数,再对 4 取模即可。

特殊情况处理

  • x % 10 == 0(即个位是 0),则无论多少次幂(只要 p ≥ 1),结果都是 0

额外知识点:快速幂思想简介(了解即可)

虽然本题因模 10 的特殊性不需要复杂算法,但快速幂是处理大指数幂取模的通用技巧,值得了解。

什么是快速幂?

快速幂是一种用 O(log p) 时间计算 a^p mod m 的方法,核心思想是二进制拆分指数

例如:
a^13 = a^(8+4+1) = a^8 * a^4 * a^1
a^2 = a*a, a^4 = (a^2)^2, a^8 = (a^4)^2……每次平方即可得到更高次幂。

为什么本题不用标准快速幂?

因为 p 是字符串形式,做“除以 2”或“判断奇偶”需要高精度操作,代码复杂。
但我们可以用十进制快速幂:从低位到高位处理每一位数字,并利用 x^10 ≡ x^2 (mod 10) 的性质简化计算(见方法三)。

💡 建议:先掌握循环节法(方法二),它是本题最直观、高效且易懂的解法。快速幂可后续在处理一般模数(如 mod 1e9+7)时深入学习。


参考代码

方法一:BigInteger 直接计算(Java 特供)

import java.math.BigInteger;
import java.util.Scanner;

public class Main {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        int T = sc.nextInt();
        while (T-- > 0) {
            BigInteger x = sc.nextBigInteger();
            BigInteger p = sc.nextBigInteger();
            // 使用BigInteger内置的模幂运算: x^p mod 10
            BigInteger result = x.modPow(p, BigInteger.valueOf(10));
            System.out.println(result);
        }
    }
}

方法二:循环节 + 大数取模(推荐)

import java.util.Scanner;

public class Main {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        int T = sc.nextInt();
        while (T-- > 0) {
            int x = sc.nextInt();
            String p = sc.next();
            int base = x % 10;
            if (base == 0) {
                System.out.println(0);
                continue;
            }
            int cnt = getMod4(p);
            long result = pow(base, cnt);
            System.out.println(result);
        }
    }

    /**
     * 计算超长数字字符串 p 对 4 取模的结果
     * @param p 指数字符串(可能长达200000位)
     * @return p mod 4, 若结果为0则返回4(对应循环节最后一项)
     */
    private static int getMod4(String p) {
        int num;
        if (p.length() == 1) {
            num = p.charAt(0) - '0';
        } else {
            // 取末两位(因100是4的倍数,高位不影响mod4)
            num = Integer.parseInt(p.substring(p.length() - 2));
        }
        int mod = num % 4;
        return mod == 0 ? 4 : mod;
    }

    /**
     * 计算 base^cnt mod 10(cnt ∈ {1,2,3,4},直接循环即可)
     * @param base 底数个位 (0~9)
     * @param cnt 指数 (1~4)
     * @return 结果
     */
    public static long pow(int base, int cnt) {
        long result = 1;
        for (int i = 0; i < cnt; i++) {
            result = (result * base) % 10;
        }
        return result;
    }
}

方法三:十进制快速幂(扩展)

import java.util.Scanner;

public class Main {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        int T = sc.nextInt();
        while (T-- > 0) {
            int x = sc.nextInt();
            String p = sc.next();
            long result = modPow(x, p, 10);
            System.out.println(result);
        }
    }

    /**
     * 十进制快速幂:从低位到高位处理指数字符串
     * 利用 mod 10 下 x^10 ≡ x^2 的性质简化倍增步骤
     * @param x 底数
     * @param p 指数字符串
     * @param mod 模数 (此处为10)
     * @return x^p mod mod
     */
    private static long modPow(int x, String p, int mod) {
        x %= mod;
        if (x == 0) return 0;
        long result = 1;
        // 从个位开始逐位处理
        for (int i = p.length() - 1; i >= 0; i--) {
            int digit = p.charAt(i) - '0';
            // 计算当前位贡献: (current_base)^digit
            long contrib = 1;
            for (int j = 0; j < digit; j++) {
                contrib = (contrib * x) % mod;
            }
            result = (result * contrib) % mod;
            // 为下一位准备:x = x^10 mod 10 → 实际只需 x = x^2 mod 10
            // 在 mod 10 下, x^10 ≡ x^2 (由循环节性质决定)
            x = (x * x) % mod;
        }
        return result;
    }
}

循环逆序对

解题思路(从手算每一块到公式归纳的完整推导)

题目关键特征

给定一个数组,例如 [1, 3, 2],将其重复 k 次得到新序列,求其中逆序对总数。
由于 k 可达 10^9,不能显式构造序列,必须通过分析每一块的贡献规律求解。


从最小例子开始:原数组 [1, 3, 2]

先看它自己有多少逆序对:

  • 1 后面没有比它小的
  • 3 后面有 2 → 1 个
  • 2 后面没了

✅ 内部逆序对数 intra = 1

再统计每个元素在整个数组中“比它大的数”的个数:

元素 比它大的数 个数
a[0] 1 3, 2 2
a[1] 3 0
a[2] 2 3 1

总和:2 + 0 + 1 = 3,记作 sumGt = 3


手动计算 k = 1, 2, 3 的总逆序对

k = 1:只有第 0 块

序列:[1,3,2]
逆序对:1 个 → 总数 = 1

k = 2:第 0 块 + 第 1 块

第 1 块每个元素 vs 第 0 块:

元素 第0块中比它大的个数 跨块贡献
1 1 2 2
3 3 0 0
2 2 1 1

→ 跨块新增:2+0+1 = 3
块内新增:1(第1块内部)
总计:1(原) + 1(新块内) + 3(跨块) = 5

k = 3:三块

第2块前面有两整块,每个元素的跨块贡献翻倍:

元素 单块比它大的个数 两块中比它大的个数 贡献
1 2 4 4
3 0 0 0
2 1 2 2

→ 第2块跨块贡献:6块内贡献:1累计:

  • 块内:1×3 = 3
  • 跨块:3(第1块) + 6(第2块) = 9
  • 总计:12

归纳规律

  • 块内总贡献 = k * intra
  • 跨块总贡献 = sumGt * (0 + 1 + 2 + ... + (k-1)) = sumGt * k * (k - 1) / 2

因为 kk-1 是连续整数,乘积必为偶数,/2 无精度损失。


参考代码

import java.util.Scanner;

public class Main {
    public static void main(String[] args) {
        int MOD = 998244353;
        Scanner sc = new Scanner(System.in);
        int n = sc.nextInt();
        long k = sc.nextLong();
        int[] a = new int[n];
        for (int i = 0; i < n; i++) {
            a[i] = sc.nextInt();
        }
    
        // 计算原数组内部逆序对数 intra
        long intra = 0;
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < i; j++) {
                if (a[j] > a[i]) {
                    intra++;
                }
            }
        }
    
        // 计算每个元素在整个数组中比它大的总数 sumGt
        long sumGt = 0;
        for (int i = 0; i < n; i++) {
            int cnt = 0;
            for (int j = 0; j < n; j++) {
                if (a[j] > a[i]) {
                    cnt++;
                }
            }
            sumGt += cnt;
        }
    
        // 总答案 = k * intra + sumGt * k * (k - 1) / 2
        long res = (k % MOD) * (intra % MOD) % MOD;
        long t = k * (k - 1) / 2 % MOD; // 安全整除
        res = (res + (sumGt % MOD) * t) % MOD;
    
        System.out.println(res);
    }
}

旅行II

解题思路(连续区间贪心)

题意

  • n 个国家,位置是 a[0]a[n-1](可正可负,可能有 0)
  • 小蓝从 0 出发,每次走 1 单位距离
  • 他要访问 恰好 k 个国家(从这 n 个里选)
  • 最小总移动距离

关键结论

最优方案一定是:

  • 选排序后连续的 k 个点
  • 路线只有两种:
    1. 0 → 最左 → 最右
    2. 0 → 最右 → 最左
      中间的点顺路就走完了,不用额外绕

所以访问区间 [L, R] 的最小代价是:cost = (R - L) + min(|L|, |R|)

💡 为什么必须连续?
假设你跳着选点,比如选了 -10、-5、5、10,但跳过了 0。那换成 -10、-5、0、5 肯定更近——因为区间长度变短了,而且离 0 更近。所以“挤在一起”的连续 k 个点一定更优。

做法

  1. a 数组排序
  2. 枚举所有长度为 k 的连续子数组(i0n-k
    • L = a[i]
    • R = a[i + k - 1]
    • cost = (R - L) + Math.min(Math.abs(L), Math.abs(R))
  3. 所有 cost 取最小值,输出

时间复杂度 O(n log n),完全满足 n ≤ 1e5


参考代码

import java.util.*;

public class Main {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        int n = sc.nextInt();
        int k = sc.nextInt();
        long[] a = new long[n];
        for (int i = 0; i < n; i++) {
            a[i] = sc.nextLong();
        }
        // 按坐标从小到大排序
        Arrays.sort(a);

        long ans = Long.MAX_VALUE;
        // 枚举所有长度为k的连续区间
        for (int i = 0; i <= n - k; i++) {
            long L = a[i];                // 当前区间的最左端
            long R = a[i + k - 1];        // 当前区间的最右端
            // 总距离 = 区间长度 + 从0到较近端点的距离
            long cost = (R - L) + Math.min(Math.abs(L), Math.abs(R));
            if (cost < ans) {
                ans = cost;
            }
        }
        System.out.println(ans);
    }
}

一个敲代码的