【DP】【数位DP】

数位DP

  • 研究数的搭配的问题,而不是数的大小的问题。

  • 在没有被limit限制的情况下,有些计算的结果可以直接拿来使用,比如当上限为100000,

    要求999xxx和888xxx的组成的方案数,会发现xxx的组成方案数是一样的(当然是在相同的限制条件下;999和888对下一位造成的影响是相同,使得其进入到相同的搜索中)

数位dp整体思路

  • 数位dp套路感比较强

三个dp数组的基本量

  • pos,用来记录当前的位置
  • last,用来记录上一位的位置,初始化的时候要设置成一个对第一次转移没有影响的数字
  • limit,看是否逼近与上界(逼近上界,数字的变化范围比较小)

dfs模块

  • dfs里面看一下有没有记忆化过,再写个边界,然后for循环,从0扫到上限(判断一下),把答案累加一下,return dp =ans

数位DP模板

  • 可通过设计使得,dp数组降维
#include<bits/stdc++.h>
using namespace std;
数据类型  转化成数字的数组,关于长度的计数器,dp[位数][进制的上限][是否进行限制][或其他限制];
数据类型  dfs(当前位置,上一位的数字,bool limit,外加的限制条件(比如前导0等等))
{
	返回的答案
	if(终止条件(到头了))
	   return 1;
	if(是否进行过记忆了,dp!=-1)//已经算过了,有limit的话就单独算,这里就有了limit的优势了
       return 直接返回记忆了的结果
    for(int 数字=0;数字<=(是否被限制?限制下的最高位:通常的最高位);数字++)
    {
    	if(不符合的条件)
    	   continue;
    	ans += dfs(处理下一位,这一位的数字,观察一下limit是不是有必要再传递下去limit && 所选择的数字是否(又)等于所存储了的数字对应位置的数字(有没有碰到顶),其他条件);	
	}
	dp[pos][last][limit][其他条件] = ans;//先记忆 
	
	return ans; //后返回 
}


数据类型 f(int x)
{
	如果有多次调用,需将上次过程中使用的数组和涉及到的用来统计或其他目的的全局变量重新进行初始化
	预处理:将数分解成数组,方便和过程中的数组进行比对,确定要不要对其进行限制
	(reverse(数组,数组+数组长度))//这里看个人习惯,颠倒数组比较好处理,还是直接处理
	return dfs(初始位置(一般置零),上一位的数字(一般设置成大于10的数),true);//第一位它是有上限的,limit要赋值为1 
 } 

int main()
{
		读入 
		一般是求区间内的一个答案
		可求出0到r的一个答案和0到l-1的一个答案,
		用这两个答案相减来获取区间l到r的答案
	return 0;
}

记忆化搜索的优化写法

	dp[pos][last][limit][其他条件] = ans;//先记忆 
	
	return ans; //后返回 
   return dp[pos][last][limit][其他条件] = ans;  

题目集

AcWing 1082. 数字游戏

科协里最近很流行数字游戏。

某人命名了一种不降数,这种数字必须满足从左到右各位数字呈非下降关系,如 123123,446446。

现在大家决定玩一个游戏,指定一个整数闭区间 [a,b][a,b],问这个区间内有多少个不降数。

  • for循环的下界直接从last开始
#include<bits/stdc++.h>
typedef long long ll;
using namespace std;
const int N = 35;
int num[N],dp[N][10][2],cnt=0;

int dfs(int pos,int last,bool limit)
{
	if(dp[pos][last][limit]!=-1)  
	   return dp[pos][last][limit];
	if(pos==cnt) 
	   return 1;
	int ans = 0;
	for(int i=(last==15?0:last);i<=(limit==1?num[pos]:9);i++)
		ans+=dfs( pos+1 , i ,limit && (i==num[pos]));

	return dp[pos][last][limit] = ans;
}

void init()
{
	memset(num,0,sizeof(num));
	memset(dp,-1,sizeof(dp));
	cnt = 0;
}

