新闻详情

新闻详情

首页 / 资讯中心 / 详情

基于溯源图与RGAT-GRU的APT攻击检测优化实战

发布时间:2026/9/28 19:09:21来源:尧图网络
基于溯源图与RGAT-GRU的APT攻击检测优化实战
简介本资源为基于溯源图的APT攻击检测方法优化Python实现源码面向计算机、信息安全、人工智能等专业的在校学生与教师可用于毕业设计、课程设计、大作业或初期项目立项演示。项目围绕溯源图建模与APT攻击检测优化展开包含StreamSpot、DARPA TC/CADETS等数据集的处理脚本与模型实现并涉及RGAT、GRU等图神经网络结构适合作为入门进阶或二次开发的基础。压缩包共25个文件以11个Python源码为主辅以7个XML配置、4个Markdown说明文档及忽略文件等整体约47KB结构清晰便于按模块查阅。目前已有403人学习下载。读者可从中获取完整的数据处理、模型训练与检测评估流程理解溯源图构建、图神经网络建模及攻击检测优化的实现思路并借助说明文档快速上手与排错。1. 溯源图 APT 检测这份毕设源码到底能跑出什么结果拿到一个安全方向的毕设题目最怕的不是不会写代码而是不知道一份源码到底能不能跑通、跑出来的数字算不算「有效结果」。这份基于溯源图的 APT 攻击检测优化 Python 实现核心思路是把系统审计日志转成溯源图再用图神经网络做节点级异常检测最后在 StreamSpot 和 DARPA TC CADETS 两个公开数据集上验证。它解决的是「APT 攻击检测」这个场景里最实际的问题攻击者长期潜伏、单条日志看不出异常但把进程、文件、网络连接之间的因果关系连成图之后异常子图就会暴露出来。适合谁用信息安全、数据科学、人工智能方向的在校学生做毕设或课程大作业也适合刚接触图神经网络想找一个完整安全场景练手的人。源码里已经包含数据处理、模型定义、训练脚本和两个数据集的适配代码不是空壳框架。下面按「数据怎么进 → 模型怎么搭 → 怎么跑 → 坑在哪」的顺序拆开讲。2. 溯源图数据管线从审计日志到模型可读的图结构2.1 为什么不能直接把日志丢给模型APT 攻击检测的原始数据是系统调用审计日志比如 DARPA TC 数据集里的 CADETS 子集每条记录包含时间戳、进程 ID、操作类型读/写/执行/连接、源对象和目标对象。直接把这些日志当成序列喂给 LSTM会丢掉进程与文件之间的多跳因果关系——攻击者用 A 进程写恶意文件B 进程再读取执行序列模型只能看到时间上的先后看不到「谁导致了谁」。溯源图的做法是把每个系统实体进程、文件、socket当成节点把操作当成有向边边上的属性记录操作类型和时间。这样一张图里正常行为和攻击行为在结构上就有区别正常进程的文件访问模式相对固定攻击链会引入跨主机的异常边。常见做法是用图神经网络在节点级别做二分类判断每个节点是否属于攻击子图。2.2 数据加载与图构建的关键代码源码里create_object_database.py负责把原始日志转成对象数据库map_info_reader.py读取映射信息streamspot.py和darpa_cadets_RGAT.py分别处理两个数据集。下面这段是图构建的核心逻辑我按源码结构整理成可读版本# 基于源码 create_object_database.py 和 map_info_reader.py 整理 import json import networkx as nx from collections import defaultdict def build_provenance_graph(log_path, node_typesNone): 从审计日志构建溯源图 log_path: 原始日志文件路径 node_types: 需要保留的节点类型None 表示全部保留 G nx.DiGraph() node_attr defaultdict(dict) with open(log_path, r) as f: for line in f: record json.loads(line) # 提取源节点和目标节点 src record[src_id] dst record[dst_id] op record[operation] # 节点类型过滤减少图规模 if node_types and record[src_type] not in node_types: continue # 添加节点属性类型、首次出现时间 if src not in G: G.add_node(src, typerecord[src_type], first_seenrecord[timestamp]) if dst not in G: G.add_node(dst, typerecord[dst_type], first_seenrecord[timestamp]) # 添加边同一对节点多次操作合并权重 if G.has_edge(src, dst): G[src][dst][weight] 1 G[src][dst][ops].add(op) else: G.add_edge(src, dst, weight1, ops{op}, timestamprecord[timestamp]) return G逻辑说明这段代码把每条审计记录拆成源节点、目标节点和操作类型用有向图存储。节点属性里的type区分进程、文件、socket后续模型会把它转成 one-hot 特征。边上的weight记录同一对节点之间的操作次数ops集合保留操作类型避免信息丢失。参数说明node_types是过滤开关StreamSpot 数据集节点少可以全保留DARPA TC 数据量大我一般只保留process、file、socket三类能把图规模压到原来的三分之一左右。first_seen时间戳用于后续按时间窗口切分子图源码里darpa_cadets_RGAT_GRU.py会用到。2.3 子图切分与特征初始化整张溯源图可能包含几十万节点直接送进 GNN 显存扛不住。源码的做法是按时间窗口切分成子图每个子图对应一个检测单元。streamspot_RGAT.py里用滑动窗口窗口大小默认 1000 条边步长 500。特征初始化时节点类型用 one-hot度数、入度出度比、首次出现时间归一化后拼成特征向量。# 子图切分与节点特征初始化参考 streamspot_RGAT.py import numpy as np import torch def extract_subgraphs(G, window_size1000, stride500): 按边序列滑动窗口切分子图 edges list(G.edges(dataTrue)) subgraphs [] for start in range(0, len(edges) - window_size 1, stride): window_edges edges[start:start window_size] subG G.edge_subgraph([(u, v) for u, v, _ in window_edges]) subgraphs.append(subG) return subgraphs def init_node_features(subG, type2idx): 节点特征类型 one-hot 度数 时间归一化 features [] for node in subG.nodes(): type_vec np.zeros(len(type2idx)) type_vec[type2idx[subG.nodes[node][type]]] 1 degree subG.degree(node) in_degree subG.in_degree(node) out_degree subG.out_degree(node) ratio in_degree / (out_degree 1e-6) feat np.concatenate([type_vec, [degree, in_degree, out_degree, ratio]]) features.append(feat) return torch.tensor(np.array(features), dtypetorch.float)逻辑说明extract_subgraphs按边顺序切窗口保证时间上相邻的边落在同一个子图里。init_node_features把节点类型转成 one-hot再拼上度数统计量。入度出度比这个特征对 APT 检测特别有用——恶意进程往往大量读取文件但很少被其他进程调用比值会异常。参数说明window_size和stride是检测粒度的关键。窗口太小攻击链被切碎漏报率上升窗口太大正常行为被稀释误报率上升。源码默认 1000/500 是在 StreamSpot 上试出来的换到 DARPA TC 我一般会调到 2000/1000因为 CADETS 的交互更密集。3. RGAT-GRU 模型图注意力加门控循环的检测网络3.1 为什么选 RGAT 而不是普通 GCN普通 GCN 在聚合邻居信息时对所有邻居一视同仁但溯源图里不同边的重要性差别很大。一个进程连接了 50 个文件其中 49 个是正常读取1 个是写入恶意载荷GCN 会把恶意信号平均掉。RGATRelational Graph Attention Network在聚合时给每条边算注意力权重让模型自己学出哪些邻居更关键。源码里model_RGAT.py和model_RGAT_GRU.py是两个版本前者纯图注意力后者在 RGAT 后面接了 GRU 层处理子图序列。model.py是基础模型定义。我建议先用model_RGAT.py跑通单子图检测再换model_RGAT_GRU.py看序列建模带来的提升。3.2 RGAT 层的实现细节# 基于 model_RGAT.py 整理的核心注意力层 import torch import torch.nn as nn import torch.nn.functional as F class RGATLayer(nn.Module): def __init__(self, in_dim, out_dim, num_relations, dropout0.2): super().__init__() self.num_relations num_relations self.out_dim out_dim # 每种关系类型独立的变换矩阵 self.relation_weights nn.ModuleList([ nn.Linear(in_dim, out_dim) for _ in range(num_relations) ]) # 注意力参数a^T [Wh_i || Wh_j] self.attn nn.Parameter(torch.randn(2 * out_dim, 1)) self.dropout nn.Dropout(dropout) self.leaky_relu nn.LeakyReLU(0.2) def forward(self, x, edge_index, edge_type): x: 节点特征 [N, in_dim] edge_index: 边索引 [2, E] edge_type: 每条边的关系类型 [E] src, dst edge_index N x.size(0) # 按关系类型分别变换源节点特征 h_src torch.zeros(N, self.out_dim, devicex.device) for rel in range(self.num_relations): mask (edge_type rel) if mask.sum() 0: h_src[mask] self.relation_weights[rel](x[src[mask]]) h_dst self.relation_weights[0](x[dst]) # 目标节点统一变换 # 计算注意力系数 attn_input torch.cat([h_src, h_dst], dim1) e self.leaky_relu(torch.matmul(attn_input, self.attn)).squeeze() e self.dropout(e) # softmax 归一化按目标节点分组 alpha self._segment_softmax(e, dst, N) # 加权聚合 out torch.zeros(N, self.out_dim, devicex.device) out.index_add_(0, dst, alpha.unsqueeze(1) * h_src) return F.elu(out) def _segment_softmax(self, e, index, N): 按目标节点做 softmax避免跨节点归一化 max_e torch.zeros(N, devicee.device).scatter_reduce_( 0, index, e, reduceamax) e e - max_e[index] exp_e torch.exp(e) sum_exp torch.zeros(N, devicee.device).scatter_add_( 0, index, exp_e) return exp_e / (sum_exp[index] 1e-10)逻辑说明relation_weights为每种关系类型读、写、执行、连接维护独立的线性变换这样模型能区分「进程读文件」和「进程写文件」的不同语义。注意力系数用源节点和目标节点变换后的拼接算出来再按目标节点做 softmax 归一化。_segment_softmax是手动实现的分组 softmax因为 PyTorch 的softmax不支持按索引分组。参数说明num_relations要和数据集里的操作类型数对齐。StreamSpot 有 5 种操作DARPA TC 有 10 种左右设错了会在relation_weights[rel]处报索引越界。dropout0.2是源码默认值如果训练集小比如只有几百个子图可以调到 0.3 到 0.4 防过拟合。3.3 GRU 层如何利用子图序列model_RGAT_GRU.py在 RGAT 后面接了 GRU把连续几个子图的图级表示当成序列输入。这样做的理由是APT 攻击往往跨越多个时间窗口单个子图可能只看到攻击链的一小段。GRU 能记住前几个窗口的异常信号在后续窗口里放大检测分数。# model_RGAT_GRU.py 中的序列建模部分 class RGATGRUDetector(nn.Module): def __init__(self, node_dim, hidden_dim, num_relations, gru_hidden64, num_layers2): super().__init__() self.rgat_layers nn.ModuleList([ RGATLayer(node_dim if i 0 else hidden_dim, hidden_dim, num_relations) for i in range(num_layers) ]) self.gru nn.GRU(hidden_dim, gru_hidden, batch_firstTrue) self.classifier nn.Linear(gru_hidden, 2) def forward(self, subgraph_sequence): subgraph_sequence: 子图列表每个元素是 (x, edge_index, edge_type) graph_embeds [] for x, edge_index, edge_type in subgraph_sequence: h x for layer in self.rgat_layers: h layer(h, edge_index, edge_type) # 图级表示节点特征平均池化 graph_embeds.append(h.mean(dim0)) # 序列建模 seq torch.stack(graph_embeds).unsqueeze(0) # [1, T, hidden] gru_out, _ self.gru(seq) # 用最后一个时间步分类 logits self.classifier(gru_out[:, -1, :]) return logits逻辑说明每个子图先过两层 RGAT 得到节点表示再平均池化成图级向量。所有子图的图级向量按时间顺序组成序列送进 GRU最后用最后一个时间步的输出做二分类。mean(dim0)是最简单的图池化源码里也支持注意力池化但平均池化在小数据集上更稳。参数说明gru_hidden64控制序列建模的容量太大容易过拟合太小记不住长攻击链。num_layers2是 RGAT 层数源码里试过 3 层但效果下降原因是过平滑——节点特征经过太多层聚合后趋于一致异常信号被抹平。4. 训练与评估两个数据集的跑通流程和参数配置4.1 StreamSpot 数据集的训练命令StreamSpot 数据集相对小适合先跑通流程。源码里streamspot.py是数据处理入口main.py是训练主脚本。我按实际跑通的顺序整理# 第一步生成对象数据库 python create_object_database.py --dataset streamspot \ --raw_dir ./data/streamspot/raw \ --output ./data/streamspot/object_db.pkl # 第二步构建溯源图并切分子图 python streamspot.py --object_db ./data/streamspot/object_db.pkl \ --window_size 1000 --stride 500 \ --output ./data/streamspot/subgraphs.pkl # 第三步训练 RGAT 模型 python main.py --dataset streamspot \ --subgraph_path ./data/streamspot/subgraphs.pkl \ --model RGAT --epochs 50 --lr 0.001 \ --batch_size 32 --hidden_dim 128 # 第四步训练 RGAT-GRU 模型做对比 python main_GRU.py --dataset streamspot \ --subgraph_path ./data/streamspot/subgraphs.pkl \ --model RGAT_GRU --epochs 50 --lr 0.001 \ --seq_len 5 --gru_hidden 64逻辑说明四步分别对应数据准备、图构建、单图模型训练、序列模型训练。create_object_database.py把原始日志转成统一的对象数据库格式streamspot.py读这个数据库建图并切窗口。main.py和main_GRU.py的区别在于是否加载子图序列。参数说明--epochs 50在 StreamSpot 上足够收敛损失曲线在 30 轮左右就平了。--lr 0.001是 Adam 的默认学习率如果 loss 震荡可以降到 0.0005。--batch_size 32受显存限制8G 显存跑 RGAT-GRU 时建议降到 16。--seq_len 5表示每次输入 5 个连续子图对应时间跨度约 2500 条边。4.2 DARPA TC CADETS 数据集的适配DARPA TC 的数据格式和 StreamSpot 不同源码里darpa_cadets_RGAT.py和darpa_cadets_RGAT_GRU.py是专门适配的。CADETS 数据量更大、攻击场景更复杂训练时间明显更长。# DARPA TC CADETS 训练流程 python create_object_database.py --dataset darpa_tc \ --raw_dir ./data/darpa_tc/cadets \ --output ./data/darpa_tc/object_db.pkl python darpa_cadets_RGAT.py --object_db ./data/darpa_tc/object_db.pkl \ --window_size 2000 --stride 1000 \ --node_types process,file,socket \ --output ./data/darpa_tc/subgraphs.pkl python darpa_cadets_RGAT_GRU.py --subgraph_path ./data/darpa_tc/subgraphs.pkl \ --epochs 80 --lr 0.0005 --batch_size 8 \ --hidden_dim 256 --seq_len 8逻辑说明DARPA TC 的窗口调大到 2000/1000因为 CADETS 的进程交互更密集小窗口会把正常行为切得太碎。--node_types过滤掉注册表等低频节点类型降低图规模。--batch_size 8是因为 CADETS 子图平均节点数是 StreamSpot 的 3 到 4 倍显存占用高。参数说明--epochs 80配合--lr 0.0005是 CADETS 上比较稳的组合学习率再高容易在后期震荡。--hidden_dim 256比 StreamSpot 的 128 大一倍因为 CADETS 的节点特征维度更高。--seq_len 8对应约 8000 条边的上下文能覆盖大多数 APT 攻击链的长度。4.3 评估指标与结果解读源码里评估部分输出准确率、精确率、召回率和 F1。APT 检测场景下召回率比精确率更重要——漏掉一个攻击子图的代价远大于多报几个正常子图。StreamSpot 上 RGAT 的 F1 通常在 0.85 到 0.90 之间加上 GRU 后能到 0.90 以上。DARPA TC 因为攻击更隐蔽F1 会低一些RGAT 约 0.75 到 0.82RGAT-GRU 约 0.80 到 0.86。数据集模型准确率精确率召回率F1StreamSpotRGAT0.880.860.890.87StreamSpotRGAT-GRU0.920.900.930.91DARPA TCRGAT0.790.810.760.78DARPA TCRGAT-GRU0.840.850.820.83注意这些数字是源码默认参数下的参考值实际跑出来会有波动主要受随机种子、数据切分方式和硬件影响。如果 F1 明显低于这个范围先检查数据预处理阶段有没有把攻击子图误删。5. 避坑与排查跑这份源码最容易翻车的五个地方5.1 现象训练 loss 不下降准确率卡在 0.5 左右原因最常见的是标签对齐错了。源码里子图标签是按时间窗口生成的如果create_object_database.py输出的对象数据库里时间戳格式和streamspot.py读取时解析的格式不一致窗口切分就会错位导致正常子图被标成攻击、攻击子图被标成正常。解决在streamspot.py里加一行打印检查每个子图的标签分布。正常情况攻击子图占比在 10% 到 20% 之间如果接近 50% 说明标签乱了。然后回头检查create_object_database.py里时间戳字段的解析逻辑确保两个脚本用的是同一种时间格式。5.2 现象CUDA out of memorybatch_size 降到 1 还是爆原因DARPA TC 数据集里有些子图节点数超过 5 万RGAT 的注意力矩阵是[E, out_dim]边数一多显存直接炸。源码默认没有对子图大小做上限截断。解决在darpa_cadets_RGAT.py的子图切分逻辑里加一个节点数过滤超过 3 万节点的子图直接跳过或者随机采样到 3 万。我一般会在extract_subgraphs后面加MAX_NODES 30000 subgraphs [g for g in subgraphs if g.number_of_nodes() MAX_NODES]如果跳过太多子图导致训练集不够可以把window_size从 2000 降到 1500让每个窗口包含的边少一些。5.3 现象RGAT-GRU 训练时 loss 剧烈震荡F1 忽高忽低原因GRU 对序列长度敏感seq_len设得太大比如 15 以上时梯度在时间维度上传播路径太长容易出现梯度爆炸。源码里没有加梯度裁剪。解决在main_GRU.py的训练循环里加梯度裁剪torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm1.0)。同时把seq_len降到 5 到 8 之间。如果还震荡检查子图序列的采样逻辑——源码里是按顺序取连续子图如果数据里正常和攻击子图交替太频繁GRU 学不到稳定模式可以改成按攻击标签分层采样。5.4 现象评估指标很高但实际检测不出攻击原因数据泄露。源码里streamspot.py在切分子图时如果先切分再打标签而标签信息来自全局的攻击时间戳就可能出现「子图里包含未来信息」的情况。模型在训练集上表现很好换到真实场景就崩。解决确保切分、标签生成、训练集/测试集划分三个步骤严格按时间顺序。训练集用前 70% 时间窗口的子图测试集用后 30%。不要在切分前做任何全局统计比如全局节点度数归一化所有归一化参数只能从训练集算。5.5 现象换自己的数据集后报 KeyError 或维度不匹配原因源码里节点类型和操作类型的映射是硬编码的type2idx和relation2idx字典只包含 StreamSpot 和 DARPA TC 里出现的类型。换数据集后出现新类型索引越界。解决在create_object_database.py里把类型映射改成动态生成先扫一遍所有日志收集类型集合再建字典。同时检查model_RGAT.py里num_relations参数是否和新的操作类型数一致。如果新数据集的操作类型超过 15 种建议先做类型合并把低频操作归到other类否则注意力层参数太多训不动。6. 进阶技巧用注意力权重做攻击链可视化与阈值调优跑通训练只是第一步这份源码真正有价值的地方是 RGAT 的注意力权重可以导出用来定位攻击链的关键边。我一般会在测试集上挑一个被正确检测为攻击的子图把每层 RGAT 的alpha值导出来按边聚合后排序看哪些边获得了高注意力。# 导出注意力权重并定位关键攻击边 def export_attention(model, subgraph, top_k20): 导出 RGAT 最后一层的注意力权重 model.eval() x, edge_index, edge_type subgraph with torch.no_grad(): # 逐层前向保留最后一层的 alpha h x for i, layer in enumerate(model.rgat_layers): if i len(model.rgat_layers) - 1: # 手动计算注意力保留中间结果 src, dst edge_index h_src layer.relation_weights[0](h[src]) h_dst layer.relation_weights[0](h[dst]) attn_input torch.cat([h_src, h_dst], dim1) e layer.leaky_relu(torch.matmul(attn_input, layer.attn)).squeeze() alpha layer._segment_softmax(e, dst, h.size(0)) h layer(h, edge_index, edge_type) # 按注意力权重排序边 edge_scores list(zip(edge_index[0].tolist(), edge_index[1].tolist(), alpha.tolist())) edge_scores.sort(keylambda x: x[2], reverseTrue) return edge_scores[:top_k]逻辑说明这段代码在最后一层 RGAT 前向时手动计算注意力系数并保留然后按权重降序排列边。高权重的边就是模型认为最可疑的因果关系。实际用的时候把 top-20 的边对应的节点 ID 映射回原始日志就能看到具体的进程和文件路径。参数说明top_k20是经验值一个攻击子图的关键边通常在 10 到 30 条之间。如果 top-20 里混入了大量正常边说明模型还没训好或者window_size太大导致正常行为稀释了攻击信号。另一个进阶用法是用注意力权重调检测阈值。源码默认用 0.5 作为二分类阈值但在实际场景里可以按业务需求调。如果更怕漏报把阈值降到 0.3召回率能提升 5 到 8 个百分点代价是精确率下降。我一般会画一条 P-R 曲线选 F1 最高点对应的阈值再根据具体场景微调。从那以后我每次跑这类图神经网络的安全检测项目都强制先导出注意力权重看一遍 top 边确认模型学到的确实是攻击链而不是数据集偏差。这个习惯帮我省了很多「指标好看但实际没用」的返工。希望帮到你。本文还有配套的精品资源点击获取
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

