数位DP学习笔记以及数位统计问题

\qquad 数位 DP 问题往往都是这样的题型,给定一个闭区间 [ l , r ] [l,r] ,让你求这个区间中满足 某种条件 的数的总数。首先我们将问题转化成更加简单的形式。设 a n s i ans_i 表示在区间 [ 1 , i ] [1,i] 中满足条件的数的数量,那么所求的答案就是 a n s r a n s l 1 ans_r-ans_{l-1}

个人感觉数位DP终点是找状态,不重不漏,找相邻几位的关系,进行状态的转移,因为要记忆化就要正确找准状态。至于找状态就是找变量?

数位DP的 d f s dfs 函数般都有 p o s pos :从高位开始的位置, l i m i t limit :一个标志,用来判断给定数位上的上界, s t a sta :就是状态(比如记录前一位的数是多少),其余的流程一般都差不多,所以只要找好状态就和套板子差不多。

优化
1、memset 优化 将memset提到外面(limit值与所设状态无关)
给定不同 l i m i t limit 值,可增加一维存储状态 d p [ p o s ] [ s t a t e ] [ l i m i t ] dp[pos][state][limit] ,或者改变dp策略(如下)
2、减法优化(改变dp策略,以便可以使用memset优化)

1、hdu2089 不要62(基础入门)
题意:求 [ a , b ] [a,b] 中没有62和4的数的个数。
这个题目状态的转移只和前一位是否是6有关。
d p [ i ] [ 0 ] dp[i][0] 是枚举到第 i i 位并且前一位不是6的数的个数
d p [ i ] [ 1 ] dp[i][1] 是枚举到第 i i 位并且前一位是6的数的个数

int a[20];
int dp[20][2];
int dfs(int pos,int pre,bool sta,bool limit){
	if(pos == -1) return 1;
	if(!limit&&dp[pos][sta]!=-1) return dp[pos][sta];
	
	int up = limit?a[pos]:9;
	int tmp = 0;
	for(int i = 0;i <= up;++i){
		if(i == 4 ) continue;
		if(pre == 6 && i == 2) continue;
		tmp += dfs(pos-1,i,i==6,limit&&i == a[pos]);
	}
	if(!limit) return dp[pos][sta] = tmp;
	else return tmp;
}
int solve(int x){
	int pos = 0;
	while(x){
		a[pos++] = x % 10;
		x /= 10;
	}
	return dfs(pos - 1,-1,0,1);
}
int main()
{
    //freopen("in.txt","r",stdin);
    //freopen("out.txt","w",stdout);
    int n,m;
	while(cin >> n >> m&&n + m){
		memset(dp,-1,sizeof dp);
		cout << solve(m) - solve(n-1)<<endl;
	}
    return 0;
}

hdu4734 F(x)(减法优化)
[ 0 , b ] [0,b] 中小于 F [ A ] F[A] 的个数。
d p [ i ] [ j ] dp[i][j] 是到第在第i位后小于等于j的数的个数。

int dp[30][N/10];
int a[20];
int all = 0;
int dfs(int pos,int sum,bool limit){
    if(pos == -1) return sum <= all;
    if(sum > all) return 0;
    if(!limit&&dp[pos][all - sum]!=-1) return dp[pos][all - sum ];
    int up = limit?a[pos]:9;
    int tmp = 0;
    for(int i = 0;i <= up;++i){
        tmp += dfs(pos-1,sum + i*(1<<pos) ,limit &&i == a[pos]);
    }
    if(!limit) dp[pos][all - sum] = tmp;
    return tmp;
}
int solve(int x){
    int pos = 0;
    while(x){
        a[pos++] = x % 10;
        x /= 10;
    }
    return dfs(pos - 1,0 ,1);
}
int main(){
    int t;
    scanf("%d",&t);
	memset(dp,-1,sizeof dp);
    for(int p = 1;p <= t;++p){
        all = 0;
        //memset(dp,-1,sizeof dp);
        int n,m;
        scanf("%d%d",&n,&m);
        int s = 0;
        int b = 1;
        while(n){
            all += n % 10 * b;
            n /= 10;
            b *= 2;
        }
        printf("Case #%d: %d\n",p,solve(m));
    }
}

