3443. Maximum Manhattan Distance After K Changes
Problem Description
You are given a string s
consisting of characters 'N'
, 'S'
, 'E'
, and 'W'
that represent movements on an infinite grid. Starting from the origin (0, 0)
, each character tells you to move one unit in a specific direction:
'N'
: Move north (up) by 1 unit'S'
: Move south (down) by 1 unit'E'
: Move east (right) by 1 unit'W'
: Move west (left) by 1 unit
You must execute these movements in the exact order they appear in the string. However, you can change at most k
characters in the string to any of the four directions before starting the movement sequence.
Your goal is to find the maximum Manhattan distance from the origin that you can achieve at any point during your movement sequence. The Manhattan distance from the origin (0, 0)
to any point (x, y)
is calculated as |x| + |y|
.
For example, if you have the string "NNE"
and k = 1
, you could change one 'N'
to 'E'
to get "ENE"
or "NEE"
. As you execute the movements, you would track your position after each step and keep track of the maximum Manhattan distance reached at any point.
The key insight is that to maximize Manhattan distance, you want to move consistently in the same general direction. The solution considers four diagonal directions (SE, SW, NE, NW) and for each pair, treats movements in those two directions as beneficial (increasing distance) while movements in the opposite directions are detrimental (decreasing distance). When you have changes available (cnt < k
), you can convert detrimental moves into beneficial ones.
Intuition
To maximize the Manhattan distance from the origin, we need to move as far as possible in a consistent direction. The Manhattan distance is |x| + |y|
, which means we want to maximize the absolute values of our x and y coordinates.
Think about it geometrically - to get far from the origin, we should try to move diagonally rather than zigzagging back and forth. The four diagonal directions are: Northeast (NE), Northwest (NW), Southeast (SE), and Southwest (SW).
For example, if we're trying to maximize distance in the Southeast direction, both 'S'
and 'E'
movements are helpful - they increase our distance from the origin. Meanwhile, 'N'
and 'W'
movements work against us by bringing us closer to the origin.
Here's the key insight: we can use our k
changes strategically. When we encounter a movement that goes against our chosen direction (like encountering 'N'
when we want to go Southeast), we can change it to a favorable direction if we still have changes left. This converts a distance-reducing move into a distance-increasing move.
The greedy strategy emerges naturally: as we traverse the string in order, we keep track of our current Manhattan distance. When we see a favorable move ('S'
or 'E'
for Southeast), we increase our distance. When we see an unfavorable move and have changes remaining, we use a change to make it favorable. If we're out of changes, we have no choice but to execute the unfavorable move, decreasing our distance.
Since we don't know which diagonal direction will yield the maximum distance, we try all four possibilities: SE, SW, NE, and NW. Each attempt uses the same greedy strategy, and we return the maximum distance achieved across all four attempts.
The algorithm is efficient because it makes a single pass through the string for each direction pair, using a greedy approach to decide when to use our precious k
changes for maximum benefit.
Learn more about Math patterns.
Solution Approach
The solution implements the greedy strategy by defining a helper function calc(a, b)
that computes the maximum Manhattan distance when treating directions a
and b
as favorable.
Let's walk through the implementation step by step:
1. Helper Function calc(a, b)
:
This function takes two direction characters that represent our favorable directions (e.g., 'S'
and 'E'
for Southeast). It maintains three key variables:
mx
: Current Manhattan distance from origincnt
: Number of changes used so farans
: Maximum Manhattan distance achieved at any point
2. Processing Each Character:
As we traverse the string s
character by character:
-
Case 1: Favorable move (
c == a
orc == b
):- Increment
mx
by 1 since this move takes us further from origin
- Increment
-
Case 2: Unfavorable move with changes available (
cnt < k
):- Use one change to convert this unfavorable move to favorable
- Increment
mx
by 1 (as if it were favorable) - Increment
cnt
by 1 to track the used change
-
Case 3: Unfavorable move with no changes left:
- Must execute the unfavorable move as-is
- Decrement
mx
by 1 since this brings us closer to origin
After processing each character, we update ans = max(ans, mx)
to track the maximum distance achieved.
3. Trying All Four Diagonal Directions:
The main function calls calc
four times:
calc("S", "E")
: Southeast directioncalc("S", "W")
: Southwest directioncalc("N", "E")
: Northeast directioncalc("N", "W")
: Northwest direction
4. Return Maximum:
The function returns the maximum value among all four attempts: max(a, b, c, d)
.
Time Complexity: O(n)
where n
is the length of string s
, as we make four passes through the string.
Space Complexity: O(1)
as we only use a constant amount of extra space.
The brilliance of this approach lies in its simplicity - by abstracting the problem to favorable vs unfavorable moves and using a greedy strategy to maximize distance at each step, we avoid complex state tracking while still finding the optimal solution.
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 concrete example with s = "NNESE"
and k = 1
.
We'll try the Southeast (SE) direction in detail, where 'S'
and 'E'
are favorable moves.
Initial state: Position = (0, 0), Manhattan distance = 0, changes used = 0
Step 1: Process 'N'
- 'N' is unfavorable for SE direction
- We have changes available (0 < 1)
- Use 1 change to convert 'N' → 'S' or 'E'
- Manhattan distance: 0 → 1
- Changes used: 0 → 1
Step 2: Process 'N'
- 'N' is unfavorable for SE direction
- No changes left (1 = 1)
- Must execute 'N' as-is (move north)
- Manhattan distance: 1 → 0
- Changes used: 1
Step 3: Process 'E'
- 'E' is favorable for SE direction
- Execute 'E' (move east)
- Manhattan distance: 0 → 1
- Changes used: 1
Step 4: Process 'S'
- 'S' is favorable for SE direction
- Execute 'S' (move south)
- Manhattan distance: 1 → 2
- Changes used: 1
Step 5: Process 'E'
- 'E' is favorable for SE direction
- Execute 'E' (move east)
- Manhattan distance: 2 → 3
- Changes used: 1
Result for SE direction: Maximum distance = 3
The algorithm would similarly try SW, NE, and NW directions:
- SW ('S', 'W'): Would change 'N' to favorable, but 'E' moves would hurt later → max = 1
- NE ('N', 'E'): Both 'N' and 'E' are favorable, only 'S' hurts → max = 3
- NW ('N', 'W'): Would change one 'E' or 'S' to favorable → max = 2
Final answer: max(3, 1, 3, 2) = 3
The key insight: by greedily using our change early when moving SE, we converted a distance-reducing move into a distance-increasing one, allowing us to build momentum in our chosen direction.
Solution Implementation
1class Solution:
2 def maxDistance(self, s: str, k: int) -> int:
3 def calculate_max_distance(direction1: str, direction2: str) -> int:
4 """
5 Calculate maximum distance achievable using two primary directions
6 and up to k moves in other directions.
7
8 Uses a sliding window approach where:
9 - Moving in direction1 or direction2 increases distance
10 - Other moves decrease distance but are limited to k times
11 """
12 max_distance = 0 # Maximum distance achieved so far
13 current_distance = 0 # Current distance from origin
14 other_moves_count = 0 # Count of moves not in direction1 or direction2
15
16 for direction in s:
17 if direction == direction1 or direction == direction2:
18 # Move in favorable direction increases distance
19 current_distance += 1
20 elif other_moves_count < k:
21 # Can still use other directions (within k limit)
22 other_moves_count += 1
23 current_distance += 1
24 else:
25 # Exceeded k limit for other directions, must backtrack
26 current_distance -= 1
27
28 # Update maximum distance if current is better
29 max_distance = max(max_distance, current_distance)
30
31 return max_distance
32
33 # Try all four combinations of perpendicular directions
34 # South-East diagonal
35 distance_se = calculate_max_distance("S", "E")
36 # South-West diagonal
37 distance_sw = calculate_max_distance("S", "W")
38 # North-East diagonal
39 distance_ne = calculate_max_distance("N", "E")
40 # North-West diagonal
41 distance_nw = calculate_max_distance("N", "W")
42
43 # Return the maximum distance among all diagonal strategies
44 return max(distance_se, distance_sw, distance_ne, distance_nw)
45
1class Solution {
2 private char[] directions;
3 private int maxChanges;
4
5 /**
6 * Finds the maximum distance that can be traveled in one dimension
7 * by optimally choosing and changing directions.
8 *
9 * @param s String containing direction characters (N, S, E, W)
10 * @param k Maximum number of direction changes allowed
11 * @return Maximum achievable distance in either horizontal or vertical dimension
12 */
13 public int maxDistance(String s, int k) {
14 this.directions = s.toCharArray();
15 this.maxChanges = k;
16
17 // Try all four combinations of opposite direction pairs
18 // Horizontal: S-E and S-W represent different diagonal movements
19 // Vertical: N-E and N-W represent different diagonal movements
20 int southEastDistance = calculateMaxDistance('S', 'E');
21 int southWestDistance = calculateMaxDistance('S', 'W');
22 int northEastDistance = calculateMaxDistance('N', 'E');
23 int northWestDistance = calculateMaxDistance('N', 'W');
24
25 // Return the maximum distance among all combinations
26 return Math.max(
27 Math.max(southEastDistance, southWestDistance),
28 Math.max(northEastDistance, northWestDistance)
29 );
30 }
31
32 /**
33 * Calculates maximum distance for a specific pair of directions.
34 * Uses a sliding window approach where we can change at most k other directions
35 * to match our target directions.
36 *
37 * @param firstDirection First target direction
38 * @param secondDirection Second target direction
39 * @return Maximum distance achievable with these two directions
40 */
41 private int calculateMaxDistance(char firstDirection, char secondDirection) {
42 int maxDistance = 0; // Track the maximum distance achieved
43 int currentDistance = 0; // Current window distance
44 int changesUsed = 0; // Number of direction changes used
45
46 for (char currentChar : directions) {
47 if (currentChar == firstDirection || currentChar == secondDirection) {
48 // Current direction matches our target - extend window
49 currentDistance++;
50 } else if (changesUsed < maxChanges) {
51 // Can still change this direction - use a change and extend window
52 currentDistance++;
53 changesUsed++;
54 } else {
55 // No more changes available - shrink window
56 currentDistance--;
57 }
58
59 // Update maximum distance if current is better
60 maxDistance = Math.max(maxDistance, currentDistance);
61 }
62
63 return maxDistance;
64 }
65}
66
1class Solution {
2public:
3 int maxDistance(string s, int k) {
4 // Lambda function to calculate maximum distance for a given pair of directions
5 // Parameters: firstDir and secondDir represent two valid directions
6 // Returns: maximum achievable distance using these two directions plus k wildcards
7 auto calculateMaxDistance = [&](char firstDir, char secondDir) {
8 int maxDistance = 0; // Tracks the maximum distance achieved
9 int currentDistance = 0; // Current running distance
10 int wildcardsUsed = 0; // Number of wildcards (other directions) used
11
12 // Iterate through each character in the string
13 for (char direction : s) {
14 // If current character matches one of our target directions
15 if (direction == firstDir || direction == secondDir) {
16 ++currentDistance;
17 }
18 // If it doesn't match but we have wildcards available
19 else if (wildcardsUsed < k) {
20 ++currentDistance;
21 ++wildcardsUsed;
22 }
23 // If no wildcards left and character doesn't match, decrease distance
24 else {
25 --currentDistance;
26 }
27
28 // Update maximum distance if current is better
29 maxDistance = max(maxDistance, currentDistance);
30 }
31 return maxDistance;
32 };
33
34 // Calculate maximum distance for all four possible direction pairs
35 // S-E represents South-East diagonal
36 int southEast = calculateMaxDistance('S', 'E');
37 // S-W represents South-West diagonal
38 int southWest = calculateMaxDistance('S', 'W');
39 // N-E represents North-East diagonal
40 int northEast = calculateMaxDistance('N', 'E');
41 // N-W represents North-West diagonal
42 int northWest = calculateMaxDistance('N', 'W');
43
44 // Return the maximum distance among all four diagonal directions
45 return max({southEast, southWest, northEast, northWest});
46 }
47};
48
1/**
2 * Calculates the maximum distance that can be achieved with at most k direction changes
3 * @param s - String containing directional characters (N, S, E, W)
4 * @param k - Maximum number of allowed direction changes
5 * @returns Maximum distance achievable
6 */
7function maxDistance(s: string, k: number): number {
8 /**
9 * Helper function to calculate maximum distance for a specific pair of directions
10 * @param firstDirection - First allowed direction character
11 * @param secondDirection - Second allowed direction character
12 * @returns Maximum distance achievable with these two directions
13 */
14 const calculateMaxDistanceForPair = (firstDirection: string, secondDirection: string): number => {
15 let maxDistanceSoFar: number = 0; // Tracks the maximum distance found
16 let currentDistance: number = 0; // Current running distance
17 let changesUsed: number = 0; // Number of direction changes used
18
19 // Iterate through each character in the string
20 for (const currentChar of s) {
21 // If current character matches one of our allowed directions
22 if (currentChar === firstDirection || currentChar === secondDirection) {
23 currentDistance++;
24 }
25 // If we haven't used all k changes yet, we can change this direction
26 else if (changesUsed < k) {
27 currentDistance++;
28 changesUsed++;
29 }
30 // If we've used all changes and can't use this direction, decrease distance
31 else {
32 currentDistance--;
33 }
34
35 // Update maximum distance if current is higher
36 maxDistanceSoFar = Math.max(maxDistanceSoFar, currentDistance);
37 }
38
39 return maxDistanceSoFar;
40 };
41
42 // Calculate maximum distance for each pair of opposite directions
43 const southEastDistance: number = calculateMaxDistanceForPair('S', 'E');
44 const southWestDistance: number = calculateMaxDistanceForPair('S', 'W');
45 const northEastDistance: number = calculateMaxDistanceForPair('N', 'E');
46 const northWestDistance: number = calculateMaxDistanceForPair('N', 'W');
47
48 // Return the maximum distance among all direction pairs
49 return Math.max(southEastDistance, southWestDistance, northEastDistance, northWestDistance);
50}
51
Time and Space Complexity
The time complexity is O(n)
, where n
is the length of the string s
. This is because the calc
function iterates through the entire string s
once with a single loop, performing constant-time operations for each character. Since calc
is called exactly 4 times (for the four different direction pairs), the total time complexity is 4 * O(n) = O(n)
.
The space complexity is O(1)
. The algorithm only uses a fixed number of variables (ans
, mx
, cnt
in the calc
function, and variables a
, b
, c
, d
in the main function) regardless of the input size. No additional data structures that scale with the input are created, so the space usage remains constant.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
Pitfall 1: Misunderstanding the Greedy Strategy
The Problem: Many developers initially think that the greedy approach of always converting unfavorable moves to favorable ones (when k > 0) might miss optimal solutions. They worry about cases where "saving" changes for later might yield better results.
Why It Happens: The concern is that using changes early in the sequence might prevent us from using them at more critical points later.
The Reality: The greedy strategy actually IS optimal for this problem. Here's why:
- When moving in a consistent diagonal direction (e.g., SE), each favorable move adds +1 to distance
- Each unfavorable move subtracts -1 from distance
- Converting an unfavorable move to favorable creates a +2 swing in distance
- This +2 swing has the same value regardless of when it's applied in the sequence
Solution: Trust the greedy approach. Use changes as soon as you encounter unfavorable moves:
# Correct greedy approach if direction == direction1 or direction == direction2: current_distance += 1 elif other_moves_count < k: # Use change immediately when available other_moves_count += 1 current_distance += 1 else: current_distance -= 1
Pitfall 2: Forgetting to Track Maximum Distance Throughout the Journey
The Problem: Some implementations only check the final distance after processing all moves, missing that the maximum might occur in the middle of the sequence.
Example Scenario:
- String: "NNNNSSSS" with k=2
- If we go North-East, after 4 N's we're at distance 4
- The subsequent S's will decrease our distance
- Final distance might be 0, but maximum was 4
Solution: Always update the maximum after processing each character:
for direction in s:
# Process the move
if direction == direction1 or direction == direction2:
current_distance += 1
# ... other cases ...
# Critical: Update maximum after EVERY move, not just at the end
max_distance = max(max_distance, current_distance)
Pitfall 3: Over-complicating with Actual Coordinate Tracking
The Problem: Attempting to track actual (x, y) coordinates and compute Manhattan distance at each step, which adds unnecessary complexity.
Why It's Unnecessary: The key insight is that when moving in a diagonal direction (e.g., SE), the Manhattan distance simply increases by 1 for each step in either of those directions. We don't need actual coordinates.
Incorrect Approach:
# Overly complex - tracking actual positions
x, y = 0, 0
for direction in s:
if direction == 'N': y += 1
elif direction == 'S': y -= 1
elif direction == 'E': x += 1
elif direction == 'W': x -= 1
distance = abs(x) + abs(y)
Correct Simplified Approach:
# Just track distance changes current_distance = 0 for direction in s: if direction in favorable_directions: current_distance += 1 else: current_distance -= 1 # or +1 if we have changes left
Pitfall 4: Not Considering All Four Diagonal Directions
The Problem: Some solutions might only test one or two diagonal directions, missing the optimal solution that could come from a different diagonal.
Example: For string "EEENNNN", the NE diagonal would be optimal, but if you only tested SE and SW, you'd get a suboptimal result.
Solution: Always test all four diagonal combinations:
return max(
calculate_max_distance("S", "E"), # Southeast
calculate_max_distance("S", "W"), # Southwest
calculate_max_distance("N", "E"), # Northeast
calculate_max_distance("N", "W") # Northwest
)
Which two pointer techniques do you use to check if a string is a palindrome?
Recommended Readings
Math for Technical Interviews How much math do I need to know for technical interviews The short answer is about high school level math Computer science is often associated with math and some universities even place their computer science department under the math faculty However the reality is that you
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!