Rating changes for last rounds are temporarily rolled back. They will be returned soon. ×

clams's blog

By clams, history, 11 months ago,

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:

normal code

And this is the optimized code:

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:

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.

code

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

code

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

This can be very handy when handling problems relating to the structure of the numbers, eg: Palindromic Numbers

Example code:

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.