数位dp学习小结:
数位dp解决的基本问题:给定区间[l,r],求出某些满足条件限制的的的数字总数。这些数字并不一定是10进制下,也有可能在26进制下进行。
举个栗子:
1)给定区间[l,r]统计区间内数字数位上没有连续的 62 和 4的数字个数。例如621,40都是非法数字。
2)26进制数字给出区间[l,r]统计数字上有连续的SHU出现的数字个数。
解题核心:dp状态的建立和递推方程的构建。
理论上状态的增加可以通过增加dp数组的维数来表示。
下面的一道道例题中自行体会。
给出个模板框架(我也是学长手中传下来的 )
#include<bits/stdc++.h>
typedef long long LL;
int a[20];
LL dp[20][state];
LL dfs(int pos,/*state变量*/,bool limit/*数位上界变量*/)
{
if(pos==-1) return 1;
if(!limit && dp[pos][state]!=-1) return dp[pos][state];
int up=limit?a[pos]:9;
LL ans=0;
for(int i=0;i<=up;i++)
{
if() ...
else if()...
ans+=dfs(pos-1,/*状态转移*/,limit && i==a[pos])
}
if(!limit ) dp[pos][state]=ans;
return ans;
}
LL solve(LL x)
{
int pos=0;
while(x)
{
a[pos++]=x%10;
x/=10;
}
return dfs(pos-1/*从最高位枚举*/, /*一系列状态 */ ,true);
}
int main()
{
LL le,ri;
memset(dp,-1,sizeof dp);
while(~scanf("%lld%lld",&le,&ri))
{
printf("%lld\n",solve(ri)-solve(le-1));
}
}
看不懂模板没有关系,看一道例题分析分析:
HDU 2089
题意:
给定区间[l,r]统计区间内数字数位上没有连续的 62 和 4的数字个数。
思路:
1)先转化成求0~n中满足条件的的数字个数solve(n),则ans=solve ( r ) -solve(l-1)
2)首先对数字x某个数位pos来说如果不考虑限制:则该pos位可取的数字为0~9,考虑大小限制则为0 ~ a[pos] (x的第pos位数字是什么)
3)这题可划分三种情况:
pos位下一位填4,直接跳过不枚举
pos位填了6,下一位填2也应当跳过不枚举
其他情况合法。
4)dp数组维数的确定,第一维:数位,第二位:0,1代表枚举到当前数位的前一位是否含有数字6.
AC code:
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
int a[20]; //数位数组
ll dp[20][2]; //位数+state数位限制
//state 表示i == 6 该数字是否包含数位6
ll dfs(int pos,int state,bool limit){ //数位,限制条件,数位限制变量
if(pos==-1) return 1;
if(!limit&&dp[pos][state]!=-1){ //记忆化搜索答案在dp数组之中如果已经搜索过不需要再搜索
return dp[pos][state];
}
int up = limit?a[pos]:9;//数位限制
ll ans = 0;
for(int i=0;i<=up;++i){
if((state&&i==2)||i==4) continue;
/*三种情况
1)本位有6下一位为2跳过
2)下一位直接为4跳过
3)符合题意
*/
ans += dfs(pos-1,i==6,limit&&i==a[pos]);
}
if(!limit){
dp[pos][state] = ans;
}
return ans;
}
ll solve(ll 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));
ll l,r;
while(~scanf("%lld%lld",&l,&r)){
if(l+r==0) break;
printf("%lld\n",solve(r)-solve(l-1));
}
return 0;
}
HDU 3652
题意:给出数字n,统计小于n是13的倍数且数位中含有13的数字
思路:
dp数组开三维
第一维: 数位
第二维state: state=0 表示没出现过13 state=1表示上一位是1 ,state=2表示前面出现过13
第三维mod: 前i位数字 mod 13 的余数
其他同模板。
AC code:
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
int a[30];
ll dp[30][3][14];
ll dfs(int pos,int state,int mod,bool limit){
if(pos==-1){
if(state==2&&mod==0) return 1;
return 0;
}
if(!limit&&dp[pos][state][mod]!=-1){
return dp[pos][state][mod];
}
int up = limit?a[pos]:9;
ll ans = 0;
for(int i=0;i<=up;++i){
int sta = 0;
if(state==2||(state==1&&i==3)) sta = 2;
else if(i==1) sta = 1;
ans += dfs(pos-1,sta,(mod*10+i)%13,limit&&i==a[pos]);
}
return limit?ans:(dp[pos][state][mod]=ans);
}
ll solve(ll x){
int pos = 0;
while(x){
a[pos++] = x % 10;
x /= 10;
}
return dfs(pos-1,0,0,true);
}
int main(){
memset(dp,-1,sizeof(dp));
ll n;
while(~scanf("%lld",&n)){
printf("%lld\n",solve(n));
}
return 0;
}
题意:求区间[a,b]中数字转化为2进制后 0的个数>=1的个数的数字的个数
思路:与前面略有不同:
1)a数组中存的的0和1
2)dfs中需要增加一个bool变量lead判断是否含有前导零,lead=0表示非前导零。
dp数组三维
第一维: 数位
第二维:存储zero的个数
第三维:存储非前导零one的个数
AC code:
#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
using namespace std;
typedef long long ll;
ll a[40];
ll dp[40][40][40];
ll dfs(int pos,int zero,int one,bool lead,bool limit){
if(pos==-1){
if(zero>=one) return 1;
return 0;
}
if(!limit&&dp[pos][zero][one]!=-1){
return dp[pos][zero][one];
}
ll ans = 0;
int up = limit?a[pos]:1;
for(int i=0;i<=up;++i){
int z=zero,o=one;
if(i) ++o,lead = 0;
//非前导零
if(!lead&&!i) ++z;
ans += dfs(pos-1,z,o,lead,limit&&i==a[pos]);
}
return limit?ans:(dp[pos][zero][one]=ans);
}
ll solve(ll x){
int pos = 0;
while(x){
a[pos++] = x%2;
x >>= 1;
}
return dfs(pos-1,0,0,1,1);
}
int main(){
ll a,b;
memset(dp,-1,sizeof(dp));
while(~scanf("%lld%lld",&a,&b)){
if(a>b) swap(a,b);
printf("%lld\n",solve(b)-solve(a-1));
}
return 0;
}
Windy数
题意:找出[l,r]之间不含前导零且相邻两个数字之差至少为2的正整数的数字的个数。注意:1~9这九个数字也是windy数字
思路: 这题和模板也差不多,首先dp数组显然是开二维,第一维:数位,第二维:记录差值。 判断添加abs(i-state)<2的都是应该跳过的,但应当注意state初值的传入.即改变state初值的传入,见代码注释。
AC code:
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
ll a[40];
ll dp[40][12];
ll dfs(int pos,int state,bool limit){
if(pos==-1){
return 1;
}
if(!limit&&dp[pos][state]!=-1&&state>=0){
return dp[pos][state];
}
ll ans = 0;
int up = limit?a[pos]:9;
for(int i=0;i<=up;++i){
if(abs(i-state)<2) continue;
int sta = i;
if(i==0&&state==-10){
sta = state;
}
ans += dfs(pos-1,sta,limit&&i==a[pos]);
}
if(!limit&&state>=0){
dp[pos][state] = ans;
}
return ans;
}
ll solve(ll x){
int pos = 0;
while(x){
a[pos++]=x%10;
x /= 10;
}
return dfs(pos-1,-10,1);
//-10不是必要只要0-state>2就可以 可以-7,-3
//-10作为开始可以枚举出比次高位小的数字
//1298->0298
//不传入-10直接传入0会导致枚举缺少部分windy数字
}
int main(){
memset(dp,-1,sizeof(dp));
ll l,r;
while(~scanf("%lld%lld",&l,&r)){
printf("%lld\n",solve(r)-solve(l-1));
}
return 0;
}
Shu oj水题
题意:题意:给一个数n,求0~n内有多少个数满足其二进制形式不存在相邻的1,前导零不影响数字统计
思路:将数字n转转换成二进制形式,二维dp数组,第二维度表示其二进制数字是否存在相邻的1。
AC code:
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
ll a[70];
ll dp[70][2];
ll dfs(int pos,int state,bool limit){
if(pos==-1){
return 1;
}
if(!limit&&dp[pos][state]!=-1){
return dp[pos][state];
}
ll ans= 0;
int up = limit?a[pos]:1;
for(int i=0;i<=up;++i){
if(state&&i) continue; //前一位是1当前位也是1,跳过
ans += dfs(pos-1,i,limit&&i==a[pos]);
}
return limit?ans:(dp[pos][state]=ans);
}
ll solve(ll x){
int pos = 0;
while(x){
a[pos++] = x%2;
x >>= 1;
}
return dfs(pos-1,0,1);
}
int main(){
memset(dp,-1,sizeof(dp));
ll n;
while(~scanf("%lld",&n)){
printf("%lld\n",solve(n));
}
return 0;
}
明7暗7加强版
题意:1≤M,N≤100000000, 给出M,N;找出M之后第N个数位中含有7或者是7倍数的数字。
思路:
数位dp是解决统计一类数位中符合某些规律的数字的方法, 那么我们可以用一次数位dp找出对于数字x,在0-x内其符合题意的数字有多少个。
这里我们利用反向统计的方法,即统计数位中不包括7,不是7倍数的数字,这样可以降低搜索的复杂度 f(x) = x - solve(x); 表示0-x中数位中含有7或是7倍数的数字的个数,这一部分就是裸的数位dp.
那么找M之后第N个数字怎么查找呢? 精确查找是不太可能的,但注意M,N<=1e8所以我们可以直接取一个上界r = 3e9计算[M,r]中查找符合f( r ) - f(M) ==N的数字的个数, 二分查找缩小区间[M,r]出满足题意且最小的数字x即为答案.
AC code:
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll INF = 3e9;
int a[12];
ll dp[12][8];
ll dfs(int pos,int state,bool limit){
if(pos==-1){
if(!state) return 0;
return 1;
}
if(!limit&&dp[pos][state]!=-1){
return dp[pos][state];
}
ll ans = 0;
int up = limit?a[pos]:9;
for(int i=0;i<=up;++i){
if(i==7) continue;
ans += dfs(pos-1,(state*10+i)%7,limit&&i==a[pos]);
}
return limit?ans:(dp[pos][state]=ans);
}
ll solve(int x){
int pos=0;
while(x){
a[pos++] = x % 10;
x /= 10;
}
return dfs(pos-1,0,1);
}
int main(){
memset(dp,-1,sizeof(dp));
ll M,N;
while(~scanf("%lld%lld",&M,&N)){
ll t = M - solve(M);
ll l = M,r = INF,mid,ans,tmp;
while(l<=r){
mid = (l+r)>>1;
tmp = mid - solve(mid) - t;
if(tmp>=N){
r = mid - 1;
ans = mid;
}
else{
l = mid + 1;
}
}
printf("%lld\n",ans);
}
return 0;
}
题意:
定义平衡数字:从该数字上某一位做轴,该位跳过不计算,分成左右两半,权值相等.
eg:4139 从第三位开始数字进行划分: 42 + 11 = 91.
eg:2317 第三位开始划分两半:22 + 31 = 71。
eg:0,1,2,3,4,5,6,7,8,9也是平衡数字.
给定[l,r]求出其中的平衡数字个数。
思路:枚举平衡中轴,假设中轴位mid,当前数位pos上数字为i,左右权值相等可以转换成: sigma(i*(pos-mid)) = 0; 三维dp数数组,并注意0的处理,因为每一次枚举中轴都会算一次0.
AC code
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
int a[20];
ll dp[20][20][1800];
ll dfs(int pos,int state,int sum,bool limit){
if(pos==-1){
return sum==0;
}
if(!limit&&dp[pos][state][sum]!=-1){
return dp[pos][state][sum];
}
int up=limit?a[pos]:9;
ll ans=0;
for(int i=0;i<=up;++i){
ans += dfs(pos-1,state,sum+i*(pos-state),limit&&i==a[pos]);
}
return limit?ans:(dp[pos][state][sum]=ans);
}
ll solve(ll x){
if(x==-1) return 0;
int pos=0;
while(x){
a[pos++]=x%10;
x/=10;
}
ll ans=0;
for(int i=pos-1;i>=0;--i){
ans += dfs(pos-1,i,0,1);
}
return (ans-pos+1);
}
int main(){
int T;
ll l,r;
memset(dp,-1,sizeof(dp));
cin>>T;
while(T--){
cin>>l>>r;
cout<<solve(r)-solve(l-1)<<endl;
}
return 0;
}
/* 题意:
f(x)的定义:F(x) = An * 2n-1 + An-1 * 2n-2 + ... + A2 * 2 + A1 * 1,
Ai是十进制数位,
然后给出a,b求区间[0,b]内满足f(i)<=f(a)的i的个数。
思路:
求出sum = f(x),枚举数数字权值,构造当前数位的前缀和state,
统计sum-state>=0的数字个数
其他同模板
*/
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn = 10000+5;
int a[15];
ll dp[15][maxn];
int sum;
ll dfs(int pos,int state,bool limit){
if(pos==-1){
return state<=sum;
}
if(sum<state) return 0;
if(!limit&&dp[pos][sum-state]!=-1){
return dp[pos][sum-state];
}
ll ans = 0;
int up = limit?a[pos]:9;
for(int i=0;i<=up;++i){
ans += dfs(pos-1,state+i*(1<<pos),limit&&i==a[pos]);
}
return limit?ans:(dp[pos][sum-state]=ans);
}
ll solve(ll x){
int pos = 0;
while(x){
a[pos++] = x % 10;
x /= 10;
}
return dfs(pos-1,0,1);
}
int getsum(int x){
int a[20],pos=0;
while(x){
a[pos++] = x%10;
x /= 10;
}
int ans = 0;
for(int i=pos-1;i>=0;--i){
ans = ans * 2 + a[i];
}
return ans;
}
int main(){
memset(dp,-1,sizeof(dp));
int T,kase=0,x,b;
scanf("%d",&T);
while(T--){
scanf("%d%d",&x,&b);
sum = getsum(x);
printf("Case #%d: %lld\n",++kase,solve(b));
}
return 0;
}
从计数转向求和:
HDU 4507
题意:
求区间[L,R]中满足下面三个条件数字的的平方和:
1)数字中不带有7
2)数字数位之和不能是7的倍数
3)该数字不能是7的倍数
思路:
数位dp显然,但是数位dp通常计,这里需要将计数转换为求和
假设用cnt表示满足条件的小于X的数字个数,sum表示cnt个数的数字之和,sqsum表示cnt个数字平方之和
可开一个结构体变量Node记录三个变量值,ans表示当前pos位满足条件,nex表示pos位之后满足条件的结构体变量
1)cnt求解基础数位dp:
开三维Node dp数组,第一维:pos,第二维:sta1,前pos个数字之和mod7的余数
第三维:sta2,前pos位数字组成的数字mod7的余数 (sta2*10+i)%mod;
2)sum解决:
ans.sum = (ans.sum + i*10^pos*nex.cnt+nex.sum) i表示第pos位填充的数字
i*10^pos*nex.cnt满足前pos条件下数字有nex.cnt个,得该位填充数字对求和的总贡献
3)sqsum解决:
假设当前状态下表示的某个数字(i*10^pos+xj) x是pos位之后填充的完整数字
当前状态的某个数字 sqsum = (i*10^pos+xj)^2 = (i*10^pos+2*10^pos*xj + xj^2;
当前状态下所有满足条件数字(共计nex.cnt个)的平方和:
ans.sqsum = ans.sqsum + (i*10^pos)*nex.cnt + 2*i^10^pos*sigma(xj)+sigma(xj^2)
sigma(xj) = nex.sum;
sigma(xj^2) = nex.sqsum;
ans.sqsum = ans.sqsum + (i*10^pos)*nex.cnt + 2*i^10^pos*nex.sum+nex.sqsum;
到此思路完成
最后切记取模!!!乘法取模,疯狂取模,(solve(r)-solve(l-1)+mod)%mod!!!!
AC code:
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll mod = 1e9+7;
struct Node{
ll cnt,sum,sqsum;
Node(){};
Node(ll c,ll s,ll sq):cnt(c),sum(s),sqsum(sq){};
};
int a[20];
ll p[20]; //10^pos
Node dp[20][8][8];
void init(){
p[0] = 1;
for(int i=1;i<20;++i){
p[i] = p[i-1]*10LL%mod;
}
for(int i=0;i<20;++i){
for(int j=0;j<8;++j){
for(int k=0;k<8;++k){
dp[i][j][k].cnt = -1;
}
}
}
}
Node dfs(int pos,int sta1,int sta2,bool limit){
if(pos==-1){
return Node(sta1!=0&&sta2!=0,0,0);
}
if(!limit&&dp[pos][sta1][sta2].cnt!=-1){
return dp[pos][sta1][sta2];
}
int up = limit?a[pos]:9;
Node ans(0,0,0),nex(0,0,0);
for(int i=0;i<=up;++i){
if(i==7) continue;
nex = dfs(pos-1,(sta1+i)%7,(sta2*10+i)%7,limit&&i==a[pos]);
ll A = i*p[pos]%mod;
ans.cnt = (ans.cnt + nex.cnt)%mod;
ans.sum = (ans.sum+(A*nex.cnt)%mod+nex.sum)%mod;
ans.sqsum = (ans.sqsum+(A*A%mod*nex.cnt%mod+2*A*nex.sum%mod+nex.sqsum%mod)%mod)%mod;
}
return limit?ans:(dp[pos][sta1][sta2]=ans);
}
ll solve(ll x){
int pos = 0;
while(x){
a[pos++] = x % 10;
x /= 10;
}
return dfs(pos-1,0,0,1).sqsum%mod;
}
int main(){
init();
ll l,r;
int T;
scanf("%d",&T);
while(T--){
scanf("%lld%lld",&l,&r);
printf("%lld\n",((solve(r)-solve(l-1))+mod)%mod);
}
return 0;
}
暂且写到这里,以后碰到新的继续放入,QAQ还是太弱了。