在R语言中优雅实现多词搜索的方法(含进阶匹配需求)
Let's break down how to build a flexible, R-idiomatic solution for your word search game—one that handles any number of words and the advanced cross-letter constraints marked by numbers. We'll use tidyverse tools (dplyr, purrr, stringr) to keep the code clean and scalable.
Step 1: Basic Multi-Word Search (Dynamic for N Words)
The core idea here is to generate filtered word lists for each pattern, then dynamically cross-join all these lists. This avoids hardcoding joins for 2 words and scales to any number of patterns.
Helper Function for Single Pattern Filtering
First, a utility function to filter words matching a single dot-based pattern:
filter_single_pattern <- function(pattern, words_df) { len <- nchar(pattern) regex <- paste0('\\b', pattern, '\\b') words_df %>% filter(word_length == len, str_detect(word, regex)) }
Dynamic Cross Join for N Patterns
For any number of patterns, we'll generate filtered lists, rename columns to avoid conflicts, then cross-join them all:
library(tidyverse) # Sample word dataset (replace with your full 173k-word list) words <- data.frame( word = c('test', 'word', 'active', 'angina', 'endite', 'endive', 'engine', 'entire', 'alanine', 'evening', 'escape', 'entered'), word_length = c(4, 4, 6, 6, 6, 6, 6, 6, 7, 7, 7, 7) ) # Example: Basic 2-word search find_basic <- str_split('.n.... ...n.n.', ' ')[[1]] # Generate filtered lists for each pattern (rename columns to word1, word2, etc.) filtered_list <- map2(find_basic, seq_along(find_basic), function(pat, idx) { filter_single_pattern(pat, words) %>% rename(!!paste0('word', idx) := word) %>% select(-word_length) }) # Cross-join all filtered lists to get all valid combinations basic_result <- filtered_list %>% reduce(full_join, by = character()) basic_result
Step 2: Advanced Cross-Letter Constraints
To handle number-marked constraints (same number = same letter across positions), we need to:
- Convert number patterns to dot patterns for initial filtering.
- Parse number positions into constraint groups.
- Generate dynamic filter expressions to enforce these constraints.
Helper Functions for Constraint Handling
Let's build tools to parse and apply constraints:
# Convert number patterns to dot patterns for initial filtering convert_to_dot_pattern <- function(pattern) { str_replace_all(pattern, '\\d', '.') } # Parse patterns into constraint groups (number → list of (word index, position)) parse_constraints <- function(find_patterns) { map_df(seq_along(find_patterns), function(word_idx) { chars <- str_split(find_patterns[word_idx], '')[[1]] tibble( word_idx = word_idx, position = which(str_detect(chars, '\\d')), number = chars[str_detect(chars, '\\d')] ) }) %>% group_by(number) %>% nest() %>% ungroup() } # Generate a filter expression for a single constraint group create_constraint_filter <- function(constraint_data) { if(nrow(constraint_data) < 2) return(NULL) # No constraint needed for single position # Use the first position in the group as the reference first_word <- sym(paste0('word', constraint_data$word_idx[1])) first_pos <- constraint_data$position[1] first_expr <- expr(str_sub(!!first_word, !!first_pos, !!first_pos)) # Create expressions to compare all other positions to the reference other_exprs <- map2(constraint_data$word_idx[-1], constraint_data$position[-1], function(w_idx, pos) { current_word <- sym(paste0('word', w_idx)) current_expr <- expr(str_sub(!!current_word, !!pos, !!pos)) expr(!!first_expr == !!current_expr) }) # Combine all comparisons with logical AND reduce(other_exprs, function(a, b) expr(!!a & !!b)) }
General Function for Advanced Search
Combine all steps into a single function that works for both basic and advanced cases:
search_words <- function(words_df, find_patterns) { # Step 1: Initial filtering with dot patterns dot_patterns <- map_chr(find_patterns, convert_to_dot_pattern) filtered_list <- map2(dot_patterns, seq_along(find_patterns), function(pat, idx) { filter_single_pattern(pat, words_df) %>% rename(!!paste0('word', idx) := word) %>% select(-word_length) }) # Step 2: Cross-join all filtered word lists cross_joined <- filtered_list %>% reduce(full_join, by = character()) # Step 3: Apply cross-word constraints if numbers are present if(any(str_detect(find_patterns, '\\d'))) { constraints <- parse_constraints(find_patterns) filter_exprs <- constraints$data %>% map(create_constraint_filter) %>% compact() # Remove groups with only one position (no constraint) if(length(filter_exprs) > 0) { final_filter <- reduce(filter_exprs, function(a, b) expr(!!a & !!b)) cross_joined <- cross_joined %>% filter(!!final_filter) } } # Return only the word columns cross_joined %>% select(starts_with('word')) }
Testing the Advanced Case
Let's test with your example where cross-word constraints are required:
# Advanced 2-word search with constraints find_advanced <- c('1n2341', '151n3n6') advanced_result <- search_words(words, find_advanced) advanced_result
This returns the exact same valid pairs as your manual implementation:
word1 word2 1 angina alanine 2 endite evening 3 endive evening 4 engine evening 5 entire evening
Scaling to N Words
This solution works seamlessly for any number of words. For example, if you have 3 patterns, just pass a vector of 3 patterns to search_words—the function will handle filtering, cross-joining, and applying any cross-word constraints automatically.
Notes for Large Word Lists
- Initial filtering is critical for your 173k-word list to reduce the number of combinations before cross-joining.
- If memory becomes an issue, use
dplyr::inner_joininstead offull_joinif you only want valid combinations (no NA values).
内容的提问来源于stack exchange,提问作者Matt_B

