数位DP(记忆化搜索)

Acwing.338. 计数问题

给定两个整数 a� 和 b�,求 a� 和 b� 之间的所有数字中 0∼90∼9 的出现次数。

例如,a=1024,b=1032�=1024,�=1032,则 a� 和 b� 之间共有 99 个数如下:

1024 1025 1026 1027 1028 1029 1030 1031 1032

其中 0 出现 1010 次,1 出现 1010 次,2 出现 77 次,3 出现 33 次等等…

输入格式

输入包含多组测试数据。

每组测试数据占一行,包含两个整数 a� 和 b�。

当读入一行为 0 0 时,表示输入终止,且该行不作处理。

输出格式

每组数据输出一个结果,每个结果占一行。

每个结果包含十个用空格隔开的数字,第一个数字表示 0 出现的次数,第二个数字表示 1 出现的次数,以此类推。

数据范围

0<a,b<1000000000<�,�<100000000

输入样例:

1 10
44 497
346 542
1199 1748
1496 1403
1004 503
1714 190
1317 854
1976 494
1001 1960
0 0

输出样例:

1 2 1 1 1 1 1 1 1 1
85 185 185 185 190 96 96 96 95 93
40 40 40 93 136 82 40 40 40 40
115 666 215 215 214 205 205 154 105 106
16 113 19 20 114 20 20 19 19 16
107 105 100 101 101 197 200 200 200 200
413 1133 503 503 503 502 502 417 402 412
196 512 186 104 87 93 97 97 142 196
398 1375 398 398 405 499 499 495 488 471
294 1256 296 296 296 296 287 286 286 247
难度:中等
时/空限制:1s / 64MB
总通过数:16190
总尝试数:23674
来源:《算法竞赛进阶指南》, 模板题,UVA1640
算法标签 数位DP 动态规划

思路求1~n中,x出现的次数
例如:n=abcdefg , 求x在第4位出现的次数
分类讨论:
①前三位:000~abc-1,x,后三位000~999. 方案数:abc*1000
②前三位:abc,x
.................2.1 d < x , 后三位:无解. 方案数:0
.................2.2 d = x , 后三位:000~dfg. 方案数:dfg+1
.................2.3 d > x , 后三位:000~999. 方案数:1000

注意:
1.当判断x在第1位出现的次数时,不存在情况① 2.当x=0且在分类①时,因为不能前导全0,因此得从001开始,(这一步特判即可)

代码实现:

#include <iostream>
#include <cstring>
#include <algorithm>
#include <vector>

using namespace std;

int get(vector<int>& v, int l, int r)
{
    int res = 0;
    for (int i = l; i >= r; i -- ) res = res * 10 + v[i];
    return res;
}

int power(int x)
{
    int res = 1;
    while (x -- ) res *= 10;
    return res;
}

int count(int n, int x)
{
    if (!n) return 0;
    // return 0;
    vector<int> res;
    while (n)
    {
        res.push_back(n % 10);
        n /= 10;
    }
    
    n = res.size() - 1;
    int cnt = 0;
    for (int i = n - !x; i >= 0; i -- )
    {
        if (i < n)
        {
            cnt += get(res, n, i + 1) * power(i);
            if (!x) cnt -= power(i);
        }
        
        if (res[i] == x) cnt += get(res, i - 1, 0) + 1;
        else if (res[i] > x) cnt += power(i);
    }
    return cnt;
}

int main()
{
    int l, r;
    while (cin >> l >> r, l || r )
    {
        if (l > r) swap(l, r);
        for (int i = 0; i < 10; i ++ )
            cout << count(r, i) - count(l - 1, i) << ' ';
        puts("");
    }
    return 0;
}

(洛谷)P4999 烦人的数学作业

题目描述

Mr.G最近在看一些关于数字题的书,他每天愁同学们太聪明了,所有的作业同学们都能做到全对(拿到答案)。Mr.G蒙在鼓里(心知肚明)。为了使同学们进步,Mr.G总是创造一些简单(毒瘤)题来作为作业。以下是数学作业的最后一题题干——

给出一个区间L~R,求L到R区间内每个数的数字和,如123这个数的数字和为1+2+3=6。

(1≤L≤R≤102)

同学们纷纷做出来了,Mr.G一看这最后一题跟摆设没区别了呀,于是他迅速修改了题目,把范围定得非常非常大,且有T组数据,将最终的答案mod 109+7109+7。

(1≤L≤R≤10^18) (1≤T≤20)

同学们纷纷被难住了。但H为了备战NOIP2018,没有时间完成Mr.G的数学作业~~(其实是不想做QwQ)~~,所以Ta找到了你,希望你帮助Ta和同学完成这烦人的数学作业!

输入格式

输入共�+1T+1行,

第11行读入T。代表有T组数据;

第2~T+1行。读入Li​和Ri​

输出格式

输出共T行,

每行输出Li​和Ri​的区间数字和mod 109+7109+7。

输入输出样例

输入 #1复制

2
24 69
70 120

输出 #1复制

411
498

说明/提示

对于 50%50% 的数据,1≤L≤R≤10^8;

对于 100%100% 的数据,1≤L≤R≤10^18,1≤T≤20。

解题思路:

<1>:我们只要求出区间每一个数的每一位并进行操作就可以了

<2>:因为Li和Ri的数据范围比较大, 所以我们优先考虑数位DP(记忆化搜索或者DP)

<3>:数位dp的实质就是换一种暴力枚举的方式,使得新的枚举方式满足dp的性质,然后就可以记忆化搜索了

