【HDU 七道例题】【数位dp总结】数位dp | dp | 记忆化搜索 |【CGWR④】| E

HDU 数位dp七连,入门数位dp qwq




数位dp简介


数位dp,顾名思义就是对数逐位分析的进行的dp。

数位dp的特点是:高维小上界、常常采用记忆化搜索而不是循环递推进行dp、常以区间计数方式呈现。

因此数位dp有两个很通用的记忆化搜索板子(当然,dp数组不一定只有二维,dfs参数也不一定就只有仨;State也不一定就是bool,而一般是枚举类型。下面只是为了方便展示架构才这样写):


  • 正搜: 直接搜满足题给特殊条件的数的个数。(正搜没有什么限制条件,就是得特别注意一下状态转移)
vint dfs(const int len, const State sta, const bool lim)
{
	if (!len)
		return sta == GOAL_STA;	// 判断sta是否==满足条件的枚举值,满足则返回1,否则返回0

	if (!lim && ~dp[len][sta])	// 非限制情况下,如果已经记忆,则直接返回。有限制的时候相当于得特判。
		return dp[len][sta];

	vint sum = 0;
	const int upper_bound = lim ? num[len] : 9;	// 确定这一位的上界。如果受限,那么上界是区间上界对应位置的数。否则是9
	for (int x=0; x<=upper_bound; ++x)
	{
		State next;

		/*
			...
			...
			(根据当前状态sta和当前位x 推出下一状态next)
		*/

		sum += dfs(len-1, next, lim && x==ub);	// 传递受限:本层受限且本层选的数达到上界时,下层才受限
	}

	return lim ? sum : dp[len][sta] = sum;		// 非限制情况下才可记忆化。有限制的时候相当于得特判。
}



  • 反搜: 搜不满足题给特殊条件的数的个数,再拿 N N N 减它

反搜只能用于【当搜到第k位发现满足条件时,无论之后第k-1到第1位再填什么都仍然会保持满足条件】的这种情况,否则反搜的粗剪枝将会出错(第k位满足而之后填数后又不满足了,剪枝剪多了)

例如像出现连续数字这种,就可以反搜;像能整除、求数位和这种,就不能(要把一切数位都给确定了才可判定,不可过早剪枝)

vint dfs(const int len, const State sta, const bool lim)
{
	if (!len)
		return 1;	// 反搜直接返回1,因为上一层循环没continue说明这里必有贡献1

	if (!lim && ~dp[len][sta])	// 非限制情况下,如果已经记忆,则直接返回
		return dp[len][sta];

	vint sum = 0;
	const int upper_bound = lim ? num[len] : 9;	// 确定这一位的上界
	for (int x=0; x<=upper_bound; ++x)
	{
		State next;

		/*
			...
			...
			(根据当前状态sta和当前位x 推出下一状态next)
		*/

		if (next == GOAL_STA)	// 满足题给条件,直接剪枝,不予统计
			continue;
		sum += dfs(len-1, next, lim && x==ub);
	}

	return lim ? sum : dp[len][sta] = sum;	// 非限制情况下才可记忆化
}


虽然有正反搜两种记忆化搜索方式,但他们的计数逻辑本质是一样的:所有的贡献都是在搜到个位数(len==0)时产生的(因为只有搜到个位数位置了,整个数才能确定),然后再不断回滚、积累从而得到最后答案(最后答案倒是可能很大,但都是从最底层的那些1叠加起来的)。