int f(ll x)
{
	if(x==0) return 1;//一般的话由于两个数都大于1,0的部分会抵消掉 
	init();
	while(x)
	   num[cnt++] = x%10,x/=10;
	reverse(num,num+cnt);
	return  dfs(0,15,true);
}

int main()
{
	ll a,b;
	while(cin>>a>>b)cout<<f(b)-f(a-1)<<endl;
	return 0;
}

AcWing 1084. 数字游戏 II

由于科协里最近真的很流行数字游戏。

某人又命名了一种取模数,这种数字必须满足各位数字之和 mod N 为 0。

现在大家又要玩游戏了,指定一个整数闭区间 [a.b][a.b],问这个区间内有多少个取模数。

  • 思路:增设一个维度来专门保存每一次计算并取余后的结果。
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int L = 35,N = 1E2;
int dp[L][10][2][N],num[L],cnt;
int Mod;

int dfs(int pos,int last,bool limit,int cur)
{
	if(dp[pos][last][limit][cur]!=-1)
	  return dp[pos][last][limit][cur];
	
	if(pos==cnt)
	   return cur == 0;
	
	int ans=0;
	for(int i=0;i<=(limit==1?num[pos]:9);i++)
		ans+=dfs(pos+1,i,limit&&(num[pos]==i),(cur+i)%Mod);
    	
	return dp[pos][last][limit][cur]=ans; 
}

void init()
{
	memset(dp,-1,sizeof(dp));
        cnt = 0;
}

int f(ll x)
{
	init();
	while(x>0)
	   num[cnt++] = x%10,x/=10;
	reverse(num,num+cnt);
	return dfs(0,15,true,0);
}

int main()
{   
        int a,b;
        while(cin>>a>>b>>Mod)
           cout<<f(b)-f(a-1)<<endl;
	return 0;
}

AcWing 1083.Windy数

Windy 定义了一种 Windy 数:不含前导零且相邻两个数字之差至少为 22 的正整数被称为 Windy 数。

Windy 想知道,在 A 和 B 之间,包括 A和 B,总共有多少个 Windy 数?

  • 107和007是两种不同的生物,107的十位0会使得个位不能出现1,而007的十位0,能使得个位上出现1。
#include<bits/stdc++.h>
using namespace std;
const int N = 12;
int num[N],cnt,dp[N][N][2][2];

int dfs(int pos,int last,bool limit,bool forzero)
{
    int ans=0;
	
    if(dp[pos][last][limit][forzero]!=-1)//已经记忆过的话,就直接进行读档
       return dp[pos][last][limit][forzero];

    if(pos==cnt)//cnt会比正常的长度再多一 
       return 1;
       
    for(int i=0;i<=(limit==1?num[pos]:9);i++)
    {
    	if(!forzero&&abs(last-i)<2)continue;//在不具有前导0的情况下,限制条件才有必要存在,前面有位数,才具有比较的意义
    	ans += dfs(pos+1,i,limit && (num[pos]==i),forzero&&(i==0));
	}
	
    return	dp[pos][last][limit][forzero] = ans;//存档
}

void init()//算两次,每次都要初始化
{
    memset(dp,-1,sizeof(dp));
    memset(num,0,sizeof(num));
    cnt=0;
}

int f(int x)
{
    if(x<10)return x+1;//个位数不受其他位数的影响,可以直接返回答案
    init();
    while(x>0)
        num[cnt++]=x%10,x/=10;
    reverse(num,num+cnt);//num低位保留数字高位比较好处理 
    return dfs(0,12,1,1);//这里的12相当于初始化,只要能够允许第一次要处理所产生的所有情况没有受到影响即可
}

int main()
{
    int a,b;
    cin>>a>>b;
    cout<<f(b)-f(a-1)<<endl;//差分的思想
    return 0;
}
原文地址:https://www.cnblogs.com/BeautifulWater/p/15064860.html