refactor: 删除旧版拓扑算法

This commit is contained in:
unanmed 2026-03-30 12:54:27 +08:00
parent ef79dd4d31
commit 01c9e1972e
5 changed files with 0 additions and 586 deletions

View File

@ -1,29 +0,0 @@
import { buildTopologicalGraph } from './graph';
import { GinkaTopologicalGraphs } from './interface';
import { overallSimilarity } from './similarity';
const cache = new Map<string, GinkaTopologicalGraphs>();
export function getTopologicalGraph(
floorId: string,
map: number[][]
): GinkaTopologicalGraphs {
if (cache.has(floorId)) return cache.get(floorId)!;
const graphs = buildTopologicalGraph(map);
cache.set(floorId, graphs);
return graphs;
}
export function compareMap(
floorId1: string,
floorId2: string,
map1: number[][],
map2: number[][]
) {
const graph1 = getTopologicalGraph(floorId1, map1);
const graph2 = getTopologicalGraph(floorId2, map2);
const kernel = overallSimilarity(graph1, graph2);
return kernel;
}

View File

@ -1,255 +0,0 @@
import {
ResourceArea,
GinkaGraph,
BranchNode,
GinkaTopologicalGraphs,
ResourceNode,
NodeType
} from './interface';
export const tileType = new Set(
Array(13)
.fill(0)
.map((_, i) => i)
);
const branchType = new Set([6, 7, 8, 9]);
const entranceType = new Set([10, 11]);
const resourceType = new Set([0, 2, 3, 4, 5, 10, 11, 12, 13]);
export const directions: [number, number][] = [
[-1, 0],
[1, 0],
[0, -1],
[0, 1]
];
function buildGraphFromEntrance(
map: number[][],
entrance: number,
resourceMap: Map<number, number>,
areaMap: ResourceArea[]
): GinkaGraph {
const width = map[0].length;
const height = map[1].length;
const visitedEntrance = new Set<number>([entrance]);
const visited = new Set<number>();
const queue: [number, number][] = [];
queue.push([entrance % width, Math.floor(entrance / width)]);
const branchNodes = new Set<number>();
// 1. BFS 检测所有分支节点
while (queue.length > 0) {
const item = queue.shift();
if (!item) continue;
const [nx, ny] = item;
const index = ny * width + nx;
if (visited.has(index)) continue;
const tile = map[ny][nx];
if (entranceType.has(tile)) {
visitedEntrance.add(index);
}
if (branchType.has(tile)) {
branchNodes.add(index);
}
visited.add(index);
for (const [dx, dy] of directions) {
const px = dx + nx;
const py = dy + ny;
if (px < 0 || px >= width || py < 0 || py >= height) {
continue;
}
const tile = map[py][px];
if (tile !== 1) {
// 非墙区域可通行
queue.push([px, py]);
}
}
}
// 2. 从分支节点构建拓扑图
const graph = new Map<number, BranchNode | ResourceNode>();
branchNodes.forEach(v => {
const nx = v % width;
const ny = Math.floor(v / width);
if (!graph.get(v)) {
graph.set(v, {
type: NodeType.Branch,
neighbor: new Set(),
tile: map[ny][nx]
});
}
const node = graph.get(v)!;
for (const [dx, dy] of directions) {
const px = nx + dx;
const py = ny + dy;
if (px < 0 || px >= width || py < 0 || py >= height) {
continue;
}
const index = py * width + px;
// 先检查临近节点是不是分支节点,是的话链接到自己
if (branchNodes.has(index)) {
node.neighbor.add(index);
} else {
// 检查是不是资源节点
const pointer = resourceMap.get(index);
if (pointer === void 0) continue;
const area = areaMap[pointer];
if (!area) continue;
area.neighbor.add(v);
area.members.forEach(v => {
node.neighbor.add(v);
});
}
}
});
// 3. 把资源节点拆分成并排,并放入拓扑图
areaMap.forEach(v => {
v.members.forEach(index => {
const nx = index % width;
const ny = Math.floor(index / width);
const tile = map[ny][nx];
if (tile === 0) return;
const node: ResourceNode = {
type: NodeType.Resource,
resourceType: tile,
neighbor: v.neighbor,
resourceArea: v
};
graph.set(index, node);
});
});
return { graph, resourceMap, areaMap, visitedEntrance, visited };
}
function findResourceNodes(map: number[][]) {
const width = map[0].length;
const height = map[1].length;
const visited = new Set<number>();
const areas: ResourceArea[] = [];
const resourcesMap: Map<number, number> = new Map();
for (let ny = 0; ny < height; ny++) {
for (let nx = 0; nx < width; nx++) {
const tile = map[ny][nx];
const index = ny * width + nx;
if (visited.has(index) || !resourceType.has(tile)) {
continue;
}
const queue: [number, number][] = [];
queue.push([nx, ny]);
const area: ResourceArea = {
type: NodeType.Resource,
resources: new Map([[tile, 1]]),
members: new Set([index]),
neighbor: new Set()
};
while (queue.length > 0) {
const item = queue.shift();
if (!item) continue;
const [nx, ny] = item;
const index = ny * width + nx;
if (visited.has(index)) {
continue;
}
const tile = map[ny][nx];
if (!resourceType.has(tile)) {
continue;
}
visited.add(index);
const exists = area.resources.get(tile);
if (!exists) {
area.resources.set(tile, 1);
} else {
area.resources.set(tile, exists + 1);
}
area.members.add(index);
resourcesMap.set(index, areas.length);
for (const [dx, dy] of directions) {
const px = nx + dx;
const py = ny + dy;
if (px < 0 || px >= width || py < 0 || py >= height) {
continue;
}
queue.push([px, py]);
}
}
areas.push(area);
}
}
return { areaMap: areas, resourcesMap };
}
export function buildTopologicalGraph(map: number[][]): GinkaTopologicalGraphs {
const width = map[0].length;
const height = map[1].length;
// 1. 找到所有入口
const entrances = new Set<number>();
for (let ny = 0; ny < height; ny++) {
for (let nx = 0; nx < width; nx++) {
const tile = map[ny][nx];
if (entranceType.has(tile)) {
entrances.add(ny * width + nx);
}
}
}
// 2. 找到所有的资源节点
const { areaMap, resourcesMap } = findResourceNodes(map);
// 3. 对每个入口计算拓扑图
const graphs: GinkaGraph[] = [];
const usedEntrance = new Set<number>();
const totalVisited = new Set<number>();
/** 入口位置到拓扑图的映射 */
const entranceMap = new Map<number, GinkaGraph>();
entrances.forEach(v => {
if (usedEntrance.has(v)) {
return;
}
const nx = v % width;
const ny = Math.floor(v / width);
const entranceGraph = buildGraphFromEntrance(
map,
v,
resourcesMap,
areaMap
);
const { graph, visited, visitedEntrance } = entranceGraph;
graphs.push(entranceGraph);
// 标记已经探索到的入口,并标记这个入口对应了哪个图
visitedEntrance.forEach(v => {
usedEntrance.add(v);
entranceMap.set(v, entranceGraph);
});
visited.forEach(v => {
totalVisited.add(v);
});
});
// 3. 计算不可到达区域
const unreachable = new Set<number>();
for (let ny = 0; ny < height; ny++) {
for (let nx = 0; nx < width; nx++) {
const index = ny * width + nx;
if (!totalVisited.has(index) && map[ny][nx] !== 1) {
unreachable.add(index);
}
}
}
return { graphs, entranceMap, unreachable };
}

