Detailed explanation of digital dp, memory search, recursion, OJ's detailed explanation

Preface

Digital DP, also known as digital dynamic programming, is another sub-problem of dynamic programming. It is moderately difficult and the questions have similar ideas. You can even refine a template. Of course, if you have a certain foundation in dynamic programming, such as the longest increasing subsequence ( LIS), longest common subsequence (LCS), memorized search, state machine, knapsack problem, etc., we have a certain understanding of it. Without understanding the digital dp, we can generally solve the problem by analyzing the design state, but we are not interested in dynamics. The classification and summary of various planning problems will help us better grasp the idea of ​​dynamic programming and improve the height of thinking.


Quotation-no descending

Number games are very popular in the Association for Science and Technology recently. Someone named a non-descending number. This kind of number must satisfy the relationship that the digits from left to right are less than or equal to each other, such as 123, 446. Now everyone decides to play a game, specify a closed interval of integers [a, b], and ask how many non-decreasing numbers there are in this interval.

For all data, 1 <= a <= b <= 2^31 - 1.

If we violently preprocess all the non-decreasing numbers, then each number requires O© (C is the number of digits) time to judge, and finally the time complexity of O(1e9 * C) can be reached, which is obviously unacceptable. We naturally have to Find another way.

prerequisite knowledge

Differential conversion

Our common approach to interval solutions is the difference method decomposition .

For the number of non-decreasing numbers nondec® in the interval [0, r], the number of non-decreasing numbers nondec(l - 1) in [0, l - 1] obviously satisfies

nondec® - nondec(l - 1) = the number of non-decreasing numbers in [l, r]

The external link image transfer failed. The source site may have an anti-leeching mechanism. It is recommended to save the image and upload it directly.

Enumeration techniques

For a decimal number _ Enumeration from high to low, for the nth bit, it is natural to satisfy an <= dn, but when we enumerate, we will find:

  • If an < dn, then an-1 ∈ [0, 9]
  • If an = dn, then an-1 ∈ [0, dn-1]

For example, given the number 2024, we are currently enumerating 1abc, which is the first enumerated 1, then the following can be taken arbitrarily, such as 19bc, 18bc or 199c or 1999, etc., which are all less than 2024

Then it can be generalized: for the K-base number an-2..., if the prefix pre has been enumerated , and the jth position of the enumeration is currently used, lim is used to represent whether pre is less than dn-1...dj-1. If it is true, then:

  • lim is true, then aj ∈ [0, K-1]
  • lim is false, then an-1 ∈ [0, dj]

prefix status

The so-called prefix refers to the enumerated prefix pre mentioned earlier, but the prefix status must be combined with the specific conditions of the question. For example, if our example requires us to find a non-decreasing number, then we only need to record the previous digit of the current enumeration position as pre, that is, Yes, because we only need the previous digit to determine what range of numbers we are currently taking to ensure non-degradation.

Similarly, if we ask to find the number of numbers that do not contain consecutive 1's in the interval, then pre only needs two states of 0/1 to represent it.

status analysis

There is a certain transitive relationship between the numbers. We try to use dynamic programming to reduce the time complexity of the brute force solution, so it is natural to perform the analysis step of dynamic programming.

state design

Design states are about breaking down big problems into smaller ones.

It’s still an example - no descending problem. During our bitwise enumeration process, if the prefix status is pre, there are still n bits left that are not enumerated. We know that pre will limit the enumeration range of the remaining n bits. Also That is to say, the number of non-decreasing numbers with a length of n digits under the limit of pre is the number of non-decreasing numbers with a length of n digits under the prefix status. As we said before, whether pre is equal to the prefix can be expressed as lim.

Then we might as well design the state f(n,pre,lim), which means that the remaining n bits still need to be enumerated. The prefix state is pre. Whether the prefix has been enumerated is less than the given number x and whether the corresponding prefix is ​​represented by lim (lim is true to mean that it has been The non-decreasing number when the enumeration prefix is ​​smaller than the corresponding prefix of x). A detailed analysis of the three dimensions:

  • n means that the remaining n bits are not enumerated, n means the high bit, such as 201234, 1 is the 4th bit
  • pre is the prefix status. For example, we are currently enumerating 1123xxx, and x represents the unenumerated bit, then the prefix status is the last digit of the enumerated prefix, which is 3
  • lim represents whether the enumerated prefix is ​​less than the corresponding prefix of Any number of bits can be smaller than the given number. For example, given the decimal number 45521, and the current enumeration prefix is ​​3xxxx, then even if all 9s are taken later, which is 39999, there will still be 39999 < 45521

