2151. Maximum Good People Based on Statements
Problem Description
You have n
people, and each person can be either a good person (always tells the truth) or a bad person (might tell the truth or might lie).
You're given a 2D array statements
of size n x n
where statements[i][j]
represents what person i
says about person j
:
0
means personi
says personj
is bad1
means personi
says personj
is good2
means personi
makes no statement about personj
No one makes statements about themselves, so statements[i][i] = 2
for all i
.
The key insight is that good people always tell the truth, while bad people can say anything (truth or lies).
Your task is to find the maximum number of people who can be good such that all statements are logically consistent.
For example, if person i
is good and says person j
is bad (statements[i][j] = 0
), then person j
must actually be bad. But if person i
is bad, their statement can be ignored since bad people might lie.
The solution uses bit masking to try all possible combinations of who is good (represented by 1) and who is bad (represented by 0). For each combination, it checks if all statements from good people are consistent with the assumed configuration. The function returns the maximum count of good people across all valid configurations.
Flowchart Walkthrough
First, let's pin down the algorithm using the Flowchart. Here's a step-by-step walkthrough:
Is it a graph?
- No: While we have relationships between people (who says what about whom), this isn't a traditional graph traversal problem with nodes and edges.
Need to solve for kth smallest/largest?
- No: We're looking for the maximum number of good people, not the kth smallest or largest element.
Involves Linked Lists?
- No: The problem uses a 2D array to represent statements, not linked lists.
Does the problem have small constraints?
- Yes: The constraint is typically
n ≤ 15
people, which means we have at most2^15 = 32,768
possible combinations to check. This is small enough for brute force approaches.
Brute force / Backtracking?
- Yes: Since we have small constraints and need to try all possible combinations of assigning people as "good" or "bad" to find the maximum valid configuration, brute force/backtracking is the appropriate approach.
Conclusion: The flowchart correctly leads us to use a Brute Force/Backtracking approach. The solution uses bit masking to enumerate all possible combinations (each person can be either good=1 or bad=0), validates each combination against the statements made by good people, and returns the maximum count of good people across all valid configurations.
Intuition
The key insight is recognizing that good people create constraints while bad people don't. If someone is good, everything they say must be true. If someone is bad, we can completely ignore what they say.
This means we need to try different assumptions about who is good and who is bad, then check if our assumption is consistent. Think of it like a logic puzzle where we say "let's assume persons 0, 2, and 3 are good and everyone else is bad" - can this work given what the good people say?
Since we have a small number of people (at most 15), we can try all possible combinations. With n
people, there are 2^n
ways to assign them as good or bad. We can represent each combination as a binary number where bit i
indicates if person i
is good (1) or bad (0).
For each combination (mask), we only need to validate statements from people we assumed are good. If person i
is good (bit i
is 1 in our mask) and they say person j
is good (statements[i][j] = 1
), then person j
must actually be good in our assumption (bit j
must be 1). Similarly, if they say person j
is bad (statements[i][j] = 0
), then bit j
must be 0.
If any statement from a good person contradicts our assumption, this combination is invalid. Otherwise, we count how many people are good in this valid combination.
The brute force approach works because:
- We can enumerate all possibilities efficiently with bit manipulation
- Checking each combination is fast - just verify statements from assumed good people
- The small constraint (
n ≤ 15
) makes2^n
combinations manageable
Learn more about Backtracking patterns.
Solution Approach
The solution uses bit masking to enumerate all possible combinations of good and bad people, then validates each combination.
Algorithm Overview:
- Generate all possible assignments of people as good (1) or bad (0) using numbers from
1
to2^n - 1
- For each assignment (mask), check if it's valid
- Track the maximum count of good people across all valid assignments
Implementation Details:
The main function iterates through all possible masks from 1
to 2^n - 1
:
return max(check(i) for i in range(1, 1 << len(statements)))
Here, 1 << len(statements)
equals 2^n
, giving us all possible combinations.
The check
function validates a specific mask and returns the count of good people if valid:
-
Extract good people from mask: For each person
i
, check if biti
is set usingmask >> i & 1
. If it's 1, personi
is assumed good. -
Validate statements from good people:
if mask >> i & 1: # Person i is good for j, x in enumerate(row): if x < 2 and (mask >> j & 1) != x: return 0
- If person
i
is good, check all their statements (row
=statements[i]
) - For each statement about person
j
wherex < 2
(not a "no statement" case):- If
x = 1
(says j is good), thenmask >> j & 1
must be 1 - If
x = 0
(says j is bad), thenmask >> j & 1
must be 0
- If
- If any statement contradicts our assumption, return 0 (invalid)
- If person
-
Count good people: If all statements from good people are consistent, count the number of 1-bits in the mask (number of good people).
Why this works:
- We only validate statements from people we assume are good because bad people can lie
- Each mask represents a complete hypothesis about who is good/bad
- By checking all
2^n
possibilities, we guarantee finding the maximum valid configuration
Time Complexity: O(2^n × n^2)
- we check 2^n
masks, and for each mask, we potentially examine n×n
statements.
Space Complexity: O(1)
- only using variables for counting and iteration.
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a small example with n = 3
people and the following statements:
statements = [[2, 1, 2], [1, 2, 2], [2, 2, 2]]
This means:
- Person 0 says person 1 is good (statements[0][1] = 1)
- Person 1 says person 0 is good (statements[1][0] = 1)
- Person 2 makes no statements about anyone
Step 1: Try different masks (combinations)
We'll check masks from 1 to 7 (binary: 001 to 111). Each bit position represents a person (rightmost bit is person 0).
Mask = 3 (binary: 011)
- Person 0 is good (bit 0 = 1)
- Person 1 is good (bit 1 = 1)
- Person 2 is bad (bit 2 = 0)
Validation:
- Person 0 is good and says person 1 is good → Check if person 1 is actually good in our mask → YES (bit 1 = 1) ✓
- Person 1 is good and says person 0 is good → Check if person 0 is actually good in our mask → YES (bit 0 = 1) ✓
- Person 2 is bad, so we ignore their statements
This mask is valid! Count of good people = 2.
Mask = 5 (binary: 101)
- Person 0 is good (bit 0 = 1)
- Person 1 is bad (bit 1 = 0)
- Person 2 is good (bit 2 = 1)
Validation:
- Person 0 is good and says person 1 is good → Check if person 1 is actually good in our mask → NO (bit 1 = 0) ✗
This mask is invalid because person 0 (who we assume is good) says person 1 is good, but in our mask person 1 is bad. This is a contradiction!
Mask = 7 (binary: 111)
- All three people are good
Validation:
- Person 0 is good and says person 1 is good → Check if person 1 is good → YES ✓
- Person 1 is good and says person 0 is good → Check if person 0 is good → YES ✓
- Person 2 is good but makes no statements → Nothing to validate ✓
This mask is valid! Count of good people = 3.
Step 2: Return the maximum
After checking all masks from 1 to 7, we find that masks 3 and 7 are valid with counts of 2 and 3 good people respectively. The maximum is 3.
The key insight: When we assume someone is good, their statements become constraints we must satisfy. When we assume someone is bad, we completely ignore what they say since they could be lying.
Solution Implementation
1class Solution:
2 def maximumGood(self, statements: List[List[int]]) -> int:
3 """
4 Find the maximum number of good people based on their statements.
5
6 Args:
7 statements: 2D array where statements[i][j] represents what person i says about person j
8 0 = person j is bad, 1 = person j is good, 2 = no statement
9
10 Returns:
11 Maximum number of people that can be good
12 """
13
14 def check_validity(bitmask: int) -> int:
15 """
16 Check if a given configuration of good/bad people is valid.
17
18 Args:
19 bitmask: Binary representation where bit i = 1 means person i is good
20
21 Returns:
22 Number of good people if configuration is valid, 0 otherwise
23 """
24 good_count = 0
25
26 # Check each person's statements
27 for person_idx, person_statements in enumerate(statements):
28 # Check if current person is assumed to be good (bit is 1)
29 if (bitmask >> person_idx) & 1:
30 # If person is good, all their statements must be true
31 for target_idx, statement_value in enumerate(person_statements):
32 # Skip if no statement made (value is 2)
33 if statement_value < 2:
34 # Get the assumed state of the target person
35 target_is_good = (bitmask >> target_idx) & 1
36
37 # Check if statement matches the assumed state
38 if target_is_good != statement_value:
39 # Configuration is invalid
40 return 0
41
42 good_count += 1
43
44 return good_count
45
46 # Try all possible configurations (excluding all bad scenario)
47 n = len(statements)
48 max_good_people = 0
49
50 for configuration in range(1, 1 << n):
51 max_good_people = max(max_good_people, check_validity(configuration))
52
53 return max_good_people
54
1class Solution {
2 /**
3 * Finds the maximum number of good people based on their statements.
4 * Uses bit masking to try all possible combinations of good/bad people.
5 *
6 * @param statements 2D array where statements[i][j] represents what person i says about person j
7 * 0: person j is bad, 1: person j is good, 2: no statement
8 * @return Maximum number of good people
9 */
10 public int maximumGood(int[][] statements) {
11 int maxGoodPeople = 0;
12 int totalPeople = statements.length;
13
14 // Try all possible combinations using bit mask (1 = good, 0 = bad)
15 // Start from 1 to skip the case where everyone is bad
16 for (int mask = 1; mask < (1 << totalPeople); ++mask) {
17 maxGoodPeople = Math.max(maxGoodPeople, check(mask, statements));
18 }
19
20 return maxGoodPeople;
21 }
22
23 /**
24 * Validates if a given configuration of good/bad people is consistent with statements.
25 *
26 * @param mask Bit representation where bit i = 1 means person i is good
27 * @param statements The statements matrix
28 * @return Number of good people if configuration is valid, 0 otherwise
29 */
30 private int check(int mask, int[][] statements) {
31 int goodPeopleCount = 0;
32 int totalPeople = statements.length;
33
34 // Check each person in the current configuration
35 for (int person = 0; person < totalPeople; ++person) {
36 // If current person is marked as good in this configuration
37 if (((mask >> person) & 1) == 1) {
38 // Good people always tell the truth, so verify their statements
39 for (int targetPerson = 0; targetPerson < totalPeople; ++targetPerson) {
40 int statement = statements[person][targetPerson];
41
42 // If person made a statement about targetPerson
43 if (statement < 2) {
44 int isTargetGood = (mask >> targetPerson) & 1;
45
46 // Check if statement matches the actual status of targetPerson
47 if (isTargetGood != statement) {
48 // Contradiction found - this configuration is invalid
49 return 0;
50 }
51 }
52 }
53 ++goodPeopleCount;
54 }
55 }
56
57 return goodPeopleCount;
58 }
59}
60
1class Solution {
2public:
3 int maximumGood(vector<vector<int>>& statements) {
4 int maxGoodPeople = 0;
5 int totalPeople = statements.size();
6
7 // Try all possible combinations of good/bad people using bitmask
8 // Each bit represents whether person i is good (1) or bad (0)
9 for (int bitmask = 1; bitmask < (1 << totalPeople); ++bitmask) {
10 maxGoodPeople = max(maxGoodPeople, validateAndCount(bitmask, statements));
11 }
12
13 return maxGoodPeople;
14 }
15
16private:
17 int validateAndCount(int bitmask, vector<vector<int>>& statements) {
18 int goodPeopleCount = 0;
19 int totalPeople = statements.size();
20
21 // Check each person in the current configuration
22 for (int person = 0; person < totalPeople; ++person) {
23 // If current person is assumed to be good in this configuration
24 if ((bitmask >> person) & 1) {
25 // Validate all statements made by this good person
26 for (int target = 0; target < totalPeople; ++target) {
27 int statement = statements[person][target];
28
29 // If statement is not "unknown" (2)
30 if (statement < 2) {
31 // Check if the statement matches our assumption
32 // statement == 0: target is bad, statement == 1: target is good
33 bool targetIsGood = (bitmask >> target) & 1;
34 if (targetIsGood != statement) {
35 // Contradiction found - this configuration is invalid
36 return 0;
37 }
38 }
39 }
40 goodPeopleCount++;
41 }
42 }
43
44 return goodPeopleCount;
45 }
46};
47
1/**
2 * Finds the maximum number of good people based on their statements about each other.
3 * A person is "good" if all their statements are true.
4 *
5 * @param statements - 2D array where statements[i][j] represents what person i says about person j
6 * 0: person j is bad, 1: person j is good, 2: no statement
7 * @returns The maximum number of people that can be good
8 */
9function maximumGood(statements: number[][]): number {
10 const n: number = statements.length;
11
12 /**
13 * Validates if a given configuration of good/bad people is consistent with their statements.
14 *
15 * @param mask - Binary representation where bit i = 1 means person i is assumed to be good
16 * @returns The count of good people if configuration is valid, 0 otherwise
17 */
18 function checkConfiguration(mask: number): number {
19 let goodPeopleCount: number = 0;
20
21 // Check each person in the current configuration
22 for (let i = 0; i < n; i++) {
23 // If person i is assumed to be good in this configuration
24 if ((mask >> i) & 1) {
25 // Verify all statements made by person i
26 for (let j = 0; j < n; j++) {
27 const statement: number = statements[i][j];
28
29 // If person i made a statement about person j
30 if (statement < 2) {
31 const isPersonJGood: number = (mask >> j) & 1;
32
33 // Check if the statement contradicts the current configuration
34 if (isPersonJGood !== statement) {
35 return 0; // Invalid configuration
36 }
37 }
38 }
39 goodPeopleCount++;
40 }
41 }
42
43 return goodPeopleCount;
44 }
45
46 let maxGoodPeople: number = 0;
47
48 // Try all possible configurations (2^n possibilities)
49 for (let mask = 1; mask < (1 << n); mask++) {
50 maxGoodPeople = Math.max(maxGoodPeople, checkConfiguration(mask));
51 }
52
53 return maxGoodPeople;
54}
55
Time and Space Complexity
Time Complexity: O(n² * 2^n)
where n
is the number of people (length of statements array).
- The outer loop iterates through all possible masks from 1 to
2^n - 1
, which gives usO(2^n)
iterations - For each mask, the
check
function is called:- It iterates through all
n
people to check if they are marked as "good" in the current mask - For each "good" person (bit set to 1), it iterates through their
n
statements about other people - In the worst case, all people could be marked as good, resulting in
O(n²)
operations per mask
- It iterates through all
- Overall:
O(2^n) * O(n²) = O(n² * 2^n)
Space Complexity: O(1)
excluding the input array.
- The algorithm uses only a constant amount of extra space for variables like
mask
,cnt
,i
,j
, andx
- The recursive call stack is not used as this is an iterative solution
- The generator expression
max(check(i) for i in range(...))
evaluates one value at a time and doesn't store all results simultaneously - Therefore, the space complexity is
O(1)
auxiliary space
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Starting iteration from 0 instead of 1
A common mistake is iterating through all masks starting from 0:
# WRONG - includes the case where everyone is bad
for configuration in range(0, 1 << n):
max_good_people = max(max_good_people, check_validity(configuration))
Why it's wrong: When configuration = 0
, all bits are 0, meaning everyone is assumed to be bad. In this case, there are no good people making statements, so any configuration would be "valid" by default (no statements to verify). This could incorrectly return 0 as a valid answer when there might be valid configurations with good people.
Solution: Start from 1 to ensure at least one person is good:
# CORRECT
for configuration in range(1, 1 << n):
max_good_people = max(max_good_people, check_validity(configuration))
2. Incorrectly checking if a person is good in the bitmask
Beginners often make errors with bit manipulation:
# WRONG - checks if entire mask equals 1 if bitmask & 1: # WRONG - forgets to shift before checking if bitmask & person_idx:
Solution: Use proper bit shifting and masking:
# CORRECT - shifts right by person_idx positions, then checks least significant bit if (bitmask >> person_idx) & 1:
3. Validating statements from bad people
A critical error is checking statements from people assumed to be bad:
# WRONG - validates everyone's statements
for person_idx, person_statements in enumerate(statements):
for target_idx, statement_value in enumerate(person_statements):
if statement_value < 2:
target_is_good = (bitmask >> target_idx) & 1
if target_is_good != statement_value:
return 0
Why it's wrong: Bad people can lie, so their statements don't need to be consistent with reality. Only good people's statements must be validated.
Solution: Only validate statements from people marked as good:
# CORRECT
if (bitmask >> person_idx) & 1: # Only check if person is good
for target_idx, statement_value in enumerate(person_statements):
# ... validation logic
4. Using wrong comparison for statement validation
The statement value directly represents whether the target is good (1) or bad (0):
# WRONG - inverted logic
if statement_value == 0 and target_is_good == 1:
return 0
if statement_value == 1 and target_is_good == 0:
return 0
# WRONG - treating statement_value as a boolean incorrectly
if bool(statement_value) != bool(target_is_good):
return 0
Solution: Direct comparison since both use the same encoding (1 = good, 0 = bad):
# CORRECT if target_is_good != statement_value: return 0
5. Not handling the "no statement" case properly
Forgetting to skip when statement_value == 2
:
# WRONG - tries to validate even "no statement" cases
for target_idx, statement_value in enumerate(person_statements):
target_is_good = (bitmask >> target_idx) & 1
if target_is_good != statement_value: # This fails when statement_value = 2
return 0
Solution: Skip validation when no statement is made:
# CORRECT if statement_value < 2: # Only process actual statements (0 or 1) target_is_good = (bitmask >> target_idx) & 1 if target_is_good != statement_value: return 0
What's the output of running the following function using input 56
?
1KEYBOARD = {
2 '2': 'abc',
3 '3': 'def',
4 '4': 'ghi',
5 '5': 'jkl',
6 '6': 'mno',
7 '7': 'pqrs',
8 '8': 'tuv',
9 '9': 'wxyz',
10}
11
12def letter_combinations_of_phone_number(digits):
13 def dfs(path, res):
14 if len(path) == len(digits):
15 res.append(''.join(path))
16 return
17
18 next_number = digits[len(path)]
19 for letter in KEYBOARD[next_number]:
20 path.append(letter)
21 dfs(path, res)
22 path.pop()
23
24 res = []
25 dfs([], res)
26 return res
27
1private static final Map<Character, char[]> KEYBOARD = Map.of(
2 '2', "abc".toCharArray(),
3 '3', "def".toCharArray(),
4 '4', "ghi".toCharArray(),
5 '5', "jkl".toCharArray(),
6 '6', "mno".toCharArray(),
7 '7', "pqrs".toCharArray(),
8 '8', "tuv".toCharArray(),
9 '9', "wxyz".toCharArray()
10);
11
12public static List<String> letterCombinationsOfPhoneNumber(String digits) {
13 List<String> res = new ArrayList<>();
14 dfs(new StringBuilder(), res, digits.toCharArray());
15 return res;
16}
17
18private static void dfs(StringBuilder path, List<String> res, char[] digits) {
19 if (path.length() == digits.length) {
20 res.add(path.toString());
21 return;
22 }
23 char next_digit = digits[path.length()];
24 for (char letter : KEYBOARD.get(next_digit)) {
25 path.append(letter);
26 dfs(path, res, digits);
27 path.deleteCharAt(path.length() - 1);
28 }
29}
30
1const KEYBOARD = {
2 '2': 'abc',
3 '3': 'def',
4 '4': 'ghi',
5 '5': 'jkl',
6 '6': 'mno',
7 '7': 'pqrs',
8 '8': 'tuv',
9 '9': 'wxyz',
10}
11
12function letter_combinations_of_phone_number(digits) {
13 let res = [];
14 dfs(digits, [], res);
15 return res;
16}
17
18function dfs(digits, path, res) {
19 if (path.length === digits.length) {
20 res.push(path.join(''));
21 return;
22 }
23 let next_number = digits.charAt(path.length);
24 for (let letter of KEYBOARD[next_number]) {
25 path.push(letter);
26 dfs(digits, path, res);
27 path.pop();
28 }
29}
30
Recommended Readings
Backtracking Template Prereq DFS with States problems dfs_with_states Combinatorial search problems Combinatorial search problems involve finding groupings and assignments of objects that satisfy certain conditions Finding all permutations combinations subsets and solving Sudoku are classic combinatorial problems The time complexity of combinatorial problems often grows rapidly with the size of
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Want a Structured Path to Master System Design Too? Don’t Miss This!