poj3522 Round Numbers
[ a , b ] [a,b] 的数中二进制0的个数大于等于1的个数。
d p [ p o s ] [ s t a ] dp[pos][sta] 表示第 i i c n t 0 c n t 1 = s t a cnt_0-cnt_1=sta 后满足条件数的个数。
因为 c n t 0 c n t 1 cnt_0-cnt_1 的个数可能为负,所以加上个40.

int dp[40][80];
int a[40];
ll dfs(int pos,int sta,int lead,int limit){
	if(pos == -1) return sta >= 40;
	if(!lead&&!limit&&dp[pos][sta] != -1 ) return dp[pos][sta];
	int up = limit?a[pos]:1;
	ll tmp = 0;
	for(int i = 0;i <= up;++i){
		if(lead&&i == 0) tmp += dfs(pos-1,sta ,lead,limit&&i == a[pos]);
		else tmp += dfs(pos - 1,sta+(i == 0?1:-1),lead&&i == 0,limit&&i == a[pos]);
	}
	if(!limit&&!lead) dp[pos][sta] = tmp;
	return tmp;
}
ll solve(ll x){
	int pos = 0;
	while(x){
		a[pos++] = x % 2;
		x /= 2;
	}
	return dfs(pos - 1,40,1,1);
}
int main()
{
    //freopen("in.txt","r",stdin);
    //freopen("out.txt","w",stdout);
 	memset(dp,-1,sizeof dp);
	ll n,m;
	while(cin >> n >> m){
		printf("%lld\n",solve(m) - solve(n-1));
	}
    return 0;
}

hdu3652 B-number
[ 1 , n ] [1,n] 中含有数字13并且可以被13整除的数的数目。
d p [ p o s ] [ s t a ] [ m o d ] dp[pos][sta][mod]
pos表示第几位(高位开始)
sta:1代表着前一位为1,2代表着在前几位中已经存在13这个数字了,0表示前一位不是1.
mod 代表着前几位%13的余数。
要开三维,因为紧靠pos和mod不能确定完整的状态,因为sta也是可变的

int a[20];
int dp[20][3][13];
int t(int x,int y,int z){
	if(z == 2) return 2;
	if(x == 1&&y == 3) return 2;
	if(y == 1) return 1;
	return 0;
}
int dfs(int pos,int pre,int mod,int sta, bool limit){
	if(pos == -1) return mod == 0 && sta == 2;
	if(!limit&&dp[pos][sta][mod] != -1) return dp[pos][sta][mod];
	int up = limit?a[pos]:9;
	int ans = 0;
	for(int i = 0;i <= up;++i){
		ans += dfs(pos-1,i,(mod*10 + i)%13,t(pre,i,sta),limit&&i == a[pos]);
	}
	if(!limit) dp[pos][sta][mod] = ans;
	return ans;
}
int solve(int x){
	int pos = 0;
	while(x){
		a[pos++] = x % 10;
		x /= 10;
	}
	return dfs(pos - 1,-1,0,0,1); 

}
int main()
{
    //freopen("in.txt","r",stdin);
    //freopen("out.txt","w",stdout);
    int n;
	memset(dp,-1,sizeof dp);
	while(cin >> n){
		printf("%d\n",solve(n));
	}
    return 0;
}

hdu4507吉哥系列故事——恨7不成妻
题意:
首先定义与7有关的数
如果一个整数符合下面3个条件之一,那么我们就说这个整数和7有关——
  1、整数中某一位是7;
  2、整数的每一位加起来的和是7的整数倍;
  3、这个整数是7的整数倍;
现在问题来了:吉哥想知道在一定区间内和7无关的数字的平方和。
思路:
如果单存的考计数就是数位dp的模板题了。
现在是求 [ l , r ] [l,r] 内与7无关数字的平方和。
我们定义 d p [ p o s ] [ M o d ] [ s u m ] dp[pos][Mod][sum] 表示第pos位,余数为Mod,数字和为sum的状态
每个状态有 c n t , s u m , s q s u m cnt,sum,sqsum :当前状态下符合要求的数的个数,数的和,数的平方和
假设我们第pos位为 i i
然后 ( i 1 0 p o s + n e x t ) 2 = ( ( i 1 0 p o s ) 2 + 2 i 1 0 p o s n e x t + n e x t 2 ) (i\cdot 10^{pos}+next)^2=((i*10^{pos})^2+2\cdot i\cdot10^{pos}*next+next^2)
n e x t next 表示从 p o s 1 pos-1 位开始的数,比如有 c n t cnt
因为我们要统计这些数平方的和
那么上式就变为了
j = 1 c n t ( i 1 0 p o s + n e x t ) 2 \sum_{j=1}^{cnt}(i\cdot 10^{pos}+next)^2

= c n t i 1 0 p o s + 2 i 1 0 p o s j = 1 c n t n e x t + j = 1 c n t n e x t 2 =cnt\cdot i\cdot10^{pos}+2\cdot i\cdot10^{pos}\sum_{j=1}^{cnt}next+\sum_{j=1}^{cnt}next^2
c n t cnt 表示下个状态符合要求的数, s u m sum 表示下个状态符合要求的数的和, j = 1 c n t n e x t 2 \sum_{j=1}^{cnt}next^2 就是下个状态的 s q s u m sqsum 。这就是 d p dp 中的状态转移吧,变成了子问题。这题感觉好棒啊!

const int N = 1e5 + 10;
const ll mod = 1e9 + 7;
using namespace std;
int a[20];
ll p[20];
struct node{
	ll cnt;
	ll sum;
	ll sqsum;
	node(ll a,ll b,ll c):cnt(a),sum(b),sqsum(c){}
	node(){cnt = -1;sum = 0;sqsum = 0;}
}dp[20][7][7];
node dfs(int pos,int Mod,ll sum,bool limit){
	if(pos == -1) return node(Mod!=0&&sum!=0,0,0);
	if(!limit&&dp[pos][Mod][sum].cnt != -1) return dp[pos][Mod][sum];
	int up = limit?a[pos]:9;
	node tmp(0,0,0),ans(0,0,0);
	for(int i = 0;i <= up;++i){
		if(i == 7) continue;
		tmp = dfs(pos-1,(Mod*10+i)%7,(sum+i)%7,limit&&i == a[pos]);
		ans.cnt = (ans.cnt + tmp.cnt) % mod;
		ans.sum = (ans.sum + tmp.cnt%mod*i*p[pos]%mod + tmp.sum)%mod;
		ans.sqsum = (ans.sqsum + tmp.sqsum % mod + 2LL*i*p[pos]%mod*tmp.sum) % mod;
		ans.sqsum = (ans.sqsum + i*i*p[pos]%mod*p[pos]%mod*tmp.cnt%mod) % mod;
	}
	if(!limit) dp[pos][Mod][sum] = ans;
	return ans;
}
ll solve(ll x){
	int pos = 0;
	while(x){
		a[pos++] = x % 10;
		x /= 10;
	}
	return dfs(pos-1,0,0,1).sqsum;
}
int main()
{
    //freopen("in.txt","r",stdin);
    //freopen("out.txt","w",stdout);
 	int t;
	p[0] = 1;
	for(int i = 1;i <= 18;++i) p[i] = p[i-1] * 10 % mod;
	scanf("%d",&t);
	while(t--){
		ll l,r;
		scanf("%lld%lld",&l,&r);
		printf("%lld\n",((solve(r) - solve(l-1)%mod+mod))%mod);
	}
    return 0;
}

#10164. 「一本通 5.3 例 2」数字游戏
题意:
让你求 [ l , r ] [l,r] 内不降的数字,如123445。
思路:
满足区间减法。我们只需求 [ 1 , n ] [1,n] 这个状态比较简单,因为这一位只和上一位有关,所以我们设 d p [ p o s ] [ s t a ] dp[pos][sta] 表示当前pos位前一位是sta的状态保存的非降数字数目。剩下的就是模板稍微改下即可。

