CF1985G D-Function 题解

day11 数位 dp 总结

数位 dp 的定义:所谓数位 dp 也就是根据一个数的每一位来做动态规划,而数位 dp 的题目不会像有些题目,需要你来仔细看需要用到什么算法,而数位 dp 的特点很显著。第一,一般情况下肯定是要往数字每一位数上去考虑的,第二,也就是数据范围,好一点的,可能 n ≤ 1 0 9 n \leq 10 ^ {9} n109 或者 n ≤ 1 0 18 n \leq 10 ^ {18} n1018,狠一点的直接干到 n ≤ 1 0 2000 n \leq 10 ^ {2000} n102000,这样就更加明显了,不可往数字的大小上看,必定得往数字的各个位数上考虑。

例题:

P2657 [SCOI2009] windy 数

题目大致意思:

不含前导零且相邻两个数字之差至少为 2 2 2​ 的正整数被称为 windy 数。windy 想知道,在 a a a​ 和 b b b​ 之间,包括 a a a​ 和 b b b​ ,总共有多少个 windy 数?

题目大致思路:

这道题目很显然数位 dp,毕竟学的就是数位 dp,再接着我们可以想到一个很显然的结论,用数位 dp 处理区间 [ l , r ] [l, r] [l,r] 的区间不好处理,那么我们可以计算出 [ 0 , l − 1 ] [0, l - 1] [0,l1] 的区间和 [ 0 , r ] [0, r] [0,r] 的区间,凭感性理解一下就会得知,答案就是区间到 r r r 的答案减去区间到 l − 1 l - 1 l1 的答案。

接下来就是数位 dp 部分,我使用记忆化搜索的方法来写,当然也可以用递推的方法,我的 dfs 流程如下:

  • d p i , j dp_{i, j} dpi,j 表示数位个数为 i i i,前一位数为 j j j 时,windy 数的个数。

  • 4 4 4 个参数 n o w , l a s t , l a z y , f now, last, lazy, f now,last,lazy,f 来记录,分别表示当前数位个数,上一位数字,是否有前导 0 0 0,以及最高位是否有限制。

  • 首先,我们设定边界,当位数超过,我们当前边界的数位时,显然返回 1 1 1

  • 最高位无限制时,且当前点被记录过,直接可以返回答案,这也就是记忆化。

  • 初始化遍历范围,因为如果最高位有限制,那么我们要找到它最多能遍历到哪里,没有限制显然能够遍历到 9 9 9

  • 遍历, l a s t − i < 2 last - i < 2 lasti<2 也就是数位不满足条件,continue 跳过本次循环,这里及以下的 i i i 表示遍历的数字。

  • 有前导 0 0 0,那么就 l a z y lazy lazy 一直为 1 1 1,继续往下搜,加入答案。

  • 无前导 0 0 0,那么 l a z y lazy lazy 可以变成 0 0 0 l a s t last last 也就可以变成当前遍历的数字 i i i,继续往下搜,记录答案。

  • 最后只要他没有前导 0 0 0​,并且最高位没有限制, d p n o w , l a s t dp_{now, last} dpnow,last​​ 就可以记录答案存起来了,最终返回此答案。

代码实现:
#include<bits/stdc++.h>
#define int long long
using namespace std;
int l, r, siz, p, mn, mx;
int a[50], dp[50][50];
int dfs(int now, int last, int lazy, int f)//now表示当前数位个数,last表示前一位的数字,lazy表示是否有前导0,f表示最高位是否有限制 
{
	if(now > siz) return 1;
	if(!f && dp[now][last] > -1) return dp[now][last];
	int ans = 0, res = f ? (a[siz - now + 1]) : (9);//看看f有没有限制以确定遍历范围
	for(int i = 0;i <= res; ++ i)
	{
		if(abs(last - i) < 2) continue;//不满足条件
		if(lazy == 1 && i == 0)//还是前导0 
		{
			ans += dfs(now + 1, -5, 1, f & i == res);
		}
		else
		{
			ans += dfs(now + 1, i, 0, f & i == res);
		}
	}
	if(!lazy && !f)
	{
		dp[now][last] = ans;
	}
	return ans;
}
void add(int x)
{
	++ p;
	siz = 0;
	memset(dp, -1, sizeof dp);
	while(x > 0)
	{
		a[ ++ siz] = x % 10;//将每一位存入数组
		x /= 10;//siz表示数字位数 
	}
	if(p == 1) mn = dfs(1, -5, 1, 1);
	else mx = dfs(1, -5, 1, 1);
}
signed main()
{
	cin >> l >> r;
	add(l - 1);
	add(r);
	cout << mx - mn << "\n";//答案显然为r的答案减去l-1的答案 
	return 0;
}

UVA12517 Digit Sum

大致意思:

给定两个正整数 l , r l, r l,r,请求出 ∑ i = l r Digit(i) ⁡ \sum_{i = l}^{r}\operatorname{Digit(i)} i=lrDigit(i) Digit ⁡ \operatorname{Digit} Digit 指的是十进制下 i i i 的数位之和。

大致思路:

