Skip to content

Commit b2715eb

Browse files
authored
ADHOC: More solutions (#194)
1 parent 582ab61 commit b2715eb

File tree

6 files changed

+288
-0
lines changed

6 files changed

+288
-0
lines changed

src/array/maximum-swap.ts

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
// DIFFICULTY: MEDIUM
2+
//
3+
// You are given an integer num. You can swap two digits at most once to get the maximum valued number.
4+
//
5+
// Return the maximum valued number you can get.
6+
//
7+
// See {@link https://leetcode.com/problems/maximum-swap/}
8+
export { maximumSwap };
9+
10+
// SOLUTION:
11+
//
12+
// The simple idea of swapping the largest number with the first digit doesn't always work. It works a lot of the time,
13+
// but a couple of cases mess up this algorithm:
14+
//
15+
// - 98368 would stay 98368; we need to swap the 3 and 8 to get 98863.
16+
// - 1993 could become 9193, but we need to swap the 1 and other 9 to get 9913.
17+
//
18+
// The way to think about this is:
19+
//
20+
// 1. Find the FIRST digit that can be made bigger (larger digits appear later).
21+
// 2. Swap it with the LAST occurrence of the largest digit.
22+
//
23+
// COMPLEXITY:
24+
//
25+
// The time complexity is O(n) where n is the number of digits. We iterate through the digits once to create our array
26+
// and map. We iterate through the digits a second time to find the swap point. There is a nested loop, but the inner
27+
// loop is bounded by the 10 digits, so it doesn't cause O(n^2) time complexity.
28+
//
29+
// The space complexity is O(n) because we store an array and map of the digits.
30+
function maximumSwap(num: number): number {
31+
// First convert the number to an array of digits.
32+
const digits = num.toString().split('').map(Number);
33+
34+
// Create a map of the last seen index of each digit. Use this to find out the last occurrence of any digit.
35+
type Digit = number;
36+
type LastSeen = number;
37+
const map = new Map<Digit, LastSeen>();
38+
for (let i = 0; i < digits.length; i++) {
39+
const digit = digits[i];
40+
map.set(digit, i);
41+
}
42+
43+
// Find the first digit that can be made bigger by iterating through the digits list.
44+
for (let i = 0; i < digits.length; i++) {
45+
const smaller = digits[i];
46+
47+
// Find a larger digit to swap the smaller digit with. To do so, start at 9 and iterate downwards until we find a
48+
// digit that is larger, and appears later in the number.
49+
for (let larger = 9; larger >= 0; larger--) {
50+
// Skip the digit if it's not larger.
51+
if (larger <= smaller) {
52+
break;
53+
}
54+
55+
// Skip the digit if it doesn't appear at all.
56+
if (!map.has(larger)) {
57+
continue;
58+
}
59+
60+
// Skip the digit if it appears before the smaller digit.
61+
const j = map.get(larger)!;
62+
if (j <= i) {
63+
continue;
64+
}
65+
66+
// Success! We have found the largest digit that occurs as late as possible! Swap the digits and return the
67+
// number.
68+
[digits[i], digits[j]] = [digits[j], digits[i]];
69+
return Number(digits.join(''));
70+
}
71+
}
72+
73+
// We made no swaps, so just return the number as is.
74+
return num;
75+
}
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
// DIFFICULTY: MEDIUM
2+
//
3+
// Given an integer array nums and an integer k, return true if nums has a good subarray or false otherwise.
4+
//
5+
// A good subarray is a subarray where:
6+
//
7+
// - its length is at least two, and
8+
// - the sum of the elements of the subarray is a multiple of k.
9+
//
10+
// Note that:
11+
//
12+
// - A subarray is a contiguous part of the array.
13+
// - An integer x is a multiple of k if there exists an integer n such that x = n * k. 0 is always a multiple of k.
14+
//
15+
// See {@link https://leetcode.com/problems/continuous-subarray-sum/}
16+
export { checkSubarraySum };
17+
18+
// SOLUTION:
19+
//
20+
// The brute force solution is to calculate the sum of all subarrays of length 2 or more and check if it's a multiple of
21+
// of k. To do so, compute the prefix sum of the array and use it to calculate the sum of the subarray from [i, j].
22+
//
23+
// A subarray sum from [i, j] can be calculated as prefixSum[j] - prefixSum[i] + nums[i]. We can check each subarray
24+
// sum to see if it's a multiple of k and return true if we find one. It is; however, O(n^2) to calculate subarray
25+
// sums in this way. Unfortunately, this will exceed the runtime limit for large arrays in LeetCode.
26+
//
27+
// To get a better solution we have to note that:
28+
//
29+
// sum[i, j] = prefixSum[j] - prefixSum[i] + nums[i]
30+
// sum[i, j] = prefixSum[j] - prefixSum[i - 1]
31+
//
32+
// This is because the sum[i, j] is inclusive, and subtracting prefixSum[i] subtracts out nums[i]. That's why we either
33+
// have to add it back in or use i - 1 as the index instead. Afterwards, we can look at the sum modulo k:
34+
//
35+
// sum[i, j] (mod k) = (prefixSum[j] - prefixSum[i - 1]) (mod k)
36+
//
37+
// If we want sum[i, j] % k === 0, then we should write:
38+
//
39+
// (prefixSum[j] - prefixSum[i - 1]) (mod k) = 0
40+
// prefixSum[j] (mod k) = prefixSum[i - 1] (mod k)
41+
//
42+
// In other words, if prefixSums at positions (i - 1) and j have the same remainder modulo k, then the subarray sum from
43+
// [i, j] has remainder 0 modulo k.
44+
//
45+
// COMPLEXITY:
46+
//
47+
// Time complexity is O(n^2) because we are doing an outer and inner loop on the prefix sum array.
48+
//
49+
// Space complexity is O(n) because we are storing the prefix sum array.
50+
function checkSubarraySum(nums: number[], k: number): boolean {
51+
// Use a map of remainders to positions to store the prefix sum remainders, so we can check later if we have seen any
52+
// remainders that are the same modulo k.
53+
type Remainder = number;
54+
type Index = number;
55+
const remainders = new Map<Remainder, Index>();
56+
57+
// Maintain a running prefix sum as we loop through the array.
58+
let prefixSum = 0;
59+
for (let i = 0; i < nums.length; i++) {
60+
prefixSum += nums[i];
61+
let remainder = prefixSum % k;
62+
63+
// It might be the case that the remainder is negative, just as a quirk of how JavaScript handles the modulo
64+
// operator.
65+
//
66+
// For example, -5 % 3 = -2. But this is wrong because -2 is not congruent to -5 modulo 3. Instead:
67+
//
68+
// -5 (mod 3) = -5 + 3 (mod 3) = -2 (mod 3) = -2 + 3 (mod 3) = 1 (mod 3)
69+
//
70+
// That is, we want -5 % 3 = 1 instead. To fix this, we half to add the modulus to the remainder if it's negative.
71+
if (remainder < 0) {
72+
remainder += k;
73+
}
74+
75+
// It's possible that the prefix sum already is a multiple of k. And it's possible that the remainder 0 isn't in
76+
// the remainders map yet. In this case, just check if we have a subarray of length 2 or more.
77+
if (remainder === 0 && i >= 1) {
78+
return true;
79+
}
80+
81+
// If this is a previously seen remainder, we can check if we have a "good" subarray.
82+
if (remainders.has(remainder)) {
83+
const j = remainders.get(remainder)!;
84+
if (Math.abs(i - j) >= 2) {
85+
return true;
86+
}
87+
}
88+
// If we're seeing this remainder for the first time, store it in the map.
89+
else {
90+
remainders.set(remainder, i);
91+
}
92+
}
93+
94+
return false;
95+
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
// DIFFICULTY: MEDIUM
2+
//
3+
// Given a string s, return the longest palindromic substring in s.
4+
//
5+
// See {@link https://leetcode.com/problems/longest-palindromic-substring/}
6+
export { longestPalindrome };
7+
8+
// SOLUTION:
9+
//
10+
// The naive solution is to iterate through each character, then treat that character as the center of a palindrome,
11+
// expanding outwards to check if the characters match.
12+
//
13+
// Note that for each character, we have to do two checks:
14+
//
15+
// 1. Check if there's a palindrome centered at i, for odd length palindromes.
16+
// 2. Check if there's a palindrome centered at i and i + 1, for even length palindromes.
17+
//
18+
// There is a dynamic programming solution that can solve this problem in O(n^2) time and O(n^2) space.
19+
//
20+
// There is a more sophisticated algorithm called Manacher's algorithm that can solve this problem in O(n) time, but
21+
// it's pretty complicated.
22+
//
23+
// COMPLEXITY:
24+
//
25+
// Each checkPalindrome() call is O(n) where n is the length of the string. We call this function twice for each
26+
// character in the string, so the total time complexity is O(n^2).
27+
//
28+
// The space complexity is O(1) because we only use a constant amount of space.
29+
function longestPalindrome(s: string): string {
30+
let start = 0;
31+
let maxLength = 0;
32+
33+
function checkPalindrome(left: number, right: number) {
34+
let matched = false;
35+
36+
// Expand the left and right pointers outwards from the center.
37+
while (left >= 0 && right < s.length) {
38+
// If the characters match, expand the pointers.
39+
if (s[left] === s[right]) {
40+
left--;
41+
right++;
42+
matched = true;
43+
}
44+
// Otherwise, break out of the loop.
45+
else {
46+
break;
47+
}
48+
}
49+
50+
// If we didn't match any characters, then there's no need to update the length of the longest palindrome.
51+
if (!matched) {
52+
return;
53+
}
54+
55+
// When the while loop ends, s[left] !== s[right], which means they don't match. Adjust them back to a valid
56+
// position. If they didn't match, then there's no need to
57+
left++;
58+
right--;
59+
60+
// The length of the palindrome we have is bounded by [left, right] inclusive. To get the length of this range,
61+
// we need to do right - left + 1.
62+
const length = right - left + 1;
63+
if (right - left + 1 > maxLength) {
64+
start = left;
65+
maxLength = length;
66+
}
67+
}
68+
69+
if (s.length === 0) {
70+
return '';
71+
}
72+
73+
for (let i = 0; i < s.length; i++) {
74+
// Check if there's a palindrome centered at i.
75+
checkPalindrome(i, i);
76+
// Check if there's a palindrome centered at i and i + 1.
77+
checkPalindrome(i, i + 1); // Even-length palindrome
78+
}
79+
80+
return s.substring(start, start + maxLength);
81+
}

test/array/maximum-swap.test.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { maximumSwap } from '../../src/array/maximum-swap';
2+
3+
describe('maximum swap', () => {
4+
test('maximumSwap - test case 1', () => {
5+
expect(maximumSwap(2736)).toEqual(7236);
6+
});
7+
8+
test('maximumSwap - test case 2', () => {
9+
expect(maximumSwap(1993)).toEqual(9913);
10+
});
11+
12+
test('maximumSwap - test case 1', () => {
13+
expect(maximumSwap(98368)).toEqual(98863);
14+
});
15+
});
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { checkSubarraySum } from '../../src/prefix-sum/continuous-subarray-sum';
2+
3+
describe('continuous subarray sum', () => {
4+
test('checkSubarraySum - test case 1', () => {
5+
expect(checkSubarraySum([23, 2, 4, 6, 7], 6)).toBe(true);
6+
});
7+
8+
test('checkSubarraySum - test case 2', () => {
9+
expect(checkSubarraySum([1, 1], 2)).toBe(true);
10+
});
11+
12+
test('checkSubarraySum - test case 3', () => {
13+
expect(checkSubarraySum([-10, 10], 1)).toBe(true);
14+
});
15+
});
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { longestPalindrome } from '../../src/string/longest-palindromic-substring';
2+
3+
describe('longest palindromic substring', () => {
4+
test('longestPalindrome - test case 1', () => {
5+
expect(longestPalindrome('babad')).toEqual('bab');
6+
});
7+
});

0 commit comments

Comments
 (0)