RedEnginePress logo
RedEnginePress
AlgorithmsLanguagesPlaygroundAbout

Iddfs

S
# 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()