2961. Double Modular Exponentiation

MediumArrayMathSimulation
Leetcode Link

Problem Description

In this problem, you are given a two-dimensional array named variables where each element is a list of four integers [a_i, b_i, c_i, m_i]. In addition, you are provided with an integer target. An index i in the variables array is considered good if it satisfies a certain mathematical condition based on the values of a_i, b_i, c_i, and m_i. The condition is that the expression ((a_i^b_i % 10)^c_i) % m_i must be equal to the target. Here the ^ represents exponentiation, and % is the modulo operation.

The task is to identify all the indices that are good and return them as an array in any order. An index is good if the above formula matches the target when evaluated with the values at that index in the variables array. You need to consider each index i from 0 to variables.length - 1 and check the formula for each.

Intuition

The intuition behind the solution lies in understanding how modulo arithmetic can be used to simplify calculations and the properties of exponents. Since the final expression involves taking a_i to the power of b_i, then to the power of c_i, and applying modulo m_i to check if it equals the target, we can take advantage of the following properties:

  1. Exponentiation by Squaring (Fast Power Method): This method allows us to compute a^b efficiently by squaring and reducing the number of multiplications we need to perform. This is especially useful when b is very large.

  2. Modulo Properties: We can use the property (a * b) % m = ((a % m) * (b % m)) % m, which states that the result of a product modulo m is the same regardless of whether we perform the modulo before or after multiplication. We can also apply this principle to exponentiation.

By applying these optimizations, we can effectively reduce the calculations needed to verify whether an index i satisfies the condition. Hence, the process for each index i is to apply the modulo operation as soon as possible during the exponentiation steps to keep the numbers manageable and prevent overflow. This is done by calculating a^b % 10 and then ((a^b % 10)^c) % m_i.

The solution applies the Python pow function, which takes an additional modulo argument, making these operations straightforward and efficient. By doing so, we loop through each variables[i] and apply the property (a^b % 10)^c % m_i and compare it to the target. If it matches, we add the index i to our output list. This approach is both clean and efficient, allowing us to handle even large powers effectively.

Learn more about Math patterns.

Not Sure What to Study? Take the 2-min Quiz to Find Your Missing Piece:

What's the output of running the following function using input 56?

1KEYBOARD = {
2    '2': 'abc',
3    '3': 'def',
4    '4': 'ghi',
5    '5': 'jkl',
6    '6': 'mno',
7    '7': 'pqrs',
8    '8': 'tuv',
9    '9': 'wxyz',
10}
11
12def letter_combinations_of_phone_number(digits):
13    def dfs(path, res):
14        if len(path) == len(digits):
15            res.append(''.join(path))
16            return
17
18        next_number = digits[len(path)]
19        for letter in KEYBOARD[next_number]:
20            path.append(letter)
21            dfs(path, res)
22            path.pop()
23
24    res = []
25    dfs([], res)
26    return res
27
1private static final Map<Character, char[]> KEYBOARD = Map.of(
2    '2', "abc".toCharArray(),
3    '3', "def".toCharArray(),
4    '4', "ghi".toCharArray(),
5    '5', "jkl".toCharArray(),
6    '6', "mno".toCharArray(),
7    '7', "pqrs".toCharArray(),
8    '8', "tuv".toCharArray(),
9    '9', "wxyz".toCharArray()
10);
11
12public static List<String> letterCombinationsOfPhoneNumber(String digits) {
13    List<String> res = new ArrayList<>();
14    dfs(new StringBuilder(), res, digits.toCharArray());
15    return res;
16}
17
18private static void dfs(StringBuilder path, List<String> res, char[] digits) {
19    if (path.length() == digits.length) {
20        res.add(path.toString());
21        return;
22    }
23    char next_digit = digits[path.length()];
24    for (char letter : KEYBOARD.get(next_digit)) {
25        path.append(letter);
26        dfs(path, res, digits);
27        path.deleteCharAt(path.length() - 1);
28    }
29}
30
1const KEYBOARD = {
2    '2': 'abc',
3    '3': 'def',
4    '4': 'ghi',
5    '5': 'jkl',
6    '6': 'mno',
7    '7': 'pqrs',
8    '8': 'tuv',
9    '9': 'wxyz',
10}
11
12function letter_combinations_of_phone_number(digits) {
13    let res = [];
14    dfs(digits, [], res);
15    return res;
16}
17
18function dfs(digits, path, res) {
19    if (path.length === digits.length) {
20        res.push(path.join(''));
21        return;
22    }
23    let next_number = digits.charAt(path.length);
24    for (let letter of KEYBOARD[next_number]) {
25        path.push(letter);
26        dfs(digits, path, res);
27        path.pop();
28    }
29}
30

