Skip to content

Feature/sherlock and anagrams #410

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Jul 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# [Sherlock and Anagrams](https://www.hackerrank.com/challenges/sherlock-and-anagrams)

- Difficulty: `#medium`
- Category: `#ProblemSolvingMedium` `#DictionariesAndHashmaps` `#Strings`

## About solution

To answer the question of "how many pairs" of words can be anagrammed
using fragments from adjacent letters of an initial word, two steps are needed:

1) Obtain all possible fragment candidates to be anagrams,
from each of the possible fragments that can be generated
from adjacent letters of a word.

2) For each list of candidate anagrams,
calculate all possible permutations and add them up.
The total gives the answer.

The second part of this problem can be solved with the binomial coefficient formula:

<https://en.wikipedia.org/wiki/Binomial_coefficient>

But the entire cost of this formula falls on the "factorial" function.

In javascript, the factorial quickly reaches results that return large numbers,
in scientific notation, losing precision.
This loss of precision can result in an erroneous result
in the final calculation of permutations.

To avoid this problem, it is necessary to introduce large number handling using BigInt.
Original file line number Diff line number Diff line change
Expand Up @@ -2,51 +2,14 @@ import { describe, expect, it } from '@jest/globals';
import { logger as console } from '../../../logger';

import { sherlockAndAnagrams } from './sherlock_and_anagrams';

const TEST_CASES = [
{
title: 'Sample Test Case 0',
tests: [
{
input: 'abba',
expected: 4
},
{
input: 'abcd',
expected: 0
}
]
},
{
title: 'Sample Test Case 1',
tests: [
{
input: 'ifailuhkqq',
expected: 3
},
{
input: 'kkkk',
expected: 10
}
]
},
{
title: 'Sample Test Case 1',
tests: [
{
input: 'cdcd',
expected: 5
}
]
}
];
import TEST_CASES from './sherlock_and_anagrams.testcases.json';

describe('sherlock_and_anagrams', () => {
it('sherlockAndAnagrams test cases', () => {
expect.assertions(5);
expect.assertions(15);

TEST_CASES.forEach((testSet) => {
testSet.tests.forEach((test) => {
testSet?.tests.forEach((test) => {
const answer = sherlockAndAnagrams(test.input);

console.debug(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
[
{
"title": "Sample Test Case 0",
"tests": [
{
"input": "abba",
"expected": 4
},
{
"input": "abcd",
"expected": 0
}
]
},
{
"title": "Sample Test Case 1",
"tests": [
{
"input": "ifailuhkqq",
"expected": 3
},
{
"input": "kkkk",
"expected": 10
}
]
},
{
"title": "Sample Test Case 1",
"tests": [
{
"input": "cdcd",
"expected": 5
}
]
},
{
"title": "Test case 3",
"tests": [
{
"input":
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"expected": 166650
},
{
"input":
"bbcaadacaacbdddcdbddaddabcccdaaadcadcbddadababdaaabcccdcdaacadcababbabbdbacabbdcbbbbbddacdbbcdddbaaa",
"expected": 4832
},
{
"input":
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"expected": 166650
},
{
"input":
"cacccbbcaaccbaacbbbcaaaababcacbbababbaacabccccaaaacbcababcbaaaaaacbacbccabcabbaaacabccbabccabbabcbba",
"expected": 13022
},
{
"input":
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"expected": 166650
},
{
"input":
"bbcbacaabacacaaacbbcaabccacbaaaabbcaaaaaaaccaccabcacabbbbabbbbacaaccbabbccccaacccccabcabaacaabbcbaca",
"expected": 9644
},
{
"input":
"cbaacdbaadbabbdbbaabddbdabbbccbdaccdbbdacdcabdbacbcadbbbbacbdabddcaccbbacbcadcdcabaabdbaacdccbbabbbc",
"expected": 6346
},
{
"input":
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"expected": 166650
},
{
"input":
"babacaccaaabaaaaaaaccaaaccaaccabcbbbabccbbabababccaabcccacccaaabaccbccccbaacbcaacbcaaaaaaabacbcbbbcc",
"expected": 8640
},
{
"input":
"bcbabbaccacbacaacbbaccbcbccbaaaabbbcaccaacaccbabcbabccacbaabbaaaabbbcbbbbbaababacacbcaabbcbcbcabbaba",
"expected": 11577
}
]
}
]
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
/**
* @link Problem definition [[docs/hackerrank/interview_preparation_kit/dictionaries_and_hashmaps/sherlock_and_anagrams.md]]
* @see Solution Notes: [[docs/hackerrank/interview_preparation_kit/dictionaries_and_hashmaps/sherlock_and_anagrams-solution-notes.md]]
*/

function factorial(n: number): number {
if (n == 0) {
return 1;
}
return n * factorial(n - 1);
import { logger as console } from '../../../logger';

function extraLongFactorials(n: number): bigint {
const rs = [...Array(n)].reduce((a, b, i) => a * BigInt(i + 1), 1n);
return rs;
}

export function sherlockAndAnagrams(s: string): number {
Expand All @@ -16,6 +17,9 @@ export function sherlockAndAnagrams(s: string): number {
for (let i = 0; i < size; i++) {
for (let j = 0; j < size - i; j++) {
const substr = s.substring(i, size - j);
console.debug(
`i: ${i}, size: ${size}, size - j: ${size - j} | substr: ${substr}`
);

// Add substrings to a candidate list.
// two strings are anagrams if sorted strings are the same.
Expand All @@ -32,7 +36,8 @@ export function sherlockAndAnagrams(s: string): number {
}
}

let count = 0;
let total: bigint = BigInt(0);
let q_candidates = 0;
// Final Anagram list
for (const word of Object.keys(candidates)) {
const quantity_of_anagrams = candidates[word].length;
Expand All @@ -42,15 +47,23 @@ export function sherlockAndAnagrams(s: string): number {
delete candidates[word];
} else {
// Binomial coefficient: https://en.wikipedia.org/wiki/Binomial_coefficient
count += Math.floor(
factorial(quantity_of_anagrams) /
(factorial(k) * factorial(quantity_of_anagrams - k))
);
q_candidates += quantity_of_anagrams;

const count =
extraLongFactorials(quantity_of_anagrams) /
(extraLongFactorials(k) *
extraLongFactorials(quantity_of_anagrams - k));
total += count;

console.debug(`'Partial anagrams of ${word}: ${count}`);
}
}
console.debug(`filtered candidates: ${count}`);
console.debug(
`'sherlockAndAnagrams(${s}) Filtered # candidates: ${q_candidates}`
);
console.debug(`'sherlockAndAnagrams(${s}) # anagrams: ${total}`);

return count;
return Number(total);
}

export default { sherlockAndAnagrams };