int a[20];
int dp[20][10];
int dfs(int pos,int sta,bool limit){
    if(pos == -1) return 1;
    if(!limit&&dp[pos][sta]!=-1) return dp[pos][sta];
    int up = limit?a[pos]:9;
    if(up < sta) return 0;
    int ans = 0;
    for(int i = sta;i <= up;++i){
        ans += dfs(pos-1,i,limit&&i == a[pos]);
    }
    if(!limit) dp[pos][sta] = ans;
    return ans;
}
int solve(int x){
    int pos = 0;
    while(x){
        a[pos++] = x % 10;
        x /= 10;
    }
    return dfs(pos-1,0,true);
}
int main(){
    memset(dp,-1,sizeof dp);
    int l,r;
    while(cin >> l >> r)
     cout << solve(r) - solve(l-1) << endl;
}

「一本通 5.3 例 3」Windy 数
题意:
让你求 [ l , r ] [l,r] w i n d y windy 数的个数。
所谓windy数是相邻两个数字的差值不能小于2。
我们设 d p [ p o s ] [ s t a ] dp[pos][sta] 状态为第 p o s pos 位,前一位为 s t a sta 的符合要求的数字个数。
当前位的数只和上一位有关,注意前导零即可。

int a[20];
int dp[20][12];
int dfs(int pos,int pre,bool lead,bool limit){
	if(pos == -1) return 1;
	if(!lead&&!limit&&dp[pos][pre]!=-1) return dp[pos][pre];
	int up = limit?a[pos]:9;
	int tmp = 0;
	for(int i = 0;i <= up;++i){
		if(lead) tmp += dfs(pos-1,i,lead&&i == 0,limit&&i == a[pos]);
		else if(abs(i-pre) >= 2) tmp += dfs(pos-1,i,lead,limit&&i == a[pos]);
	}
	if(!lead&&!limit) dp[pos][pre] = tmp;
	return tmp;
}
int solve(int x){
	int pos = 0;
	while(x){
		a[pos++] = x % 10;
		x /= 10;
	}
	return dfs(pos-1,0,true,true);
}
int main()
{
    //freopen("in.txt","r",stdin);
    //freopen("out.txt","w",stdout);
 	int l,r;
	cin >> l >> r;
	memset(dp,-1,sizeof dp);
	cout << solve(r) - solve(l-1)<<endl;
    return 0;
}

P3413 SAC#1 - 萌数
题意:
让你找 [ l , r ] [l,r] 之间萌数的个数。萌数定义如下:
只有满足“存在长度至少为2的回文子串”的数是萌的——也就是说,101是萌的,因为101本身就是一个回文数;110是萌的,因为包含回文子串11;但是102不是萌的,1201也不是萌的。
现在SOL想知道从l到r的所有整数中有多少个萌数。
由于答案可能很大,所以只需要输出答案对1000000007(10^9+7)的余数。
思路:
如果萌数存在,则至少存在一位置上的数字,他和前一位或者前2位相等。
所以我们定义 d p [ p o s ] [ s t a 1 ] [ s t a 2 ] [ s t a ] dp[pos][sta1][sta2][sta] 保存状态
p o s pos: p o s pos
s t a 1 sta1: p o s 1 pos-1 位上的数字
s t a 2 sta2: p o s 2 pos-2 位上的数字
s t a sta: 是否已经存在回文 ( 0 o r 1 ) (0or1)
调了很长时间。。。。

char s[N],t[N];
int a[1005];
ll dp[1005][65][65][2];
ll dfs(int pos,int sta1,int sta2,bool sta,bool lead,bool limit){
	if(pos == -1) return lead == 0&&sta == 1;
	if(!lead&&!limit&&dp[pos][sta1][sta2][sta]!=-1) return dp[pos][sta1][sta2][sta];
	int up = limit?a[pos]:9;
	ll ans = 0;
	for(int i = 0;i <= up;++i){
		if(lead&&i == 0) ans = (ans + dfs(pos-1,sta1,sta2,false,true,limit&&i == a[pos])) % mod;
		else  ans =(ans + dfs(pos-1,sta2,i,sta||(i == sta2||i==sta1),false,limit&&i == a[pos])) % mod;
	}
	if(!lead&&!limit) dp[pos][sta1][sta2][sta] = ans;
	return ans;
}
ll solve(char str[]){
	int l = strlen(str);
	int pos = 0;
	int down = (str[0] == '0'?1:0);
	for(int i = l - 1;i >= down;--i){
		a[pos++] = str[i] - '0';
	}
	return dfs(pos-1,11,12,false,true,true);
}
int main(){
	cin >> s >> t;
	memset(dp,-1,sizeof dp);
	int l = strlen(s);
	s[l-1]--;
	for(int i = l-1;i >= 0;--i){
		 if(s[i] < '0') s[i-1] --,s[i] += 10;
	}
	cout << ((solve(t) - solve(s))%mod + mod) % mod;
}

