Prereq: this digit dp blog
Cut the flag dimension
Usually, what ever states you use in the recursive dp function, you will memoize it. And often you will have some thing like this
int memo[pos][...][...][low]
Where low
is the flag that checks if the current number is already smaller than the considered number.
It is totally possible to subtract this dimension (half the memory needed) by manipulating it in the recursive function:
Example problem: Perfect Number
This is what "normal" code would look like:
And this is the optimized code:
Basically this trick only store the number if the low flag is on, since low isn't necessary to be memoized because its only meaning is to set the limit for the current digit.
Full submission for the "normal" code: "normal" code
Full submission for the optimized code: optimized code
Different ways to memset
"Normal" memset
You memset every time you dp. This would takes a huge amount of time if you have to call dp many time or the memory is large. Example problem: LIDS
Example slow code:
You can clearly see that memset is executed many times for all digits, this would have complexity $$$\mathcal{O}(T * digits * memsize)$$$ where $$$T$$$ is the number of testcases, and $$$digits$$$ is the number of digits (from 0 to 9 in this case), and $$$memsize$$$ is the size of memory.
This is extremely slow.
Improvement using "time"
Now instead of memset every time you dp, you can keep an additional array vis[pos][...][...]
which will store the "time" that the value in mem[pos][...][...]
is set.
This way, you can cut the $$$memsize$$$ complexity. But this is still slow for many problems.
memset only once
You might wonder, "but how? you are doing dp many times on many different numbers!". Well actually, we are doing dp on the digits.
You might notice that we're always doing dp from the most significant digit to the least, usually from left to right, the most significant digit will be at position $$$0$$$ and the least at position $$$length - 1$$$.
This way, the memory for each number is different, like number $$$100$$$ will have different memory from number $$$1234$$$ since they have different $$$length$$$ and other states.
However, what if we let the most significant digit to be at position $$$length - 1$$$ and the least at position $$$0$$$?
Now, every digits of every number line up, and you only need to memset once only.
Example solution of: Perfect Number
This is an extremely important optimization for digit dp
Other optimizations problem-wise
Check sum of digits divisibility
- For a single number
If you want to check if the sum of digits of a number is divisible by $$$D$$$. Instead of storing the whole sum(could lead to MLE), you can store only the remainder of the sum when divided by $$$D$$$.
- For many numbers
Example problem: WORKCHEF (highly recommended, you will need to use a lot of optimizations to AC)
For many numbers, instead of having a state for the remainder for each number, eg: dp[...][rem2][rem3][...]
you can store the remainder of their LCM, eg: checking sum of digits divisible by 1, 2, 3, ... , 9 -> check divisibility by $$$LCM(1, 2, ..., 9) = 2520$$$.
- For numbers with special properties
If you want to check divisibility by 5, the last digit need to be 0 or 5. For 10, the last digit obviously must be 0. ...
There are also many properties for different numbers.
Another way of digit dp
From this stackoverflow question
This can be very handy when handling problems relating to the structure of the numbers, eg: Palindromic Numbers
Example code: