Mastering Graph Search: DFS and BFS Strategies in Competitive Programming

Understanding Search Paradigms

When approaching algorithmic challenges involving traversal, two primary methods dominate: Depth-First Search (DFS) and Breadth-First Search (BFS). While both traverse nodes in a graph or tree, their utility differs based on the problem constraints. BFS is fundamentally tied to the concept of shortest paths in unweighted graphs because it explores layer by layer. Consequently, problems requiring minimum steps, operations, or distances often favor BFS.

The Recursive Backtracking Template

DFS relies heavily on recursion to explore branches before retreating. The core logic involves marking a state, exploring deeper options, and then unmarking the state to restore the previous context. This mechanism is known as backtracking.

#include <iostream>
#include <vector>

using namespace std;

// Generic backtracking structure
void explore(int index, int target) {
    // Base case: check if solution criteria are met
    if (index == target + 1) {
        printSolution();
        return;
    }
    
    // Iterate through available choices
    for (int i = 1; i <= maxOptions; ++i) {
        if (isValidChoice(i)) {
            markUsed(i);               // Make decision
            explore(index + 1, target); // Recurse
            unmarkUsed(i);             // Backtrack
        }
    }
}

Combinatorial Explorations

Permutation generation is a classic application of backtracking. The objective is to arrange numbers such that no element repeats within a single sequence. This requires tracking which numbers have already been placed in the current branch.

#include <cstdio>
#include <cstring>

const int MAXN = 100;
int n;
int sequence[MAXN];
bool visited[MAXN];

void generatePermutation(int depth) {
    if (depth > n) {
        for (int i = 1; i <= n; ++i) {
            printf("%d ", sequence[i]);
        }
        puts("");
        return;
    }

    for (int i = 1; i <= n; ++i) {
        if (!visited[i]) {
            sequence[depth] = i;      // Place number
            visited[i] = true;        // Mark as used
            generatePermutation(depth + 1);
            visited[i] = false;       // Reset for next iteration
        }
    }
}

int main() {
    scanf("%d", &n);
    generatePermutation(1);
    return 0;
}

For larger scale permutations or when using standard libraries, efficient built-in functions can simplify iteration over sorted sequences.

Optimization Techniques

In problems where brute force is insufficient, pruning strategies are vital. By establishing bounds or caching states, we avoid redundant computations. Consider a scenario where items must be divided into two groups to minimize imbalance. Here, we track the sum of elements on the left and right sides dynamically.

#define _CRT_SECURE_NO_WARNINGS
#include <algorithm>
#include <cmath>
#include <climits>

const int GROUPS = 4;
int groupCounts[GROUPS];
int items[GROUPS][30];
int minDiff = INT_MAX;

void balanceGroups(int groupIdx, int itemIdx, int leftSum, int rightSum) {
    if (itemIdx > groupCounts[groupIdx]) {
        minDiff = std::min(minDiff, abs(leftSum - rightSum));
        return;
    }

    int weight = items[groupIdx][itemIdx];
    // Option 1: Add to Left
    balanceGroups(groupIdx, itemIdx + 1, leftSum + weight, rightSum);
    // Option 2: Add to Right
    balanceGroups(groupIdx, itemIdx + 1, leftSum, rightSum + weight);
}

int main() {
    for (int g = 1; g <= GROUPS; ++g) {
        scanf("%d", &groupCounts[g]);
        for (int k = 1; k <= groupCounts[g]; ++k) {
            scanf("%d", &items[g][k]);
        }
        
        int currentMin = INT_MAX;
        // Solve for this specific group independently
        balanceGroups(g, 1, 0, 0); 
        // ... Logic aggregates results per group ...
    }
    return 0;
}

Grid and Pathfinding Challenges

Traversing grids often utilizes directional arrays to move Up, Down, Left, or Right. When calculating the count of unique paths formed by digit sequences on a grid, memoization with sets helps identify distinct numeric values generated by different routes.

#include <iostream>
#include <unordered_set>
#include <cstring>

using namespace std;

const int MAX_N = 5;
int grid[MAX_N][MAX_N];
int dimensionsX, dimensionsY, stepsLimit;
unordered_set<int> distinctValues;
int dirs[4][2] = {{0, 1}, {0, -1}, {1, 0}, {-1, 0}};

void traversePaths(int x, int y, int currentSteps, long long numValue) {
    if (currentSteps == stepsLimit) {
        distinctValues.insert(numValue);
        return;
    }

    for (auto& dir : dirs) {
        int nx = x + dir[0];
        int ny = y + dir[1];
        if (nx >= 0 && nx < dimensionsX && ny >= 0 && ny < dimensionsY) {
            long long newValue = numValue * 10 + grid[nx][ny];
            traversePaths(nx, ny, currentSteps + 1, newValue);
        }
    }
}

int main() {
    cin >> dimensionsX >> dimensionsY >> stepsLimit;
    for (int i = 0; i < dimensionsX; ++i)
        for (int j = 0; j < dimensionsY; ++j)
            cin >> grid[i][j];

    for (int i = 0; i < dimensionsX; ++i) {
        for (int j = 0; j < dimensionsY; ++j) {
            traversePaths(i, j, 0, grid[i][j]);
        }
    }
    
    cout << distinctValues.size() << endl;
    return 0;
}
</int>

Scheduling Problems

When assigning items to bins (like cable cars or trucks) where capacity is limited, sorting items in descending order often provides better pruning opportunities. The goal is to minimize the total count of vehicles used.

#include <algorithm>
#include <cstdio>
#include <cstdlib>

const int MAX_ITEMS = 30;
int weights[MAX_ITEMS];
int binCapacity[MAX_ITEMS];
int itemCounts, maxCapacity;
int globalMinVehicles = MAX_ITEMS;

void loadBins(int currentItem, int currentCount) {
    if (currentCount >= globalMinVehicles) return; // Pruning
    
    if (currentItem > itemCounts) {
        globalMinVehicles = currentCount;
        return;
    }

    // Try placing in existing bins
    for (int b = 1; b <= currentCount; ++b) {
        if (binCapacity[b] + weights[currentItem] <= maxCapacity) {
            binCapacity[b] += weights[currentItem];
            loadBins(currentItem + 1, currentCount);
            binCapacity[b] -= weights[currentItem];
        }
    }

    // Try opening a new bin
    binCapacity[currentCount + 1] = weights[currentItem];
    loadBins(currentItem + 1, currentCount + 1);
    binCapacity[currentCount + 1] = 0;
}

int main() {
    scanf("%d %d", &itemCounts, &maxCapacity);
    for (int i = 1; i <= itemCounts; ++i) {
        scanf("%d", &weights[i]);
    }
    std::sort(weights + 1, weights + itemCounts + 1, std::greater<int>());
    
    loadBins(1, 1);
    printf("%d\n", globalMinVehicles);
    return 0;
}
</int>

Constraint Satisfaction (N-Queens)

Placing N queens on an N*N board such that no two attack eachother requires checking column conflicts and diagonal conflicts efficiently. Using boolean arrays to mark occupied columns and diagonals optimizes the checks from O(N) to O(1).

#include <iostream>
#include <vector>

using namespace std;

const int N_LIMIT = 14; // Typical constraint size
char board[N_LIMIT][N_LIMIT];
bool colUsed[N_LIMIT];
bool diagMain[2 * N_LIMIT]; // row + col
bool diagAnti[2 * N_LIMIT]; // row - col + offset

int solutionsFound = 0;

void placeQueen(int r) {
    if (r > N_LIMIT) {
        if (solutionsFound < 3) {
            for (int i = 1; i <= N_LIMIT; ++i) {
                for (int j = 1; j <= N_LIMIT; ++j) {
                    if (board[i][j] == 'Q') printf("%d ", j);
                }
            }
            printf("\n");
        }
        solutionsFound++;
        return;
    }

    for (int c = 1; c <= N_LIMIT; ++c) {
        if (!colUsed[c] && !diagMain[r + c] && !diagAnti[r - c + N_LIMIT]) {
            board[r][c] = 'Q';
            colUsed[c] = diagMain[r + c] = diagAnti[r - c + N_LIMIT] = true;
            
            placeQueen(r + 1);
            
            // Backtrack
            board[r][c] = '.';
            colUsed[c] = diagMain[r + c] = diagAnti[r - c + N_LIMIT] = false;
        }
    }
}

int main() {
    int size;
    cin >> size;
    // Clear board
    for (int i = 1; i <= size; ++i)
        for (int j = 1; j <= size; ++j)
            board[i][j] = '.';
            
    placeQueen(1);
    return 0;
}

Breadth-First Search Implementations

BFS utilizes a queue data structure to process nodes level-by-level. It is ideal for determining the minimum distance between two points in an unweighted environment. Unlike DFS, it maintains a persistent frontier.

Flood Fill Applications

Consider a problem identifying connected components in a grid. A simple BFS starting from a land cell fills the entire region, marking cells as visited to prevent cycles.

#include <queue>
#include <cstring>
#include <cstdio>

const int GRID_SIZE = 30;
char worldMap[GRID_SIZE][GRID_SIZE];
bool visited[GRID_SIZE][GRID_SIZE];
int width, height;
int offsets[4][2] = {{0, 1}, {1, 0}, {0, -1}, {-1, 0}};

int bfsStartPoints(int startX, int startY) {
    int count = 0;
    std::queue<pair int="">> q;
    
    visited[startX][startY] = true;
    q.push({startX, startY});
    
    while (!q.empty()) {
        pair<int int=""> curr = q.front();
        q.pop();
        count++;
        
        for (int i = 0; i < 4; ++i) {
            int nx = curr.first + offsets[i][0];
            int ny = curr.second + offsets[i][1];
            
            if (nx >= 0 && nx < height && ny >= 0 && ny < width) {
                if (!visited[nx][ny] && worldMap[nx][ny] == '.') {
                    visited[nx][ny] = true;
                    q.push({nx, ny});
                }
            }
        }
    }
    return count;
}
</int></pair>

Elevators and Dynamic States

Problems involving movement rules (like elevator buttons jumping specific floors) can be modeled as a graph where floors are nodes. Finding the minimum press count to reach a floor is a shortest-path BFS problem.

#include <queue>
#include &;iostream>

using namespace std;

struct State {
    int currentFloor;
    int presses;
};

int getMinPresses(int totalFloors, int start, int target, const vector<int>& jumpMoves) {
    vector<bool> vis(totalFloors + 1, false);
    queue<state> q;
    
    q.push({start, 0});
    vis[start] = true;
    
    while (!q.empty()) {
        State s = q.front();
        q.pop();
        
        if (s.currentFloor == target) return s.presses;
        
        // Calculate up/down moves
        int upMove = s.currentFloor + jumpMoves[s.currentFloor];
        int downMove = s.currentFloor - jumpMoves[s.currentFloor];
        
        if (upMove <= totalFloors && !vis[upMove]) {
            vis[upMove] = true;
            q.push({upMove, s.presses + 1});
        }
        
        if (downMove >= 1 && !vis[downMove]) {
            vis[downMove] = true;
            q.push({downMove, s.presses + 1});
        }
    }
    return -1;
}
</state></bool></int>

Multi-Dimensional BFS

Extending BFS to 3D environments requires managing coordinate triples (z, x, y) and corresponding displacement vectors for all 6 neighbors.

#include <queue>
#include <cstring>

struct Point {
    int z, x, y;
};

Point offsets6[] = {
    {1,0,0}, {-1,0,0}, {0,1,0}, {0,-1,0}, {0,0,1}, {0,0,-1}
};

char maze[21][21][21];
int distMap[21][21][21];
int L, R, C;

void bfs3D(Point startNode) {
    queue<point> q;
    memset(distMap, -1, sizeof(distMap));
    
    q.push(startNode);
    distMap[startNode.z][startNode.x][startNode.y] = 0;
    
    while(!q.empty()){
        Point u = q.front();
        q.pop();
        
        for(auto& d : offsets6){
            Point v = {u.z + d.z, u.x + d.x, u.y + d.y};
            if(v.z >= 0 && v.z < L && v.x >= 0 && v.x < R && v.y >= 0 && v.y < C){
                 if(maze[v.z][v.x][v.y] != '#' && distMap[v.z][v.x][v.y] == -1){
                     distMap[v.z][v.x][v.y] = distMap[u.z][u.x][u.y] + 1;
                     
                     if(maze[v.z][v.x][v.y] == 'E'){
                         printf("Escaped in %d minute(s).\n", distMap[v.z][v.x][v.y]);
                         return;
                     }
                     q.push(v);
                 }
            }
        }
    }
    printf("Trapped!\n");
}
</point>

Complex State Management (A* / Hash Map)

When the state space grows exponentially, compressing the state in to a string key allows the use of hash maps to track visited configurations efficiently. This is common in sliding tile puzzles where the 'empty' space moves around digits.

#include <queue>
#include <unordered_map>
#include <algorithm>
#include <string>

using namespace std;

int solvePuzzle(string initialConfig) {
    string target = "12345678x";
    unordered_map<string int=""> distances;
    queue<string> q;
    
    q.push(initialConfig);
    distances[initialConfig] = 0;
    
    int dx[4] = {1, -1, 0, 0};
    int dy[4] = {0, 0, 1, -1};
    
    while(!q.empty()){
        string curr = q.front();
        q.pop();
        
        int d = distances[curr];
        if(curr == target) return d;
        
        int k = curr.find('x');
        int cx = k / 3;
        int cy = k % 3;
        
        for(int i=0; i<4; ++i){
            int nx = cx + dx[i];
            int ny = cy + dy[i];
            
            if(nx >= 0 && nx < 3 && ny >= 0 && ny < 3){
                int nk = nx * 3 + ny;
                swap(curr[k], curr[nk]);
                
                if(!distances.count(curr)){
                    distances[curr] = d + 1;
                    q.push(curr);
                }
                swap(curr[k], curr[nk]); // Restore
            }
        }
    }
    return -1;
}
</string></string>

Tags: C++ depth-first-search breadth-first-search backtracking graph-traversal

Posted on Thu, 09 Jul 2026 17:24:30 +0000 by studot