gnudgnaoh's blog

By gnudgnaoh, history, 3 years ago, In English

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

From this stackoverflow question

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.

  • Vote: I like it
  • +31
  • Vote: I do not like it

»
3 years ago, # |
  Vote: I like it 0 Vote: I do not like it

Auto comment: topic has been updated by gnudgnaoh (previous revision, new revision, compare).

»
3 years ago, # |
  Vote: I like it 0 Vote: I do not like it

Auto comment: topic has been updated by gnudgnaoh (previous revision, new revision, compare).

»
3 years ago, # |
  Vote: I like it +20 Vote: I do not like it

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)

  • »
    »
    3 years ago, # ^ |
      Vote: I like it 0 Vote: I do not like it

    thanks for taking your time reading this blog, hope it helped you and as always jalsol orz

»
3 years ago, # |
  Vote: I like it 0 Vote: I do not like it

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.

  • »
    »
    3 years ago, # ^ |
      Vote: I like it 0 Vote: I do not like it

    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!

»
3 years ago, # |
  Vote: I like it 0 Vote: I do not like it

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]

  • »
    »
    3 years ago, # ^ |
      Vote: I like it 0 Vote: I do not like it

    I just take that as an example for a slow solution that wouldn't AC.

    • »
      »
      »
      3 years ago, # ^ |
        Vote: I like it 0 Vote: I do not like it

      stack overflow approach was out of the box for me. Liked it.