View File

@ -1,59 +0,0 @@
export const enum NodeType {
Branch,
Resource
}
export interface ResourceArea {
/** 节点类型 */
readonly type: NodeType.Resource;
/** 每种资源对应的数量 */
readonly resources: Map<number, number>;
/** 资源区域包含的所有资源图块坐标索引 */
readonly members: Set<number>;
/** 资源区域的邻居节点 */
readonly neighbor: Set<number>;
}
export interface BranchNode {
/** 节点类型 */
readonly type: NodeType.Branch;
/** 分支节点的邻居节点 */
readonly neighbor: Set<number>;
/** 分支节点图块 */
readonly tile: number;
}
export interface ResourceNode {
/** 节点类型 */
readonly type: NodeType.Resource;
/** 资源类型 */
readonly resourceType: number;
/** 邻居节点 */
readonly neighbor: Set<number>;
/** 资源节点所属的资源区域 */
readonly resourceArea: ResourceArea;
}
export type GinkaNode = BranchNode | ResourceNode;
export interface GinkaGraph {
/** 拓扑图内容,键表示位置,值表示这一点的节点 */
readonly graph: Map<number, GinkaNode>;
/** 资源指针,键表示位置,值表示这一点对应的资源节点在 areaMap 的索引 */
readonly resourceMap: Map<number, number>;
/** 资源区域列表 */
readonly areaMap: ResourceArea[];
/** 这个拓扑图包含的入口位置 */
readonly visitedEntrance: Set<number>;
/** 这个拓扑图能够造访的所有位置 */
readonly visited: Set<number>;
}
export interface GinkaTopologicalGraphs {
/** 这个地图包含的所有独立的图 */
readonly graphs: GinkaGraph[];
/** 每个入口对应哪个图 */
readonly entranceMap: Map<number, GinkaGraph>;
/** 这个图从入口开始的不可到达区域 */
readonly unreachable: Set<number>;
}

