Facebook Pixel

1996. The Number of Weak Characters in the Game

Problem Description

You have a game with multiple characters where each character has two properties: attack and defense. These properties are given as a 2D array properties where properties[i] = [attack_i, defense_i] represents the attack and defense values for the i-th character.

A character is considered weak if there exists at least one other character that has both higher attack AND higher defense values. In other words, character i is weak if there's another character j such that:

  • attack_j > attack_i
  • defense_j > defense_i

Your task is to count how many weak characters exist in total.

The solution uses a clever sorting strategy. By sorting characters with attack in descending order and defense in ascending order (when attacks are equal), we can process characters in a specific order. The key insight is that when we sort by (-attack, defense):

  1. Characters are grouped by attack value (highest to lowest)
  2. Within the same attack value, characters are ordered by defense (lowest to highest)

As we traverse through the sorted list:

  • We maintain a variable mx to track the maximum defense seen so far
  • For each character, if its defense is less than mx, it means there's a previously seen character with both higher attack (because we're moving from higher to lower attacks) and higher defense
  • The sorting ensures we don't incorrectly count characters with the same attack value as dominating each other

For example, with properties = [[5,5],[6,3],[3,6]]:

  • After sorting: [[6,3],[5,5],[3,6]]
  • Process [6,3]: mx = 3, no weak character
  • Process [5,5]: defense 5 > mx 3, so not weak, update mx = 5
  • Process [3,6]: defense 6 > mx 5, so not weak, update mx = 6
  • Result: 1 weak character (the one with [5,5] is dominated by [3,6])
Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

Intuition

The key challenge is determining which characters are weak without comparing every pair, which would be inefficient. We need a way to systematically check if any character dominates another.

Let's think about what makes a character weak: it needs to have both lower attack AND lower defense than some other character. If we randomly check characters, we'd need to compare each one against all others, leading to O(n²) comparisons.

The breakthrough comes from realizing that if we process characters in a specific order, we can keep track of just one value (maximum defense) instead of comparing with all previous characters.

Why sort by attack descending? When we process characters from highest to lowest attack, any character we've already seen is guaranteed to have higher or equal attack. This handles one condition automatically.

But what about characters with the same attack value? They can't dominate each other (since domination requires strictly greater values). To avoid false positives, we sort characters with the same attack by defense in ascending order. This ensures that when we encounter a character with the same attack value, the maximum defense we've tracked so far doesn't include characters with the same attack level that have higher defense.

Here's the clever part: as we traverse the sorted list:

  • If current defense < maximum defense seen so far, then there must be a previous character with both higher attack (due to our traversal order) and higher defense
  • If current defense ≥ maximum defense, this character isn't weak (at least not by any character we've seen so far)

The sorting pattern (-attack, defense) creates this perfect processing order where we only need to track one variable (mx for maximum defense) and make one comparison per character, reducing complexity from O(n²) to O(n log n).

Learn more about Stack, Greedy, Sorting and Monotonic Stack patterns.

Solution Approach

The implementation follows these steps:

Step 1: Sort the characters

properties.sort(key=lambda x: (-x[0], x[1]))

We sort the properties array using a custom key. The lambda function (-x[0], x[1]) creates a tuple where:

  • -x[0] sorts by attack in descending order (negative value reverses the order)
  • x[1] sorts by defense in ascending order when attacks are equal

This ensures characters are ordered from highest to lowest attack, and within the same attack level, from lowest to highest defense.

Step 2: Initialize tracking variables

ans = mx = 0
  • ans: Counter for weak characters
  • mx: Tracks the maximum defense value seen so far

Step 3: Traverse and count weak characters

for _, x in properties:
    ans += x < mx
    mx = max(mx, x)

For each character in the sorted list:

  • We only care about the defense value x (attack is handled by sorting)
  • x < mx: If current defense is less than the maximum defense seen, this character is weak
  • ans += x < mx: In Python, True equals 1 and False equals 0, so this increments ans when the character is weak
  • mx = max(mx, x): Update the maximum defense for future comparisons

Why this works:

  • When we check x < mx, we know that mx comes from characters with higher attack values (due to our traversal order)
  • The ascending defense sort within same attack values prevents false positives
  • Each character is processed exactly once, making this O(n) for traversal after O(n log n) sorting

Time Complexity: O(n log n) for sorting + O(n) for traversal = O(n log n)
Space Complexity: O(1) if we don't count the sorting space, otherwise O(log n) for sorting

Ready to land your dream job?

Unlock your dream job with a 5-minute evaluator for a personalized learning plan!

Start Evaluator

Example Walkthrough