Solution Approach

The implementation of the solution uses a relatively simple approach aligned with the properties of modulo arithmetic and exponentiation. Here's how it works:

  1. Iteration: We iterate through each index i of the input variables array using a for-loop.

  2. Exponentiation: For every tuple (a, b, c, m) at index i, we need to calculate the expression ((a^b % 10)^c) % m and check if it equals the target.

  3. Exponentiation with Modulo: The Python pow function is perfect for this task because it allows us to efficiently calculate powers with a modulo. The regular power operation a^b may lead to very large numbers, especially when b and c are large, which is computationally expensive and can cause integer overflows. However, by using the pow function with a modulo, we can calculate a^b % 10 much more quickly and safely.

    • The first power operation is pow(a, b, 10) which computes a^b % 10. We modulo by 10 because exponentiation modulo 10 gives us the last digit of a^b, which is all we need for the next power.
    • The resulting number then needs to be exponentiated again with c and taken modulo m. This is continued with another pow operation - pow(result, c, m) where result is a^b % 10.
  4. Check Against Target: The final result of the nested pow function is compared to the target. If they match, the index i is considered good.

  5. Building the Output List: The indices where the condition holds true are gathered into a list using a list comprehension. This list is returned as the final output.

The process uses a direct simulation of the problem's formula. The fast power method (also known as exponentiation by squaring) is implicitly applied through Python's built-in pow function with mod argument, as this is a known efficient way to compute powers in modular arithmetic.

Here's a breakdown of the algorithm based on the solution code:

1class Solution:
2    def getGoodIndices(self, variables: List[List[int]], target: int) -> List[int]:
3        return [
4            i
5            for i, (a, b, c, m) in enumerate(variables)
6            if pow(pow(a, b, 10), c, m) == target
7        ]

In the provided code, enumerate(variables) gives us both the index i and the list [a, b, c, m]. For each index and list, if pow(pow(a, b, 10), c, m) == target evaluates the condition. If the condition is true, the i is included in the output list. This is an elegant and effective way to solve the problem with minimal code and high readability.

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

A person thinks of a number between 1 and 1000. You may ask any number questions to them, provided that the question can be answered with either "yes" or "no".

What is the minimum number of questions you needed to ask so that you are guaranteed to know the number that the person is thinking?

Example Walkthrough

Let's work through a small example to illustrate the solution approach.

Assume we have the following variables array and the target value:

1variables = [
2    [2, 3, 1, 4],  # Index 0
3    [3, 2, 2, 3],  # Index 1
4    [2, 5, 2, 1],  # Index 2
5]
6
7target = 1

We need to find all indices where the condition ((a_i^b_i % 10)^c_i) % m_i == target holds true.

Step 1: We start by iterating over each index in the variables array.

For index 0: The tuple is [2, 3, 1, 4], representing a=2, b=3, c=1, m=4.

  • We calculate 2^3 % 10, which is 8 % 10 giving us 8.
  • Next, we calculate (8^1) % 4, which is simply 8 % 4 giving us 0.
  • The result is 0, which does not match the target of 1. Hence, index 0 is not good.

For index 1: The tuple is [3, 2, 2, 3], representing a=3, b=2, c=2, m=3.

  • We calculate 3^2 % 10, which is 9 % 10 giving us 9.
  • Next, we calculate (9^2) % 3, which is 81 % 3 giving us 0.
  • The result is 0, which does not match the target of 1. Hence, index 1 is not good.

For index 2: The tuple is [2, 5, 2, 1], representing a=2, b=5, c=2, m=1.

  • We calculate 2^5 % 10, which is 32 % 10 giving us 2.
  • Next, we calculate (2^2) % 1, which is 4 % 1 giving us 0.
  • However, since any number modulo 1 is 0, this does not match our target of 1. Hence, index 2 is not good.

At the end of the iteration, unfortunately, no indices satisfy the condition for our small example, and therefore the resulting array of good indices would be empty - [].

The solution code using this approach would be:

1class Solution:
2    def getGoodIndices(self, variables: List[List[int]], target: int) -> List[int]:
3        return [
4            i
5            for i, (a, b, c, m) in enumerate(variables)
6            if pow(pow(a, b, 10), c, m) == target
7        ]

The code works efficiently through the properties of exponents and modulo arithmetic, using Python's pow function to simplify the computations and avoid overflow, while checking each index against the target.

Solution Implementation

1from typing import List
2
3class Solution:
4    def get_good_indices(self, variables: List[List[int]], target: int) -> List[int]:
5        # Iterate over the list enumerating the variables
6        # where 'i' is the index and 'variable' is a list [a, b, c, m]
7        return [
8            i
9            for i, variable in enumerate(variables)
10            # Check if the complex power expression matches the target
11            # a raised to the power of b mod 10, raised to the power of c mod m should equal target
12            if pow(pow(variable[0], variable[1], 10), variable[2], variable[3]) == target
13        ]
14
15# Example usage:
16# solution = Solution()
17# result = solution.get_good_indices([[2, 3, 1, 10], [3, 3, 2, 12]], 8)
18# print(result) # Output: list of indices that match the criteria
19
1class Solution {
2    // Method to find all the indices in 'variables' array where the recursive power operation result equals 'target'.
3    public List<Integer> getGoodIndices(int[][] variables, int target) {
4        List<Integer> goodIndices = new ArrayList<>(); // List to hold the indices that meet the criteria.
5        // Iterate over each variable array.
6        for (int i = 0; i < variables.length; ++i) {
7            int[] variableSet = variables[i];
8            // Extract individual variables from the current set.
9            int a = variableSet[0];
10            int b = variableSet[1];
11            int c = variableSet[2];
12            int m = variableSet[3];
13            // If the recursive power operation result equals 'target', add the index to the list.
14            if (quickPow(quickPow(a, b, 10), c, m) == target) {
15                goodIndices.add(i);
16            }
17        }
18        return goodIndices; // Return the list of indices.
19    }
20
21    // Method to compute (a^b) % mod quickly using binary exponentiation.
22    private int quickPow(long a, int n, int mod) {
23        long result = 1; // Initialize result to 1.
24        // Loop until n is zero.
25        for (; n > 0; n >>= 1) {
26            // If the current bit in n is set, multiply result by 'a' and take mod.
27            if ((n & 1) == 1) {
28                result = (result * a) % mod;
29            }
30            // Square 'a' and take mod to use for the next iteration.
31            a = (a * a) % mod;
32        }
33        return (int) result; // Cast the long result back to int and return.
34    }
35}
36
1#include <vector>
2
3class Solution {
4public:
5    // Function to find all the indices of variables that meet a certain condition.
6    std::vector<int> getGoodIndices(std::vector<std::vector<int>>& variables, int target) {
7        std::vector<int> goodIndices; // This will store the indices that meet the condition
8
9        // Lambda function for fast exponentiation under modulo (to avoid integer overflow)
10        auto fastPowerModulo = [&](long long base, int exponent, int modulo) -> int {
11            long long result = 1; // Start with a result of 1
12
13            // Continuously square base and multiply by base when the exponent's current bit is 1
14            while (exponent > 0) {
15                if (exponent & 1) { // Check if the current bit is 1
16                    result = (result * base) % modulo; // Multiply by base and take modulo
17                }
18                base = (base * base) % modulo; // Square the base and take modulo
19                exponent >>= 1; // Shift exponent right by 1 bit (divide by 2)
20            }
21
22            return static_cast<int>(result); // Cast result to int before returning
23        };
24
25        // Iterate over all the variable sets
26        for (int i = 0; i < variables.size(); ++i) {
27            auto& variableSet = variables[i]; // Reference for better performance and readability
28            int a = variableSet[0]; // Base part of the power
29            int b = variableSet[1]; // Exponent part
30            int c = variableSet[2]; // Exponent for the result of a^b
31            int modulo = variableSet[3]; // The modulo value
32
33            // Apply the fast power modulo function twice as specified by the problem
34            // and compare with the target value
35            if (fastPowerModulo(fastPowerModulo(a, b, 10), c, modulo) == target) {
36                goodIndices.push_back(i); // If condition is met, add index to the list
37            }
38        }
39
40        return goodIndices; // Return the list of good indices
41    }
42};
43
1// Function to perform quick exponentiation by squaring, modulo a given modulus 'mod'.
2const quickPowerMod = (base: number, exponent: number, modulus: number): number => {
3  let answer = 1;
4  while(exponent > 0) {
5    if(exponent & 1) {
6      answer = Number((BigInt(answer) * BigInt(base)) % BigInt(modulus));
7    }
8    base = Number((BigInt(base) * BigInt(base)) % BigInt(modulus));
9    exponent >>= 1;
10  }
11  return answer;
12};
13
14// Function to find and return the indices of the 'variables' array where the double exponentiation
15// result matches the target after applying the modulus 'm' in each inner calculation.
16function getGoodIndices(variables: number[][], target: number): number[] {
17  const resultIndices: number[] = [];
18
19  for(let index = 0; index < variables.length; ++index) {
20    // Destructuring each 'variables' element into separate constants for clarity.
21    const [base, firstExponent, secondExponent, modulus] = variables[index];
22  
23    // Compute the nested power-mod operation: ((a^b)^c) % m.
24    const doubleExponentiationResult = quickPowerMod(
25      quickPowerMod(base, firstExponent, 10),
26      secondExponent,
27      modulus
28    );
29
30    // If the result matches the target, add the current index to the 'resultIndices' array.
31    if(doubleExponentiationResult === target) {
32      resultIndices.push(index);
33    }
34  }
35
36  // Return the array with the good indices.
37  return resultIndices;
38}
39
Not Sure What to Study? Take the 2-min Quiz:

