Problem Statement

Given an integer array arr[] and an integer k, find a subsequence of exactly k elements whose product is maximum.

Return the maximum product that can be obtained.

Example 1

Input:
arr[] = [1, 2, 0, 3]
k = 2

Output:
6

The subsequence {2, 3} gives:

2 × 3 = 6

Example 2

Input:
arr[] = [1, 2, -1, -3, -6, 4]
k = 4

Output:
144

The subsequence:

{2, -3, -6, 4}

gives:

2 × (-3) × (-6) × 4 = 144

Approach

The important difficulty in this problem is the presence of negative numbers.

When multiplying numbers, a negative value can change the result significantly.

For example:

(-5) × (-4) = 20

So two negative numbers can produce a positive product.

Also:

(-10) × (-2) = 20

Therefore, keeping only the maximum product is not enough.

We need to maintain both:

max[j] = maximum product possible using j elements

min[j] = minimum product possible using j elements

Why Do We Need min[j]?

Suppose:

min[j - 1] = -20
num = -5

Then:

(-20) × (-5) = 100

The minimum product can become the maximum product when multiplied by a negative number.

Therefore, for every subsequence size, we maintain both the maximum and minimum possible products.

Java Solution

class Solution {
    public int maxProduct(int[] arr, int k) {
        int n = arr.length;

        long[] max = new long[k + 1];
        long[] min = new long[k + 1];

        boolean[] possible = new boolean[k + 1];

        max[0] = 1;
        min[0] = 1;
        possible[0] = true;

        for (int num : arr) {

            for (int j = Math.min(k, n); j >= 1; j--) {

                if (possible[j - 1]) {

                    long product1 = max[j - 1] * num;
                    long product2 = min[j - 1] * num;

                    if (!possible[j]) {
                        max[j] = Math.max(product1, product2);
                        min[j] = Math.min(product1, product2);
                        possible[j] = true;
                    } else {
                        max[j] = Math.max(max[j],
                                Math.max(product1, product2));

                        min[j] = Math.min(min[j],
                                Math.min(product1, product2));
                    }
                }
            }
        }

        return (int) max[k];
    }
}

Step-by-Step Explanation

1. Get the Array Size

int n = arr.length;

This stores the number of elements in the array.

For example:

arr = [1, 2, -3, 4]

Then:

n = 4

2. Create the max and min Arrays

long[] max = new long[k + 1];
long[] min = new long[k + 1];

These arrays store the maximum and minimum products for different subsequence sizes.

For example, if:

k = 4

then:

max[0]
max[1]
max[2]
max[3]
max[4]

are available.

They represent:

max[1] → maximum product using 1 element
max[2] → maximum product using 2 elements
max[3] → maximum product using 3 elements
max[4] → maximum product using 4 elements

Similarly:

min[1]
min[2]
min[3]
min[4]

store the minimum products.

3. Why Use long?

long[] max
long[] min

Products can become larger than the normal int range in general versions of this problem.

Using long provides a larger range for intermediate calculations.

At the end:

return (int) max[k];

returns the required int result according to the given problem signature.

4. The possible Array

boolean[] possible = new boolean[k + 1];

This tells us whether it is possible to create a subsequence of a particular size.

Initially:

possible[0] = true;

because selecting zero elements is always possible.

All other values are initially:

false

5. Initialize the Product for Zero Elements

max[0] = 1;
min[0] = 1;

We use 1 because 1 is the multiplicative identity.

For example:

1 × 5 = 5

So when we select the first element:

max[0] * num

becomes:

1 × num = num

6. Traverse Every Array Element

for (int num : arr) {

This processes each element one by one.

For:

arr = [1, 2, -3, 4]

the loop processes:

num = 1
num = 2
num = -3
num = 4

7. Traverse j Backwards

for (int j = Math.min(k, n); j >= 1; j--) {

Here, j represents the number of elements selected.

The loop goes backwards.

This is very important because we are using one-dimensional DP arrays.

If we went forward, the current element could accidentally be used multiple times during the same iteration.

For example:

j = k
j = k - 1
j = k - 2
...
j = 1

Processing the states in reverse ensures that every array element is used at most once.

8. Check Whether j - 1 Elements Are Possible

if (possible[j - 1]) {

To create a subsequence containing j elements, we need to already have a valid subsequence containing j - 1 elements.

For example, to create a 3-element subsequence:

existing 2 elements + current element

Therefore:

possible[j - 1]

must be true.

9. Calculate Both Possible Products

long product1 = max[j - 1] * num;
long product2 = min[j - 1] * num;

This is the most important part of the solution.

We calculate the product using both:

maximum previous product
minimum previous product

Why?

Because the current number can be positive or negative.

If num Is Positive

A larger previous product generally gives a larger result.

If num Is Negative

A smaller negative previous product can produce a larger positive result.

For example:

max[j - 1] = -2
min[j - 1] = -10
num = -5

Then:

-2 × -5 = 10

-10 × -5 = 50

Therefore, the minimum previous product produces the maximum new product.

That is why both max and min are required.

10. First Time Creating a Particular Size

if (!possible[j]) {

If we have never created a subsequence of size j before, we directly initialize it.

max[j] = Math.max(product1, product2);
min[j] = Math.min(product1, product2);
possible[j] = true;

For example:

product1 = -10
product2 = 20

Then:

max[j] = 20
min[j] = -10

11. Update the Existing Maximum

If a subsequence of size j already exists:

max[j] = Math.max(max[j],
        Math.max(product1, product2));

We compare:

old maximum

with:

new maximum

and keep the larger value.

12. Update the Existing Minimum

Similarly:

min[j] = Math.min(min[j],
        Math.min(product1, product2));

We keep the smallest product.

This minimum is important because it can later become the maximum when multiplied by a negative number.

13. Return the Answer

return (int) max[k];

After processing the complete array:

max[k]

contains the maximum product possible using exactly k elements.

Therefore, we return it.

Dry Run

Consider:

arr = [-1, -2, -3, -4, -5]
k = 3

We need exactly 3 elements.

Some possible products are:

(-1) × (-2) × (-3) = -6

(-1) × (-2) × (-4) = -8

(-1) × (-2) × (-5) = -10

(-1) × (-3) × (-4) = -12

(-2) × (-3) × (-4) = -24

The maximum is:

-6

The algorithm correctly keeps track of both positive and negative intermediate products and finally obtains:

max[3] = -6

Therefore:

Output: -6

Why the Previous Greedy Approach Failed

A greedy approach that selects the best pair at every step does not always work.

For example:

arr = [-1, -2, -3, -4, -5]
k = 3

If we only compare pairs, we may select:

(-5) × (-4) = 20

But we still need one more element.

The resulting product could become:

20 × (-1) = -20

However, the correct answer is:

(-1) × (-2) × (-3) = -6

Since:

-6 > -20

the greedy pair selection fails.

The DP approach avoids this problem by considering both maximum and minimum products at every subsequence size.

Handling Zero

The solution also handles zero correctly.

For example:

arr = [-5, 0, 2]
k = 2

Possible products include:

(-5) × 0 = 0
0 × 2 = 0
(-5) × 2 = -10

The maximum is:

0

The DP naturally considers zero as a possible product.

Why Do We Iterate Backwards?

This line is important:

for (int j = Math.min(k, n); j >= 1; j--)

Suppose:

arr = [2]
k = 2

We must not use the same 2 twice.

If we iterate forward:

j = 1
j = 2

the value generated for j = 1 could immediately be used to generate j = 2, effectively using the same array element twice.

By iterating backwards:

j = 2
j = 1

we use the previous state before it can be modified by the current element.

Thus, each array element is selected at most once.

Complexity

The outer loop processes every element:

O(n)

The inner loop processes up to k states:

O(k)

Therefore:

Time Complexity: O(n × k)

The arrays have size k + 1:

Space Complexity: O(k)

Given the constraint:

n <= 30

this solution is efficient enough.

Key Takeaway

The main idea is:

For every subsequence size:
    Keep the maximum product
    Keep the minimum product

When processing a new number:

new maximum = max(max[j - 1] × num,
                  min[j - 1] × num)

new minimum = min(max[j - 1] × num,
                  min[j - 1] × num)

The reason we keep both values is that negative numbers can turn the minimum product into the maximum product.

This makes the solution work correctly for:

The final answer is stored in:

max[k]

and represents the maximum product obtainable from exactly k elements.