【笔记】latex做表格:用 TaoToken 统一 Key 跑通 AI 辅助生成 LaTeX 表格配置 2026/9/28 19:57:42

【笔记】latex做表格:用 TaoToken 统一 Key 跑通 AI 辅助生成 LaTeX 表格配置

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
国产视频协议栈升级:ONVIF-Go v2、国标全支持与嵌入式ONVIF-C发布 2026/9/28 19:57:42

国产视频协议栈升级:ONVIF-Go v2、国标全支持与嵌入式ONVIF-C发布

1. 协议库集中发版背后的行业信号:一次被低估的国产视频接入基础设施升级 最近在几个嵌入式设备厂商的内部技术群和开源协议栈维护者的小圈子中,几乎同时刷出一条消息:“onvif-go 进 v2、国标设备侧收官、onvif-c 首发”——没有通稿&#x…

阅读更多 →
Codex 桌面版接入 DeepSeek API Key 教程:用 cc-switch 管理多模型配置 2026/9/28 19:57:35

Codex 桌面版接入 DeepSeek API Key 教程:用 cc-switch 管理多模型配置

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
RISC-V内核工业低功耗MCU选型:CH32L103资源与外设实战解析 2026/9/28 19:57:35

RISC-V内核工业低功耗MCU选型:CH32L103资源与外设实战解析

聊工业低功耗MCU选型,大多数人脑子里蹦出来的还是ST那一串L系列,或者是GD32L233这类跟着做的国产型号。但最近一年我陆续在几个传感器终端和协议转换器项目里用了沁恒的CH32L103,发现这颗RISC-V内核的工业MCU,在低功耗和外设集成度…

阅读更多 →
CH585M多外设隔离与低功耗模式设计实战 2026/9/28 19:57:35

CH585M多外设隔离与低功耗模式设计实战

1. 为什么"关不干净的外设"是低功耗设备的头号杀手做低功耗项目最磨人的阶段,往往不是算法设计,而是拿着万用表一点一点查电流。芯片明明According to手册进了睡眠模式,实测电流就是下不来,最后发现是某个外设没有真正隔…

阅读更多 →
leetcode 困难题 1847. Closest Room 2026/9/28 19:57:35

leetcode 困难题 1847. Closest Room

Problem: 1847. 最近的房间 二分查找的&#xff0c;只需要重放的&#xff0c;然后排序的&#xff0c;最后二分查找满足条件的房间&#xff0c;最后找到id最小的房间即可 Code class Solution { public:vector<int> closestRoom(vector<vector<int>>& r…

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞 ✉