Recursion: Geppetto
Geppetto
In the previous article we showed the algorithm for assigning items to specific slots using backtracking. Now, we try to find the total number of valid subsets and drop the requirement that every item must be assigned.
The Problem
Given $N$ ingredients and $M$ constraints, find the total number of valid pizzas that can be created. A constraint is a pair of ingredients $u$ and $v$ that cannot be placed on the same pizza. A valid pizza is any subset of the $N$ ingredients where no two ingredients violate a constraint. An empty pizza is considered valid.
The Approach: Include or Exclude
We must transition from a “slot-filling” approach to an “include or exclude” approach, altering how we manage state.
Subset Selection: The recursive function must evaluate every ingredient sequentially. For any given
ingredient_id, there are exactly two choices: skip it, or take it.- For the “skip” branch, we move to
ingredient_id + 1and accumulate the count. This is always a valid move. - For the “take” branch, we must first verify if adding the current ingredient violates any constraints with the ingredients we have already selected.
- For the “skip” branch, we move to
Explicit Cleanup: The reset term within the “take” branch provides strict state isolation. When we commit to an ingredient (
taken[ingredient_id] = 1), we pass this shared array state down the recursive tree. After the recursive call returns, thetakenarray must be reverted back to0. This ensures that parallel branches in the recursion tree do not contaminate each other, as subsets containing the ingredient are evaluated separately from subsets excluding it.Bidirectional Constraints: The combination of a 2D matrix and bidirectional storage ensures that the validation function can perform an $O(1)$ check. By setting both
constraints[u][v] = 1andconstraints[v][u] = 1during input reading, the order of the pairs becomes irrelevant.Bounded Validation: The validation logic only checks the current
iidagainst previously evaluated ingredients. Iteratingistrictly up toiidprevents the code from reading uninitialized or irrelevant memory further down thetakenarray.
The Solution
Here is the implementation of the validation and recursive functions demonstrating these mechanics.
#define MAX_M 400
int is_valid(int iid, int* taken, int constraints[][MAX_M]) {
int valid = 1;
for (int i = 0; i < iid; i++) {
// No constraints
if (constraints[i][iid] == 0) {
continue;
}
// Prohibited
if (constraints[i][iid] == 1 && taken[i] == 1 && taken[iid] == 1) {
valid = 0;
break;
}
}
return valid;
}
int solve(int ingredient_id, int N, int* taken, int constraints[][MAX_M]) {
if (ingredient_id > N) {
return 1;
}
int count = 0;
// Skip it
count += solve(ingredient_id + 1, N, taken, constraints);
// Take it if valid
taken[ingredient_id] = 1;
if (is_valid(ingredient_id, taken, constraints)) {
count += solve(ingredient_id + 1, N, taken, constraints);
}
taken[ingredient_id] = 0;
return count;
}
Conclusion
By combining a subset selection tree, explicit array cleanup, and a bidirectional adjacency matrix, we exhaustively calculate all valid combinations while maintaining stable memory state.