Featured image of post 数位DP

数位DP

动态规划

# 前言

先将数转为字符串s,记忆化搜索,枚举s的每一位

一般有4个参数:pos, state, is_limit, is_num

  • pos 表示当前枚举到哪一位,必须的
  • state 表示枚举到当前位时,之前的位的一些状态,可以是已选取数字集合等等,不是必须的,视题中的限制而决定
  • is_limit 表示枚举到当前位时,之前的位是否达到了上限,比如n=1234,pos=3,此时is_limit=true表示前面枚举的是123。这一项决定枚举当前位的上限数字
  • is_num 表示枚举到当前位时,之前的位是否表示一个合法的数,用来处理前导0的情况。如果is_num=true,那么当前位可从0开始枚举;如果is_num=false,那么当前位要从1开始枚举(后续is_num为true),或者跳过(后续is_num仍为false)
1
2
3
4
5
6
7
//is_limit要初始化为true,不然后面都会为false

//is_num初始化为false

//is_limit为true时不会存在重复运算,不会重复访问一个状态

//is_num为false时前面都为空,也不会存在重复运算,也不需要记忆化

# 至少有1位重复的数字

# 题目

给定正整数 n,返回在 [1, n] 范围内具有 至少 1 位 重复数字的正整数的个数。

示例 1:

输入:n = 20 输出:1 解释:具有至少 1 位重复数字的正数(<= 20)只有 11 。 示例 2:

输入:n = 100 输出:10 解释:具有至少 1 位重复数字的正数(<= 100)有 11,22,33,44,55,66,77,88,99 和 100 。 示例 3:

输入:n = 1000 输出:262

提示:

1 <= n <= 10^9

来源:力扣(LeetCode) 链接:https://leetcode.cn/problems/numbers-with-repeated-digits 著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

# 解题思路

求含至少1位重复数字的正整数的个数,从反面更容易求,即求不含重复数字的正整数的个数x,那么结果即为n-x

state表示前面所填数字的集合,用位来记录

# 代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
class Solution {
public:
    int numDupDigitsAtMostN(int n) {
        string num = to_string(n);
        int m = num.size(), dp[m][1<<10];
        memset(dp,-1,sizeof(dp));
        //从pos开始填数字,pos前面填的数字的集合是state,能构造出的不含重复数字的正整数的个数
        //is_limit 表示前面填的数字是否都是n对应位上的,如果为true,那么当前位至多为int(num[i]),否则至多为9
        //is_num 表示前面是否填了数字(是否跳过),如果为true,那么当前位可以从0开始,如果为false,那么可以跳过,或者从1开始填数字(不能前导0)

        function<int(int,int,bool,bool)> dfs =
            [&](int pos, int state, bool is_limit, bool is_num)->int{
            if(pos == m) return is_num;
            if(!is_limit && is_num && dp[pos][state]>=0) return dp[pos][state];
            int res=0;
            //如果前面都没填,第i位明显不受限制,如n=123,第0位没填的话,第1位可填9
            if(!is_num) res = dfs(pos+1,state,false,false);//可以跳过,不填数字
            //根据is_num的值决定从0开始还是1开始,上界由is_limit决定
            //枚举要填的数字
            for(int d=1-is_num,up=is_limit?num[pos]-'0':9;d<=up;d++){
                if((state>>d &1)==0){//只有当前填的数字与之前填的数字没有重复时 才填
                    res += dfs(pos+1,state|(1<<d),is_limit&&d==up,true);
                }
            }
            if(!is_limit && is_num) dp[pos][state] = res;
            return res;
        };
        return n - dfs(0,0,true,false);
        //is_limit要初始化为true,不然后面都会为false
        //is_num初始化为false
        //is_limit为true时不会存在重复运算,不会重复访问一个状态
        //is_num为false时前面都为空,也不会存在重复运算,也不需要记忆化
    }
};

# 最大为 N 的数字组合

# 题目

给定一个按 非递减顺序 排列的数字数组 digits 。你可以用任意次数 digits[i] 来写的数字。例如,如果 digits = [‘1’,‘3’,‘5’],我们可以写数字,如 ‘13’, ‘551’, 和 ‘1351315’。

返回 可以生成的小于或等于给定整数 n 的正整数的个数 。

示例 1:

输入:digits = [“1”,“3”,“5”,“7”], n = 100 输出:20 解释: 可写出的 20 个数字是: 1, 3, 5, 7, 11, 13, 15, 17, 31, 33, 35, 37, 51, 53, 55, 57, 71, 73, 75, 77. 示例 2:

输入:digits = [“1”,“4”,“9”], n = 1000000000 输出:29523 解释: 我们可以写 3 个一位数字,9 个两位数字,27 个三位数字, 81 个四位数字,243 个五位数字,729 个六位数字, 2187 个七位数字,6561 个八位数字和 19683 个九位数字。 总共,可以使用D中的数字写出 29523 个整数。 示例 3:

输入:digits = [“7”], n = 8 输出:1

提示:

1 <= digits.length <= 9

digits[i].length == 1

digits[i] 是从 ‘1’ 到 ‘9’ 的数

digits 中的所有值都 不同

digits 按 非递减顺序 排列

1 <= n <= 10^9

来源:力扣(LeetCode) 链接:https://leetcode.cn/problems/numbers-at-most-n-given-digit-set 著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

# 解题思路

限制条件为只能用给定的数字

用不到state

注意,这题是需要is_num的,虽然给定的数字中不会出现0,如果没有is_num,将没办法跳过高位数字,若n=2345,那么千位必须要填一个数,实际上是可以跳过的,所以需要is_num位标记

