2355. Maximum Number of Books You Can Take đź”’
Problem Description
You have a bookshelf with n
shelves arranged in a row, numbered from 0
to n-1
. Each shelf contains a certain number of books, given by the array books
where books[i]
represents the number of books on shelf i
.
You want to take books from a contiguous section of the bookshelf. This means you'll choose a starting shelf l
and an ending shelf r
(where 0 <= l <= r < n
), and take books from all shelves in the range [l, r]
.
However, there's a constraint on how many books you can take from each shelf:
- For any two consecutive shelves
i
andi+1
within your chosen range (wherel <= i < r
), you must take strictly fewer books from shelfi
than from shelfi+1
- This creates a strictly increasing sequence of books taken from left to right within your chosen range
The goal is to find the maximum total number of books you can take while following this constraint.
For example, if books = [8, 5, 2, 7, 9]
and you choose the range [1, 4]
, you might take:
- 1 book from shelf 1
- 2 books from shelf 2
- 7 books from shelf 3
- 9 books from shelf 4
This gives a total of 19 books, where each shelf has strictly more books taken than the previous one in the range.
Note that you don't have to take all books from a shelf - you can take any number from 0 up to books[i]
from shelf i
, as long as the strictly increasing constraint is satisfied.
Intuition
The key insight is that for any ending position i
, we want to find the optimal starting position where we can form a strictly increasing sequence of books taken, maximizing the total.
First, let's think about what makes a valid strictly increasing sequence. If we end at position i
and take books[i]
books from it, then at position i-1
we can take at most books[i]-1
books, at position i-2
we can take at most books[i]-2
books, and so on. This forms an arithmetic sequence counting backwards.
However, we're limited by the actual number of books available on each shelf. So at position j
, we can take min(books[j], books[i] - (i - j))
books. This means we take either all available books or the amount dictated by our arithmetic sequence, whichever is smaller.
The challenge is finding where to start our sequence. We can't always extend backwards indefinitely because:
- We might run out of shelves (reach index 0)
- The arithmetic sequence might demand negative books (when
books[i] - (i - j) <= 0
) - A shelf might have too few books to maintain the strictly increasing pattern
To handle this efficiently, we transform the problem. We create a new array nums[i] = books[i] - i
. This transformation helps us identify "blocking points" - positions where we cannot extend our arithmetic sequence further backward. If nums[j] >= nums[i]
for some j < i
, then position j
blocks us from extending past it when ending at position i
.
We use a monotonic stack to find, for each position i
, the nearest left position that would block our sequence. This gives us array left[i]
, where left[i]
is the rightmost position to the left of i
that has nums[left[i]] >= nums[i]
.
Finally, we use dynamic programming. For each position i
:
- We find how many positions we can include in our sequence (from
left[i] + 1
toi
) - We calculate the sum of the arithmetic sequence for these positions
- We add the optimal sum from position
left[i]
(if it exists) since we can combine non-overlapping ranges
The maximum value among all dp[i]
gives us our answer.
Learn more about Stack, Dynamic Programming and Monotonic Stack patterns.
Solution Approach
Let's walk through the implementation step by step:
Step 1: Transform the array
nums = [v - i for i, v in enumerate(books)]
We create a transformed array where nums[i] = books[i] - i
. This transformation helps identify blocking points. If nums[j] >= nums[i]
for j < i
, then position j
prevents us from extending our arithmetic sequence past it when ending at position i
.
Step 2: Find blocking positions using monotonic stack
left = [-1] * n
stk = []
for i, v in enumerate(nums):
while stk and nums[stk[-1]] >= v:
stk.pop()
if stk:
left[i] = stk[-1]
stk.append(i)
We maintain a monotonic increasing stack to find, for each position i
, the nearest left position that blocks our sequence:
- Pop elements from stack while their
nums
values are>= v
- The remaining top of stack (if exists) is our blocking position
- Add current index to stack
left[i] = -1
means no blocking position exists (we can extend to the beginning)
Step 3: Dynamic programming to find maximum books
dp = [0] * n
dp[0] = books[0]
for i, v in enumerate(books):
j = left[i]
cnt = min(v, i - j)
u = v - cnt + 1
s = (u + v) * cnt // 2
dp[i] = s + (0 if j == -1 else dp[j])
ans = max(ans, dp[i])
For each position i
:
j = left[i]
: Get the blocking positioncnt = min(v, i - j)
: Calculate how many positions we can include in our sequence- Limited by either the number of books at position
i
or the distance to blocking position
- Limited by either the number of books at position
u = v - cnt + 1
: Calculate the first term of our arithmetic sequence- If we take
v
books from positioni
, we takev-1
fromi-1
,v-2
fromi-2
, etc. - The first term is
v - (cnt - 1) = v - cnt + 1
- If we take
s = (u + v) * cnt // 2
: Sum of arithmetic sequence fromu
tov
- Using formula: sum = (first + last) Ă— count / 2
dp[i] = s + dp[j]
: Add the optimal sum from the blocking position (if it exists)- Track the maximum value seen
Time Complexity: O(n)
- single pass for stack and single pass for DP
Space Complexity: O(n)
- for the auxiliary arrays nums
, left
, dp
, and stack
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a small example with books = [8, 5, 2, 7, 9]
.
Step 1: Transform the array
nums[0] = 8 - 0 = 8
nums[1] = 5 - 1 = 4
nums[2] = 2 - 2 = 0
nums[3] = 7 - 3 = 4
nums[4] = 9 - 4 = 5
So nums = [8, 4, 0, 4, 5]
Step 2: Find blocking positions using monotonic stack
Processing each index:
i=0
: Stack empty,left[0] = -1
, push 0. Stack:[0]
i=1
:nums[0]=8 >= 4
, pop 0. Stack empty,left[1] = -1
, push 1. Stack:[1]
i=2
:nums[1]=4 >= 0
, pop 1. Stack empty,left[2] = -1
, push 2. Stack:[2]
i=3
:nums[2]=0 < 4
,left[3] = 2
, push 3. Stack:[2, 3]
i=4
:nums[3]=4 < 5
,left[4] = 3
, push 4. Stack:[2, 3, 4]
Result: left = [-1, -1, -1, 2, 3]
Step 3: Dynamic programming
For each position i
:
i=0:
j = -1
(no blocking position)cnt = min(8, 0-(-1)) = min(8, 1) = 1
- Taking 8 books from shelf 0
u = 8 - 1 + 1 = 8
s = (8 + 8) Ă— 1 / 2 = 8
dp[0] = 8 + 0 = 8
i=1:
j = -1
(no blocking position)cnt = min(5, 1-(-1)) = min(5, 2) = 2
- Can take: 4 books from shelf 0, 5 books from shelf 1
u = 5 - 2 + 1 = 4
s = (4 + 5) Ă— 2 / 2 = 9
dp[1] = 9 + 0 = 9
i=2:
j = -1
(no blocking position)cnt = min(2, 2-(-1)) = min(2, 3) = 2
- Can take: 1 book from shelf 1, 2 books from shelf 2
u = 2 - 2 + 1 = 1
s = (1 + 2) Ă— 2 / 2 = 3
dp[2] = 3 + 0 = 3
i=3:
j = 2
(blocked by position 2)cnt = min(7, 3-2) = min(7, 1) = 1
- Taking 7 books from shelf 3 only
u = 7 - 1 + 1 = 7
s = (7 + 7) Ă— 1 / 2 = 7
dp[3] = 7 + dp[2] = 7 + 3 = 10
i=4:
j = 3
(blocked by position 3)cnt = min(9, 4-3) = min(9, 1) = 1
- Taking 9 books from shelf 4 only
u = 9 - 1 + 1 = 9
s = (9 + 9) Ă— 1 / 2 = 9
dp[4] = 9 + dp[3] = 9 + 10 = 19
Answer: 19
This corresponds to taking books from two separate ranges:
- Range [1,2]: Take 1 book from shelf 1 and 2 books from shelf 2 (total: 3)
- Range [3,4]: Take 7 books from shelf 3 and 9 books from shelf 4 (total: 16)
- Combined total: 19 books
Solution Implementation
1class Solution:
2 def maximumBooks(self, books: List[int]) -> int:
3 # Transform array: subtract index from each value
4 # This helps identify positions where we need to restart counting
5 adjusted_values = [value - index for index, value in enumerate(books)]
6 n = len(adjusted_values)
7
8 # left[i] stores the index of the previous element with adjusted_value < adjusted_values[i]
9 # -1 means no such element exists
10 left_boundary = [-1] * n
11
12 # Monotonic stack to find previous smaller element
13 stack = []
14 for i, current_value in enumerate(adjusted_values):
15 # Pop elements from stack that are >= current value
16 while stack and adjusted_values[stack[-1]] >= current_value:
17 stack.pop()
18
19 # If stack has elements, top element is the previous smaller element
20 if stack:
21 left_boundary[i] = stack[-1]
22
23 stack.append(i)
24
25 # Dynamic programming to calculate maximum books
26 max_books = 0
27 dp = [0] * n # dp[i] = maximum books we can take ending at position i
28 dp[0] = books[0]
29
30 for i, shelf_books in enumerate(books):
31 # j is the left boundary (previous position with smaller adjusted value)
32 j = left_boundary[i]
33
34 # Calculate how many consecutive positions we can take books from
35 # We can take at most 'shelf_books' books and at most (i - j) positions
36 consecutive_positions = min(shelf_books, i - j)
37
38 # Calculate the starting value of the arithmetic sequence
39 # We take books in decreasing order: shelf_books, shelf_books-1, ..., start_value
40 start_value = shelf_books - consecutive_positions + 1
41
42 # Sum of arithmetic sequence from start_value to shelf_books
43 arithmetic_sum = (start_value + shelf_books) * consecutive_positions // 2
44
45 # Add books from current sequence plus optimal solution up to position j
46 dp[i] = arithmetic_sum + (0 if j == -1 else dp[j])
47
48 # Update maximum books found so far
49 max_books = max(max_books, dp[i])
50
51 return max_books
52
1class Solution {
2 public long maximumBooks(int[] books) {
3 int n = books.length;
4
5 // Transform array: nums[i] = books[i] - i
6 // This helps identify positions where we can form a decreasing sequence
7 int[] transformedValues = new int[n];
8 for (int i = 0; i < n; ++i) {
9 transformedValues[i] = books[i] - i;
10 }
11
12 // Find the nearest left index where transformedValues[left[i]] < transformedValues[i]
13 // Using monotonic stack to find previous smaller element
14 int[] previousSmallerIndex = new int[n];
15 Arrays.fill(previousSmallerIndex, -1);
16 Deque<Integer> monotonicStack = new ArrayDeque<>();
17
18 for (int i = 0; i < n; ++i) {
19 // Pop elements from stack while current element is smaller or equal
20 while (!monotonicStack.isEmpty() && transformedValues[monotonicStack.peek()] >= transformedValues[i]) {
21 monotonicStack.pop();
22 }
23 // If stack has elements, top is the previous smaller element
24 if (!monotonicStack.isEmpty()) {
25 previousSmallerIndex[i] = monotonicStack.peek();
26 }
27 monotonicStack.push(i);
28 }
29
30 // Dynamic programming to find maximum books
31 long maxBooks = 0;
32 long[] dp = new long[n]; // dp[i] = maximum books we can take ending at position i
33 dp[0] = books[0];
34
35 for (int i = 0; i < n; ++i) {
36 int leftBoundary = previousSmallerIndex[i];
37 int currentShelfBooks = books[i];
38
39 // Calculate how many consecutive shelves we can take in decreasing order
40 int consecutiveShelves = Math.min(currentShelfBooks, i - leftBoundary);
41
42 // Calculate the starting value of the arithmetic sequence
43 int startValue = currentShelfBooks - consecutiveShelves + 1;
44
45 // Sum of arithmetic sequence: (first + last) * count / 2
46 long currentSum = (long)(startValue + currentShelfBooks) * consecutiveShelves / 2;
47
48 // Add the maximum books from previous non-overlapping segment if exists
49 dp[i] = currentSum + (leftBoundary == -1 ? 0 : dp[leftBoundary]);
50
51 // Update the global maximum
52 maxBooks = Math.max(maxBooks, dp[i]);
53 }
54
55 return maxBooks;
56 }
57}
58
1using ll = long long;
2
3class Solution {
4public:
5 long long maximumBooks(vector<int>& books) {
6 int n = books.size();
7
8 // Transform array: nums[i] = books[i] - i
9 // This helps identify positions where we can take consecutive decreasing books
10 vector<int> transformedBooks(n);
11 for (int i = 0; i < n; ++i) {
12 transformedBooks[i] = books[i] - i;
13 }
14
15 // Find the nearest left index where transformedBooks[left[i]] < transformedBooks[i]
16 // Using monotonic stack to find previous smaller element
17 vector<int> previousSmallerIndex(n, -1);
18 stack<int> monotonicStack;
19
20 for (int i = 0; i < n; ++i) {
21 // Pop elements from stack while they are >= current element
22 while (!monotonicStack.empty() &&
23 transformedBooks[monotonicStack.top()] >= transformedBooks[i]) {
24 monotonicStack.pop();
25 }
26
27 // If stack is not empty, top element is the previous smaller element
28 if (!monotonicStack.empty()) {
29 previousSmallerIndex[i] = monotonicStack.top();
30 }
31
32 monotonicStack.push(i);
33 }
34
35 // Dynamic programming: dp[i] = maximum books we can take ending at position i
36 vector<ll> dp(n);
37 dp[0] = books[0];
38 ll maxBooks = 0;
39
40 for (int i = 0; i < n; ++i) {
41 int currentBookCount = books[i];
42 int prevIndex = previousSmallerIndex[i];
43
44 // Calculate how many consecutive positions we can take books from
45 // Limited by either the book count or the distance to previous boundary
46 int consecutivePositions = min(currentBookCount, i - prevIndex);
47
48 // Calculate the starting value of the arithmetic sequence
49 int startValue = currentBookCount - consecutivePositions + 1;
50
51 // Sum of arithmetic sequence: (first + last) * count / 2
52 ll currentSum = 1ll * (startValue + currentBookCount) * consecutivePositions / 2;
53
54 // Add the maximum books from the previous boundary (if exists)
55 dp[i] = currentSum + (prevIndex == -1 ? 0 : dp[prevIndex]);
56
57 // Update the global maximum
58 maxBooks = max(maxBooks, dp[i]);
59 }
60
61 return maxBooks;
62 }
63};
64
1type ll = number;
2
3function maximumBooks(books: number[]): number {
4 const n: number = books.length;
5
6 // Transform array: transformedBooks[i] = books[i] - i
7 // This helps identify positions where we can take consecutive decreasing books
8 const transformedBooks: number[] = new Array(n);
9 for (let i = 0; i < n; i++) {
10 transformedBooks[i] = books[i] - i;
11 }
12
13 // Find the nearest left index where transformedBooks[left[i]] < transformedBooks[i]
14 // Using monotonic stack to find previous smaller element
15 const previousSmallerIndex: number[] = new Array(n).fill(-1);
16 const monotonicStack: number[] = [];
17
18 for (let i = 0; i < n; i++) {
19 // Pop elements from stack while they are >= current element
20 while (monotonicStack.length > 0 &&
21 transformedBooks[monotonicStack[monotonicStack.length - 1]] >= transformedBooks[i]) {
22 monotonicStack.pop();
23 }
24
25 // If stack is not empty, top element is the previous smaller element
26 if (monotonicStack.length > 0) {
27 previousSmallerIndex[i] = monotonicStack[monotonicStack.length - 1];
28 }
29
30 monotonicStack.push(i);
31 }
32
33 // Dynamic programming: dp[i] = maximum books we can take ending at position i
34 const dp: ll[] = new Array(n);
35 dp[0] = books[0];
36 let maxBooks: ll = 0;
37
38 for (let i = 0; i < n; i++) {
39 const currentBookCount: number = books[i];
40 const prevIndex: number = previousSmallerIndex[i];
41
42 // Calculate how many consecutive positions we can take books from
43 // Limited by either the book count or the distance to previous boundary
44 const consecutivePositions: number = Math.min(currentBookCount, i - prevIndex);
45
46 // Calculate the starting value of the arithmetic sequence
47 const startValue: number = currentBookCount - consecutivePositions + 1;
48
49 // Sum of arithmetic sequence: (first + last) * count / 2
50 const currentSum: ll = (startValue + currentBookCount) * consecutivePositions / 2;
51
52 // Add the maximum books from the previous boundary (if exists)
53 dp[i] = currentSum + (prevIndex === -1 ? 0 : dp[prevIndex]);
54
55 // Update the global maximum
56 maxBooks = Math.max(maxBooks, dp[i]);
57 }
58
59 return maxBooks;
60}
61
Time and Space Complexity
The time complexity is O(n)
, where n
is the length of the books array. The algorithm consists of:
- A first pass to create the
nums
array:O(n)
- A monotonic stack operation to fill the
left
array:O(n)
- each element is pushed and popped at most once - A final pass to compute the DP values:
O(n)
The space complexity is O(n)
due to:
- The
nums
array storingn
elements - The
left
array storingn
elements - The
dp
array storingn
elements - The stack
stk
which can contain at mostn
elements in the worst case
Note: The reference answer appears to be for a different problem involving a matrix grid with O(n^3)
time complexity, not for this books shelf problem which uses dynamic programming with a monotonic stack.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Integer Overflow in Arithmetic Sum Calculation
The Pitfall:
When calculating the sum of an arithmetic sequence using (start_value + shelf_books) * consecutive_positions // 2
, the intermediate multiplication can cause integer overflow for large values. In Python, this isn't typically an issue due to arbitrary precision integers, but in languages like Java or C++, this can lead to incorrect results.
Example scenario:
- If
shelf_books = 10^9
andconsecutive_positions = 10^5
- The multiplication
(start_value + shelf_books) * consecutive_positions
could exceed the integer limit before division
Solution: Use a safer formula or ensure proper type casting:
# Instead of: arithmetic_sum = (start_value + shelf_books) * consecutive_positions // 2 # Use: arithmetic_sum = consecutive_positions * start_value + consecutive_positions * (consecutive_positions - 1) // 2 # This breaks down the calculation to avoid large intermediate values
2. Misunderstanding the Adjusted Values Transformation
The Pitfall:
The transformation adjusted_values[i] = books[i] - i
might seem arbitrary and developers often make mistakes by:
- Using the wrong comparison operator when checking blocking positions
- Forgetting why we need
>=
instead of>
in the monotonic stack comparison
Why this matters:
If position j
has adjusted_values[j] >= adjusted_values[i]
where j < i
, it means we cannot extend a strictly increasing sequence from position j
through position i
. This is because:
- At position
j
: we can take at mostbooks[j]
books - At position
i
: we need to take at leastbooks[j] + (i-j)
books for strict increase - But if
books[j] - j >= books[i] - i
, thenbooks[j] + (i-j) > books[i]
, which is impossible
Solution: Always validate the logic with a simple example:
# Example: books = [4, 5, 3, 8] # adjusted = [4, 4, 1, 5] # Position 1 blocks position 0 because adjusted[0] >= adjusted[1] # We cannot have a strictly increasing sequence from index 0 through index 1
3. Off-by-One Error in Consecutive Positions Calculation
The Pitfall:
Calculating consecutive_positions = min(shelf_books, i - j)
seems straightforward, but it's easy to make an off-by-one error by using i - j - 1
or i - j + 1
.
Why i - j
is correct:
- If
j = -1
(no blocking position), theni - j = i + 1
, which means we can use positions[0, 1, ..., i]
(total ofi+1
positions) - If
j = 2
andi = 5
, theni - j = 3
, meaning we can use positions[3, 4, 5]
(total of 3 positions, starting right after position 2)
Solution: Test with concrete examples:
# If j = -1, i = 2: we can use positions [0, 1, 2] → count = 3 = i - j # If j = 1, i = 4: we can use positions [2, 3, 4] → count = 3 = i - j
4. Incorrect DP Initialization
The Pitfall:
Forgetting to initialize dp[0] = books[0]
or incorrectly initializing all dp values to 0 can lead to wrong answers for cases where the optimal solution includes the first shelf.
Solution: Always handle the base case explicitly:
dp = [0] * n dp[0] = books[0] # Critical: first position can always take all its books max_books = books[0] # Don't forget to consider dp[0] in the maximum
Which of the following uses divide and conquer strategy?
Recommended Readings
Stack Intro Imagine you have a pile of books on your desk If you want to add a new book you place it on top If you want to read a book you take it from the top And if you simply want to see which book is on the top you
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
Monotonic Stack Deque Intro If you'd prefer a video div class responsive iframe iframe src https www youtube com embed Dq_ObZwTY_Q title YouTube video player frameborder 0 allow accelerometer autoplay clipboard write encrypted media gyroscope picture in picture web share allowfullscreen iframe div The word monotonic means a list or
Want a Structured Path to Master System Design Too? Don’t Miss This!