明七暗七(数位dp-二分查找 + dfs)

1. 问题描述:

今天是个特殊的日子,CSL和他的小伙伴们围坐在一张桌子上玩起了明七暗七的游戏。游戏规则是这样的:一个人报出一个起始数,接下来按照逆时针的顺序轮流报数,如果碰到数是7的倍数或含有7,则拍手,下一个人接着报数。直到有一个人报错了数字或者没有及时拍手为止。玩游戏嘛,当然得有惩罚。这么简单的游戏对CSL的学霸小伙伴而言实在是太无脑了,轻轻松松数到上万根本不在话下。但是对于数学是体育老师教的CSL来说,实在是太难了。快帮他算算什么时候应该拍手吧。

输入描述:

输入两个整数m和n。(1 ≤ m, n ≤ 10 ^ 12)

输出描述:

输出一个整数,表示m以后第n个需要拍手的数字。

示例1

输入
30 7
输出
57

示例2
输入
56 1
输出
57

输出链接:https://ac.nowcoder.com/acm/problem/17867
来源:牛客网

2. 思路分析:

① 这道题目是关于数位dp的内容,一开始的时候并不知道有这个知识点,于是在网上查找了相关视频的内容和代码。数位dp的题目通常需要统计一个区间[L,R]内满足某些限制条件的个数或者是关于某个区间中某些数字的位之间满足的关系的数目。感觉比较常规的解决方法是先求解出数字x各个位上的数字,并且将这些数字存储到一个列表或者是数组中,然后结合题目给出的位上数字的限制条件来依次搜索每一个位置上可能的数字,牛客中对于这道题目的题解也是基于dfs搜索每一位上可能的数字解决的,下面是我对于题解思路的理解,后面将题解中的c++代码修改为了python代码。

② 因为数字m之后的数字越大那么能够找到数字中的位含有7或者是能够被7整除的数就越多,所以我们我们利用递增的特点使用二分查找进行优化,二分查找的左边界为m + 1,右边界为m + 7 * (n + 1),通过二分查找出[0, mid]区间中数中的位中包含7或者是能够被7整除的个数,减去在[0, m]区间中数中的位中包含7或者是能够被7整除的个数判断两者之间的差值是否大于等于n来更新左右边界,在更新左右边界的时候使用一个变量res来记录下答案。最核心的方法是dfs递归搜索的方法,可以声明一个20 * 10 * 2的三维列表来记录递归过程的结果(初始化的值为-1),也即记忆化搜索减少重复性子问题的求解,一个长度为20的长度一维列表用来记录数字中各个位上的数字。这两个变量可以作为全局的变量这样在递归方法调用的时候可以很方便的修改。递归方法中需要传递的参数有以下几个:当前递归的位置(我们可以从高位开始递归)pos,到当前位置中除以7的余数的变量mod(最后到pos = 0的位置就可以判断出pos-0填的数字是否可以被7整除),用来记录数位中是否包含7的变量have,用来控制当前可以尝试填的数字的变量flag。递归的思想是尝试从数字的高位开始填数字一直到最后的最低位,对于当前的位置pos尝试可以填的数字,所以可以需要使用for循环尝试当前位置可以填的数字,在递归方法中传递flag变量的目的就是为了能够根据flag变量来确定当前位置可以填的数字范围,一般是判断当前的位置是否是最高位,如果是最高位那么往下递归的时候也即低一位需要可以填的数字为[0,x],x为一开始计算的mid中对应的位置上的数字,其实举一个例子就很好理解了。比如二分查找mid = 123,递归的时候计算的是[0, 123]中满足题目条件的数目,从高位1这个位置开始填,对于第一位可以填的数字为0,1,当第一位数字为0的时候那么后面低位的数字可以随便填也就是0-9的数字,当第一位为1的时候那么第二位填的数字就只能是0,1,2,也即不能够超过2,而2就是mid第二位的数字,对于每一个位置上的数字都是这样填的,所以在递归方法中使用这个flag标志就很好理解了,通过判断当前位是否是最高位决定下一位可以填的数字的范围是[0,9]还是[0,x]。我们在递归的时候就可以累加每一次递归的结果,这样就可以求解出递归当前位置中尝试的可能数字的结果。因为是有返回值的递归方法所以我们可以使用一个列表来记录中间求解的值,因为方法中主要涉及到三个动态变化的参数,所以声明20 * 10 * 2三维的列表来记录递归的结果即可,在递归方法的看是判断当前对应的列表位置是否之前求解过如果求解过那么直接返回列表记录的值即可。在方法中计算余数的公式为:(mod * 10 + i) % 7,其实也很好理解,比如148,前两位填的数字为14那么mod = 0在填第三位的时候i = 8那么余数为1,相当于是8 % 7所以我们在计算当前位置的余数的时候可以对7取余。