View File

@ -1,179 +0,0 @@
import { cosineSimilarity } from 'src/utils';
import { GinkaGraph, GinkaTopologicalGraphs, NodeType } from './interface';
interface WLNode {
originalPos: number;
originalLabel: string;
currentLabel: string;
neighbors: WLNode[];
}
function encodeNodeLabels(graph: GinkaGraph) {
const nodes: WLNode[] = [];
const nodeMap = new Map<number, WLNode>();
graph.graph.forEach((node, pos) => {
let label: string;
// 编码为唯一哈希值用字符串就行V8 会自动帮你算哈希)
if (node.type === NodeType.Branch) {
label = `B:${node.tile}`;
} else {
label = `R:${node.resourceType}`;
}
const wlNode: WLNode = {
originalPos: pos,
originalLabel: label,
currentLabel: label,
neighbors: []
};
nodeMap.set(pos, wlNode);
nodes.push(wlNode);
});
// 映射邻居节点
nodes.forEach(node => {
const ginkaNode = graph.graph.get(node.originalPos);
ginkaNode?.neighbor.forEach(v => {
const wl = nodeMap.get(v);
if (wl) node.neighbors.push(wl);
});
});
return nodes;
}
function weisfeilerLehmanIteration(
nodes: WLNode[],
iterations: number,
decay: number = 0.6 // 衰减权重,减小长距离图的权重
) {
const labelHistory: string[][] = [];
for (let i = 0; i < iterations; i++) {
const newLabels: string[] = [];
// 生成新标签
nodes.forEach(node => {
const neighborLabels = node.neighbors
.map(n => n.currentLabel)
.sort();
const compositeLabel = `${node.currentLabel}|${neighborLabels.join(
','
)}`.slice(0, 8192);
newLabels.push(compositeLabel);
});
// 更新节点标签并记录
nodes.forEach((node, idx) => {
node.currentLabel = newLabels[idx];
});
labelHistory.push([...newLabels]);
}
// 统计每个节点的数量
let weight = 1;
const numMap = new Map<string, number>();
labelHistory.forEach(iter => {
iter.forEach(v => {
if (!numMap.has(v)) {
numMap.set(v, weight);
} else {
numMap.set(v, numMap.get(v)! + weight);
}
});
weight *= decay;
});
// 把每个节点的原始标签也加上,权重使用最远权重,可以认为是资源重复率
nodes.forEach(node => {
if (!numMap.has(node.originalLabel)) {
numMap.set(node.originalLabel, weight);
} else {
numMap.set(
node.originalLabel,
numMap.get(node.originalLabel)! + weight
);
}
});
return numMap;
}
function vectorizeFeatures(features: Map<string, number>, vocab: string[]) {
const vec: number[] = new Array(vocab.length).fill(0);
features.forEach((count, label) => {
const index = vocab.indexOf(label);
if (index !== -1) {
vec[index] += count;
}
});
return vec;
}
function wlKernel(
graphA: GinkaGraph,
graphB: GinkaGraph,
iterations = 3
): number {
// 编码节点
const nodesA = encodeNodeLabels(graphA);
const nodesB = encodeNodeLabels(graphB);
// 迭代生成标签
const featuresA = weisfeilerLehmanIteration(nodesA, iterations);
const featuresB = weisfeilerLehmanIteration(nodesB, iterations);
// 构建特征向量
const vocab = [...new Set([...featuresA.keys(), ...featuresB.keys()])];
const vecA = vectorizeFeatures(featuresA, vocab);
const vecB = vectorizeFeatures(featuresB, vocab);
// 计算余弦相似度
return cosineSimilarity(vecA, vecB);
}
export function overallSimilarity(
a: GinkaTopologicalGraphs,
b: GinkaTopologicalGraphs
) {
// 使用 Weisfeiler-Lehman Kernel 方式计算拓扑图相似度
const graphsA = a.graphs;
const graphsB = b.graphs;
let totalSimilarity = 0;
const comparedGraph = new Set<GinkaGraph>();
graphsA.forEach(ga => {
let maxSimilarity = 0;
let maxGraph: GinkaGraph | null = null;
// 图之间两两比较,找到最接近的作为相似度
for (const gb of graphsB) {
if (comparedGraph.has(gb)) continue;
// 计算迭代次数
const min = Math.min(ga.graph.size, gb.graph.size);
const iterations = Math.ceil(Math.max(1, Math.log(min)));
const similarity = wlKernel(ga, gb, iterations);
if (similarity > maxSimilarity && !isNaN(similarity)) {
maxSimilarity = similarity;
maxGraph = gb;
}
if (similarity === 1) break;
}
totalSimilarity += maxSimilarity;
if (maxGraph) comparedGraph.add(maxGraph);
});
// 不可达区域惩罚
const reduction =
1 / (1 + Math.abs(a.unreachable.size - b.unreachable.size));
// 取根号使结果更接近线性
if (graphsA.length === 0) {
return 0;
} else {
return Math.sqrt(totalSimilarity / graphsA.length) * reduction;
}
}