Let's walk through a concrete example with properties = [[2,2],[3,3],[1,5],[4,1]].

Step 1: Initial Setup

  • Original array: [[2,2],[3,3],[1,5],[4,1]]
  • We need to find characters where another character has BOTH higher attack AND defense

Step 2: Sort by (-attack, defense)

  • Apply sorting key: (-x[0], x[1])
  • [2,2] → key: (-2, 2)
  • [3,3] → key: (-3, 3)
  • [1,5] → key: (-1, 5)
  • [4,1] → key: (-4, 1)
  • Sorted: [[4,1],[3,3],[2,2],[1,5]]

Step 3: Process each character

  • Initialize: ans = 0, mx = 0

Processing [4,1]:

  • Defense = 1, check if 1 < 0? No
  • Not weak, update mx = max(0, 1) = 1
  • ans = 0

Processing [3,3]:

  • Defense = 3, check if 3 < 1? No
  • Not weak, update mx = max(1, 3) = 3
  • ans = 0

Processing [2,2]:

  • Defense = 2, check if 2 < 3? Yes!
  • This is weak! (There's a character with attack ≥ 3 and defense = 3)
  • Update mx = max(3, 2) = 3
  • ans = 1

Processing [1,5]:

  • Defense = 5, check if 5 < 3? No
  • Not weak, update mx = max(3, 5) = 5
  • ans = 1

Result: 1 weak character ([2,2] is dominated by [3,3])

Why the sorting works:

  • When we checked [2,2], the mx = 3 came from [3,3] which has attack 3 > 2
  • The descending attack sort guarantees any mx value comes from higher attack characters
  • The ascending defense sort (within same attack) prevents same-attack characters from incorrectly dominating each other

Solution Implementation

1class Solution:
2    def numberOfWeakCharacters(self, properties: List[List[int]]) -> int:
3        # Sort characters by attack in descending order, 
4        # and by defense in ascending order for same attack values
5        # This ensures when we traverse, characters with higher attack come first
6        # For same attack, weaker defense comes first (prevents false counting)
7        properties.sort(key=lambda character: (-character[0], character[1]))
8      
9        # Initialize counter for weak characters and max defense seen so far
10        weak_count = 0
11        max_defense = 0
12      
13        # Traverse through sorted characters
14        for attack, defense in properties:
15            # If current defense is less than max defense seen,
16            # this character is weak (attack is guaranteed to be less due to sorting)
17            if defense < max_defense:
18                weak_count += 1
19          
20            # Update the maximum defense value encountered
21            max_defense = max(max_defense, defense)
22      
23        return weak_count
24
1class Solution {
2    public int numberOfWeakCharacters(int[][] properties) {
3        // Sort characters by attack in descending order
4        // If attacks are equal, sort by defense in ascending order
5        // This ensures when we traverse, characters with same attack won't count each other as weak
6        Arrays.sort(properties, (a, b) -> {
7            if (b[0] - a[0] == 0) {
8                return a[1] - b[1];  // Same attack: sort defense ascending
9            }
10            return b[0] - a[0];      // Different attack: sort attack descending
11        });
12      
13        // Count weak characters
14        int weakCharacterCount = 0;
15        int maxDefenseSoFar = 0;
16      
17        // Traverse sorted array
18        for (int[] character : properties) {
19            int currentDefense = character[1];
20          
21            // If current defense is less than max defense seen so far,
22            // this character is weak (attack is already less due to sorting)
23            if (currentDefense < maxDefenseSoFar) {
24                weakCharacterCount++;
25            }
26          
27            // Update the maximum defense value seen
28            maxDefenseSoFar = Math.max(maxDefenseSoFar, currentDefense);
29        }
30      
31        return weakCharacterCount;
32    }
33}
34
1class Solution {
2public:
3    int numberOfWeakCharacters(vector<vector<int>>& properties) {
4        // Sort characters by attack in descending order
5        // If attacks are equal, sort by defense in ascending order
6        // This ensures when we traverse, characters with same attack won't be counted as weak
7        sort(properties.begin(), properties.end(), [](const vector<int>& a, const vector<int>& b) {
8            if (a[0] == b[0]) {
9                return a[1] < b[1];  // Same attack: sort defense ascending
10            }
11            return a[0] > b[0];  // Different attack: sort attack descending
12        });
13      
14        int weakCharacterCount = 0;
15        int maxDefenseSeen = 0;
16      
17        // Traverse through sorted characters
18        // Since we process in descending attack order, any character with defense
19        // less than maxDefenseSeen is weak (has both lower attack and defense)
20        for (const auto& character : properties) {
21            if (character[1] < maxDefenseSeen) {
22                weakCharacterCount++;
23            }
24            maxDefenseSeen = max(maxDefenseSeen, character[1]);
25        }
26      
27        return weakCharacterCount;
28    }
29};
30
1/**
2 * Counts the number of weak characters in the game.
3 * A character is weak if there exists another character with both higher attack and defense.
4 * 
5 * @param properties - 2D array where properties[i] = [attack_i, defense_i]
6 * @returns The number of weak characters
7 */
8function numberOfWeakCharacters(properties: number[][]): number {
9    // Sort characters by attack in descending order
10    // If attack values are equal, sort by defense in ascending order
11    // This ensures when we traverse, characters with same attack won't interfere
12    properties.sort((a: number[], b: number[]): number => {
13        if (a[0] === b[0]) {
14            return a[1] - b[1];  // Same attack: ascending defense
15        }
16        return b[0] - a[0];  // Different attack: descending attack
17    });
18  
19    let weakCharacterCount: number = 0;
20    let maxDefenseSoFar: number = 0;
21  
22    // Traverse the sorted array
23    // Since attack is sorted descending, any character we've seen before has attack >= current
24    for (const property of properties) {
25        const currentDefense: number = property[1];
26      
27        // If current defense is less than max defense seen so far,
28        // this character is weak (there exists a character with higher attack and defense)
29        if (currentDefense < maxDefenseSoFar) {
30            weakCharacterCount++;
31        } else {
32            // Update the maximum defense value seen
33            maxDefenseSoFar = currentDefense;
34        }
35    }
36  
37    return weakCharacterCount;
38}
39

Time and Space Complexity

The time complexity is O(n × log n), where n is the number of characters (length of the properties list). This is dominated by the sorting operation properties.sort(), which uses Python's Timsort algorithm with O(n × log n) complexity. The subsequent iteration through the sorted array takes O(n) time, but this is absorbed by the sorting complexity.

The space complexity is O(log n). While the sorting is done in-place (modifying the original list), Python's Timsort algorithm requires O(log n) space for its internal operations, specifically for maintaining the stack used in the merge process. The additional variables ans and mx only require O(1) space, so the overall space complexity remains O(log n).

Learn more about how to find time and space complexity quickly.

Common Pitfalls

Pitfall 1: Incorrect Sorting Order for Defense Values

The Problem: A common mistake is sorting defense values in descending order when attacks are equal, like this:

properties.sort(key=lambda x: (-x[0], -x[1]))  # WRONG!

This creates a critical issue where characters with the same attack value can incorrectly dominate each other.

Example of the Bug: Consider properties = [[7,7],[7,3],[7,5]]

With incorrect sorting (-attack, -defense):

  • Sorted: [[7,7],[7,5],[7,3]]
  • Process [7,7]: mx = 7
  • Process [7,5]: 5 < 7, counted as weak (WRONG!)
  • Process [7,3]: 3 < 7, counted as weak (WRONG!)
  • Result: 2 weak characters (incorrect)

The Fix: Sort defense in ascending order when attacks are equal:

properties.sort(key=lambda x: (-x[0], x[1]))  # CORRECT!

With correct sorting:

  • Sorted: [[7,3],[7,5],[7,7]]
  • Process [7,3]: mx = 3
  • Process [7,5]: 5 > 3, not weak, mx = 5
  • Process [7,7]: 7 > 5, not weak, mx = 7
  • Result: 0 weak characters (correct)

Pitfall 2: Not Understanding Why Defense Must Be Ascending

The Problem: Some might think sorting order doesn't matter as long as we group by attack. This misunderstanding leads to counting characters with the same attack as dominating each other.

Key Insight: By processing lower defense values first within the same attack group:

  • The mx variable only gets updated after processing all characters with the current attack level
  • This prevents any character from being dominated by another character with the same attack value
  • When we move to a lower attack group, mx already contains the highest defense from all higher attack groups

Pitfall 3: Using Nested Loops Instead of Sorting

The Problem: Attempting a brute force O(n²) solution:

def numberOfWeakCharacters(self, properties):
    count = 0
    for i in range(len(properties)):
        for j in range(len(properties)):
            if i != j:
                if properties[j][0] > properties[i][0] and properties[j][1] > properties[i][1]:
                    count += 1
                    break
    return count

Issues:

  • Time complexity is O(n²), which times out for large inputs
  • Less elegant and harder to optimize

The Fix: Use the sorting approach which reduces complexity to O(n log n) and elegantly handles the comparison logic through ordering.

Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

The three-steps of Depth First Search are:

  1. Identify states;
  2. Draw the state-space tree;
  3. DFS on the state-space tree.

Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More