Which algorithm is best for finding the shortest distance between two points in an unweighted graph?

Time and Space Complexity

The given Python code in the Solution class contains a method called getGoodIndices which computes good indices based on the power operation condition.

The time complexity of this code is primarily dependent on the enumerate function that iterates through the variables list and the pow function used inside the list comprehension. The pow function in Python has a time complexity of O(log n), where n is the exponent.

Here, the pow function is used twice; the first call executes pow(a, b, 10), and the second call executes pow(result_of_first_call, c, m). Since b and c are the exponents used in each call, the time complexity for each iteration is controlled by these values.

Considering M as the maximum value among all values of b_i and c_i, and with M being limited to 10^3 in the problem, the time complexity per iteration is O(log M). Since there are n elements in variables, where n is the length of variables, the total time complexity is O(n * log M).

The space complexity of the code is O(1) because the extra space required is constant and does not depend on the input size. There are no additional data structures that grow with the size of variables. The list comprehension simply produces the final list of indices, which the function returns and does not count towards extra space as it is the required output.

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

Fast Track Your Learning with Our Quick Skills Quiz:

Which of the following problems can be solved with backtracking (select multiple)


Recommended Readings


Got a question? Ask the Teaching Assistant anything you don't understand.

Still not clear? Ask in the Forum,  Discord or Submit the part you don't understand to our editors.


TA 👨‍🏫