2020牛客寒假算法基础集训营3 牛牛的随机数
在这里插入图片描述
答案是
x = l 1 r 1 y = l 2 r 2 x y ( r 1 l 1 + 1 ) ( r 2 l 2 + 1 ) \dfrac{\sum\limits_{x=l1}^{r1}\sum\limits_{y=l2}^{r2}x\oplus y}{(r1-l1+1)\cdot(r2-l2+1)}

关于 x = l 1 r 1 y = l 2 r 2 x y \sum\limits_{x=l1}^{r1}\sum\limits_{y=l2}^{r2}x\oplus y 考虑按位贡献,

即对于每 i i 位来说,对答案产生的贡献为:
[ l 1 , r 1 ] [l1,r1] 中0的个数 * [ l 2 , r 2 ] [l2,r2] 中1的个数 * 2 i 2^i + + [ l 1 , r 1 ] [l1,r1] 中1的个数 * [ l 2 , r 2 ] [l2,r2] 中0的个数 * 2 i 2^i

所以我们可以统计 [ l 1 , r 1 ] [l1,r1] [ l 2 , r 2 ] [l2,r2] 中1的个数。
由于满足区间减法,只需统计 [ 1 , n ] [1,n] 中第 i i 为1的个数的树即可。到这儿很显然就是数位DP了。
不过记得dfs的时候dp数组也要取模,debug真的好累。。。。

int a[100];
ll dp[100][2][100];
inline ll mul(ll a, ll b, ll Mod)
{
    ll c = a*b-(ll)((long double)a*b/Mod+0.5)*Mod;
    return c<0 ? c+Mod : c;  //就是算的a*b%Mod;
}
ll dfs(int pos,bool sta,int k,bool limit){
    if(pos == -1) return sta == 1;
    if(!limit&&dp[pos][sta][k]!=-1) return dp[pos][sta][k];
    int up = limit?a[pos]:1;
    ll ans = 0;
    for(int i = 0;i <= up;++i) ans += dfs(pos-1,sta||(pos == k&&i),k,limit&&i == a[pos]),ans %= mod;
    if(!limit) dp[pos][sta][k] = ans;
    return ans;
}
ll solve(ll x,int k){
    int pos = 0;
    while(x){
        a[pos++] = x % 2;
        x >>= 1;
    }
    return dfs(pos-1,0,k,true);
}
long long inv(long long x,long long mod)
{
    long long k=mod-2,ans=1;
    while(k)
    {
        if (k&1) ans=ans*x%mod;
        x=x*x%mod;
        k>>=1;
    }
    return ans;
}
int main(){
    int t;
    memset(dp,-1,sizeof dp);
    scanf("%d",&t);
    while(t--){
        ll l1,r1,l2,r2;
        scanf("%lld%lld%lld%lld",&l1,&r1,&l2,&r2);
        ll n1 = r1 - l1 + 1;
        ll n2 = r2 - l2 + 1;
        ll ans = 0;
        for(int i = 0;i <= 60;i++){
            ll cnt1 = solve(r1,i) - solve(l1 - 1,i);
            ll cnt2 = solve(r2,i) - solve(l2 - 1,i);
            ans += mul(mul(cnt1,n2-cnt2,mod),(1LL<<i)%mod,mod);
            ans %=  mod;
            ans += mul(mul(n1-cnt1,cnt2,mod),(1LL<<i)%mod,mod);
            ans %= mod;
        }
        ans = (ans % mod + mod ) % mod;
        ans=ans*inv((n1%mod)*(n2%mod)%mod,mod)%mod;
        printf("%lld\n",ans);
    }
}

数位类统计问题

思路:从高位到低位逐步确定。然后看情况组合。

void init() //预处理组合数
{
    C[0][0] = 1;
    for (int i = 1; i <= 31; ++i)
    {
        C[i][0] = C[i - 1][0];
        for (int j = 1; j <= i; ++j)
            C[i][j] = C[i - 1][j] + C[i - 1][j - 1];
    }
}

