IP路由最长匹配

127 阅读7分钟

IP路由最长匹配

问题背景

最长前缀匹配 (Longest Prefix Match) 是 IP 路由器的一项核心功能。当路由器接收到一个 IP 数据包时,它需要查询自己的本地路由表,为该数据包找到最合适的下一跳路径。这个决策过程基于将数据包的“目的IP地址”与路由表中的条目进行匹配。

核心概念

  1. IP地址 (IPv4):

    一个32位的二进制数字,通常为了方便阅读而写成点分十进制字符串形式(例如 192.168.0.3)。

  2. 路由表条目 (Routing Entry):

    格式为 entryIp/m,例如 10.166.50.0/23。

    • entryIp: 一个网络地址。
    • m: 掩码长度 (Mask Length) ,表示用于网络匹配的、从左到右的二进制位数。例如,m=24 意味着只比较IP地址的前24位。

匹配规则

一条路由 entryIp/m 与一个目的IP地址 dstIp 匹配,当且仅当:

将 entryIp 和 dstIp 都转换为32位二进制数后,它们最左边的 m 位完全相同。

  • 特例:默认路由:

    路由 0.0.0.0/0 的掩码长度 m 为 0,意味着它需要比较 0 位前缀。因此,它与任何目的IP地址都永远匹配。

选择规则 (如何找到最佳匹配)

从所有匹配的路由中,路由器会根据以下规则选出唯一的最佳路由:

  1. 最长匹配优先: 优先选择掩码长度 m 最大的那条路由。m 越大,代表匹配得越精确。
  2. 顺序优先 (Tie-Breaker) : 如果有多条路由的掩码长度 m 相同且均为最大值,则选择在输入的路由表 ipTable最先出现的那一条。

任务要求

给定一个目的IP地址 dstIp 和一个本地路由表 ipTable,请找出最佳匹配的路由。

  • 如果存在匹配,则返回该路由字符串。
  • 如果没有任何路由匹配(包括默认路由),则返回字符串 empty

输入格式

  • 第一行: 一个字符串 dstIp,表示目的IP地址。

  • 第二行: 一个字符串数组 ipTable,表示本地路由表。

    • 1 <= ipTable.length <= 10000
    • ipTable 中每个元素的掩码长度 m 的范围为 [0, 32]

输出格式

  • 一个字符串,表示最长匹配的路由;如果没有任何路由匹配,则输出字符串 empty

限制与要求

  • 时间限制: C/C++ 1000ms, 其他语言 2000ms
  • 内存限制: C/C++ 256MB, 其他语言 512MB

样例说明

样例 1

  • 输入:

    • dstIp: "192.168.0.3"
    • ipTable: ["10.166.50.0/23", "192.0.0.0/8", "10.255.255.255/32", "192.168.0.1/24", "127.0.0.0/8", "192.168.0.0/24"]
  • 输出: "192.168.0.1/24"

  • 解释:

    1. 寻找所有匹配的路由:

      • "192.0.0.0/8": dstIp 的前8位 (192) 匹配。匹配,m=8
      • "192.168.0.1/24": dstIp 的前24位 (192.168.0) 匹配。匹配,m=24
      • "192.168.0.0/24": dstIp 的前24位 (192.168.0) 匹配。匹配,m=24
      • 其他路由不匹配。
    2. 应用选择规则:

      • 最长匹配: 匹配的路由中,最大掩码长度 m 为 24。候选路由为 "192.168.0.1/24""192.168.0.0/24"
      • 顺序优先: 在这两个候选路由中,"192.168.0.1/24" 在输入列表中出现得更早。
    3. 最终结果: 选择 "192.168.0.1/24"

样例 2

  • 输入:

    • dstIp: "202.96.96.68"
    • ipTable: ["200.18.24.0/24"]
  • 输出: "empty"

  • 解释: dstIp 的第一部分 202 与路由 200.18.24.0/24 的第一部分 200 不匹配。没有其他路由,因此没有任何匹配项。

样例 3

  • 输入:

    • dstIp: "10.110.32.77"
    • ipTable: ["127.0.0.1/8", "0.0.0.0/0"]
  • 输出: "0.0.0.0/0"

  • 解释:

    • "127.0.0.1/8" 不匹配。
    • "0.0.0.0/0" 是默认路由,永远匹配。
    • 由于这是唯一匹配的路由,它就是最佳匹配。
import java.util.Arrays;
import java.util.Scanner;
import java.util.stream.Collectors;

public class Solution {
    /**
     * 将点分十进制的 IPv4 地址字符串转换为 32 位数值。
     * 为了避免符号位问题,使用 long 类型来存储无符号的 32 位整数值。
     *
     * @param ipStr IP 地址字符串,例如 "192.168.0.3"
     * @return 对应的 32 位数值
     */
    private long ipToLong(String ipStr) {
        String[] parts = ipStr.split("\\.");
        long result = 0;
        // 遍历四个部分
        for (int i = 0; i < 4; i++) {
            // 将当前部分解析为 long
            long partValue = Long.parseLong(parts[i]);
            // 将该部分的值左移相应的位数,然后通过“按位或”操作合并到结果中
            // part[0] 左移 24 位, part[1] 左移 16 位, part[2] 左移 8 位, part[3] 不移位
            result |= (partValue << (24 - 8 * i));
        }
        return result;
    }