state transfer

We represent the given decimal number x as follows:
x = dndn − 1 . . . d 1 , where dn represents the highest bit and d 1 represents the lowest bit. {1}, where dn represents the highest bit and d1 represents the lowest bitx=dndn1...d1, where d n represents the highest bit and d 1 represents the lowest bit
. Then there is the following state transition equation:
f ( n , pre , lim ) = ∑ k = preceilf ( n − 1 , k , lim or k < ceil ) f (n, pre,lim) = \sum_{k = pre}^{ceil}f(n-1,k,lim \, or \, k < ceil)f(n,pre,lim)=k = p receilf(n1,k,limork<ceil)

  • k represents the number taken at the nth digit. Since it is guaranteed not to degrade, it starts from pre and does not exceed the maximum value ceil, where ceil
    • If lim = true, then ceil = 9
    • Otherwise, ceil = d[n]
  • The third dimension (lim or k < ceil) in the summation formula in the transfer equation can also be changed to (lim or k < d[n]). The effect is the same. Readers can think about it themselves.

initial state

For the above state transition equation, we can solve it recursively. Then the initial state at this time is our recursive exit when n = 0, that is, there are 0 bits left to enumerate, so the non-decreasing number at this time is that we have enumerated prefix, just return 1 directly.

Memoized search

We found that if we solve a problem, there will be many overlapping sub-problems, which leads to a lot of repeated calculations, so we can prune through memoization, that is, use a three-dimensional array f[n][pre][lim] to save the solved problems out status.

Let’s look at the recursion tree below.

The external link image transfer failed. The source site may have an anti-leeching mechanism. It is recommended to save the image and upload it directly.

For the third dimension [lim] of our memoized array, there are only two values ​​0/1, and according to our definition of lim, when lim is false, it means the prefix of the previous enumeration and the prefix x of the given number Consistent, there is only one path on the corresponding recursive tree (the blue path on the far right in the figure), so since the third dimension has only two values, and one value corresponds to a unique recursive path, then We might as well optimize out the third dimension and only remember the situation where the third dimension is true, that is, the red node in our diagram is pruned

And if you require a blue path, just ask for it directly. In this way we optimize three dimensions into one dimension.

At this point, we only need to use the two-dimensional array f[n][pre] to represent the remaining n bits, the non-decreasing number when the prefix status is pre.

For our recursive function, if the recursive function parameter is f(n, pre, false), then ordinary deep search is used. If it is f(n, pre, true), then memorized search is used.


Example code implementation

Our code implementation is divided into three steps:

  • State initialization
  • Digital initialization
  • Memoized search

State initialization

Initialize the f array to -1, which means it has not been visited

int d[N] {0}, cnt;
int f[N][N] {0}; // 剩余i位,上一位为j

int dfs(int n, int pre, bool lim) {
    //……
}

int nondec(int x) {
	//……
}

int main() {
	//……
    memset(f, -1, sizeof(f));
	//……
    return 0;
}

Digital initialization

That is, the numbers on each digit of the given number x are stored in the corresponding subscript array.

int nondec(int x) {
    cnt = 0;

    while (x)
        d[++cnt] = x % 10, x /= 10;

    return dfs(cnt, 0, false);
}

Memoized search

Memoized search is also our core code, as follows:

int dfs(int n, int pre, bool lim) {
    if (n == 0)
        return 1;								//(1)

    if (lim && ~f[n][pre])
        return f[n][pre];						 //(2)

    int res = 0, ceil = lim ? 9 : d[n];			  //(3)

    for (int i = pre; i <= ceil; i++)
        res += dfs(n - 1, i, lim || i < ceil);	   //(4)

    if (lim)
        f[n][pre] = res;					     //(5)

    return res;
}

The dfs function can be divided into 5 blocks:

  1. The recursive exit when n = 0, each digit has been enumerated and can enter the function, indicating that it is satisfied without decreasing the number and returns 1 directly.
  2. If lim = true (the part of our recursive tree pruning) and it has already been accessed, access the saved state directly
  3. Find the upper limit ceil of the enumerated digits based on lim. If lim is true, then the upper limit is the base minus one. The corresponding decimal here is 9. Otherwise, the upper limit is the corresponding digit d[n].
  4. Recurse to the sub-problem and pass in the new state and new lim according to the enumerated digits
  5. If lim is true, then we perform memory search and pruning