P2518 [HAOI2010]计数
题意:
你有一组非零数字(不一定唯一),你可以在其中插入任意个0,这样就可以产生无限个数。比如说给定{1,2},那么可以生成数字12,21,102,120,201,210,1002,1020,等等。

现在给定一个数,问在这个数之前有多少个数。(注意这个数不会有前导0)

思路:
这个题目是数位DP思想,按位确定状态,然后计数。
比如1020,从高位开始,首先找< 1的数字有没有,有的话,放在这里,然后其余的数字多重集排列计数,然后留这儿原来的数字,继续下一位。
多重集排列:
已知 m = a 1 + a 2 + . . . . + a n m=a_1+a_2+....+a_n

m ! a 1 ! a 2 ! a 3 ! . . . a n ! \dfrac{m!}{a_1!\cdot a_2!\cdot a_3!...\cdot a_n!}

= C m a 1 C m a 1 a 2 C m a 1 a 2 a 3 . . . . . . . . . C m i = 1 n a i a n C_{m}^{a_1}\cdot C_{m-a1}^{a_2}\cdot C_{m-a_1-a_2}^{a3}.........\cdot C_{m-\sum_{i=1}^{n} a_i}^{a_n}

long long C(int x,int y){
	long long tot=1;
	for(int i=x;i>=x-y+1;i--) tot*=i;
	for(int i=y;i>=2;i--) tot/=i;
	return tot;
}
ll calAns(ll n){
	ll ans = 1;
	for(int i = 0;i <= 9;++i)if(b[i]){
		ans *= C(n,b[i]);
		n -= b[i];
	}
	return  ans;
}
int main(){
	cin >> s;
	int l = strlen(s);
	for(int i = 0;i < l;++i){
		a[i] = s[i] - '0';
		b[a[i]] ++;
	}
	ll ans = 0;
	for(int i = 0;i < l;++i){
		for(int j = 0;j < a[i];++j){
			if(b[j]) {b[j]--;ans += calAns(l -i - 1);b[j]++;}
		}
		b[a[i]] --;
	}
	cout << ans << endl;
}

#10163. 「一本通 5.3 例 1」Amount of Degrees
题意:
让你求 [ l , r ] [l,r] 内满足 n = b c 1 + b c 2 + . . . . + b c k n=b^{c_1}+b^{c_2}+....+b^{c_k} 的数的个数。
思路:
上述问题满足区间减法。所以我们只需求 [ 1 , n ] [1,n] 满足的条件的数的个数。
n n 分解为 b b 进制数。
然后从高位开始
如果当前为是1,先填0,后面的位数可以填 k k 个1,组合数计算累加。然后填1,往下一位走。
如果是0,直接往下位走。
如果大于1,则本位以及以后的位都可以填 1,组合数跳出。(说明我们填的所有数都比 n n 小)

最后注意:可能包含 n n 的情况即可。

int k,b;
int a[50];
int C[32][32];
void init() //预处理组合数
{
    C[0][0] = 1;
    for (int i = 1; i <= 31; ++i)
    {
        C[i][0] = C[i - 1][0];
        for (int j = 1; j <= i; ++j)
            C[i][j] = C[i - 1][j] + C[i - 1][j - 1];
    }
}
int solve(int x){
    int pos = 0;
    while(x){
        a[pos++] = x % b;
        x /= b;
    }
    int cnt = k;
    ll ans = 0;
    bool r = 1;
    for(int i = pos - 1;i >= 0;i --){
        if(a[i] !=0){
            if(a[i] == 1) ans += C[i][cnt--];
            else {ans += C[i+1][cnt];break;}
        }
        if(cnt == 0){ans ++; break;}//包含x的情况
    }
    return ans;
    
}
int main(){
    init();
    int l,r;
    cin >> l >> r >> k >> b;
    cout << solve(r) - solve(l - 1);
    
}




注:本文是吾看了其他大神的讲解学习数位dp怕有所遗忘所留,如有重复,实为借鉴。

发布了589 篇原创文章 · 获赞 31 · 访问量 5万+

猜你喜欢

转载自blog.csdn.net/qq_43408238/article/details/104389556