# 代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class Solution {
public:
    int atMostNGivenDigitSet(vector<string>& digits, int n) {
        auto s = to_string(n);
        int m = s.size();
        vector<int> dp(m,-1);
        set<int> ok;
        for(auto &x: digits) ok.insert(stoi(x));
        function<int(int,bool,bool)> f = [&](int pos, bool is_limit, bool is_num)->int{
            if(pos == m) return is_num;
            if(!is_limit && is_num && dp[pos]>=0) return dp[pos];
            int res = 0;
            if(!is_num) res = f(pos+1, false, false);
            for(int d=1-is_num,up=is_limit?s[pos]-'0':9;d<=up;d++){
                if(ok.count(d)){//只用给定的数字
                    res += f(pos+1, is_limit && d==up, true);
                }
            }
            if(!is_limit && is_num) dp[pos] = res;
            return res;
        };


        return f(0,true,false);
    }
};

# 2出现的次数

# 题目

编写一个方法,计算从 0 到 n (含 n) 中数字 2 出现的次数。

示例:

输入: 25 输出: 9 解释: (2, 12, 20, 21, 22, 23, 24, 25)(注意 22 应该算作两次) 提示:

n <= 10^9

来源:力扣(LeetCode) 链接:https://leetcode.cn/problems/number-of-2s-in-range-lcci 著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

# 解题思路

state位需要,记录前面2出现的次数即可

is_num位不必须,就算考虑了前导0的情况,也不影响2出现的次数

# 代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Solution {
public:
    int numberOf2sInRange(int n) {
        auto s = to_string(n);
        int m = s.size();
        int dp[m][m];
        memset(dp,-1,sizeof(dp));
        function<int(int,int,bool)> f = [&](int pos, int cnt2, bool is_limit)->int{
            if(pos == m) return cnt2;
            if(!is_limit && dp[pos][cnt2]>=0) return dp[pos][cnt2];
            int res = 0;
            
            int up = is_limit?s[pos]-'0':9;
            for(int d=0;d<=up;d++){
                res += f(pos+1, cnt2 + (d==2), is_limit&&d==up);//只有当前位是2时,2出现的次数+1
            }
            if(!is_limit) dp[pos][cnt2] = res;
            return res;
        };
        return f(0,0,true);
    }
};

# 不含连续1的非负整数

# 题目

给定一个正整数 n ,返回范围在 [0, n] 都非负整数中,其二进制表示不包含 连续的 1 的个数。

示例 1:

输入: n = 5 输出: 5 解释: 下面是带有相应二进制表示的非负整数<= 5: 0 : 0 1 : 1 2 : 10 3 : 11 4 : 100 5 : 101 其中,只有整数3违反规则(有两个连续的1),其他5个满足规则。 示例 2:

输入: n = 1 输出: 2 示例 3:

输入: n = 2 输出: 3

提示:

1 <= n <= 10^9

来源:力扣(LeetCode) 链接:https://leetcode.cn/problems/non-negative-integers-without-consecutive-ones 著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

# 解题思路

注意题中限制是二进制表示中的限制,故先求出其二进制表示

求不包含连续1的情况

可以先求包含连续1的情况,再拿n+1减

state是需要的,记录出现的连续的1的情况

is_num不必需,不影响连续的1

# 代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
class Solution {
public:
    int findIntegers(int n) {
        string s;
        //高位到低位的二进制表示
        for(int i=31;i>=0;i--) s.push_back(((n>>i)&1)+'0');
        
        int m = s.size();
        int dp[m][4];
        //state==2 前面的数中已经出现了连续的1
        //state==1 前面的数中还没出现连续的1,但前一个数正好是1
        //state==0 前面的数中还没出现连续的1,且前一个数也不是1
        memset(dp,-1,sizeof(dp));
        function<int(int,int,bool)> f = [&](int pos, int state, bool is_limit)->int{
            if(pos == m) {
                return state == 2;
            };
            if(!is_limit &&dp[pos][state]>=0) return dp[pos][state];
            int res = 0;
            
            int up = is_limit?s[pos]-'0':1;
            for(int d = 0; d<=up; d++){
                int nxt;
                //根据之前状态与当前位是否是1,决定下一状态
                if(state==2 || (state==1 && d==1)) nxt = 2;
                else if(d == 1) nxt = 1;
                else nxt = 0;
                res += f(pos+1, nxt, is_limit && d==up);
            }
            if(!is_limit) dp[pos][state] = res;
            return res;
        };
        //二进制表示中含连续的1的个数为f,所求即为n+1-f
        return n+1-f(0, 0, true);
    }
};

# 数字1的个数1

# 题目

给定一个整数 n,计算所有小于等于 n 的非负整数中数字 1 出现的个数。

示例 1:

输入:n = 13 输出:6 示例 2:

输入:n = 0 输出:0

提示:

0 <= n <= 10^9

来源:力扣(LeetCode) 链接:https://leetcode.cn/problems/number-of-digit-one 著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

# 解题思路

state记录数字1出现的个数即可

is_num不必须,不影响1出现的个数

# 代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
class Solution {
public:
    int countDigitOne(int n) {
        auto s = to_string(n);
        int m = s.size();
        int dp[m][m];//cnt: 数字1出现的个数
        memset(dp,-1,sizeof(dp));
        function<int(int,int,bool)> f = [&](int pos,int cnt,int is_limit)->int{
        	if(pos == m) return cnt;
        	if(!is_limit && dp[pos][cnt]>=0) return dp[pos][cnt];
        	int res = 0;
        	int up = is_limit?s[pos]-'0':9;
        	for(int d=0;d<=up;d++){
            	res += f(pos+1, cnt+(d==1), is_limit&&d==up);
        	}
        	if(!is_limit) dp[pos][cnt]=res;
        	return res;
    		};
        return f(0,0,true);
    }
};
Built with Hugo
Theme Stack designed by Jimmy