那正搜反搜的差别主要在哪呢?主要体现在:

  • 计数逻辑区别:

    1. 正搜搜到个位数时不一定会返回 1 1 1:只有此时的 sta \text{sta} sta 是目标状态时,这一次 len==0 \text{len==0} len==0 才可返回 1 1 1

    2. 反搜搜到个位数时一定返回 1 1 1:因为不需要统计的情况已经 continue \text{continue} continue 了。只要在没 continue \text{continue} continue 的情况下递归进入了 len==0 \text{len==0} len==0 的情况,就必定要计这个 1 1 1

    3. 总结就是:正搜不断记录、传递状态,到 len==0 \text{len==0} len==0 时只有当前状态是目标状态 return 1 \text{return 1} return 1;而反搜每次循环时会判断是否满足题给特殊要求,满足就 continue \text{continue} continue 跳过(剪枝),所以 len==0 \text{len==0} len==0必返回 1 1 1(也正是由于这个区别,有时候反搜只要一个 bool \text{bool} bool 表示状态就足够去判断该不该 continue \text{continue} continue 了,而正搜还是需要把状态记录地更详细些。如下面第一道例题,反搜只要用一个 bool \text{bool} bool 表示刚才填的位是不是 4 4 4 就够了,而正搜需要有三种状态:{没出现49且末尾不是4、没出现49且末尾是4、出现过49} )

  • 初始化区别

    1. 正搜, dp \text{dp} dp 数组值为 0 0 0 的状态也是经过搜索得到的、有意义、需要记忆的值。所以一开始必须要 dp \text{dp} dp 全赋为 − 1 -1 1,然后当 !lim &amp;&amp; dp[ ][ ] != -1 \text{!lim \&amp;\&amp; dp[ ][ ] != -1} !lim && dp[ ][ ] != -1 时才返回 dp[ ][ ] \text{dp[ ][ ]} dp[ ][ ],当 !lim &amp;&amp; dp[ ][ ] == -1 \text{!lim \&amp;\&amp; dp[ ][ ] == -1} !lim && dp[ ][ ] == -1 时就就计算然后再记忆。

    2. 而反搜,在大多数题目中, dp[ ][ ] \text{dp[ ][ ]} dp[ ][ ] 0 0 0 的值都是无意义的(要不就说明至少个位数全都被筛掉了…一般不太可能)所以反搜一般可以不初始化 dp[ ][ ] \text{dp[ ][ ]} dp[ ][ ] − 1 -1 1。然后当 !lim &amp;&amp; dp[ ][ ] != 0 \text{!lim \&amp;\&amp; dp[ ][ ] != 0} !lim && dp[ ][ ] != 0 时就返回,而当 !lim &amp;&amp; dp[ ][ ] == 0 \text{!lim \&amp;\&amp; dp[ ][ ] == 0} !lim && dp[ ][ ] == 0 时就计算然后再记忆。(不过为了保险起见,最好还是认为反搜时 dp[ ][ ] \text{dp[ ][ ]} dp[ ][ ] 0 0 0 的状态也是有意义的。否则就会多出一些重复的计算,可能导致 TLE

然后具体解题步骤大概是,先获得区间上界 N N N,然后把它逐位分解。然后直接上 dfs \text{dfs} dfs 即可。

注意调用 dfs \text{dfs} dfs lim \text{lim} lim 参数必须传 true \text{true} true,且存储 N N N 各个数位的数组要是全局的。这样才可以正常引发后续的 lim \text{lim} lim 传递。


下面是一些例题(七道)。





数位dp例题


[01] 3555. Bomb


题意

[ 0 , N ] [0, N] [0,N] 中含有相邻 49 49 49 的数字个数


思路

相邻特定数字,可以正搜,可以反搜。


代码

正搜:

/*
  http://acm.hdu.edu.cn/showproblem.php?pid=3555
  
  正搜:搜满足题给特殊条件的数的个数。然后直接输出搜出的结果。
  反搜:搜不满足题给特殊条件的数的个数。然后输出 N减去搜到的结果。
  【注意】:当题给特殊条件需要把整个数确定下来(精确到个位)时才能判定是否满足的时候,
  			只能用正搜,不能用反搜!(因为反搜的continue是粗剪枝。比如本题,一出现49就可以剪枝了,但如果是其他条件(比如%13==0就去掉)就不能在某层rr==0时剪一大枝,因为整个数都没确定呢!再递归下去rr可能还会变化!有可能又不满足条件了!但出现49不一样,只要一出现,不管后面数位填什么都不会改变已经出现的事实)
			换句话说,如果处理到第k位时发现某个条件满足,且不管第k+1到len位再填什么这个条件仍然都会满足,则可用反搜,否则只能用正搜。
 
  正反搜的相同点:
	正反搜计数时,所有的贡献都是在搜到某个个位数时产生的(因为只有搜到个位数位置了,整个数才能确定)。
	然后再不断回滚、积累从而得到最后答案(最后答案倒是可能很大,但都是从最底层的那些1叠加起来的)。

  正反搜的区别:
	①计数逻辑区别:
		1. 正搜搜到个位数时不一定会返回1:
			如本题,如果此时的sta说明曾经有49出现,那么这一次len==0时才可返回1。
		2. 反搜搜到个位数时一定返回1:
			因为不需要统计的情况已经continue了。只要在没continue的情况下递归进入了len==0的情况,就必定要计这个1。
	总结就是:正搜不断记录、传递状态,到len==0时如果状态满足要求就记1;而反搜每次循环时会判断是否满足题给特殊要求,满足就continue,所以len==0时必返回1
	(也正是由于这个区别,有时候反搜只要一个bool表示状态就足够去判断该不该continue了,而正搜还是需要记录更详细的状态。如本题正搜需要记录三种状态:{没出现49且末尾不是4、没出现49且末尾是4、出现过49})

	②初始化区别:
		1. 正搜dp数组值为0的状态也是经过搜索得到的、有意义、需要记忆的值。所以一开始要把dp全赋为-1,然后当 !lim && dp[][]!=-1 时就返回dp[][]。
		2. 而反搜,在大多数题目中,dp[][]为0的值都是无意义的(dp[1][...]一般不会为0吧,要不就说明0123456789全都被筛掉了...而dp[2][...]就更不可能为0了,不然二位数也全被筛掉了))
			所以反搜一般可以不初始化dp[][]为-1。然后 !lim && dp[][] 时就返回 dp[][],!lim && !dp[][] 时就计算dp[][]然后再记忆。
			(不过为了保险起见,最好还是认为反搜时dp为0状态也是有意义的。否则万一dp[][]在本题==0也是可能的,但还是认为它无意义,那就会多出一些重复的计算,可能导致TLE)

	以下是正搜的代码:
 */

#include <cstdio>
#include <cstring>

#define FD(i, n) for(int i=0; i<=n; ++i)
#define MB 21

using LL = long long;

enum Sta
{
    NO, P4, YES, SNT	// 本题正搜的三种状态:NO--没出现49且末尾不是4、P4--没出现49且末尾是4、YES--出现过49
};

LL dp[MB][SNT];
int num[MB];

inline int scan(LL x)
{
	int top = 0;
	do num[++top] = x % 10;
	while (x /= 10);
	return top;
}


LL dfs(const int len, const Sta sta, const bool lim)
{
	if (!len)
		return sta == YES;	// 正搜,曾出现过49才能返回1

	if (!lim && ~dp[len][sta])
		return dp[len][sta];

	LL sum = 0;
	const int ub = lim ? num[len] : 9;
	FD(x, ub)
	{
		Sta next;	// 在本题,正搜传递sta的逻辑比反搜稍复杂一些。
		if (sta==YES || (sta==P4&&x==9))
			next = YES;
		else if (x==4)
			next = P4;
		else
			next = NO;
		sum += dfs(len-1, next, lim && x==ub);
	}

	return lim ? sum : dp[len][sta] = sum;
}


int main()
{
	memset(dp, -1, sizeof(dp));

	int T;
	long long x;
	scanf("%d", &T);
	while (T--)
	{
		scanf("%lld", &x);
		printf("%lld\n", dfs(scan(x), NO, true));	// 正搜,直接输出答案
	}

	return 0;
}


反搜:

/*
	http://acm.hdu.edu.cn/showproblem.php?pid=3555
	以下是反搜的代码:(本题可以用反搜)
 */

#include <cstdio>
#include <cstring>

#define FD(i, n) for(int i=0; i<=n; ++i)
#define MB 21

using LL = long long;

LL dp[MB][2];
int num[MB];

inline int scan(LL x)
{
	int top = 0;
	do num[++top] = x % 10;
	while (x /= 10);
	return top;
}


LL dfs(const int len, const bool prev4, const bool lim)
{
	if (!len)
		return 1;	// 反搜,无脑返回1

	if (!lim && ~dp[len][prev4])
		return dp[len][prev4];

	LL sum = 0;
	const int ub = lim ? num[len] : 9;
	FD(x, ub)
	{
		if (prev4 && x==9)	// 上一位4这一位9,所以剪枝,跳过不统计
			continue;
		sum += dfs(len-1, x==4, lim && x==ub);
	}

	return lim ? sum : dp[len][prev4] = sum;
}


int main()
{
	memset(dp, -1, sizeof(dp));

	int T;
	long long x;
	scanf("%d", &T);
	while (T--)
	{
		scanf("%lld", &x);
		printf("%lld\n", x+1 - dfs(scan(x), false, true));	// 反搜,容斥原理减一减
	}

	return 0;
}




[02] 2089. 不要62


题意

[ L , R ] [L, R] [L,R] 不是不吉利的数字的个数。

一个数字是不吉利的当且仅当它含有连续的 62 62 62 或者含有 4 4 4


思路

相邻特定数字,可以正搜,可以反搜。

但反搜方便,所以就只写反搜啦~


代码

#include <cstdio>

#define FD(i, n) for(int i=0; i<=n; ++i)
#define MB 8

int dp[MB][2];
int sta[MB], top;

inline void scan(int x)
{
	top = 0;
	do sta[++top] = x % 10;
	while (x /= 10);
}

int dfs(const int len, const bool prev6, const bool lim)
{
	if (!len)
		return 1;

	if (!lim && dp[len][prev6])
		return dp[len][prev6];

	int sum = 0;
	const int ub = lim ? sta[len] : 9;
	FD(cur, ub)
	{
		if (cur == 4)
			continue;
		if (prev6 && cur == 2)
			continue;
		sum += dfs(len-1, cur==6, lim && cur==ub);
	}

	if (!lim)
		dp[len][prev6] = sum;
	return sum;
}


int main()
{
	int l, r, sum_l, sum_r;
	while (scanf("%d %d", &l, &r), l||r)
	{
		scan(l-1);
		sum_l = dfs(top, false, true);

		scan(r);
		sum_r = dfs(top, false, true);

		printf("%d\n", sum_r-sum_l);
	}

	return 0;
}




[03] 3652. B-number


题意

[ 0 , N ] [0, N] [0,N] B-number \text{B-number} B-number 数字的个数。

一个数字是 B-number \text{B-number} B-number 当且仅当它含有连续的 13 13 13 且能整除 13 13 13


思路

只能正搜

因为这里涉及精确取模,需要每位都确定了才可判定。

另外传递状态的时候,取模余数的传递得稍微推导一下。


代码

#include <cstdio>
#include <cstring>

#define FD(i, n) for(int i=0; i<=n; ++i)
#define MB 11
#define MOD 13

enum Sta
{
    NO, P1, YES, SNT
};


int dp[MB][MOD][SNT];
int num[MB];

inline int scan(int x)
{
	int top = 0;
	do num[++top] = x % 10;
	while (x /= 10);
	return top;
}


int dfs(const int len, const int r, const Sta sta, const bool lim)
{
	if (!len)
		return sta==YES && r==0;	// 出现13,且除尽了

	if (!lim && ~dp[len][r][sta])
		return dp[len][r][sta];

	int sum = 0;
	const int ub = lim ? num[len] : 9;
	FD(x, ub)
	{
		const int rr = (r*10 + x) % MOD;	/* e.g. top=5, len=1, 那么现在这个数是 S_now == abcd * 10 + x
												记 S_prev == abcd, 则 S_prev 可写成 k*13 + r 的形式
												则 S_now == S_prev*10 + x == (k*13 + r)*10 + x
													S_now % 13 == ((k*13 + r)*10 + x) % 13
													== (k*13*10 + r*10 + x) % 13
													== 0 + (r*10+x) % 13
													== (r*10+x) % 13
												整个传递过程有点像秦九韶,是吧
											*/
		Sta next;
		if (sta==YES || (sta==P1&&x==3))
			next = YES;
		else if (x==1)
			next = P1;
		else
			next = NO;
		sum += dfs(len-1, rr, next, lim && x==ub);
	}

	return lim ? sum : dp[len][r][sta] = sum;
}


int main()
{
	memset(dp, -1, sizeof(dp));

	int x;
	while (~scanf("%d", &x))
		printf("%d\n", dfs(scan(x), 0, NO, true));

	return 0;
}




[04] 4722. Good Numbers


题意

[ L , R ] [L, R] [L,R] (均 ≤ 1 0 18 \le 10^{18} 1018)中有多少个数,其数位和能够被 10 10 10 整除( 0 0 0 当然也能被 10 10 10 整除)。


思路

数位和,只能正搜。


【CGWR①】关于dp数组要不要取模维、要不要数位和维:请见以下代码注释

代码

/*
	数位之和整除某个数(如本题,数位和整除10):dp数组要有数位和那一维(因为涉及到数位和了)
						但dp数组不要加取模结果那一维(不是数值取模,故不需要额外记录取模结果,也不需要用秦九韶传递取模结果)
						dfs传递数位和,当!len时数位和若整除某个数则返回1

	数字本身整除某个数(如HDU3652 含13且整除13):dp数组要加模结果那一维作为记录,并且要用秦九韶传递模结果
						dfs传递模结果,当!len时模结果如果==0则返回1

	数字本身整除数位和(如HDU4389 整除自己数位和):那都要记录了。dp数组既要记数位和,又要记模结果(实际上为了枚举还要记目标和,dp有四维)
*/


#include <iostream>
#include <cstring>
#define MB 20
#define MS 162
#define MOD 10

using vint = long long;


int num[MB];
template <typename T>
inline int scan(T x)
{
	int top = 0;
	do num[++top] = x % 10;
	while (x /= 10);
	return top;
}


vint dp[MB][MS+1];
vint dfs(const int len, const int d_sum, const int lim)
{
	if (!len)
		return d_sum % MOD == 0;
	if (!lim && ~dp[len][d_sum])
		return dp[len][d_sum];

	vint sum = 0;
	const int ub = lim ? num[len] : 9;
	for (int x=0; x<=ub; ++x)
		sum += dfs(len-1, d_sum+x, lim&&x==ub);
	return lim ? sum : dp[len][d_sum] = sum;
}


template <typename T>
inline constexpr T count(const T N)
{
	return N<0 ? 0 : dfs(scan(N), 0, true);
}


int main()
{
	std::ios::sync_with_stdio(false), std::cin.tie(nullptr), std::cout.tie(nullptr);
	memset(dp, -1, sizeof(dp));

	int T;
	vint L, R;
	std::cin >> T;
	for (int i=1; i<=T; ++i)
	{
		std::cin >> L >> R;
		std::cout << "Case #" << i << ": " << count(R) - count(L-1) << '\n';
	}
	return 0;
}




[05] 4389. X mod f(x)


题意

[ L , R ] [L, R] [L,R] (均 ≤ 1 0 9 \le 10^9 109)中有多少个数,其能够被其数位和整除。


思路

又是取模,只能正搜。

注意到数位和最大不超过 81 81 81,所以枚举数位和 [ 1 , 81 ] [1, 81] [1,81] 进行数位 dp \text{dp} dp 统计。

(不必去重,因为不可能有数在多次枚举情况下同时满足整除条件,不然它就有多个互不相等的数位和了…)


代码

#include <cstdio>
#include <cstring>

#define MAX_DIGIT 10
#define MAX_D_SUM 81
#define MAX_MOD MAX_D_SUM
#define MAX_RE MAX_MOD


int num[MAX_DIGIT+1];
inline int scan(int x)
{
	int top = 0;
	do num[++top] = x % 10;
	while (x /= 10);
	return top;
}


int goal_d_sum;
int dp[MAX_DIGIT+1][MAX_D_SUM+1][MAX_MOD+1][MAX_RE];	// dp[位数][位数和][模数(模的是目标数位和)][余数]
// 注意不同的goal_d_sum之间是不会影响的(同一批dfs共用一个goal_d_sum)所以不用担心不同批的dfs会交叉影响而不能正常完成记忆化功能。

int dfs(const int len, const int d_sum, const int r, const bool lim)	// 只能正搜,因为涉及精确取模
{
	if (!len)
		return d_sum==goal_d_sum && r==0;
	if (!lim && ~dp[len][d_sum][goal_d_sum][r])
		return dp[len][d_sum][goal_d_sum][r];

	int sum = 0;
	const int ub = lim ? num[len] : 9;
	for (int x=0; x<=ub; ++x)
		sum += dfs(len-1, d_sum+x, (r*10+x) % goal_d_sum, lim&&x==ub);

	return lim ? sum : dp[len][d_sum][goal_d_sum][r] = sum;
}


int count(const int N)
{
	int len = scan(N), sum = 0;
	for (goal_d_sum=1; goal_d_sum<=MAX_MOD; ++goal_d_sum)	// 枚举所有可能的目标数位和,然后把答案全部加起来
		sum += dfs(len, 0, 0, true);	// 同一批dfs共用一个goal_d_sum

	// 虽然枚举goal_d_sum遍历了MAX_MOD次,但彼此之间没有重复统计(因为一个数的数位和是唯一的,所以MAX_MOD次循环中不会对某个数统计多次)
	// 所以sum不用再减什么了(另一道题(3709平衡数)也枚举统计了,但那道题的多次枚举会把"0"这个数多次统计,所以那道题需要去重)
	return sum;
}


int main()
{
	memset(dp, -1, sizeof(dp));

	for (int T, i=scanf("%d", &T), L, R; i<=T && scanf("%d %d", &L, &R); ++i)
		printf("Case %d: %d\n", i, count(R) - count(L-1));
	return 0;
}




[06] 4507. 吉哥系列故事——恨7不成妻


题意

[ L , R ] [L, R] [L,R] (均 ≤ 1 0 18 \le 10^{18} 1018)。


思路


代码





[07] 3709. 平衡数


题意

[ L , R ] [L, R] [L,R] (均 ≤ 1 0 9 \le 10^9 109)中有多少个数,其是力矩平衡的。

一个 len \text{len} len 位数(记其数位组成为 b l e n b l e n − 1 . . . b 2 b 1 b_{len}b_{len-1}...b_2b_1 blenblen1...b2b1)力矩平衡当且仅当 ∃   p ∈ [ 1 , l e n ] , s . t . ∑ i   =   l e n 1 { b i × ( i − p ) } = = 0 \exist\ p \in [1, len],s.t. \sum\limits_{i\ =\ len}^{1}\{b_i\times (i-p) \} == 0  p[1,len]s.t.i = len1{bi×(ip)}==0


思路

逐位分析,只能正搜。
但可以剪枝: 当当前累加力矩小于零的时候,低位再怎么填数也不可能恢复到 0 0 0(往后只会越减越多),故可直接剪掉。

然后本题具体做法是:枚举枢纽的位置 [ 1 , l e n ] [1, len] [1,len] 进行数位 dp \text{dp} dp 统计,然后累加答案再去重即可。


【CGWR②】 像这种 枚举 + 累加数位dp答案 的做法,一定要注意多次枚举时,有没有什么数会同时满足这多种不同情况下的特殊条件。如果有的话,要去重!!!

比如本题不管枢纽位置在哪,0 这个数都能满足力矩平衡,所以最后累计的答案要减去其多被统计的次数(也就是循环次数-1)

而有些题,虽然也枚举了,但不存在这种“多面手”,所以不需要去重操作。比如上面那道 4389. X mod f(x),肯定不存在某个数对多次枚举情况同时满足条件(不然它就具备多个互不相等的数位和了… 这怎么可能嘛)。所以最终答案就是累加和,不用减去啥。


代码

【CGWR③】本题,力矩其实就是变相的数位和。所以dp数组需要有数位和维。

#include <iostream>
#include <cstring>
#define MB 20
#define MS 1666

using vint = long long;


int num[MB];
template <typename T>
inline int scan(T x)
{
	int top = 0;
	do num[++top] = x % 10;
	while (x /= 10);
	return top;
}


int pivot;
vint _dp[MS << 1][MB][MB];	// 这个力矩其实就是变相的数位和,所以dp数组需要数位和维
auto *dp = _dp + MS;

vint dfs(const int pos, const int M, const int lim)
{
	if (!pos)
		return M == 0;
	if (M < 0)
		return 0;	// 剪枝

	if (!lim && ~dp[M][pos][pivot])
		return dp[M][pos][pivot];

	vint sum = 0;
	const int ub = lim ? num[pos] : 9;
	for (int x=0; x<=ub; ++x)
		sum += dfs(pos-1, M + (pos-pivot) * x, lim&&x==ub);
	return lim ? sum : dp[M][pos][pivot] = sum;
}


template <typename T>
inline T count(const T N)
{
	T sum = 0;
	const int len = scan(N);
	for (pivot=1; pivot<=len; ++pivot)
		sum += dfs(len, 0, true);

	// 枚举pivot遍历了len次。但注意不管pivot是啥,"0"这个数总是平衡数,所以它被多统计了len-1次,得减去
	// 而除了0之外的其他数,对于两次不同的pivot,不可能同时满足力矩为0,所以不会重复统计。
	// 所以答案就是 sum - (len-1)
	return sum - (len-1);
}


int main()
{
	std::ios::sync_with_stdio(false), std::cin.tie(nullptr), std::cout.tie(nullptr);
	memset(_dp, -1, sizeof(_dp));

	int T;
	vint L, R;
	std::cin >> T;
	for (int i=1; i<=T; ++i)
	{
		std::cin >> L >> R;
		std::cout << count(R) - count(L-1) << '\n';
	}
	return 0;
}



【CGWR④】 最后再来总结一下我个人数位 dp \text{dp} dp 容易出错的几个小地方:

  • 忘记在最最开始 memset(dp, -1, sizeof(dp)) ; \text{memset(dp, -1, sizeof(dp))}; memset(dp, -1, sizeof(dp));
  • 数位循环上界写成 &lt;ub \text{&lt;ub} <ub(正确的是 &lt;=ub \text{&lt;=ub} <=ub
  • 用正搜,但是 len==0 \text{len==0} len==0 时无脑返回 1 1 1
  • dp \text{dp} dp 开几维、分别代表啥 弄不清楚。


继续加油!

  • 3
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值