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
ithat you bought at least one copy of, you receive one free copy of every itemjsuch thatj != iandfactorᵢdividesfactorⱼ(that is,factorⱼ % factorᵢ == 0). - Buying multiple copies of the same item
idoes not give additional free copies through itemi. The free copies are triggered only by whether you bought itemiat least once. - The same item
jcan 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.
How We Pick the Algorithm
Why Dynamic Programming?
This problem maps to Dynamic Programming through a short path in the full flowchart.
Overlapping subproblems with optimal substructure are solved with dynamic programming.
Open in FlowchartIntuition
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
jwhose factor is a multiple of this item's factor. So buying the first copy of itemigives youcnttotal copies, wherecntis the number of itemsjsatisfyingfactorⱼ % factorᵢ == 0(this count includes itemiitself, 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
1to the total count per copy bought. It gives no new bonuses.
This naturally splits the budget into two parts:
-
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 yieldingcntᵢcopies) or not. Letf[i]be the maximum number of copies obtainable when spending exactly up to budgetion these first purchases. -
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
1copy permnunits of money, wheremnis the minimum price among all items. With a leftover budget ofbudget - 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 itemsjwhose factor is a multiple offactor, i.e.factor_j % factor == 0. This single sum counts both the purchased copy (whenj == 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
jfrom high to low (budgetdown toprice). 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). Computingcntfor all items takesO(n²)total, and the knapsack loop takesO(n × budget). The final enumeration isO(budget). - Space complexity:
O(budget)for the DP arrayf.
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✓. Socnt₀ = 3. Buying one copy of item 0 unlocks free copies of items 1 and 2, giving3total. - Item 1 (
factor = 4): which factors are divisible by 4? →2 % 4 ≠ 0,4 % 4 == 0✓,6 % 4 ≠ 0. Socnt₁ = 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✓. Socnt₂ = 1. Only itself.
Minimum price mn = min(3, 2, 5) = 2.
| Item | factor | price | cnt |
|---|---|---|---|
| 0 | 2 | 3 | 3 |
| 1 | 4 | 2 | 1 |
| 2 | 6 | 5 | 1 |
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) = 3f[5] = max(0, f[2] + 3) = 3f[4] = max(0, f[1] + 3) = 3f[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) = 4f[4] = max(3, f[2] + 1) = max(3, 1) = 3f[3] = max(3, f[1] + 1) = max(3, 1) = 3f[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) = 4f[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⌋:
| i | f[i] | leftover = 6 − i | (6−i)//2 | total |
|---|---|---|---|---|
| 0 | 0 | 6 | 3 | 3 |
| 1 | 0 | 5 | 2 | 2 |
| 2 | 1 | 4 | 2 | 3 |
| 3 | 3 | 3 | 1 | 4 |
| 4 | 3 | 2 | 1 | 4 |
| 5 | 4 | 1 | 0 | 4 |
| 6 | 4 | 0 | 0 | 4 |
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⌋ = 1extra 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 )
341class 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}
551class 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};
481/**
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}
48Time 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, runningntimes. - Inside the loop, the line
cnt = sum(factor_j % factor == 0 for factor_j, _ in items)iterates over all items again, contributingO(n)per outer iteration. Over allnouter iterations, this accumulates toO(n^2). - The inner loop
for j in range(budget, price - 1, -1)runs up toO(m)times per outer iteration (a standard 0/1 knapsack update). Over allnouter iterations, this accumulates toO(n × m). - The final line
max(x + (budget - i) // mn for i, x in enumerate(f))iterates over thefarray of sizem + 1, contributingO(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 RoadmapWhat are the most two important steps in writing a depth first search function? (Select 2)
Recommended Readings
Greedy Introduction Greedy Algorithms A greedy algorithm builds a solution one step at a time and at every step it takes the choice that looks best right now without reconsidering earlier choices It never backtracks and never plans ahead The bet is that a sequence of locally best choices adds up to
What is Dynamic Programming Prerequisite DFS problems dfs_intro Backtracking problems backtracking Memoization problems memoization_intro Pruning problems backtracking_pruning Dynamic programming is an algorithmic optimization technique that breaks down a complicated problem into smaller overlapping sub problems in a recursive manner and uses solutions to the sub problems to construct a solution
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Want a Structured Path to Master System Design Too? Don’t Miss This!