diff --git a/graph_algorithms/iddfs.r b/graph_algorithms/iddfs.r new file mode 100644 index 00000000..521e1b1e --- /dev/null +++ b/graph_algorithms/iddfs.r @@ -0,0 +1,199 @@ +# Iterative Deepening Depth-First Search (IDDFS) +# +# IDDFS performs repeated depth-limited DFS from depth = 0..max_depth until a target +# is found. It combines the optimality (in terms of shallowest solution) of BFS with +# the space efficiency of DFS. +# +# Time Complexity: O(b^d) in the worst case, where b is branching factor and d depth +# Space Complexity: O(d) recursion/stack depth + +# Formatting +LINE_WIDTH <- 60 + +print_line <- function(char = "-", width = LINE_WIDTH) { + cat(strrep(char, width), "\n", sep = "") +} + +#' Normalize adjacency list graph +#' +#' Ensures graph is a named list with names "1".."N" where N is the maximum vertex id, +#' and that all adjacency entries are integer vectors (possibly empty). Missing vertices +#' are added with empty adjacency. +#' @param graph A list mapping vertex id (character or numeric) to numeric neighbor vector +#' @return A normalized adjacency list with names "1".."N" +normalize_graph <- function(graph) { + if (!is.list(graph)) stop("graph must be a list") + # Collect vertices from names and neighbors + name_ids <- as.integer(names(graph)) + if (any(is.na(name_ids))) { + bad_names <- names(graph)[is.na(name_ids)] + stop( + sprintf( + "graph names must be coercible to integers (e.g., '1','2',...). Problematic names: %s", + paste(bad_names, collapse = ", ") + ) + ) + } + neighbor_ids <- unlist(graph, use.names = FALSE) + if (length(neighbor_ids) == 0) neighbor_ids <- integer(0) + if (!is.integer(neighbor_ids)) neighbor_ids <- as.integer(neighbor_ids) + if (any(is.na(neighbor_ids))) stop("neighbors must be numeric/integer ids") + max_id <- max(c(0L, name_ids, neighbor_ids)) + if (max_id < 1L) { + # empty graph + return(setNames(vector("list", 0L), character(0L))) + } + adj <- vector("list", max_id) + for (i in seq_len(max_id)) adj[[i]] <- integer(0) + # Fill from provided graph + for (i in seq_along(graph)) { + vid <- name_ids[i] + if (!length(graph[[i]])) { + adj[[vid]] <- integer(0) + } else { + nbrs <- as.integer(graph[[i]]) + if (any(is.na(nbrs))) stop("neighbors must be numeric/integer ids") + # Filter out-of-range neighbors gracefully but warn + if (length(nbrs)) { + out_of_range <- nbrs < 1L + if (any(out_of_range)) { + warning("Removed neighbors < 1: ", paste(nbrs[out_of_range], collapse = ", ")) + nbrs <- nbrs[!out_of_range] + } + } + adj[[vid]] <- nbrs + } + } + names(adj) <- as.character(seq_len(max_id)) + adj +} + +#' Depth-Limited Search (recursive) +#' +#' This version avoids global state and only prevents cycles along the current path. +#' @param graph Normalized adjacency list from normalize_graph +#' @param current Current vertex (integer) +#' @param target Target vertex (integer) +#' @param limit Remaining depth limit (integer >= 0) +#' @param path Vector of vertices along the current path (for cycle avoidance) +#' @return list(found=logical, path=integer vector when found) +.depth_limited_search <- function(graph, current, target, limit, path) { + # Visit current + new_path <- c(path, current) + if (current == target) { + return(list(found = TRUE, path = new_path)) + } + if (limit == 0L) { + return(list(found = FALSE, path = integer(0))) + } + # Explore neighbors + nbrs <- graph[[current]] + for (nbr in nbrs) { + # Avoid cycles within the current path + if (!(nbr %in% new_path)) { + res <- .depth_limited_search(graph, nbr, target, limit - 1L, new_path) + if (res$found) return(res) + } + } + return(list(found = FALSE, path = integer(0))) +} + +#' Iterative Deepening DFS (IDDFS) +#' +#' @param graph A named list adjacency: names are vertices ("1","2",...), values are integer neighbors +#' @param start Start vertex (integer) +#' @param target Target vertex (integer) +#' @param max_depth Maximum depth to search (integer >= 0) +#' @param verbose If TRUE, prints progress; otherwise silent +#' @return list(found=logical, depth=integer if found, path=integer vector when found) +#' @examples +#' g <- list("1"=c(2,3), "2"=c(4), "3"=c(5), "4"=c(), "5"=c()) +#' iddfs(g, start=1, target=5, max_depth=5, verbose=TRUE) +iddfs <- function(graph, start, target, max_depth, verbose = TRUE) { + adj <- normalize_graph(graph) + if (!is.numeric(start) || !is.numeric(target)) { + stop("start and target must be numeric/integer") + } + start <- as.integer(start) + target <- as.integer(target) + if (length(adj) == 0L) return(list(found = FALSE, depth = NA_integer_, path = integer(0))) + n <- length(adj) + if (start < 1L || start > n || target < 1L || target > n) { + stop(sprintf("start and target must be in [1, %d]", n)) + } + if (!is.numeric(max_depth) || max_depth < 0) stop("max_depth must be integer >= 0") + max_depth <- as.integer(max_depth) + + if (verbose) { + cat("Iterative Deepening DFS\n") + print_line("=") + } + + # Early exit if start is target + if (start == target) { + if (verbose) { + cat(sprintf("✓ Target %d found at depth 0\n", target)) + print_line("-") + } + return(list(found = TRUE, depth = 0L, path = c(start))) + } + + for (depth in 0:max_depth) { + if (verbose) cat(sprintf("Searching at depth limit: %d\n", depth)) + res <- .depth_limited_search(adj, start, target, depth, integer(0)) + if (res$found) { + if (verbose) { + cat(sprintf("✓ Target %d found at depth %d\n", target, depth)) + print_line("-") + } + return(list(found = TRUE, depth = depth, path = res$path)) + } + } + + if (verbose) { + cat(sprintf("✗ Target %d not found up to depth %d\n", target, max_depth)) + print_line("-") + } + return(list(found = FALSE, depth = NA_integer_, path = integer(0))) +} + +#' Example demonstrations for IDDFS +#' @return NULL (prints results) +example_iddfs <- function() { + cat("\n========== Example 1: Simple Directed Graph ==========\n") + graph1 <- list( + "1" = c(2, 3), + "2" = c(4), + "3" = c(5), + "4" = c(), + "5" = c() + ) + print(iddfs(graph1, start = 1, target = 5, max_depth = 5, verbose = TRUE)) + + cat("\n========== Example 2: Target Not Found ==========\n") + graph2 <- list( + "1" = c(2), + "2" = c(3), + "3" = c() + ) + print(iddfs(graph2, start = 1, target = 6, max_depth = 3, verbose = TRUE)) + + cat("\n========== Example 3: Larger Graph ==========\n") + graph3 <- list( + "1" = c(2, 3, 4), + "2" = c(5, 6), + "3" = c(7), + "4" = c(8), + "5" = c(), + "6" = c(), + "7" = c(9), + "8" = c(), + "9" = c() + ) + print(iddfs(graph3, start = 1, target = 9, max_depth = 5, verbose = TRUE)) + + invisible(NULL) +} + +# Uncomment to run examples when sourcing this file interactively +# if (interactive()) example_iddfs()