Prereq: this digit dp blog
Cut the flag dimension
Usually, whatever 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, the complexity is better but this is still too 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:
Feel free to share any tricks or anything that people should know when doing digit dp! If there is any mistakes or suggestions, please let me know.
Auto comment: topic has been updated by gnudgnaoh (previous revision, new revision, compare).
Auto comment: topic has been updated by gnudgnaoh (previous revision, new revision, compare).
thank you very much sir bubu, impressive blog
anyway, the "memset improvement using time" is a good technique on its own, and it can also be applied for some other techniques (one of the common is Kuhn's algorithm for maximum matching)
thanks for taking your time reading this blog, hope it helped you and as always jalsol orz
Also, there is kind of DP-Digit that only save partial part when the number of counting is dense and brute-forces when the counting is sparse. Some COCI problems must use this technique to AC
Also when DP-Digit on certain range with some property, you might find it possible to cut into a DP over a DP-Digit that might improve the calculation by reducing the re-calculating parts.
Can you provide the statement(or even better, links) for the COCI problems? And can you provide some examples about the DP over a digit DP part? Thanks!
In "LIDS" you don't need to memset every time. U can solve it taking into account current index, current digit, current lids only. [0.1s]
I just take that as an example for a slow solution that wouldn't AC.
stack overflow approach was out of the box for me. Liked it.