How to implement non-recursion?

Since recursion can be used to solve the problem, there is naturally a corresponding recursive solution. The recursive solution is nothing more than a bottom-up solution process.

Let’s still take the example of non-decreasing as an example. Our state definition is still f[n][pre] represents n bits and the non-decreasing number when the previous bit is pre, then there is still a state transition equation:
f [ n ] [ pre ] = ∑ k = preceilf [ n − 1 ] [ k ] f[n][pre] = \sum_{k = pre}^{ceil}f[n-1][k]f[n][pre]=k = p receilf[n1 ] [ k ]
However, we found that if we follow the initialization method of memorized search, we cannot get the correct answer. Therefore, for digital dp, memorized search and recursive solution have similar ideas, but there are still certain differences in the implementation methods. of.

state design

Define the state f[i][j] as the number of non-decreasing numbers with a total of i bits and the highest bit being j . This is very similar to the state definition of memorization search.

You can also use the same state definition as memorized search, but it is more convenient to modify the state definition.

state transfer

f [ i ] [ j ] = ∑ k = j 9 f [ i − 1 ] [ k ] f[i][j] = \sum_{k = j}^{9}f[i-1][k] f[i][j]=k=j9f[i1][k]

It is easy to understand that for the highest bit to be j, its next bit must start from j. We find that the upper limit here is directly given to 9 instead of ceil because our state definition is not the same as in the memory search solution. lim is strongly related to pre, but simply corresponds to the bit length and the non-decreasing number of the highest bit.

Algorithm principle

For non-decreasing numbers that do not
exceed − 1 ] − 1 f [ n − 1 ] [ i ] + ∑ i = d [ n − 1 ] d [ n − 2 ] − 1 f [ n − 2 ] [ i ] + … … nondec(x) = \ sum_{i=0}^{d[n]-1}f[n][i] +\sum_{i=d[n]}^{d[n-1] - 1}f[n-1] [i] + \sum_{i=d[n-1]}^{d[n-2] - 1}f[n-2][i]+……nondec(x)=i=0d[n]1f[n][i]+i=d[n]d[n1]1f[n1][i]+i=d[n1]d[n2]1f[n2][i]+...
Explanation: We still enumerate according to the digits from high to low. If the nth digit (the highest digit) is less than d[n], the corresponding non-decreasing number is obviously less than x. If the nth digit is equal to d[n], then Some of the corresponding non-decreasing numbers may be greater than x, so we can only add the part that is less than or equal to x. In order to ensure that it is less than or equal to x, we start counting from d[n] with the nth digit as d[n] and the second digit. Take, and so on...

Algorithm implementation

initialization
#define N 15
int f[N][N]{0}; // 一共有i位。最高位为j的不降数个数
void init()
{
    for (int i = 0; i < 10; i++)
        f[1][i] = 1;
    for (int i = 2; i < N; i++)
        for (int j = 0; j <= 9; j++)
            for (int k = j; k <= 9; k++)
                f[i][j] += f[i - 1][k];
}
Recursive solution

Translate the recursive formula into code, start enumerating from the high position, and the lower limit of each enumeration is the upper limit of the previous digit pre

int nondec(int x)
{
    if (!x)
        return 1;
    cnt = 0;
    while (x)
        d[++cnt] = x % 10, x /= 10;

    int res = 0, pre = 0;
    for (int i = cnt; i >= 1; i--)
    {
        int now = d[i];
        for (int j = pre; j < now; j++)
            res += f[i][j];

        if (now < pre)
            break;
        pre = now;
        if (i == 1)
            res++;
    }
    return res;
}

OJ Lectures

Good Numbers

Problem - 4722 (hdu.edu.cn)

The definition of a good number here is that the sum of each digit is 10, then our prefix status can still be represented by one digit, that is, the sum of the enumerated digits is remainder 10

n = 0 recursive exit requires conditional judgment. If the prefix status is 0, 1 can be returned, otherwise 0 is returned.

The rest of the questions are exactly the same as the example questions, so the digital DP can be said to be a template question, with the only difference being the recursive exit and state design.

AC code

#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
#define int long long
#define N 20

int d[N], cnt, t, a, b, idx = 0;
int f[N][10]; // 剩余i位,上一位为j