我们定义 d p i , j dp_{i, j} dpi,j 表示一个 i i i 位数,最高位为 j j j 的数位和,我们可以对 d p dp dp 进行预处理,但在设想转移方程前我们要知道一个式子,那么就是以 j j j 为最高位的 i i i 位数总共有 1 0 i − 1 10 ^ {i - 1} 10i1,那么我们可以得出转移方程 d p i , j = d p i − 1 , 0 / 1 / 2 / 3 / 4 / 5 / 6 / 7 / 8 / 9 + 1 0 i − 1 × j dp_{i, j} = dp_{i - 1, 0/1/2/3/4/5/6/7/8/9} + 10 ^ {i - 1} × j dpi,j=dpi1,0/1/2/3/4/5/6/7/8/9+10i1×j,那么我们可以根据这个做数位 dp。

代码实现:
#include <bits/stdc++.h>
#define int long long
using namespace std;
int l, r, dp[50][50], a[50], siz;
void dfs()
{
	for(int i = 1;i <= 20; ++ i)
	{
		for(int j = 0;j <= 9; ++ j)
		{
			for(int k = 0;k <= 9; ++ k)
			{
				dp[i][j] += dp[i - 1][k];//预处理dp数组 
			}
			int s = 1;
			for(int k = 1;k <= i - 1; ++ k)
			{
				s *= 10;
			}
			dp[i][j] += s * j;
		}
	}
}
void add(int x)
{
	memset(a, 0, sizeof a);
	siz = 0;
	while(x > 0)
	{
		a[ ++ siz] = x % 10;
		x /= 10;
	}
}
int ask(int x)
{
	add(x);
	int res = 0, getsum = 0;
	for(int i = siz;i >= 1; -- i)
	{
		for(int j = 0;j < a[i]; ++ j)
		{
			res += dp[i][j];
			int p = 1;
			for(int k = 1;k < i; ++ k)
			{
				p *= 10;
			}
			res += (p * getsum);
		}
		getsum += a[i];//前n-i项数字和 
	}
	res += getsum;
	return res;
}

signed main()
{
//	int T;
//	cin >> T;
	dfs();//预处理dfs数组 
    while(1)
    {
		cin >> l >> r;
		if(l == 0 && r == 0) break;
		cout << ask(r) - ask(l - 1) << "\n";
	}
    return 0;
}

P3413 SAC#1 - 萌数

大致意思:

求出 [ l , r ] [l, r] [l,r]​ 区间内满足要求的萌数(包含回文子串或本身是回文串)。

大致思路:

通过记忆化搜索的方法进行数位 dp,设计状态 d p i , j , 1 / 0 dp_{i, j, 1/0} dpi,j,1/0 表示 i i i 位数前一位是 j j j 的情况下是否满串及分别方案数即可。

代码实现:
#include <bits/stdc++.h>
using namespace std;
#define mn(a, b) a < b ? a : b
#define int long long
#define LLL __int128_t
#define LL long long
#define uint unsigned;
#define ull unsigned LL;
#define qi std::queue < int >;
#define vi std::vector < int >;
#define pii std::pair < int, int >;
#define lowbit(x) ((x) & -(x))
#define pq std::priority_queue
#define ve std::vector < pair < int, int> >
#define pr std::priority_queue < int, int >
#define pri std::priority_queue<pair<int,int> ,vector<pair<int,int> > ,greater<pair<int,int> > >
#define qcin std::ios::sync_with_stdio(0)
#define pb push_back
#define me(a, b) std::memset(a, b, sizeof(a))

const double TLS = 1;
const double eps = 1e-9;
const int inf = 1e9;
const int CPS = CLOCKS_PER_SEC;
const int INF = 1 << 30;//设置一个边界
const double TLC = TLS * 0.97 * CPS;
const int N = 23;
const int M = 1 << N;
const int MOD = 1e9 + 7;
void print(int x) {
    if (!x)
        return;

    print(x / 10);
    std::putchar(x % 10 + '0');
}
int dp[N][15][5], a[N];
string l, r;
int dfs(int now, int last1, int last2, int x, int lazy, int f)
{
    if(!now) return x;//位数个数达到限制
    if(~dp[now][last1][x] && !f)
    {
        return dp[now][last1][x];
    }
    int ans = 0, res = (f == 1) ? a[now] : 9;
    for(int i = 0;i <= res; ++ i)
    {
        ans += (dfs(now - 1, i, lazy ? last1 : -1, (i == last2 & lazy) || (i == last1 & lazy) || x, lazy || (i > 0), f & res == i) % MOD);
        ans %= MOD;
    }
    if(f == 0 && last2 != -1 && lazy) dp[now][last1][x] = ans;
    return (ans % MOD + MOD) % MOD;
}
int add(string s)
{
    int siz = 0, p = s.size() - 1;
    memset(dp, -1, sizeof dp);
    while(~p)
    {
        a[++ siz] = s[p] - '0';
        -- p;
    }
    while(a[siz] == 0)
    {
        -- siz;
    }
    return dfs(siz, -1, -1, 0, 0, 1);
}
signed main(void) {
    cin >> l >> r;
    int head = l.size(), tail  = 1;
    while(head > tail && l[head - tail] == '0')//高精度减法
    {
        l[head - tail] = '9';
        ++ tail;
    }
    -- l[head - tail];
    cout << (add(r) - add(l) % MOD + MOD) % MOD << "\n";
    return 0;
}
/*
now: 当前数位(从高到低) last1: 前一个数 x: 存不存在回文

last2:前两个数,lazy:是否选过0以外的数(即:这个数开始计入答案)

f: 是不是一直都满着选

dp[now][last1][x]存在的必要条件是没有满着选。ta 的含义是 从pos位开始向下考虑,在上一个数是pre的情况,且之前有或者没有回文的条件下,有几种方案数

*/

目前先到这里。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值