注意:在这里一定要记得开long long 不然会被一半的数据卡掉

代码实现:

#include <bits/stdc++.h>

using namespace std;

typedef long long LL;

const int N = 210, mod = 1e9 + 7;

LL read()
{
	LL x = 0, f = 1;
	char ch = getchar();
	while (ch < '0' || ch > '9')
	{
		if (ch == '-') f = -1;
		ch = getchar();
	}
	while (ch >= '0' && ch <= '9')
	{
		x = (x << 3) + (x << 1) + ch - '0';
		ch = getchar();
	}
	return x * f;
}

LL f[N][N];
vector<int> v;

LL dfs(int x, LL sum, bool fl)
{
	if (x < 0) return sum;
	if (!fl && f[x][sum] >= 0) return f[x][sum];
	int res = fl ? v[x] : 9;
	LL ans = 0;
	for (int i = 0; i <= res; i ++ ) ans = (ans + dfs(x - 1, sum + i, fl && i == res)) % mod;
	if (!fl) f[x][sum] = ans;
	return ans;
}

LL get(LL x)
{
	v.clear();
	while (x)
	{
		v.push_back(x % 10);
		x /= 10;
	}
	return dfs(v.size() - 1, 0, 1) % mod;
}

void solve()
{
	memset(f, -1, sizeof f); 
	LL t = read();
	while (t -- )
	{
		LL l = read(), r = read();
		printf ("%lld\n", (get(r) - get(l - 1) % mod + mod) % mod); 
	}
}

int main()
{
	solve();
	return 0;
}

(洛谷)P2106 Sam数

题目描述

小Z最近发现了一种非常有趣的数,他将这种数称之为 Sam 数。Sam 数具有以下特征:相邻两位的数字之差不超过 2。小Z还将 Sam 数按位数进行了分类,他将一个 k 位 Sam 数称之为 k 阶 Sam 数。但不幸的是小Z发现他数不清第 k 阶的 Sam 数一共有多少个,这个时候机智的他想到了向你求助。

输入格式

第一行为一个整数 k,含义见题面。

输出格式

一行一个整数 ans,表示 k 阶的 Sam 数的个数。

由于第 k 阶 Sam 数非常多,你只需要输出 ans mod 1000000007。

输入输出样例

输入 #1复制

4

输出 #1复制

867

说明/提示

【数据规模和约定】

对于 30%的数据,1 ≤ k ≤ 10^6。

对于 60%的数据,1 ≤ k ≤ 10^12。

对于 100%的数据,1 ≤ k ≤ 10^18。

解题思路:

 普通的计数DP会TLE

因为数码可以叠加,所以可以使用倍增

转移方程需要将第j位改为有2^j位,同时加上首位为k这一维状态

转移有2^j位时枚举两个有2^(j - 1)位的,并判断第一个的末位和第二个的首位是否满足要求能够叠加

最后转移答案同样也要开个数组g,记录首位末位和第几次转移答案

最终状态转移方程:

注意:枚举时只能枚举到62, 超过62会超出long long 的范围

代码:

#include <bits/stdc++.h>

using namespace std;

typedef long long LL;

const int mod = 1000000007;

LL n, res, ans;
LL f[10][10][63], g[10][10][63];

inline LL read()
{
	LL x = 0, f = 1;
	char ch = getchar();
	while (ch < '0' || ch > '9')
	{
		if (ch == '-') f = -1;
		ch = getchar();
	}
	while (ch >= '0' && ch <= '9')
	{
		x = (x << 3) + (x << 1) + ch - '0';
		ch = getchar();
	}
	return x * f;
}

inline int abs(int x)
{
	return x < 0 ? -x : x;
}

inline void solve()
{
	n = read();
	if (n == 1)
	{
		puts("10");
		return ;
	}
	for (int i = 0; i < 10; i ++ ) f[i][i][0] = 1;
	for (int i = 1; i < 63; i ++)
		for (int j = 0; j < 10; j ++ )
			for (int k = 0; k < 10; k ++ )
				for (int t = 0; t < 10; t ++ )
					if (abs(t - k) <= 2)
					{
						for (int l = 0; l < 10; l ++ )
							f[j][l][i] = (f[j][l][i] + f[j][k][i - 1] * f[t][l][i - 1]) % mod; 
					}
	for (int i = 0; i < 63; i ++ )
		if (n >> i & 1)
		{
			if (!res)
			{
				for (int j = 0; j < 10; j ++ )
					for (int k = 0; k < 10; k ++ )
						g[j][k][res + 1] = f[j][k][i]; 
			}
			else
			{
				for (int j = 0; j < 10; j ++ )
					for (int k = 0; k < 10; k ++ )
						for (int t = 0; t < 10; t ++ )
							if (abs(t - k) <= 2)
							{
								for (int l = 0; l < 10; l ++ )
									g[j][l][res + 1] = (g[j][l][res + 1] + g[j][k][res] * f[t][l][i]) % mod;
							}
			}
			res ++;
		}
	for (int i = 1; i < 10; i ++ )
		for (int j = 0; j < 10; j ++ ) ans = (ans + g[i][j][res]) % mod;
	printf ("%lld", ans);
	return ;
}

int main()
{
	solve();
	return 0;
}

 计数DP麻烦的点不仅仅在于处理边界问题, 更多的还有对所学的知识进行一个整合

学这个专业已经快两年了, 感觉自己多少还是有点懵懵懂懂的。

另外我的粉丝量什么时候才增长啊

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

星辰予曦

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值