Leetcode 2818. Apply Operations to Maximize Score
Table of Contents
Problem Informations
- Problem Index: 2818
- Problem Link: Apply Operations to Maximize Score
- Topics: Array, Math, Stack, Greedy, Sorting, Monotonic Stack, Number Theory
Problem Description
You are given an array nums
of n
positive integers and an integer k
.
Initially, you start with a score of 1
. You have to maximize your score by applying the following operation at most k
times:
- Choose any non-empty subarray
nums[l, ..., r]
that you haven’t chosen previously. - Choose an element
x
ofnums[l, ..., r]
with the highest prime score. If multiple such elements exist, choose the one with the smallest index. - Multiply your score by
x
.
Here, nums[l, ..., r]
denotes the subarray of nums
starting at index l
and ending at the index r
, both ends being inclusive.
The prime score of an integer x
is equal to the number of distinct prime factors of x
. For example, the prime score of 300
is 3
since $300 = 2 \times 2 \times 3 \times 5 \times 5$.
Return the maximum possible score after applying at most k
operations.
Since the answer may be large, return it modulo $10^9 + 7$.
Example 1:
Input: nums = [8,3,9,3,8], k = 2
Output: 81
Explanation: To get a score of 81, we can apply the following operations:
- Choose subarray nums[2, …, 2]. nums[2] is the only element in this subarray. Hence, we multiply the score by nums[2]. The score becomes $1 \times 9 = 9$.
- Choose subarray nums[2, …, 3]. Both nums[2] and nums[3] have a prime score of 1, but nums[2] has the smaller index. Hence, we multiply the score by nums[2]. The score becomes $9 \times 9 = 81$.
It can be proven that 81 is the highest score one can obtain.
Example 2:
Input: nums = [19,12,14,6,10,18], k = 3
Output: 4788
Explanation: To get a score of 4788, we can apply the following operations:
- Choose subarray nums[0, …, 0]. nums[0] is the only element in this subarray. Hence, we multiply the score by nums[0]. The score becomes $1 \times 19 = 19$.
- Choose subarray nums[5, …, 5]. nums[5] is the only element in this subarray. Hence, we multiply the score by nums[5]. The score becomes $19 \times 18 = 342$.
- Choose subarray nums[2, …, 3]. Both nums[2] and nums[3] have a prime score of 2, but nums[2] has the smaller index. Hence, we multiply the score by nums[2]. The score becomes $342 \times 14 = 4788$.
It can be proven that 4788 is the highest score one can obtain.
Constraints:
- $1 \leq \text{nums.length} = n \leq 10^5$
- $1 \leq \text{nums}[i] \leq 10^5$
- $1 \leq k \leq \min(n \times (n + 1) / 2, 10^9)$
Intuition
The problem requires maximizing a score by selecting subarrays and the element within those subarrays that has the highest prime score. The prime score is determined by counting distinct prime factors of a number. The main challenge is to choose subarrays and elements strategically to maximize the resulting score after at most k
operations. Each operation should aim to increase the score as much as possible, leveraging the numbers within nums
that have higher prime scores and can be chosen within the allowed constraints.
Approach
The solution employs a combination of mathematical techniques and strategic data structures to efficiently find the desired result. Here’s a step-by-step explanation of the approach:
Prime Factorization and Score Calculation:
For each number in the arraynums
, we determine its prime score, which is the count of distinct prime factors. This is achieved by factoring each number using Pollard’s Rho algorithm, which efficiently finds one non-trivial factor of a composite number. This factorization is repeated recursively until all factors are found. The distinct factors are then counted.Monotonic Stack for Segment Analysis:
Two types of segment lengths are calculated for each indexi
innums
:- Max Segment Length Ending at
i
: Using a monotonic decreasing stack, we determine the maximum length of subarrays ending at each index where the current element has the highest prime score compared to subsequent elements. - Max Segment Length Starting at
i
: Similarly, using a monotonic increasing stack, we calculate the maximum length of subarrays starting at each index where the current element has a higher prime score compared to preceding elements.
- Max Segment Length Ending at
Combining Segment Information:
Each positioni
innums
will have maximum segment length determined by multiplying the two segment lengths obtained from the above steps (one ending and one starting). This product captures the size of the segment where elementnums[i]
could potentially be the maximum prime score factor.Sorting and Selecting for Maximum Score:
Construct a list of(value, index)
pairs fromnums
, sorted by value in descending order. The rationale here is to prioritize larger numbers, as they will yield higher scores if they have the same prime score product opportunity.Compute Maximum Score with Modulo Consideration:
Iterate through the sorted list, using each number the maximum number of times possible as dictated by the segment information while ensuring the total operations do not exceedk
. Use modular multiplication and exponentiation to handle potential overflow and ensure results are within specified constraints (modulo $10^9 + 7$). Update the answer by multiplying with the currently considered number raised to the power of its allowable segment length.Output the Result:
Return the computed score after considering as many operations as allowed, which maximizes the multiplication of selected elements based on their strategic placement and contribution to the score.
By organizing the problem into clear steps dealing with prime factorization, segment analysis, and strategic selection while considering numerical constraints and mathematical efficiency, the approach efficiently tackles the problem complexity and constraints.
Code
C++
typedef unsigned long long ull;
class Solution {
public:
// Perform modular multiplication (using __int128 to avoid overflow)
ull mod_mul(ull a, ull b, ull mod) {
__uint128_t res = a;
res *= b;
return (ull)(res % mod);
}
// Perform modular exponentiation
ull mod_pow(ull base, ull exp, ull mod) {
ull result = 1;
base %= mod;
while (exp > 0) {
if (exp & 1) result = mod_mul(result, base, mod);
base = mod_mul(base, base, mod);
exp >>= 1;
}
return result;
}
// Miller-Rabin primality test (suitable for 64-bit numbers)
bool is_prime(ull n) {
if (n < 2) return false;
static int testPrimes[12] = {2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37};
for (int p : testPrimes) {
if (n == (ull)p) return true;
if (n % p == 0) return false;
}
ull d = n - 1;
int s = 0;
while ((d & 1ULL) == 0) {
d >>= 1;
s++;
}
int bases[7] = {2, 3, 5, 7, 11, 13, 17};
for (int a : bases) {
if ((ull)a % n == 0) continue;
ull x = mod_pow(a, d, n);
if (x == 1 || x == n - 1) continue;
bool composite = true;
for (int i = 1; i < s; i++) {
x = mod_mul(x, x, n);
if (x == n - 1) {
composite = false;
break;
}
}
if (composite) return false;
}
return true;
}
// Pollard Rho algorithm for factorization
ull pollard_rho(ull n) {
if (n % 2ULL == 0) return 2;
ull x = rand() % (n - 2) + 2;
ull y = x;
ull c = rand() % (n - 1) + 1;
ull d = 1;
while (d == 1) {
x = (mod_mul(x, x, n) + c) % n;
y = (mod_mul(y, y, n) + c) % n;
y = (mod_mul(y, y, n) + c) % n;
d = __gcd((ull)abs((long long)x - (long long)y), n);
if (d == n) return pollard_rho(n);
}
return d;
}
// Recursive prime factorization, storing all prime factors (possibly repeated) in factors
void factorize(ull n, vector<ull>& factors) {
if (n == 1) return;
if (is_prime(n)) {
factors.push_back(n);
return;
}
ull factor = pollard_rho(n);
factorize(factor, factors);
factorize(n / factor, factors);
}
// Calculate the prime score (number of distinct prime factors)
int prime_score(ull n) {
vector<ull> factors;
factorize(n, factors);
set<ull> distinct(factors.begin(), factors.end()); // Remove duplicates using a set
return distinct.size();
}
// Function to compute minimum of two numbers
long long min(long long a, long long b) { return a < b ? a : b; }
// Function to compute the maximum possible score
int maximumScore(vector<int>& nums, int k) {
int n = nums.size();
long long ans = 1;
vector<long long> primeScores(n), maxSegmentLen(n);
// Compute prime scores for each number in nums
for (int i = 0; i < n; i++) primeScores[i] = prime_score(nums[i]);
stack<long long> monotonicStack;
maxSegmentLen[n - 1] = 1;
monotonicStack.push(n - 1);
// Determine maximum lengths of segments ending at each index
for (int i = n - 2; i >= 0; i--) {
while (!monotonicStack.empty() && primeScores[monotonicStack.top()] <= primeScores[i])
monotonicStack.pop();
maxSegmentLen[i] = monotonicStack.empty() ? n - i : monotonicStack.top() - i;
monotonicStack.push(i);
}
// Clear the stack for reuse
while (!monotonicStack.empty()) monotonicStack.pop();
monotonicStack.push(0);
// Determine maximum lengths of segments starting at each index
for (int i = 1; i < n; i++) {
while (!monotonicStack.empty() && primeScores[monotonicStack.top()] < primeScores[i])
monotonicStack.pop();
maxSegmentLen[i] *= (monotonicStack.empty() ? (i + 1) : (i - monotonicStack.top()));
monotonicStack.push(i);
}
vector<pair<long long, long long>> sortedNums;
for (int i = 0; i < n; i++) sortedNums.push_back({-nums[i], i});
// Sort numbers (stored as negative values for descending order sorting of scores)
sort(sortedNums.begin(), sortedNums.end());
for (int i = 0; i < n; i++) {
int count = min(maxSegmentLen[sortedNums[i].second], static_cast<long long>(k));
ans = mod_mul(ans, mod_pow(-sortedNums[i].first, count, 1000000007ULL), 1000000007ULL);
if (k <= maxSegmentLen[sortedNums[i].second]) break;
k -= count;
}
return ans;
}
};
Python
class Solution:
from math import gcd
from random import randrange
# Perform modular multiplication
def mod_mul(self, a, b, mod):
return (a * b) % mod
# Perform modular exponentiation
def mod_pow(self, base, exp, mod):
result = 1
base %= mod
while exp > 0:
if exp & 1:
result = self.mod_mul(result, base, mod)
base = self.mod_mul(base, base, mod)
exp >>= 1
return result
# Miller-Rabin primality test
def is_prime(self, n):
if n < 2:
return False
testPrimes = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37]
for p in testPrimes:
if n == p:
return True
if n % p == 0:
return False
d = n - 1
s = 0
while (d & 1) == 0:
d >>= 1
s += 1
bases = [2, 3, 5, 7, 11, 13, 17]
for a in bases:
if a % n == 0:
continue
x = self.mod_pow(a, d, n)
if x == 1 or x == n - 1:
continue
composite = True
for _ in range(1, s):
x = self.mod_mul(x, x, n)
if x == n - 1:
composite = False
break
if composite:
return False
return True
# Pollard Rho algorithm for factorization
def pollard_rho(self, n):
if n % 2 == 0:
return 2
x = self.randrange(2, n - 2)
y = x
c = self.randrange(1, n - 1)
d = 1
while d == 1:
x = (self.mod_mul(x, x, n) + c) % n
y = (self.mod_mul(y, y, n) + c) % n
y = (self.mod_mul(y, y, n) + c) % n
d = gcd(abs(x - y), n)
if d == n:
return self.pollard_rho(n)
return d
# Recursive prime factorization
def factorize(self, n, factors):
if n == 1:
return
if self.is_prime(n):
factors.append(n)
return
factor = self.pollard_rho(n)
self.factorize(factor, factors)
self.factorize(n // factor, factors)
# Calculate the prime score
def prime_score(self, n):
factors = []
self.factorize(n, factors)
return len(set(factors)) # Remove duplicates using a set
# Function to compute the maximum possible score
def maximumScore(self, nums, k):
n = len(nums)
ans = 1
primeScores = [0] * n
maxSegmentLen = [0] * n
# Compute prime scores for each number in nums
for i in range(n):
primeScores[i] = self.prime_score(nums[i])
monotonicStack = []
maxSegmentLen[n - 1] = 1
monotonicStack.append(n - 1)
# Determine maximum lengths of segments ending at each index
for i in range(n - 2, -1, -1):
while monotonicStack and primeScores[monotonicStack[-1]] <= primeScores[i]:
monotonicStack.pop()
maxSegmentLen[i] = n - i if not monotonicStack else monotonicStack[-1] - i
monotonicStack.append(i)
monotonicStack.clear()
monotonicStack.append(0)
# Determine maximum lengths of segments starting at each index
for i in range(1, n):
while monotonicStack and primeScores[monotonicStack[-1]] < primeScores[i]:
monotonicStack.pop()
maxSegmentLen[i] *= i + 1 if not monotonicStack else i - monotonicStack[-1]
monotonicStack.append(i)
sortedNums = []
for i in range(n):
sortedNums.append((-nums[i], i))
sortedNums.sort()
for i in range(n):
count = min(maxSegmentLen[sortedNums[i][1]], k)
ans = self.mod_mul(ans, self.mod_pow(-sortedNums[i][0], count, 1000000007), 1000000007)
if k <= maxSegmentLen[sortedNums[i][1]]:
break
k -= count
return ans
Complexity
Time complexity: The overall time complexity is $O(n \log(\text{max\_val}) + n \log n)$.
Explanation:
- Prime Score Calculation: For each number in
nums
, we calculate the prime score by decomposing the number using prime factorization. The worst-case time complexity of prime factorization using Pollard’s Rho algorithm is approximately $O(\log^3 n)$ for each number. Since we are calculating this for all numbers in the arraynums
, this part contributes $O(n \log(\text{max\_val}))$, wheremax_val
is the maximum possible value innums
. - Monotonic Stack Operations: The operations related to computing the maximum segment lengths using a monotonic stack are linear with respect to the number of elements in
nums
, i.e., $O(n)$ for both segment lengths ending and starting calculations. - Sorting: The sorted list of numbers is handled through a simple sort, which contributes $O(n \log n)$.
- Final Computation: The final score computation involves iterating over elements in
sortedNums
, which is a linear operation $O(n)$ in the worst-case scenario, assumingk
can be larger than any individual segment length.
Combined, the time complexity is dominated by the prime score calculations and sorting, thus leading to $O(n \log(\text{max\_val}) + n \log n)$.
- Prime Score Calculation: For each number in
Space complexity: The overall space complexity is $O(n)$.
Explanation:
- Prime Score Calculation: Temporary storage for factors, which is not more than $O(\log n)$ for each number principle-wise for storage, but since this is done for each element in the array at a time, it can be considered constant space with respect to
n
. - Data Structures: The use of stacks and arrays like
primeScores
,maxSegmentLen
, andsortedNums
each require linear space with respect to the number of elements innums
, i.e., $O(n)$. - Overall, since linear-sized data structures dominate space usage, the space complexity remains $O(n)$.
- Prime Score Calculation: Temporary storage for factors, which is not more than $O(\log n)$ for each number principle-wise for storage, but since this is done for each element in the array at a time, it can be considered constant space with respect to
Disclaimer: All reference materials on this website are sourced from the internet and are intended for learning purposes only. If you believe any content infringes upon your rights, please contact me at csnote.cc@gmail.com, and I will remove the relevant content promptly.
Feedback Welcome: If you notice any errors or areas for improvement in the articles, I warmly welcome your feedback and corrections. Your input will help this blog provide better learning resources. This is an ongoing process of learning and improvement, and your suggestions are valuable to me. You can reach me at csnote.cc@gmail.com.