Word Search in a 2D Character Grid
Title: Count Word Occurrences in a 2D Grid with Guards (Strings + 2D Arrays)
Level: Difficult
Concepts: C-strings, 2D array indexing (row-major on 1D), DFS/backtracking, visited tracking, 8-direction movement, input validation, case-insensitive compare, single-char wildcard, obstacles
Scenario
You’re implementing a word-search feature on an embedded device that stores a character grid in row-major 1D memory. Given a C-string word, you must count how many times it appears in the grid by walking adjacent cells in straight lines or along turns (depending on your design), respecting grid bounds, obstacles, case-insensitive matching, and optional wildcard ? in the word (matches any single non-obstacle character). Cells cannot be reused within a single match path.
Problem Statement
Design a function that searches a rows × cols character grid (stored as a 1D array) for all occurrences of word. A match is a path of positions that spells the word, where each next character is one step away in one of the allowed directions, without revisiting a cell within the same match. The function must support:
- 8-directional movement (N, NE, E, SE, S, SW, W, NW), controlled by a flag,
- Case-insensitive matching controlled by a flag,
- Single-character wildcard
?inwordif enabled, matching any non-obstacle cell, - Obstacle cells (e.g.,
'#') that cannot be entered, - Reporting the total match count, and the first match details (start row/col and direction).
Return 0 on success (even if no matches found) and fill outputs; return -1 on invalid inputs without modifying outputs.
Requirements
- Allowed types only:
int,long,double,char,bool,enum, and their pointers/arrays. - Grid & Indexing:
- Grid is provided as row-major 1D: address of cell
(r, c)isgrid[r*cols + c]. rows > 0,cols > 0,grid != NULL, androws*colsfits inint.
- Grid is provided as row-major 1D: address of cell
- Word (C-string):
word != NULL,word[0] != '\0'(non-empty).- If wildcard is enabled, the character
'?'inwordmatches any single grid character except the obstacle.
- Movement:
- If
allow_diagonal == true, use 8 directions. - Otherwise, use 4 directions (N, E, S, W).
- No wrap-around; must stay within bounds.
- No cell reuse within a single path matching the word.
- If
- Obstacles:
- Cells equal to
obstacle_charcannot be used. - A wildcard
?cannot match an obstacle.
- Cells equal to
- Case:
- If
case_insensitive == true, treat letters ‘A’–‘Z’ and ‘a’–‘z’ as equal (you must implement ASCII folding; do not rely on<ctype.h>).
- If
- Outputs on success:
*out_count— total number of matches found (can be 0).*out_first_row,*out_first_col— start of the first found match (or-1, -1if none).*out_first_dir— direction enum for the first step of the first match;DIR_NONEif no match.
- Return codes:
0— success (outputs written).-1— invalid input (do not modify outputs).
Function Details
- Name:
count_word_in_grid - Arguments:
const char *grid— row-major 1D grid buffer of sizerows * colsint rowsint colsconst char *word— null-terminated C-stringbool case_insensitivebool allow_diagonalbool allow_wildcard_qmarkchar obstacle_charint *out_countint *out_first_rowint *out_first_colenum Direction *out_first_dir
- Return Value:
int—0on success;-1on invalid input (no outputs modified). - Description:
Search for all paths that spellwordwhile:- Respecting bounds and obstacles,
- Using 4 or 8 directions based on
allow_diagonal, - Applying case-insensitive comparison if enabled,
- Allowing
?to match any non-obstacle character ifallow_wildcard_qmarkistrue, - Ensuring a path never reuses a cell for the same match.
Count all matches and record the first match’s start coordinate and initial direction. If no matches, set*out_count = 0,*out_first_row = -1,*out_first_col = -1,*out_first_dir = DIR_NONE, and return0.
Suggested enum for directions:
enum Direction {
DIR_NONE = 0,
DIR_N, DIR_NE, DIR_E, DIR_SE, DIR_S, DIR_SW, DIR_W, DIR_NW
};
Solution Approach
- Validation:
- Check all output pointers are non-NULL.
- Verify
rows > 0,cols > 0,grid != NULL,word != NULL,word[0] != '\0'. - Ensure
rows*colsdoes not overflowint(assume inputs guarantee this; otherwise guard carefully).
- Preprocessing:
- Compute
word_len(walk until'\0'). - If
word_len > rows*cols, matches are impossible (but still return success with count 0).
- Compute
- Direction vectors:
- Prepare arrays of
(dr, dc):- 4-dir:
{(-1,0),(0,1),(1,0),(0,-1)} - 8-dir adds:
{(-1,1),(1,1),(1,-1),(-1,-1)}
- 4-dir:
- Prepare arrays of
- Character compare helper:
- Implement
eq(a, b):- If
allow_wildcard_qmark && a == '?': true ifb != obstacle_char. - Else if
case_insensitive: fold both to uppercase (ASCII: if'a'<=x<='z'thenx-='a'-'A'), compare. - Else: compare raw characters.
- If
- Implement
- Search:
- For each cell
(r, c)not an obstacle:- If it matches
word[0]pereq, perform a DFS/backtracking search for the rest:- Maintain a visited bitmap/array (
rows*cols) for the current path (you may use achar visited[]local array; or a VLA in C99). - From
(r, c), recursively/iteratively try all allowed neighbors, ensuring bounds, not obstacle, not visited. - On reaching depth
word_len-1, count a match; if it’s the first match, record(r, c)and the direction of the first step:- If
word_len == 1, set directionDIR_NONE. - Else determine direction from
(r, c)to the first neighbor in the successful path.
- If
- Maintain a visited bitmap/array (
- If it matches
- For each cell
- Complexity:
- Worst case backtracking is exponential in
word_len, but typical constraints are fine for classroom sizes. - Memory: O(rows*cols) for
visited; stack depth O(word_len).
- Worst case backtracking is exponential in
Tasks to Perform
- Validate inputs; if invalid, return
-1without modifying outputs. - Count
word_len; ifword_len == 0, treat as invalid (return-1). - Initialize outputs (on success path):
count=0,first_row=-1,first_col=-1,first_dir=DIR_NONE. - Iterate all start cells
(r, c):- Skip if
grid[r*cols + c] == obstacle_char. - If start character matches
word[0], clearvisited[]and start DFS.
- Skip if
- DFS/backtracking:
- Mark current
(r, c)visited. - If
pos == word_len-1: found a match → incrementcount; if first, set outputs (derive direction). - Else, for each allowed direction
(dr, dc):nr=r+dr, nc=c+dc; check bounds, obstacle, and not visited.- Check
eq(word[pos+1], grid[nr*cols + nc]); if true, recurse to(nr, nc).
- Unmark on return (backtrack).
- Mark current
- After full search, write
*out_count = count,*out_first_row = first_row,*out_first_col = first_col,*out_first_dir = first_dir; return0.
Test Cases
| # | Grid (rows×cols) & Params | Expected Output | Notes |
|---|---|---|---|
| 1 | Grid 3×4 rows: {"CATS", "A#AT", "TACT"}, word="CAT", case_insensitive=false, diagonal=false, wildcard=false, obs='#' |
ret=0, out_count=2, first_row=0, first_col=0, first_dir=DIR_E |
Matches at (0,0)→E (“CAT”), and (2,1)→E (“ACT” not exact) → Actually only (0,0)→E and (2,0)→E (“TAC” no) → Corrected: also (0,0)→S? C(0,0)->A(1,0)->T(2,0) valid → 2 matches (E and S) |
| 2 | Same grid, word="C?T", wildcard=true, case_insensitive=false, diagonal=false |
ret=0, out_count=3, first=(0,0), dir=DIR_E |
? matches ‘A’ at (0,1) and also (1,0) for vertical; plus (2,1) for “C?T” diagonal is off, so only horizontal & vertical |
| 3 | Grid 3×3 rows: {"aBc", "d#E", "fGh"}, word="BEG", case_insensitive=true, diagonal=true, wildcard=false, obs='#' |
ret=0, out_count=1, first_row=0, first_col=1, first_dir=DIR_SE |
Case-insensitive diagonal: B(0,1)→E(1,2)→G(2,1) |
| 4 | Grid 2×3 rows: {"ABC", "DEF"}, word="FED", case_insensitive=false, diagonal=false |
ret=0, out_count=1, first_row=1, first_col=2, first_dir=DIR_W |
Reverse horizontal is allowed by stepping W |
| 5 | Grid 3×3 rows: {"AXA", "X#X", "AXA"}, word="AXA", wildcard=false, diagonal=false, obs='#' |
ret=0, out_count=4, first_row=0, first_col=0, first_dir=DIR_E |
Four straight-line matches; obstacle blocks center |
| 6 | Grid 3×3 rows: {"CAT", "A#T", "TAC"}, word="CAT", diagonal=false |
ret=0, out_count=2, first=(0,0), dir=DIR_E |
Horizontal at top; vertical (0,0)→(1,0)→(2,0) also valid |
| 7 | Grid 2×2 rows: {"AB", "CD"}, word="ABCDE", any flags |
ret=0, out_count=0, first_row=-1, first_col=-1, first_dir=DIR_NONE |
Word longer than total cells ⇒ no matches |
| 8 | Invalid: rows=0 or cols=0 or grid=NULL or word=NULL |
ret=-1 |
Do not modify outputs |
Note: In your implementation, carefully verify the expected counts by enumerating paths; ensure no cell reuse within a single match and obey the
allow_diagonalflag.