View File

@ -43,70 +43,6 @@ export function mergeDataset<T>(
return dataset;
}
export function cosineSimilarity(vecA: number[], vecB: number[]): number {
if (vecA.length !== vecB.length) {
throw new Error('Vectors must have same dimension');
}
let dot = 0,
normA = 0,
normB = 0;
for (let i = 0; i < vecA.length; i++) {
dot += vecA[i] * vecB[i];
normA += vecA[i] ** 2;
normB += vecB[i] ** 2;
}
return dot / (Math.sqrt(normA) * Math.sqrt(normB));
}
export async function parseTowerInfo(
path: string,
configName: string
): Promise<TowerInfo> {
const dataFile = await readFile(join(path, 'data.js'), 'utf-8');
const data: any = JSON.parse(dataFile.split('\n').slice(1).join('\n'));
const configFile = await readFile(join(path, configName), 'utf-8');
return {
path: path,
name: data.firstData.name as string,
floorIds: data.main.floorIds as string[],
config: JSON.parse(configFile) as BaseConfig
};
}
export function mergeFloorIds(...info: TowerInfo[]) {
const ids: string[] = [];
info.forEach(v => {
ids.push(...v.floorIds.map(id => `${v.name}:${id}`));
});
return ids;
}
export async function fromJSON(path: string) {
const file = await readFile(path, 'utf-8');
const data = JSON.parse(file) as Record<string, number[][]>;
const clip: Record<string, [number, number, number, number]> = {};
const config: BaseConfig = {
clip: {
defaults: [0, 0, 0, 0],
special: clip
}
};
const name = (Math.random() * 12).toFixed(0);
const floorMap = new Map<string, FloorData>();
for (const [key, value] of Object.entries(data)) {
const floorData: FloorData = {
map: value,
id: key,
config
};
floorMap.set(`${name}:${key}`, floorData);
}
return floorMap;
}
export function chooseFrom<T>(arr: T[], n: number): T[] {
const copy = arr.slice();
for (let i = copy.length - 1; i > 0; i--) {