数位dp~~
前言
其实对于数位dp而言,没必要执着于做到dp的形式,做得dfs+记忆化的程度就足够了,这样能够降低写代码的难度。
1.最大为N的数字组合
https://leetcode.cn/problems/numbers-at-most-n-given-digit-set/description/
题解:
class Solution {
public:
int atMostNGivenDigitSet(vector<string>& str, int n) {
int tmp=n/10,len=1,offset=1;
while(tmp){
tmp/=10;
len++;
offset*=10;
}
int m=str.size();
vector<int>digits(m);
for(int i=0;i<m;i++){
digits[i]=stoi(str[i]);
}
int cnt[11];
cnt[0]=1;
int ans=0;
for(int i=m,k=1;k<len;k++,i*=m){
cnt[k]=i;
ans+=i;
}
return ans+f1(digits,cnt,len,n,offset);
}
int f1(vector<int>digits,int cnt[],int len,int n,int offset){
if(len==0)return 1;
int ans=0;
int cur=(n/offset)%10;
for(int i:digits){
if(i<cur)ans+=cnt[len-1];
else if(i==cur)ans+=f1(digits,cnt,len-1,n,offset/10);
else break;
}
return ans;
}
};
今天又新学到一个函数:stoi,可以把string转成int,好耶好耶~
2.统计整数数目
https://leetcode.cn/problems/count-of-integers/description/
class Solution {
public:
int min1,max1,len;
int num[23];
int mem[23][500][2];
int N=1e9+7;
int count(string n1, string n2, int min_sum, int max_sum) {
min1=min_sum,max1=max_sum;
int tp=0;
len=n1.length();
for(int i=0;i<len;i++){
num[i]=n1[i]-'0';
tp+=num[i];
}
int ans=f1(0,0,0)%N;
memset(mem,0,sizeof mem);
len=n2.length();
for(int i=0;i<len;i++){
num[i]=n2[i]-'0';
}
ans=(f1(0,0,0)%N-ans+N)%N;
return tp>=min1 && tp<=max1 ? (ans+1):ans;
}
int f1(int cur,int sum,int free){
if(sum>max1)return 0;
if(sum+(len-cur)*9<min1)return 0;
if(cur==len)return (sum>=min1)&&(sum<=max1)?1:0;
if(mem[cur][sum][free])return mem[cur][sum][free];
int ans=0;
if(free==0){
for(int i=0;i<=9;i++){
if(i<num[cur])ans=(f1(cur+1,sum+i,1)+ans)%N;
else if(i==num[cur])ans=(ans+f1(cur+1,sum+i,0))%N;
else break;
}
}else{
for(int i=0;i<=9;i++){
ans=(ans+f1(cur+1,sum+i,1))%N;
}
}
mem[cur][sum][free]=ans;
return ans;
}
};
取模:ans=(ans+mod)%mod
求a~b的范围等价与0~b的数减去0~a-1的数
3.统计特殊整数
https://leetcode.cn/problems/count-special-integers/description/
class Solution {
public:
int mem[11][2][1<<10];
int countSpecialNumbers(int n) {
int tmp=n/10,len=1,offset=1;
while(tmp){
tmp/=10;
len++;
offset*=10;
}
if(len==1)return n;
int ans=9;
for(int i=9,k=1;k<len-1;k++){
i=i*(10-k);
ans+=i;
}
return ans+f(len,len,0,n,offset,0);
}
int f(int len,int curlen,int free,int n,int offset,int status){
if(curlen==0)return 1;
if(mem[curlen][free][status])return mem[curlen][free][status];
int cur=(n/offset)%10;
int ans=0;
if(free){
int k1=10-len+curlen,k2=1,m=curlen;
while(m){
k2*=k1;
k1--;
m--;
}
ans+=k2;
}
else{
if(len==curlen){
for(int i=1;i<cur;i++){
if(!((1<<i)&status)){
ans+=f(len,curlen-1,1,n,offset/10,status^(1<<i));
}
}
if(!((1<<cur)&status)){
ans+=f(len,curlen-1,0,n,offset/10,status^(1<<cur));
}
}else{
for(int i=0;i<cur;i++){
if(!((1<<i)&status)){
ans+=f(len,curlen-1,1,n,offset/10,status^(1<<i));
}
}
if(!((1<<cur)&status)){
ans+=f(len,curlen-1,0,n,offset/10,status^(1<<cur));
}
}
}
mem[curlen][free][status]=ans;
return ans;
}
};
4.[SCOI2009] windy 数
题目背景
windy 定义了一种 windy 数。
题目描述
不含前导零且相邻两个数字之差至少为 2 2 2 的正整数被称为 windy 数。windy 想知道,在 a a a 和 b b b 之间,包括 a a a 和 b b b ,总共有多少个 windy 数?
输入格式
输入只有一行两个整数,分别表示 a a a 和 b b b。
输出格式
输出一行一个整数表示答案。
样例 #1
样例输入 #1
1 10
样例输出 #1
9
样例 #2
样例输入 #2
25 50
样例输出 #2
20
提示
数据规模与约定
对于全部的测试点,保证 1 ≤ a ≤ b ≤ 2 × 1 0 9 1 \leq a \leq b \leq 2 \times 10^9 1≤a≤b≤2×109。
#include<bits/stdc++.h>
using namespace std;
#define N 30
int len1=1,len2=1,offset1=1,offset2=1;//len记录几位,offset辅助用于截取
int a,b;
//q==10代表前面都是都没选数字
int mem[11][11][2];
int f(int q,int len,int offset,int free1,int n){//free1:怎么选都比上限小,q==10:不用考虑和前一位差至少为2
if(len==0)return 1;
if(mem[q][len][free1])return mem[q][len][free1];
int ans=0;
int cur=(n/offset)%10;
if(free1){
if(q==10){
ans+=f(10,len-1,offset/10,1,n);
for(int i=1;i<=9;i++){
ans+=f(i,len-1,offset/10,1,n);
}
}else{
for(int i=0;i<=9;i++){
if(abs(q-i)>=2)ans+=f(i,len-1,offset/10,1,n);
}
}
}else{
if(q==10){
if(len!=1)ans+=f(10,len-1,offset/10,1,n);
for(int i=1;i<cur;i++){
ans+=f(i,len-1,offset/10,1,n);
}
ans+=f(cur,len-1,offset/10,0,n);
}else{
for(int i=0;i<cur;i++){
if(abs(q-i)>=2)ans+=f(i,len-1,offset/10,1,n);
}
if(abs(q-cur)>=2)ans+=f(cur,len-1,offset/10,0,n);
}
}
mem[q][len][free1]=ans;
return ans;
}
int main(){
scanf("%d %d",&a,&b);
int tmp=(a-1)/10;
while(tmp){
tmp/=10;
len1++;
offset1*=10;
}
tmp=b/10;
while(tmp){
tmp/=10;
len2++;
offset2*=10;
}
int ans1=f(10,len2,offset2,0,b);
memset(mem,0,sizeof mem);
int ans=ans1-f(10,len1,offset1,0,a-1);
cout<<ans;
return 0;
}
5.SAC#1 - 萌数
题目背景
本题由世界上最蒟蒻的 SOL 提供。
寂月城网站是完美信息教室的官网。地址:http://191.101.11.174/mgzd。
题目描述
蒟蒻 SOL 居然觉得数很萌!
好在在他眼里,并不是所有数都是萌的。只有满足“存在长度至少为 2 2 2 的回文子串”的数是萌的——也就是说, 101 101 101 是萌的,因为 101 101 101 本身就是一个回文数; 110 110 110 是萌的,因为包含回文子串 11 11 11;但是 102 102 102 不是萌的, 1201 1201 1201 也不是萌的。
现在 SOL 想知道从 l l l 到 r r r 的所有整数中有多少个萌数。
由于答案可能很大,所以只需要输出答案对 1000000007 1000000007 1000000007( 1 0 9 + 7 10^9+7 109+7)的余数。
输入格式
输入包含仅 1 1 1 行,包含两个整数: l l l, r r r。
输出格式
输出仅 1 1 1 行,包含一个整数,即为答案。
样例 #1
样例输入 #1
1 100
样例输出 #1
10
样例 #2
样例输入 #2
100 1000
样例输出 #2
253
提示
记 n n n 为 r r r 在 10 10 10 进制下的位数。
对于 10 % 10\% 10% 的数据, n ≤ 3 n \le 3 n≤3。
对于 30 % 30\% 30% 的数据, n ≤ 6 n \le 6 n≤6。
对于 60 % 60\% 60% 的数据, n ≤ 9 n \le 9 n≤9。
对于全部的数据, n ≤ 1000 n \le 1000 n≤1000, l < r l < r l<r。
2024/2/4 添加一组 hack 数据。
正向思路的解:
#include<bits/stdc++.h>
using namespace std;
#define N 30
int mod=1e9+7;
string l,r;
int l1[1001],r1[1001];
int len1,len2;
int mem[11][11][2001][2][2];//记得初始化
int quickmod(int x){
long long base=1;
for(int i=0;i<x;i++){
base=(base*10)%mod;
}
return (int)base;
}
int f(int qq,int q,int cur,int len,int free1,int free2,int num[]){//free1:不考虑大小free2:不考虑回文
if(cur==len) return free2==1 ? 1:0;
int ans=0;
int m=num[cur];
if(mem[qq][q][cur][free1][free2])return mem[qq][q][cur][free1][free2];
if(free1){
if(q==10){
ans=(ans+f(10,10,cur+1,len,1,0,num))%mod;
for(int i=1;i<=9;i++){
ans=(ans+f(10,i,cur+1,len,1,0,num))%mod;
}
}else{
if(free2){
ans=(ans+quickmod(len-cur))%mod;
}else{
for(int i=0;i<=9;i++){
if(i==q || i==qq)ans=(ans+f(q,i,cur+1,len,1,1,num))%mod;
else ans=(ans+f(q,i,cur+1,len,1,0,num))%mod;
}
}
}
}else{
if(q==10){
ans=(ans+f(10,10,cur+1,len,1,0,num))%mod;
for(int i=1;i<m;i++){
ans=(ans+f(10,i,cur+1,len,1,0,num))%mod;
}
ans=(ans+f(10,m,cur+1,len,0,0,num))%mod;
}else{
if(free2){
for(int i=0;i<m;i++){
ans=(ans+f(q,i,cur+1,len,1,1,num))%mod;
}
ans=(ans+f(q,m,cur+1,len,0,1,num))%mod;
}else{
for(int i=0;i<m;i++){
if(i==q || i==qq)ans=(ans+f(q,i,cur+1,len,1,1,num))%mod;
else ans=(ans+f(q,i,cur+1,len,1,0,num))%mod;
}
if(m==q || m==qq)ans=(ans+f(q,m,cur+1,len,0,1,num))%mod;
else ans=(ans+f(q,m,cur+1,len,0,0,num))%mod;
}
}
}
mem[qq][q][cur][free1][free2]=ans;
return ans;
}
bool check(int l1[]){
if(len1==1)return false;
if(l1[0]==l1[1])return true;
for(int i=2;i<len1;i++){
if(l1[i]==l1[i-1] || l1[i]==l1[i-2])return true;
}
return false;
}
int main(){
cin>>l>>r;
len1=l.length(),len2=r.length();
for(int i=0;i<len1;i++){
l1[i]=l[i]-'0';
}
for(int i=0;i<len2;i++){
r1[i]=r[i]-'0';
}
int ans1=f(10,10,0,len1,0,0,l1);
memset(mem,0,sizeof mem);
int ans2=f(10,10,0,len2,0,0,r1);
int ans=(ans2-ans1+mod)%mod;
if(check(l1)){
cout<<(ans+1)%mod<<endl;
}else{
cout<<ans<<endl;
}
return 0;
}
正向会超时
反向思路解:
#include<bits/stdc++.h>
using namespace std;
#define N 30
int mod=1e9+7;
string l,r;
int l1[1001],r1[1001];
int len1,len2;
int mem[11][11][2001][2];//记得初始化
int f(int qq,int q,int cur,int len,int free,int num[]){
if(cur==len) return 1;
int ans=0;
int m=num[cur];
if(mem[qq][q][cur][free])return mem[qq][q][cur][free];
if(free){
if(q==10){
if(cur!=len-1)ans=(ans+f(10,10,cur+1,len,1,num))%mod;
for(int i=1;i<=9;i++){
ans=(ans+f(10,i,cur+1,len,1,num))%mod;
}
}else{
for(int i=0;i<=9;i++){
if(i!=qq && i!=q)ans=(ans+f(q,i,cur+1,len,1,num))%mod;
}
}
}else{
if(q==10){
if(cur!=len-1)ans=(ans+f(10,10,cur+1,len,1,num))%mod;
for(int i=1;i<m;i++){
ans=(ans+f(10,i,cur+1,len,1,num))%mod;
}
ans=(ans+f(10,m,cur+1,len,0,num))%mod;
}else{
for(int i=0;i<m;i++){
if(i!=qq && i!=q)ans=(ans+f(q,i,cur+1,len,1,num))%mod;
}
if(m!=qq && m!=q)ans=(ans+f(q,m,cur+1,len,0,num))%mod;
}
}
mem[qq][q][cur][free]=ans;
return ans;
}
int cnt(int num[],int len){
long long all=0;
long long base=1;
for(int i=len-1;i>=0;i--){
all=(all+base*num[i])%mod;
base=(base*10)%mod;
}
int p=(all-f(10,10,0,len,0,num)+mod)%mod;
memset(mem,0,sizeof mem);
return p;
}
bool check(int l1[]){
if(len1==1)return false;
if(l1[0]==l1[1])return true;
for(int i=2;i<len1;i++){
if(l1[i]==l1[i-1] || l1[i]==l1[i-2])return true;
}
return false;
}
int main(){
cin>>l>>r;
len1=l.length(),len2=r.length();
for(int i=0;i<len1;i++){
l1[i]=l[i]-'0';
}
for(int i=0;i<len2;i++){
r1[i]=r[i]-'0';
}
int ans1=cnt(r1,len2),ans2=cnt(l1,len1);
int ans=(ans1-ans2+mod)%mod;
if(check(l1)){
cout<<(ans+1)%mod<<endl;
}else{
cout<<ans<<endl;
}
return 0;
}
6.[ZJOI2010] 数字计数
题目描述
给定两个正整数 a a a 和 b b b,求在 [ a , b ] [a,b] [a,b] 中的所有整数中,每个数码(digit)各出现了多少次。
输入格式
仅包含一行两个整数 a , b a,b a,b,含义如上所述。
输出格式
包含一行十个整数,分别表示 0 ∼ 9 0\sim 9 0∼9 在 [ a , b ] [a,b] [a,b] 中出现了多少次。
样例 #1
样例输入 #1
1 99
样例输出 #1
9 20 20 20 20 20 20 20 20 20
提示
数据规模与约定
- 对于 30 % 30\% 30% 的数据,保证 a ≤ b ≤ 1 0 6 a\le b\le10^6 a≤b≤106;
- 对于 100 % 100\% 100% 的数据,保证 1 ≤ a ≤ b ≤ 1 0 12 1\le a\le b\le 10^{12} 1≤a≤b≤1012。
貌似解法跟数位dp没啥关系乐乐乐乐~~
#include<bits/stdc++.h>
using namespace std;
#define int long long
int a,b;
int f(int d,int n){
int ans=0;
for(int tmp=n,left,right=1,cur;tmp!=0;right*=10){
left=tmp/10;
cur=tmp%10;
tmp/=10;
if(d==0)left--;
ans+=left*right;
if(cur>d)ans+=right;
else if(cur==d)ans+=n%right+1;
}
return ans;
}
signed main(){
cin>>a>>b;
for(int i=0;i<=9;i++){
cout<<f(i,b)-f(i,a-1)<<' ';
}
return 0;
}
总结感悟
1.函数往往含参数:1.遍历到第几个数位 2.free:当前数位选择自不自由 3前置位
2.需要的参数==与状态转移方程相关的参数
3.函数含义往往为:已经决定好了之前的数位的前提下,剩余的数位可选的方案数。