③ 总的来说是就是借助于二分查找 + dfs搜索[0,mid]区间上的满足题目要求的数目,dfs中使用for循环尝试从高位开始填数字,使用一个flag变量记录当前的位置是否是最高位上的数字(数位dp中使用dfs搜索的套路,都是需要这样一个变量控制可填数字范围),通过这个变量来控制下一位可以填的数字范围。整个过程其实还是典型的递归,只是借助于了二分查找与列表记录递归结果进行了优化。

3. 代码如下:

牛客题解的c++代码:

#include <bits/stdc++.h>
using namespace std;
#define int long long
int f[20][10][2];
int a[20];
int dp(int pos,int mod,bool have,bool flag)
{
    if(pos==0) return mod%7==0||have;
    if(flag&&f[pos][mod][have]!=-1) return f[pos][mod][have];
    int x=flag?9:a[pos];
    int ans=0;
    for(int i=0;i<=x;i++)
    {
        ans+=dp(pos-1,(mod*10+i)%7,have||i==7,flag||i<x);
    }
    if(flag) f[pos][mod][have]=ans;
    return ans;
}
int cul(int x)
{
    int pos=0;
    while(x)
    {
        a[++pos]=x%10;
        x/=10;
    }
    return dp(pos,0,false,false);
}
signed main()
{
    memset(f,-1,sizeof(f));
    int m,n;
    scanf("%lld%lld",&m,&n);
    int l=m+1,r=m+7*(n+1);
    int mid;
    int p=cul(m);
    while(l<r)
    {
        mid=(l+r)>>1;
        if(cul(mid)-p>=n) r=mid;
        else l=mid+1;
    }
    printf("%lld\n",l);
}

python代码:

from typing import List

# nums用来存储数字的各个位置上的数字
nums = [0] * 20
# rec是记录列表
rec = [[[-1] * 2 for i in range(10)] for j in range(20)]


# 从高位开始递归
def dfs(pos: int, mod: int, have: bool, flag: bool):
    if pos == 0:
        return 1 if mod % 7 == 0 or have else 0
    if flag and rec[pos][mod][have] != -1: return rec[pos][mod][have]
    # end 表示当前位置可以填的数字范围: 通过高位传递的flag变量判断上一位是否是最高位
    end = 9 if flag else nums[pos]
    res = 0
    for i in range(end + 1):
        # 累加当前位置可能数字的递归值
        res += dfs(pos - 1, (mod * 10 + i) % 7, have or i == 7, flag or i < end)
    if flag: rec[pos][mod][have] = res
    return res


# 求解x中各个位置上的数字
def cal(x: int):
    pos = 1
    while x:
        nums[pos] = x % 10
        x //= 10
        pos += 1
    return dfs(pos - 1, 0, False, False)


if __name__ == '__main__':
    m, n = map(int, input().split())
    l, r = m + 1, m + 7 * (n + 1)
    # [0:m]中能够被7整除或者是包含7的数目
    p = cal(m)
    res = 0
    while l <= r:
        mid = (l + r) // 2
        if cal(mid) - p >= n:
            res = mid
            r = mid - 1
        else:
            l = mid + 1
    print(res)

 

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值