jigsawR / R /hexagonal_adjacency_cache.R
pjt222's picture
Upload folder using huggingface_hub
e232e39 verified
Raw
History Blame Contribute Delete
7.48 kB
# Hexagonal Adjacency Cache
# Pre-computed adjacency matrix for O(1) neighbor lookups
# Eliminates O(ring^2) recursive lookups in get_hex_neighbor()
# Module-level cache environment
.hex_adjacency_cache <- new.env(hash = TRUE, parent = emptyenv())
#' Build or retrieve cached hexagonal adjacency matrix
#'
#' Creates a pre-computed matrix of all neighbor relationships for a hexagonal
#' puzzle. Uses axial coordinate math for O(n) construction instead of O(n*ring^2)
#' recursive lookups.
#'
#' @param rings Number of rings in puzzle
#' @return Matrix where adj_matrix[piece_id, side+1] = neighbor_id (or NA for boundary)
get_hex_adjacency_matrix <- function(rings) {
cache_key <- as.character(rings)
# Check cache first
if (exists(cache_key, envir = .hex_adjacency_cache, inherits = FALSE)) {
return(get(cache_key, envir = .hex_adjacency_cache, inherits = FALSE))
}
# Build adjacency matrix using axial coordinate math
num_pieces <- 3 * rings * (rings - 1) + 1
# Matrix: rows = piece_id (1 to num_pieces), cols = side (1-6 for sides 0-5)
# Value = neighbor piece_id or NA for boundary
adj_matrix <- matrix(NA_integer_, nrow = num_pieces, ncol = 6)
# Build coordinate-to-piece lookup table (hash map for O(1) lookup)
coord_to_piece <- new.env(hash = TRUE, parent = emptyenv())
piece_coords <- vector("list", num_pieces)
# First pass: map all pieces to coordinates
for (piece_id in 1:num_pieces) {
axial <- map_piece_id_to_axial(piece_id, rings)
coord_key <- paste(axial$q, axial$r, sep = ",")
coord_to_piece[[coord_key]] <- piece_id
piece_coords[[piece_id]] <- c(axial$q, axial$r)
}
# Axial direction vectors for flat-top hexagons
# Side 0 = E, 1 = NE, 2 = NW, 3 = W, 4 = SW, 5 = SE
directions <- list(
c(1, 0), # Side 0: E
c(1, -1), # Side 1: NE
c(0, -1), # Side 2: NW
c(-1, 0), # Side 3: W
c(-1, 1), # Side 4: SW
c(0, 1) # Side 5: SE
)
# Second pass: for each piece, compute neighbors using coordinate math
for (piece_id in 1:num_pieces) {
coords <- piece_coords[[piece_id]]
q <- coords[1]
r <- coords[2]
for (side in 0:5) {
dir <- directions[[side + 1]]
neighbor_q <- q + dir[1]
neighbor_r <- r + dir[2]
neighbor_key <- paste(neighbor_q, neighbor_r, sep = ",")
# Look up neighbor in coordinate table
if (exists(neighbor_key, envir = coord_to_piece, inherits = FALSE)) {
adj_matrix[piece_id, side + 1] <- coord_to_piece[[neighbor_key]]
}
# If not found, it's a boundary edge (NA already set)
}
}
# Cache the result
assign(cache_key, adj_matrix, envir = .hex_adjacency_cache)
return(adj_matrix)
}
#' Fast hexagonal neighbor lookup using cached adjacency matrix
#'
#' O(1) lookup after initial O(n) matrix construction.
#'
#' @param piece_id Piece ID (1 to num_pieces)
#' @param side Side number (0-5)
#' @param rings Number of rings in puzzle
#' @return Neighbor piece ID or NA if boundary
get_hex_neighbor_fast <- function(piece_id, side, rings) {
adj_matrix <- get_hex_adjacency_matrix(rings)
return(adj_matrix[piece_id, side + 1])
}
#' Get all neighbors for a piece using cached matrix
#'
#' Returns pre-allocated data frame with all 6 neighbors.
#' Much faster than iterative rbind approach.
#'
#' @param piece_id Piece ID (1 to num_pieces)
#' @param rings Number of rings in puzzle
#' @return Data frame with direction, neighbor_id, is_boundary
get_hex_neighbors_fast <- function(piece_id, rings) {
adj_matrix <- get_hex_adjacency_matrix(rings)
# Pre-allocate data frame (no rbind needed)
neighbors <- data.frame(
direction = as.character(0:5),
neighbor_id = adj_matrix[piece_id, ],
is_boundary = is.na(adj_matrix[piece_id, ]),
stringsAsFactors = FALSE
)
return(neighbors)
}
#' Clear hexagonal adjacency cache
#'
#' Useful for testing or when memory is constrained.
#'
clear_hex_adjacency_cache <- function() {
rm(list = ls(envir = .hex_adjacency_cache), envir = .hex_adjacency_cache)
}
#' Optimized fused edge computation using cached adjacency
#'
#' Uses hash sets for O(1) group membership and edge existence checks.
#' Uses the original get_hex_neighbors_for_fusion for topology-correct sides.
#' Uses list accumulation instead of vector c() for O(n) instead of O(n^2).
#'
#' IMPORTANT: Uses original get_hex_neighbor() for side mapping to maintain
#' compatibility with is_edge_fused() and the existing edge key format.
#' The main speedup comes from hash sets for membership/existence checks
#' and list accumulation instead of vector concatenation.
#'
#' @param fusion_groups List of piece ID vectors
#' @param rings Number of rings in puzzle
#' @return List with fused_edges, edge_to_group, piece_to_group
compute_hex_fused_edges_fast <- function(fusion_groups, rings) {
if (is.null(fusion_groups) || length(fusion_groups) == 0) {
return(NULL)
}
# Build piece-to-group hash map (O(1) lookup)
piece_to_group <- new.env(hash = TRUE, parent = emptyenv())
for (group_idx in seq_along(fusion_groups)) {
group <- fusion_groups[[group_idx]]
for (piece_id in group) {
piece_to_group[[as.character(piece_id)]] <- group_idx
}
}
# Build group membership hash sets (O(1) membership test)
group_sets <- lapply(fusion_groups, function(group) {
group_set <- new.env(hash = TRUE, parent = emptyenv())
for (piece_id in group) {
group_set[[as.character(piece_id)]] <- TRUE
}
group_set
})
# Hash set for edge deduplication (O(1) existence check)
edge_set <- new.env(hash = TRUE, parent = emptyenv())
# List accumulation for edges (O(1) append, O(n) final unlist)
fused_edges_list <- list()
edge_to_group <- list()
# Process all pieces in all groups
for (group_idx in seq_along(fusion_groups)) {
group <- fusion_groups[[group_idx]]
group_set <- group_sets[[group_idx]]
for (piece_id in group) {
# Use original function for topology-correct side mapping
neighbors <- get_hex_neighbors_for_fusion(piece_id, rings)
for (i in seq_len(nrow(neighbors))) {
direction <- neighbors$direction[i]
neighbor_id <- neighbors$neighbor_id[i]
is_boundary <- neighbors$is_boundary[i]
# Skip boundary edges
if (is_boundary || is.na(neighbor_id)) next
# Check if neighbor is in same group (O(1) hash lookup instead of O(n) %in%)
if (exists(as.character(neighbor_id), envir = group_set, inherits = FALSE)) {
# This edge is fused - create edge key using standard format
edge_key <- make_edge_key(piece_id, direction)
# Check if already added (O(1) hash lookup instead of O(n) %in%)
if (!exists(edge_key, envir = edge_set, inherits = FALSE)) {
edge_set[[edge_key]] <- TRUE
fused_edges_list[[length(fused_edges_list) + 1]] <- edge_key
edge_to_group[[edge_key]] <- group_idx
}
}
}
}
}
# Convert list to named list for piece_to_group return
piece_to_group_list <- as.list(piece_to_group)
names(piece_to_group_list) <- ls(piece_to_group)
return(list(
fused_edges = unlist(fused_edges_list),
edge_to_group = edge_to_group,
piece_to_group = piece_to_group_list
))
}