Facebook Pixel

3946. Maximum Number of Items From Sale I

Problem Description

You are given a 2D integer array items, where items[i] = [factorᵢ, priceᵢ] represents the i-th item. You are also given an integer budget.

There are unlimited copies of each item available for purchase. You may buy any number of copies of any items, as long as the total cost of the purchased copies does not exceed budget.

After buying items, you may receive free copies based on the following rules:

  • For each item i that you bought at least one copy of, you receive one free copy of every item j such that j != i and factorᵢ divides factorⱼ (that is, factorⱼ % factorᵢ == 0).
  • Buying multiple copies of the same item i does not give additional free copies through item i. The free copies are triggered only by whether you bought item i at least once.
  • The same item j can be received multiple times for free if it is triggered by purchases of different item types.

Your goal is to return the maximum total number of item copies you can obtain, counting both the purchased copies and the free copies, while spending at most budget on the purchased items.

In short, buying the first copy of an item type unlocks free items (everything whose factor is a multiple of this item's factor), while any extra copies you buy simply add to your total count without unlocking new free items. You want to plan your purchases to maximize the overall number of copies obtained.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

How We Pick the Algorithm

Why Dynamic Programming?

This problem maps to Dynamic Programming through a short path in the full flowchart.

Computemax/min?yesSplit intosubproblems?yesDynamicProgramming

Overlapping subproblems with optimal substructure are solved with dynamic programming.

Open in Flowchart

Intuition

The key observation is that purchases fall into two distinct categories with very different "values":

  • The first copy you buy of a particular item type is special: it not only counts as one copy itself, but also unlocks one free copy of every item j whose factor is a multiple of this item's factor. So buying the first copy of item i gives you cnt total copies, where cnt is the number of items j satisfying factorⱼ % factorᵢ == 0 (this count includes item i itself, accounting for the purchased copy plus all the free copies it triggers).
  • Every additional copy (whether of the same type or of a new type, but counting only the "extra" purchases beyond the first that unlock bonuses) only adds 1 to the total count per copy bought. It gives no new bonuses.

This naturally splits the budget into two parts:

  1. A portion of the budget spent on buying the first copies of selected item types, where each first copy unlocks a bunch of free items. Since each item type can contribute its bonus only once (buying it the first time), and we want to maximize the total number of items gained for a given spent amount, this is exactly a 0-1 knapsack: each item type is either "activated" (bought once, costing priceᵢ and yielding cntᵢ copies) or not. Let f[i] be the maximum number of copies obtainable when spending exactly up to budget i on these first purchases.

  2. The remaining budget can be used to buy plain extra copies. Since these copies provide no bonus, the smartest move is to spend the leftover money on the cheapest item available, getting 1 copy per mn units of money, where mn is the minimum price among all items. With a leftover budget of budget - i, this yields ⌊(budget - i) / mn⌋ extra copies.

Putting the two parts together, if we spend i on the first purchases (gaining f[i] copies) and the rest on cheap extras, the total is f[i] + ⌊(budget - i) / mn⌋. By trying every possible split point i and taking the maximum, we arrive at the final answer.

The reason this works cleanly is that there's no harm in treating the leftover as "all spent on the cheapest item": any extra copy we'd otherwise buy could be replaced by the cheapest one for at least as many copies, and any genuine bonus-unlocking purchase is already accounted for inside the knapsack part f.

Pattern Learn more about Greedy and Dynamic Programming patterns.

Solution Approach

We implement the idea above using a 0-1 knapsack for the first-copy purchases, then combine it with the cheapest-item filling for the leftover budget.

Step 1: Set up the DP array and track the minimum price.

We create a 1D DP array f of length budget + 1, where f[j] represents the maximum number of copies (purchased first copies plus the free copies they unlock) obtainable while spending at most j on these first purchases. Initially f is all zeros, meaning spending nothing yields nothing.

We also maintain mn, the minimum price among all items, initialized to inf. This will later be used for spending the leftover budget on the cheapest item.

f = [0] * (budget + 1)
mn = inf

Step 2: Process each item type as a knapsack item.

For each item [factor, price]:

  • Update the minimum price: mn = min(mn, price).
  • Compute cnt, the number of copies gained by buying the first copy of this item. This equals the number of items j whose factor is a multiple of factor, i.e. factor_j % factor == 0. This single sum counts both the purchased copy (when j == i, the divisibility holds trivially) and all the free copies it unlocks.
mn = min(mn, price)
cnt = sum(factor_j % factor == 0 for factor_j, _ in items)
  • Run the classic 0-1 knapsack inner loop, iterating the budget j from high to low (budget down to price). Iterating in reverse ensures each item type is used at most once (we never reuse the same item's just-updated state within this pass):
for j in range(budget, price - 1, -1):
    f[j] = max(f[j], f[j - price] + cnt)

Here f[j - price] + cnt means: take the best result using budget j - price, then activate the current item for price, gaining cnt copies.

Step 3: Combine first purchases with leftover spending.

After the DP is filled, f[i] is the best number of copies when budget i is used on first purchases. The remaining budget - i is spent buying the cheapest item, yielding (budget - i) // mn plain copies. We enumerate every split point i and take the maximum:

return max(x + (budget - i) // mn for i, x in enumerate(f))

where x is f[i] and i ranges over all possible budgets 0 to budget.

Complexity analysis.

  • Time complexity: O(n² + n × budget). Computing cnt for all items takes O(n²) total, and the knapsack loop takes O(n × budget). The final enumeration is O(budget).
  • Space complexity: O(budget) for the DP array f.

The whole approach hinges on the 1D reverse-iteration knapsack pattern to correctly enforce the "buy first copy once" rule, combined with a simple greedy fill of leftover budget using the cheapest item.

Example Walkthrough

Let's trace through a small example to see the solution approach in action.

Input:

  • items = [[2, 3], [4, 2], [6, 5]]
  • budget = 6

So we have three item types:

  • Item 0: factor = 2, price = 3
  • Item 1: factor = 4, price = 2
  • Item 2: factor = 6, price = 5

Step 1: Compute cnt for each item (and track mn).

cnt = number of items j whose factorⱼ is divisible by the current item's factor (this includes the item itself).

  • Item 0 (factor = 2): which factors are divisible by 2? → 2 % 2 == 0 ✓, 4 % 2 == 0 ✓, 6 % 2 == 0 ✓. So cnt₀ = 3. Buying one copy of item 0 unlocks free copies of items 1 and 2, giving 3 total.
  • Item 1 (factor = 4): which factors are divisible by 4? → 2 % 4 ≠ 0, 4 % 4 == 0 ✓, 6 % 4 ≠ 0. So cnt₁ = 1. Buying item 1 unlocks nothing new (only itself).
  • Item 2 (factor = 6): which factors are divisible by 6? → 2 % 6 ≠ 0, 4 % 6 ≠ 0, 6 % 6 == 0 ✓. So cnt₂ = 1. Only itself.

Minimum price mn = min(3, 2, 5) = 2.

Itemfactorpricecnt
0233
1421
2651

Step 2: 0-1 Knapsack on the first-copy purchases.

Start with f = [0, 0, 0, 0, 0, 0, 0] (indices 0…6).

Process Item 0 (price = 3, cnt = 3), iterate j from 6 down to 3:

  • f[6] = max(0, f[3] + 3) = max(0, 0 + 3) = 3
  • f[5] = max(0, f[2] + 3) = 3
  • f[4] = max(0, f[1] + 3) = 3
  • f[3] = max(0, f[0] + 3) = 3

f = [0, 0, 0, 3, 3, 3, 3]

Process Item 1 (price = 2, cnt = 1), iterate j from 6 down to 2:

  • f[6] = max(3, f[4] + 1) = max(3, 4) = 4 (buy item 0 then item 1: 3 + 1)
  • f[5] = max(3, f[3] + 1) = max(3, 4) = 4
  • f[4] = max(3, f[2] + 1) = max(3, 1) = 3
  • f[3] = max(3, f[1] + 1) = max(3, 1) = 3
  • f[2] = max(0, f[0] + 1) = 1

f = [0, 0, 1, 3, 3, 4, 4]

Process Item 2 (price = 5, cnt = 1), iterate j from 6 down to 5:

  • f[6] = max(4, f[1] + 1) = max(4, 1) = 4
  • f[5] = max(4, f[0] + 1) = max(4, 1) = 4

f = [0, 0, 1, 3, 3, 4, 4]

So f[j] tells us the best number of copies from first-copy purchases using at most budget j. For example, f[5] = 4 comes from activating item 0 (cost 3, +3 copies) and item 1 (cost 2, +1 copy).


Step 3: Combine with leftover spending on the cheapest item (mn = 2).

For each split i, total = f[i] + ⌊(6 - i) / 2⌋:

if[i]leftover = 6 − i(6−i)//2total
00633
10522
21423
33314
43214
54104
64004

The maximum total is 4.


Interpreting the answer.

Two equally good plans both yield 4 copies:

  • Split i = 3: Spend 3 on item 0's first copy → get item 0 + free items 1 and 2 (3 copies). Leftover 3 buys ⌊3/2⌋ = 1 extra copy of the cheapest item (item 1, price 2). Total = 3 + 1 = 4.
  • Split i = 5: Spend 5 to activate item 0 (cost 3, +3) and item 1 (cost 2, +1) → 4 copies. Leftover 1 buys nothing. Total = 4 + 0 = 4.

Both confirm the answer: the smartest strategy unlocks item 0's large bonus first, then fills any remaining budget with the cheapest copies. The final result is 4.

Solution Implementation

1from typing import List
2from math import inf
3
4
5class Solution:
6    def maximumSaleItems(self, items: List[List[int]], budget: int) -> int:
7        # dp[j] = maximum value achievable using a subset of items
8        # whose total price does not exceed j (each item used at most once)
9        dp = [0] * (budget + 1)
10
11        # min_price tracks the cheapest item price; used later so that any
12        # leftover budget can keep buying the cheapest item to gain extra value
13        min_price = inf
14
15        for factor, price in items:
16            # update the cheapest price seen so far
17            min_price = min(min_price, price)
18
19            # value of taking this item = number of items whose factor is
20            # divisible by the current item's factor
21            value = sum(other_factor % factor == 0 for other_factor, _ in items)
22
23            # standard 0/1 knapsack: iterate budget from high to low so each
24            # item is only counted once
25            for j in range(budget, price - 1, -1):
26                dp[j] = max(dp[j], dp[j - price] + value)
27
28        # for each budget point i, spend the remaining (budget - i) on the
29        # cheapest item, gaining one unit of value per purchase; take the max
30        return max(
31            value + (budget - i) // min_price
32            for i, value in enumerate(dp)
33        )
34
1class Solution {
2    /**
3     * Computes the maximum number of sale items that can be obtained
4     * given a list of items and a fixed budget.
5     *
6     * Each item is described by a "factor" and a "price". For a chosen item,
7     * its value (count) is the number of items whose factor is divisible by
8     * the chosen item's factor.
9     *
10     * @param items  a 2D array where items[i][0] is the factor and items[i][1] is the price
11     * @param budget the total amount of money available to spend
12     * @return the maximum total count achievable within the budget
13     */
14    public int maximumSaleItems(int[][] items, int budget) {
15        // dp[j] = maximum total count achievable using exactly j budget (0/1 knapsack style)
16        int[] dp = new int[budget + 1];
17
18        // Track the minimum price among all items, used later to fill leftover budget.
19        int minPrice = Integer.MAX_VALUE;
20
21        // Process each item as a potential candidate to "buy".
22        for (int[] item : items) {
23            int factor = item[0];
24            int price = item[1];
25
26            // Update the global minimum price.
27            minPrice = Math.min(minPrice, price);
28
29            // Count how many items have a factor divisible by the current item's factor.
30            // This determines the "value" gained by selecting this item.
31            int count = 0;
32            for (int[] otherItem : items) {
33                if (otherItem[0] % factor == 0) {
34                    count++;
35                }
36            }
37
38            // 0/1 knapsack update: iterate budget from high to low so each item
39            // contributes at most once per dp state.
40            for (int j = budget; j >= price; j--) {
41                dp[j] = Math.max(dp[j], dp[j - price] + count);
42            }
43        }
44
45        // For each possible spent amount i, the remaining budget (budget - i)
46        // can be used to buy additional cheapest items, each adding 1 to the count.
47        int answer = 0;
48        for (int i = 0; i <= budget; i++) {
49            answer = Math.max(answer, dp[i] + (budget - i) / minPrice);
50        }
51
52        return answer;
53    }
54}
55
1class Solution {
2public:
3    int maximumSaleItems(vector<vector<int>>& items, int budget) {
4        // dp[j] represents the maximum number of items we can obtain
5        // when spending exactly (or up to) j amount of budget via the
6        // 0/1 knapsack selection below.
7        vector<int> dp(budget + 1, 0);
8
9        // Track the minimum price among all items so that any leftover
10        // budget can be converted into extra items at the cheapest rate.
11        int minPrice = INT_MAX;
12
13        // Treat each item as a knapsack "object".
14        for (const auto& item : items) {
15            int factor = item[0];  // the factor value of the current item
16            int price = item[1];   // the cost of choosing this item
17
18            // Update the cheapest price seen so far.
19            minPrice = min(minPrice, price);
20
21            // Count how many items have a factor that is a multiple of
22            // the current item's factor. This count is the "value"
23            // gained by picking the current item.
24            int count = 0;
25            for (const auto& other : items) {
26                if (other[0] % factor == 0) {
27                    count++;
28                }
29            }
30
31            // Standard 0/1 knapsack update, iterating the budget in
32            // reverse so each item is used at most once in this step.
33            for (int j = budget; j >= price; --j) {
34                dp[j] = max(dp[j], dp[j - price] + count);
35            }
36        }
37
38        // Combine the knapsack result with the conversion of any
39        // remaining budget into additional items at the minimum price.
40        int answer = 0;
41        for (int j = 0; j <= budget; ++j) {
42            answer = max(answer, dp[j] + (budget - j) / minPrice);
43        }
44
45        return answer;
46    }
47};
48
1/**
2 * Computes the maximum number of sale items that can be obtained within a given budget.
3 *
4 * Approach:
5 *  - This is a 0/1 knapsack variant. Each item can be "bought" once, contributing a
6 *    certain count (the number of items whose factor is divisible by the current item's factor).
7 *  - After filling the knapsack, any leftover budget can be spent on the cheapest item,
8 *    each purchase yielding one additional item.
9 *
10 * @param items  An array where each element is [factor, price].
11 * @param budget The total budget available to spend.
12 * @returns      The maximum number of sale items obtainable.
13 */
14function maximumSaleItems(items: number[][], budget: number): number {
15    // dp[j] = maximum count achievable using exactly a spend of j (or less, due to relaxation).
16    const dp: number[] = new Array<number>(budget + 1).fill(0);
17
18    // Track the minimum price among all items, used later for spending leftover budget.
19    let minPrice: number = Infinity;
20
21    // Process each item as a knapsack "item".
22    for (const [factor, price] of items) {
23        // Update the cheapest price seen so far.
24        minPrice = Math.min(minPrice, price);
25
26        // Count how many items have a factor divisible by the current item's factor.
27        let count = 0;
28        for (const [otherFactor] of items) {
29            if (otherFactor % factor === 0) {
30                count++;
31            }
32        }
33
34        // 0/1 knapsack update: iterate backwards so each item is used at most once.
35        for (let j = budget; j >= price; j--) {
36            dp[j] = Math.max(dp[j], dp[j - price] + count);
37        }
38    }
39
40    // Combine the knapsack result with spending any remaining budget on the cheapest item.
41    let answer = 0;
42    for (let i = 0; i <= budget; i++) {
43        answer = Math.max(answer, dp[i] + Math.floor((budget - i) / minPrice));
44    }
45
46    return answer;
47}
48

Time and Space Complexity

Time Complexity: O(n^2 + n × m), where n is the number of items and m is the budget.

The algorithm performs the following operations:

  • The outer loop iterates over all items, running n times.
  • Inside the loop, the line cnt = sum(factor_j % factor == 0 for factor_j, _ in items) iterates over all items again, contributing O(n) per outer iteration. Over all n outer iterations, this accumulates to O(n^2).
  • The inner loop for j in range(budget, price - 1, -1) runs up to O(m) times per outer iteration (a standard 0/1 knapsack update). Over all n outer iterations, this accumulates to O(n × m).
  • The final line max(x + (budget - i) // mn for i, x in enumerate(f)) iterates over the f array of size m + 1, contributing O(m).

Combining these, the total time complexity is O(n^2 + n × m + m) = O(n^2 + n × m).

Space Complexity: O(m), where m is the budget.

The only significant auxiliary space is the array f, which has size budget + 1, requiring O(m) space. All other variables (mn, cnt, loop indices) use constant space.

Pattern Learn more about how to find time and space complexity quickly.

Common Pitfalls

Pitfall 1: Crashing on an empty DP / division by an uninitialized min_price

The final return line divides by min_price:

return max(value + (budget - i) // min_price for i, value in enumerate(dp))

If items is empty, the loop body that updates min_price = min(min_price, price) never runs, leaving min_price = inf. Although (budget - i) // inf happens to be 0 in Python (so it won't raise), the logic silently relies on this edge behavior. Worse, if anyone refactors min_price to a sentinel like 0 or None, the division immediately becomes a ZeroDivisionError or TypeError. The code's correctness here is accidental, not intentional.

Solution: Guard the empty case explicitly and only do the greedy fill when a valid min_price exists.

if not items:
    return 0

# ... knapsack as before ...

best = 0
for i, value in enumerate(dp):
    leftover = budget - i
    extra = leftover // min_price if min_price <= budget else 0
    best = max(best, value + extra)
return best

Pitfall 2: Forgetting that buying extra copies of an item also adds to the count

The greedy "spend leftover on cheapest item" step is correct, but a common misreading is to think the leftover budget can only buy items already activated, or that extra copies trigger more free items. The problem states extra copies add +1 each to the total but unlock nothing new. The cheapest item is always the optimal choice for raw count.

A subtler version of this pitfall: assuming the cheapest item used for the leftover fill must be a different item from those bought in the knapsack. It does not — you can keep buying additional copies of an item you already activated. The (budget - i) // min_price formula correctly captures this because it is agnostic to which items were activated.

Solution: Trust that min_price over all items is the right divisor. No need to exclude activated items:

extra = (budget - i) // min_price  # any item, including already-bought ones

Pitfall 3: Wrong inner-loop direction (the classic knapsack trap)

Iterating the budget forward turns the 0-1 knapsack into an unbounded knapsack, which would let a single item activate its value multiple times within one DP pass — violating the "first copy unlocks free items only once" rule.

# WRONG: forward iteration reuses the item within the same pass
for j in range(price, budget + 1):
    dp[j] = max(dp[j], dp[j - price] + value)

This overcounts: an item bought once could appear to grant its free-copy bundle repeatedly.

Solution: Always iterate from high to low so dp[j - price] refers to a state before this item was considered:

for j in range(budget, price - 1, -1):
    dp[j] = max(dp[j], dp[j - price] + value)

Pitfall 4: Miscomputing value by excluding the item itself

When computing value, it's tempting to count only the free copies (other_factor % factor == 0 and other_factor != factor) and forget the purchased copy itself. But the purchased first copy must also be counted.

# WRONG: misses the +1 for the purchased copy and mishandles duplicate factors
value = sum(of % factor == 0 and of != factor for of, _ in items)

The condition other_factor % factor == 0 already includes the case where other_factor == factor (which contributes the purchased copy), so the single comprehension correctly counts purchased + free copies together.

Solution: Keep the full divisibility count, which naturally includes the item itself:

value = sum(other_factor % factor == 0 for other_factor, _ in items)

Note: if multiple items share the same factor, each is counted separately — which is intended, since they are distinct item types.

Ready to land your dream job?

Unlock your dream job with a 5-minute quiz for a personalized study roadmap!

Get My Roadmap
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Get a Personalized Study Roadmap:

What are the most two important steps in writing a depth first search function? (Select 2)


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More