int dfs(int n, int pre, bool lim)
{
    if (n == 0)
        return !pre;
    if (lim && ~f[n][pre])
        return f[n][pre];

    int res = 0, ceil = lim ? 9 : d[n];
    for (int i = 0; i <= ceil; i++)
        res += dfs(n - 1, (i + pre) % 10, lim || i < d[n]);

    if (lim)
        f[n][pre] = res;
    return res;
}

int goodnum(int x)
{
    cnt = 0;
    while (x)
        d[++cnt] = x % 10, x /= 10;

    return dfs(cnt, 0, false);
}

signed main()
{
    // ios::sync_with_stdio(false);
    // cin.tie(nullptr), cout.tie(nullptr);
    // freopen("in.txt", "r", stdin);
    // freopen("out.txt", "w", stdout);
    memset(f, -1, sizeof(f));

    scanf("%lld", &t);

    while (t--)
    {
        scanf("%lld%lld", &a, &b);
        int ans = goodnum(b) - goodnum(a - 1);
        printf("Case #%lld: %lld\n", ++idx, ans);
    }
    return 0;
}


Don't 62

Problem - 2089 (hdu.edu.cn)

For this problem, we found that the number cannot have 4 or 62 number pairs, and the restriction became two. We can use three decimal digits to represent the prefix status, but it is not necessary, because for the illegal status, which is a non-target number, we Just stop the recursion and return 0, so we still use one bit to store the state, that is, the previous number.

This requires us to design an illegal state to mark, and exit the recursion directly if it is illegal.

For the sake of simplicity and readability of the code, we encapsulate the code segment for obtaining the status. The rest is the same as the example.

We found that different topics of digital dp only have differences in prefix state design and subtle differences in recursive exports.

AC code

#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
#define int long long
#define N 11
int inf = -123456;
int d[N], cnt, t, a, b, idx = 0;
int f[N][10]; // 剩余i位,上一位为j

int getnxt(int pre, int digit)
{
    if (pre == inf || digit == 4 || (pre == 6 && digit == 2))
        return inf;
    return digit;
}

int dfs(int n, int pre, bool lim)
{
    if (pre == inf)
        return 0;
    if (n == 0)
        return 1;

    if (lim && ~f[n][pre])
        return f[n][pre];

    int res = 0, ceil = lim ? 9 : d[n];
    for (int i = 0; i <= ceil; i++)
    {
        res += dfs(n - 1, getnxt(pre, i), lim || i < d[n]);
    }
    if (lim)
        f[n][pre] = res;
    return res;
}

int goodnum(int x)
{
    cnt = 0;
    while (x)
        d[++cnt] = x % 10, x /= 10;

    return dfs(cnt, 0, false);
}

signed main()
{
    // ios::sync_with_stdio(false);
    // cin.tie(nullptr), cout.tie(nullptr);
    // freopen("in.txt", "r", stdin);
    // freopen("out.txt", "w", stdout);
    memset(f, -1, sizeof(f));

    while (cin >> a >> b, a || b)
    {
        cout << goodnum(b) - goodnum(a - 1) << '\n';
    }
    return 0;
}


non-negative integers not containing consecutive ones

600. Non-negative integers not containing consecutive 1’s

Have you ever felt that the hard questions on leetcode seem so easy?

The target number cannot contain consecutive 1's, so we still use one bit to save the prefix state. We can only recurse the legal state. It is very simple.

AC code

int f[32][32] , d[32] , cnt = 0;
class Solution {
public:
    Solution()
    {memset(f , -1 , sizeof(f));}
    int dfs(int n , int pre , bool lim)
    {
        if(!n) return 1;
        if(lim && ~f[n][pre]) return f[n][pre];
        int ceil = lim ? 1 : d[n];
        int res = 0;
        for(int i = 0 ; i <= ceil ; i++)
            if(pre == 1 && i == 1) continue;
            else res += dfs(n - 1 , i , lim || i < ceil);
        if(lim)
        f[n][pre] = res;
        return res;
    }
    int getnum(int x)
    {
        cnt = 0;
        while(x) d[++cnt] = x % 2 , x /= 2;
        return dfs(cnt , 0 , false);
    }
    int findIntegers(int n) {
        return getnum(n);
    }
};

Summarize

Digital DP is actually a state machine DP with a small number of states. Different problems can be solved with a set of templates, but attention should be paid to the design of the prefix state and the processing of recursive exits.

Guess you like

Origin blog.csdn.net/EQUINOX1/article/details/135330779