PROBLEM LINK:
Author: Sunny Aggarwal
Tester: Mugurel Ionut Andreica
Editorialist: Lalit Kundu
DIFFICULTY:
Easy-Medium
PREREQUISITES:
dynamic programming, data structures
PROBLEM:
Given an array of N elements consisting of both negative and positive elements and M operations. Each operation is of type L, R and K which implies that you can remove any one element within range L to R(both include) by paying K cost (each operation can be used multiple times).
You have a fixed budget C. You have to maximize the total sum of the array such that the expenditure in maximizing sum of elements does not exceed your budget C.
Here, N, M \le 10^5 and C \le 200.
QUICK EXPLANATION:
First for each element find the minimum cost required to remove it. And then using DP similar to 0-1 Knapsack Problem calculate the maximum possible sum.
For finding minimum cost to remove each element:
-
For subtask 1, you can brute force i.e. for each operation traverse over all indices it effects and update the value in an array.
-
For solving subtask 2, you have to either use STL sets or you can use segment trees.
EXPLANATION:
================
The most basic observation here is that each operation allows to remove single element only. So, let’s say you want to remove A_i, you can remove it in many ways. Let’s define by set S_i the set of operations which can remove A_i. So S_i = \{\textrm{oper}_j : L_j \le i \le R_j\}. Now you can intuitively/greedily say that for removing A_i you would always choose the operation from set S_i whose cost is minimum.
Now, let’s say for all i, we have found the minimum cost to remove A_i. How we actually do this I will explain later. So our problem now is basically:
You have an array A of size N… For each element A_i there is cost of removal R_i. Remove some elements from A_i to maximize the sum of remaining elements and also total cost of removal shouldn’t exceed C. This is quite similar to 0-1 Knapsack Problem which can be solved via Dynamic Programming(DP).
So, first step in writing/formalizing any DP problem is to decide some states which defines a sub problem of the problem we are trying to solve. You can do some hit and trial before you reach the correct states. Next step is to break the current problem into smaller sub problems which can help in defining the recursive relation between the DP states. Last step is to decide the base case.
So, here we define \textrm{solve}\hspace{1mm}(i,\hspace{1mm}j) as the answer if our budget is j and our array is formed by the first i elements ie. A_1, A_2, ..., A_i. So our answer will be \textrm{solve}\hspace{1mm}(N,\hspace{1mm}C).
Now let’s try to form recursive relations. You want to reduce your current problem i.e. \textrm{solve}\hspace{1mm}(i,\hspace{1mm}j) into smaller sub problems. How can we do that? To reduce current problem in smaller parts, we have to perform some action, which here is to decide whether to remove A_i or not.
Let’s consider case 1, where we will remove A_i. This is only possible if j \ge R_i. Now, \textrm{solve}\hspace{1mm}(i,\hspace{1mm}j) = \textrm{solve}\hspace{1mm}(i-1,\hspace{1mm}j - R_i). Note that we have lost R_i cost on removing A_i and our array is now reduced to first i - 1 elements. Also, in the sum of remaining elements A_i couldn’t contribute anything. (A thought: Will we ever remove A_i if it’s positive, considering removing elements incurs cost?).
Now, case 2, let’s not remove A_i. Now, \textrm{solve}\hspace{1mm}(i,\hspace{1mm}j) = A_i + \textrm{solve}\hspace{1mm}(i-1,\hspace{1mm}C). Now, A_i is not removed and contributes to the sum of remaining elements. Also, our budget remains same and our array size is now reduced by 1.
So, our recurrence is ready which is basically:
\textrm{solve}\hspace{1mm}(i,\hspace{1mm}j) = \textrm{max}(\hspace{1mm}\textrm{solve}\hspace{1mm}(i-1,\hspace{1mm}j - R_i), \hspace{1mm} A_i + \textrm{solve}\hspace{1mm}(i-1,\hspace{1mm}j)).
Let’s see what are the base cases. The only base case is that if i==0 i.e. there is no array left, the only maximum sum possible is 0.
DP Implementation:
This is the last step of completing your DP problem. The best and the easiest way of writing DP is recursively with memoisation. There is no major difference in run time of recurisve and iterative DP.
Now, what is memoisation? It basically is method where you don’t calculate things you’ve already calculated. So you maintain a \textrm{flag} array which is same type of your DP array and intialised to \textrm{false}. Once you have calculated a certain subproblem, you mark it true in the \textrm{flag} array. If you ever reach again a state, which has already been calculated, you return the value currently stored in DP array. Things will get clear from the following implementation:
flag[N][C] #initialised to false DP[N][C] #array which stores actual answers A[N] #array A R[N] #cost array solve(i, j): #base case if i<=0: return dp[i][j]=0 #first sets dp[i][j] to 0 and returns it if flag[i][j] == true: #this dp has already been calculated return dp[i][j] #case 2: don't remove A[i] ret = A[i] + solve(i - 1, j) #case 1: remove A[i] if possible #tak ret to be the maximum of both cases if(j >= R[i]) ret = max(ret, solve(i - 1, j - R[i])) #mark flag[i][j] true since we have calculated this DP flag[i][j] = true return dp[i][j] = ret
Complexity of DP:
Let’s set what is the complexity of such a recursive implementation. Since each possible state is visited once, the complexity of DP is number of states multiplied with transition cost. Transition cost is the complexity required from transfrom from one state to another state.
Here, our total number of states is \textrm{N * C} and transition cost is constant time. So, total complexity is \textrm{O(N * C)}.
Calculating minimum cost for removing each element
Now, about the part which we skipped earlier about calculating minimum cost of removing of A_i.
First you initialize all indices of a MIN array to infinity and then for each operation you traverse through all indices which it covers and update the minimum value at each index. Here complexity is \textrm{O(M*N)}, where M is number of operations and N is size of array A. This is enough to pass Subtask 1.
For solving Subtask 2, interesting observation is that an index i is affected by operations whose left end is before i and right end is after i. Suppose we have a data structure where we can insert/delete elements and find minimum value currently stored in this data structure in sub linear time. Let’s say this structure is S.
So, let’s maintain two vector arrays L and R(means you can store a list of values at each index) and for each operation j insert at index L_j and R_j the cost of this particular operation ie. K_j. Now, when we traverse arrays L and R from left to right, say we are index i, for indices \ge i, values stored in list L[i] are going to effect them, so we add to our structure the values stored at L[i] and the values stored in R[i] are not going to affect indices \ge i, so we remove all values stored at R[i].
What could be this data structure S. If we use STL set, we can insert/delete a object only once, but this is not what we require. There might be two operations with same cost. So instead of storing values, we can store a pair of value and the indices of operations. In this way all operations will be unique and the beginning element of set will always give the minimum value operation.
If you don’t feel enough clarity, see this pseudo code and try to visualize what is happening.
struct oper{ int l, r, k; }; oper operarray[M]; //array of operations int MIN[N]; //MIN[i] stores minimum cost for removing A[i] vector L[N], R[N]; //arrays as defined in above paragraph //except now they store indices of operations instead of their cost set < pair < int, int > > iset; //first element of pair stores value of operation cost //second stores the index of operation for i = 1 to M: left = operarray[i].l right = operarray[i].r L.push_back(i) R[right].push_back(i) for i = 1 to N: //add all operations beginning at i for j = 0 to L[i].size() - 1: operindex = L[i][j] //index of operation beginning here cost = operarray[operindex].k //insert in set iset.insert(make_pair(cost, operindex)) MIN[i] = iset.begin()->first; //first element of the set //remove all operations ending at i for j = 0 to R[i].size() - 1: operindex = R[i][j] //index of operation beginning here cost = operarray[operindex].k //erase from set iset.erase(make_pair(cost, operindex))
Set is a STL data structure that inserts and deletes elements in O(\textrm{log (size of set)}). And since it keeps all elements in sorted order, we can find minimum element in constant time.
So total complexity of finding the \textrm{MIN} array is \textrm{O(N log M)}. You can also find \textrm{MIN} array using segment trees where the complexity will be \textrm{O((M + N) log N)}, if we use lazy propagation and make updates.
COMPLEXITY:
Final complexity after including complexity of DP is \textrm{O(N log M + N C)}.
AUTHOR’S, TESTER’S SOLUTIONS:
Problems to Practice:
Problems based on DP
Problems based on STL