902. 最大为 N 的数字组合
题目描述
给一个非递顺序排列的数组数组,可以用任意次数组中的元素来写数字,
问能生生成的<=给定整数n的个数?
其中数组中的值都不同,数组长度在1-9之间。
基本分析
- 这是啥类型的题?之前没有遇到过,经典的数位dp的题
- 涉及到任意区间合法数查询时考虑怎么做,这个题有啥简化的地方?
- 可以假定存在函数dp[x],能返回区间[1,x]内的合法数个数,可以配合容斥原理,得到任意区间合法数的查询ans[l, r]=dp[r]-dp[l-1]
- 在这个题中,查询的左端点是固定的1,同时dp[x]=0,答案就是dp[x]
- 考虑求dp[x]的时候,组成[1, x]的合法数可以怎么进行分类?
- 位数和x相同,最高位<x最高位,记为res1
- 位数和x相同,最高位=x最高位,记为res2
- 位数< x,记为res3
- 以上分类的数据分别可以怎么进行处理?
- 假如数字是x,数字的长度是n,数组长度m,找到最高位<x最高位的可取的个数n_h,剩下的位数n-1可以看做是n-1个坑,每个坑有m中放法,那么个数就是\(n_h * m^{n-1}\),这里需要注意break
- 当首位相等的时候,需要看后面的位数。这里以第k位举例。假如x的第k为为cur,在nums中能找到的<=cur值的下标是r,同样需要分类讨论
- nums[r]=cur,这个时候k位置有r个选择,后面的位置有n-k-1个坑,每个坑m个选择
- nums[r]<cur,这个时候k位置有r+1个选择,后面的位置有n-k-1个坑。需要注意的是由于nums[r]<cur,往后的的方案数已经被这次统计完成了,累加后要break。
- nums[r]>cur, 这个分支不再能满足要求,合法数为0。
- 当位数<x的时候,分别计算1到n-1的可能,进行累加
代码
数位dp+二分
class Solution:
def atMostNGivenDigitSet(self, digits: List[str], n: int) -> int:
x = n
nums = [int(c) for c in digits]
x_list = [int(c) for c in str(x)]
# n表示整数x的长度,m表示可选数组长
n, m = len(x_list), len(nums)
ans = 0
# 下一步需要考虑数字长度和n的关系,先遍历解决=n,再遍历解决<n
# 数字长度=n情况
for i in range(n):
cur = x_list[i]
# nums中找<=cur的位置
l, r = 0, m-1
while l < r:
mid = l + r + 1 >> 1
if nums[mid] <= cur:
l = mid
else:
r = mid - 1
# 当前位>情况,直接退出
if nums[l] > cur:
break
# 当前位为=情况,累加
elif nums[l] == cur:
# 当前位置为l个,后面有n-1-l个坑,每个坑m个选择
ans += l * math.pow(m, n-1-i)
# 当前位置如果是最后一位,需要+1
if i == n-1:
ans += 1
# 当前为<情况,累加
elif nums[l] < cur:
#当前位置为l+1个
ans += (l+1) * math.pow(m, n-1-i)
break
# 处理长度<n的情况,从1位开始累加
# 这里的last实际求的是m**(i-1)
last = 1
for i in range(1, n):
cur = last * m
ans += cur
last = cur
return int(ans)
复杂度
时间:因为nums数组长度最大是9,因此二分复杂度可以忽略,整体复杂度是n的长度,因此\(O(log n)\)
空间:\(O(C)\)
总结
- 在代码实现上,通过从高到低遍历每一位,将res1和res2放到了一起解决。逻辑上是
- i=0时,拿到首位cur,nums中找满足<=cur的值
- 如果 >直接退出
- 如果< 算完直接退出
- 如果等于,算完这一位,去考虑第二位i=1的情况
- i=1时类似于上面的步骤,继续进行
- i=0时,拿到首位cur,nums中找满足<=cur的值
- 处理长度在1到n-1的数字时候,直接进行小到大的累加
- 在处理nums[r]=cur的时候,注意有个i==n-1的判断?因为在最后一位的时候,前面公式只是统计了<的情况;如果到了最后一位,=也是能满足要求的。
- 二分的时候,是找<=x的最大值,用的是二分方法2,定义mid=l+r+1>>1, 找到的l也可能不满足要求(最左边本来就比cur大)

浙公网安备 33010602011771号