    /**
     * 在路由表中为给定的目的IP地址查找最长匹配的路由。
     *
     * 核心思想:
     * 1.  **数值化处理**:将点分十进制的IP地址字符串转换为32位的整数(用long存储以避免符号问题),
     * 这样可以方便地进行位运算。
     * 2.  **掩码匹配**:对于一条路由 `entryIp/m`,要判断它是否与 `dstIp` 匹配,
     * 需要比较它们二进制表示的前 `m` 位是否相同。这可以通过创建一个前 `m` 位为1、
     * 后 `32-m` 位为0的子网掩码 `mask` 来实现。如果 `(dstIpValue & mask) == (entryIpValue & mask)`,
     * 则说明匹配成功。
     * 3.  **遍历查找最优解**:遍历整个路由表,对每一条路由进行匹配检查。
     * -   维护一个变量 `maxMaskLength` 记录当前找到的最佳匹配的掩码长度。
     * -   维护一个变量 `bestMatchRoute` 记录对应的路由字符串。
     * -   当找到一个匹配的路由时,如果它的掩码长度 `m` 大于 `maxMaskLength`,
     * 就更新 `maxMaskLength` 和 `bestMatchRoute`。
     * -   由于我们是按输入顺序遍历的,如果遇到掩码长度相同的匹配项,我们不会更新,
     * 这就自然满足了“按先后顺序输出最先的”这一 tie-breaker 规则。
     *
     * @param dstIp   目的IP地址字符串
     * @param ipTable 路由表,每个元素是 "ip/掩码长度" 格式的字符串
     * @return 最长匹配的路由字符串;如果没有匹配的,返回 "empty"。
     */
    public String findLongestMatch(String dstIp, String[] ipTable) {
        // --- 1. 预处理 ---
        // 将目的IP地址转换为数值形式,方便进行位运算
        long dstIpValue = ipToLong(dstIp);

        // --- 2. 初始化结果变量 ---
        String bestMatchRoute = "empty"; // 存储找到的最佳匹配路由字符串,默认为 "empty"
        int maxMaskLength = -1;          // 存储找到的最佳匹配的掩码长度。初始化为 -1,
                                         // 这样即使是 /0 的默认路由也能成为第一个有效匹配。

        // --- 3. 遍历路由表 ---
        for (String routeEntry : ipTable) {
            // a. 解析路由条目,分离出 IP 和掩码长度
            String[] parts = routeEntry.split("/");
            String entryIpStr = parts[0];
            int maskLength = Integer.parseInt(parts[1]);

            // b. 将路由条目的 IP 也转换为数值形式
            long entryIpValue = ipToLong(entryIpStr);

            // c. 创建子网掩码以进行匹配检查
            // 掩码是一个 32 位的数字,前 m 位是 1,后面 32-m 位是 0。
            // 当 m=0 时,掩码应为 0。当 m=32 时,掩码应为全 1 (0xFFFFFFFF)。
            // 使用 -1L (long类型的-1,其二进制表示所有位都是1) 左移 32-m 位可以得到这个效果。
            // 特殊处理 m=0 的情况,因为在Java中左移32位是无效操作(相当于移动0位)。
            long mask = (maskLength == 0) ? 0L : (-1L << (32 - maskLength));

            // d. 检查是否匹配:
            // 将目的IP和路由IP都与掩码进行“按位与”(&)操作,
            // 这个操作会保留它们的前 m 位,并将后面的位置零。
            // 如果结果相等,说明它们的前 m 位是相同的,即匹配成功。
            if ((dstIpValue & mask) == (entryIpValue & mask)) {
                // e. 更新最佳匹配
                // 如果当前匹配的掩码长度 > 之前找到的最大掩码长度
                if (maskLength > maxMaskLength) {
                    // 更新最大掩码长度
                    maxMaskLength = maskLength;
                    // 更新最佳匹配的路由为当前路由
                    bestMatchRoute = routeEntry;
                }
                // 如果 maskLength == maxMaskLength,我们不做任何事,
                // 因为题目要求“按给出的先后顺序输出最先的”,
                // 而我们已经找到了一个同样长度的匹配,所以保留第一个找到的。
            }
        }

        // --- 4. 返回结果 ---
        return bestMatchRoute;
    }
}

class Main {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        
        // 读取第一行:目的IP地址
        String dstIp = scanner.nextLine();
        
        // 读取第二行:路由表数组的字符串表示
        String ipTableLine = scanner.nextLine();
        
        scanner.close();
        
        // --- 解析输入 ---
        // 解析形如 ["10.166.50.0/23", "192.0.0.0/8"] 的字符串
        String[] ipTable = parseStringArray(ipTableLine);
        // 清理可能存在的引号
        String cleanedDstIp = dstIp.replaceAll(""", "").trim();
        
        // 创建 Solution 类的实例并调用方法
        Solution solution = new Solution();
        String result = solution.findLongestMatch(cleanedDstIp, ipTable);
        
        // 输出结果
        System.out.println(result);
    }
    
    /**
     * 辅助方法:解析形如 ["a", "b", "c"] 的输入字符串。
     * @param line 输入行
     * @return 字符串数组
     */
    private static String[] parseStringArray(String line) {
        if (line == null || line.length() <= 2) { // 至少应包含 "[]"
            return new String[0];
        }
        // 移除首尾的 '[' 和 ']'
        line = line.trim();
        if (line.startsWith("[") && line.endsWith("]")) {
            line = line.substring(1, line.length() - 1);
        }
        if (line.isEmpty()) {
            return new String[0];
        }
        // 按 ", " 或 "," 分割,并去除每个元素首尾的引号 " 和空格
        return Arrays.stream(line.split(","))
                     .map(s -> s.trim().replaceAll(""", ""))
                     .toArray(String[]::new);
    }
}