Skip to content

Commit

Permalink
Longest Repeating Character Replacement
Browse files Browse the repository at this point in the history
  • Loading branch information
dksifoua committed Aug 26, 2024
1 parent 42f062c commit 624ec07
Show file tree
Hide file tree
Showing 6 changed files with 173 additions and 11 deletions.
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: Makefile CI
name: leetcode
on:
push:
branches:
Expand All @@ -13,19 +13,22 @@ on:
- feature/**
- ci/**
jobs:
build:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup JDK
uses: actions/setup-java@v3
- name: Setup Java
uses: actions/setup-java@v4
with:
java-version: "20.0.1"
distribution: "temurin"
server-id: github
settings-path: ${{ github.workspace }}
distribution: oracle
java-version: 21
- name: Setup Task
uses: arduino/setup-task@v2
with:
version: 3.x
repo-token: ${{ secrets.GH_TOKEN }}
- name: Unit Tests
run: ./gradlew test
run: task test
- name: Code Coverage
uses: codecov/codecov-action@v3
env:
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
| 0297 | Medium | Find the Duplicate Number | Array, Two Pointers, Binary Search, Bit Manipulation | [solution](./docs/0287-Find-The-Duplicate-Number.md) |
| 0347 | Medium | Top K Frequent Elements | Array, HashMap, Bucket Sort | [Solution](./docs/0347-Top-K-Frequent-Elements.md) |
| 0392 | Easy | Is Subsequence | Two Pointers, String, Dynamic Programming | [solution](./docs/0392-Is-Subsequence.md) |
| 0424 | Medium | Longest Repeating Character Replacement | HashTable, String, Sliding Windows | [solution](./docs/0424-Longest-Repeating-Character-Replacement.md) |
| 0704 | Easy | Binary Search | Array, Binary Search | [solution](./docs/0704-Binary-Search.md) |
| 0739 | Medium | Daily Temperatures | Array, Stack, Monotonic Stack | [solution](./docs/0139-Daily-Temperatures.md) |
| 0853 | Medium | Car Fleet | Array, Stack, Sorting, Monotonic Stack | [solution](./docs/0853-Car-Fleet.md) |
Expand Down
6 changes: 4 additions & 2 deletions Taskfile.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ tasks:
silent: true

test:
desc: Test
cmd: ./gradlew test
desc: Unit tests
cmds:
- task: clean
- ./gradlew test
silent: true
50 changes: 50 additions & 0 deletions docs/0424-Longest-Repeating-Character-Replacement.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# [Longest Repeating Character Replacement](https://leetcode.com/problems/longest-repeating-character-replacement/description/)

## Intuition

The problem asks us to find the longest substring with the same character, allowing for at most `k` character
replacements. The key insight is to use a sliding window approach, where we maintain a window that can be made valid (
all characters the same) by replacing at most `k` characters. We expand the window as long as we can make all characters
in the window the same with at most `k` replacements. When we can't, we start shrinking the window from the left.

## Approach

The solution uses a sliding window technique with two pointers (`i` and `j`) and a `HashMap` to keep track of character
frequencies. Here's how it works:

1. Initialize variables:
- `maxLength`: to store the length of the longest valid substring
- `maxCount`: to keep track of the count of the most frequent character in the current window
- `charCount`: a HashMap to store the frequency of each character in the current window
2. Start with a window of size 1 (`i` at the start, `j` at the second character).
3. Expand the window by moving `j` to the right: Add the new character to `charCount` and update `maxCount` if
necessary.
4. Check if the current window is valid:
- A window is valid if `(window size - count of most frequent char) <= k`
- If not valid, shrink the window from the left by moving `i` to the right and updating `charCount` and `maxCount`.
5. If the window is valid, update `maxLength` if the current window is longer.
6. Repeat steps 3-5 until `j` reaches the end of the string.

## Complexity

- **Time Complexity: `O(n)`** as we iterate through the string once with the `j` pointer. The `i` pointer also moves
forward, never backwards, so it also iterates through the string at most once. Operations on the hash map are
constant.
- **Space Complexity: `O(1)`** as the hash map stores at 26 entries (uppercase english letters). All other variables use
constant space.

## Code

- [Java](../src/main/java/io/dksifoua/leetcode/longestrepeatingcharacterreplacement/Solution.java)

## Summary

This solution efficiently solves the problem by using a sliding window approach. It maintains a window that can be made
valid with at most `k` replacements and expands or contracts this window as it moves through the string. The use of a
hash map to track character frequencies allows for quick updates and checks.

The algorithm is optimal in terms of time complexity, as it processes each character at most twice (once when adding to
the window, once when removing). The space complexity is constant, making it memory-efficient for large inputs.

One potential optimization could be to use an array of size 26 instead of a hash map, which might be slightly faster for
very large inputs, but the current implementation is clean and easy to understand.
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package io.dksifoua.leetcode.longestrepeatingcharacterreplacement;

import java.util.HashMap;
import java.util.Map;

public class Solution {

public int characterReplacement(String s, int k) {
int maxLength = 1, maxCount = 1;
Map<Character, Integer> charCount = new HashMap<>() {{
put(s.charAt(0), 1);
}};
int i = 0, j = 1;
while (i < s.length() && j < s.length()) {
charCount.put(s.charAt(j), charCount.getOrDefault(s.charAt(j), 0) + 1);
maxCount = Math.max(maxCount, charCount.get(s.charAt(j)));
if (maxCount + k < j - i + 1) {
if (charCount.get(s.charAt(i)) == maxCount) {
maxCount -= 1;
}
charCount.put(s.charAt(i), charCount.get(s.charAt(i)) - 1);
i += 1;
} else {
maxLength = Math.max(maxLength, j - i + 1);
}

j += 1;
}

return maxLength;
}

public int characterReplacementOptimal(String s, int k) {
int maxLength = 1, maxCount = 1;
int[] charCount = new int[26];
charCount[s.charAt(0) - 'A'] += 1;
int i = 0, j = 1;
while (i < s.length() && j < s.length()) {
charCount[s.charAt(j) - 'A'] += 1;
maxCount = Math.max(maxCount, charCount[s.charAt(j) - 'A']);
if (maxCount + k < j - i + 1) {
if (charCount[s.charAt(i) - 'A'] == maxCount) {
maxCount -= 1;
}
charCount[s.charAt(i) - 'A'] -= 1;
i += 1;
} else {
maxLength = Math.max(maxLength, j - i + 1);
}

j += 1;
}

return maxLength;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package io.dksifoua.leetcode.longestrepeatingcharacterreplacement;

import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertEquals;

public class SolutionTest {

private final Solution solution = new Solution();

@Test
void test1() {
assertEquals(4, solution.characterReplacement("ABAB", 2));
}

@Test
void testOptimal1() {
assertEquals(4, solution.characterReplacementOptimal("ABAB", 2));
}

@Test
void test2() {
assertEquals(4, solution.characterReplacement("AABABBA", 1));
}

@Test
void testOptimal2() {
assertEquals(4, solution.characterReplacementOptimal("AABABBA", 1));
}

@Test
void test3() {
assertEquals(1, solution.characterReplacement("ABAB", 0));
}

@Test
void testOptimal3() {
assertEquals(1, solution.characterReplacementOptimal("ABAB", 0));
}

@Test
void test4() {
assertEquals(4, solution.characterReplacement("AAAA", 0));
}

@Test
void testOptimal4() {
assertEquals(4, solution.characterReplacementOptimal("AAAA", 0));
}
}

0 comments on commit 624ec07

Please sign in to comment.