DP训练——数位DP
数位DP
BZOJ1026
题意
求到间,不含前导零且相邻两个数字之差至少为的正整数的个数。
题解
状态定义:表示当前处理到第位,上一位为,是否达到上限状态为的方案数
目标:
边界:
转移方程:
代码如下
/*
*/
#define method_1
#ifdef method_1
/*
*/
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cmath>
#include<set>
#include<map>
#include<queue>
#include<stack>
#include<vector>
#include<cstring>
#include<cstdlib>
#include<iomanip>
#include<ctime>
#include<string>
#include<bitset>
#define D(x) cout<<#x<<" = "<<x<<" "
#define E cout<<endl
using namespace std;
typedef long long ll;
typedef pair<int,int>pii;
const int maxn=2000000000+5;
const int maxl=10+2;
const int INF=0x3f3f3f3f;
int A,B;
vector<int>v;
int f[maxl][maxl][2];
int dfs(int x,int st,int op){
if(!x) return 1;
if(~f[x][st][op]) return f[x][st][op];
int res=0;
int maxx=op?v[x]:9;
for(int i=0;i<=maxx;i++){
if(abs(st-i)<2) continue;
if(st==11&&i==0) res+=dfs(x-1,11,op&&(i==maxx)); //存在前导0 所以后面的选择方案数依然不受限制
else res+=dfs(x-1,i,op&&(i==maxx));
}
return f[x][st][op]=res;
}
int solve(int x){
memset(f,-1,sizeof(f));
v.clear();
v.push_back(-1);
while(x){
v.push_back(x%10);
x/=10;
}
return dfs(v.size()-1,11,1); //st=11 表示没有相邻位的限制
}
int main() {
ios::sync_with_stdio(false);
// freopen("BZOJ1026.in","r",stdin);
cin>>A>>B;
cout<<solve(B)-solve(A-1);
return 0;
}
#endif
#ifdef method_2
/*
*/
#endif
#ifdef method_3
/*
*/
#endi
BZOJ3131
题意
给定的方格,每个点会为点带来一个权值为的贡献,其中表示在十进制下各位数字之积。
求最大个权值的和对取模。
题解
预处理出所有范围在内的积,离散化后存储在数组中。
接下来进行数位dp,求出数组表示有多少个数字各个位相乘后的结果是的。
于是便可以根据数组进行多路归并,求出前个最大的权值了。
状态定义:表示当前处理到第位,当前积为,是否达到上限状态为的方案数
目标:数组
边界:
转移方程:
题解链接 https://blog.csdn.net/xiaoming_p/article/details/79560210
代码如下
/*
*/
#define method_2
#ifdef method_1
#endif
#ifdef method_2
/*
*/
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cmath>
#include<set>
#include<map>
#include<queue>
#include<stack>
#include<vector>
#include<cstring>
#include<cstdlib>
#include<iomanip>
#include<ctime>
#include<string>
#include<bitset>
#define D(x) cout<<#x<<" = "<<x<<" "
#define E cout<<endl
using namespace std;
typedef long long ll;
typedef pair<int,int>pii;
const int maxn=500000+5;
const int maxm=15000+5;
const int maxl=20+5;
const int INF=0x3f3f3f3f;
const ll mod=1000000007;
ll n;
int k;
int z[maxl],sz=0,cnt=0;
ll d[maxl][maxm][2],_hash[maxm],ls[maxn];
map<ll,int>mp;
void dfs(ll now){ //预处理出所有的积,积的范围在[1,n]以内
if(now>n) return;
if(mp[now]) return;
mp[now]=1;
ls[++cnt]=now;
for(int i=1;i<=9;i++) dfs(now*i);
}
void pre(ll x){
while(x){
z[++sz]=x%10;
x/=10;
}
dfs(1);
sort(ls+1,ls+cnt+1);
cnt=unique(ls+1,ls+cnt+1)-ls-1;
}
ll dp(int pos,int j,int lim){ //当前处理到第pos位,处理的乘积是离散数组(ls)的第j个数,是否达到上限状态为lim
if(pos<1) return j==1;
if(~d[pos][j][lim]) return d[pos][j][lim];
ll ret=0;
int up=lim?z[pos]:9;
for(int i=1;i<=up;i++){
if(ls[j]%i) continue; //不能整除
ret+=dp(pos-1,lower_bound(ls+1,ls+cnt+1,ls[j]/i)-ls,lim&&(i==z[pos]));
}
return d[pos][j][lim]=ret;
}
struct data{
int id1,id2;
ll d;
friend bool operator<(data a,data b){
return a.d<b.d;
}
};
priority_queue<data>q;
void solve(){
ll ans=0;
memset(d,-1,sizeof(d));
//_hash[i]表示有多少个数字各个位相乘后的结果是ls[i]的
for(int i=1;i<=cnt;i++) _hash[i]+=dp(sz,i,1); //处理位数相同的
for(int i=1;i<sz;i++) for(int j=1;j<=cnt;j++) _hash[j]+=dp(i,j,0); //处理位数较小的
sort(_hash+1,_hash+cnt+1);
reverse(_hash+1,_hash+cnt+1);
for(int i=1;i<=cnt;i++) if(_hash[i]==0){
cnt=i-1;
break;
}
for(int i=1;i<=cnt;i++) D(_hash[i]);
E;
//_hash表里面任意选(i,j),_hash[i]*_hash[j]即表示为某个格子带来权值的原始格子数(即一个格点的最终权值)
for(int i=1;i<=cnt;i++) q.push((data){i,1,_hash[1]*_hash[i]});
for(int i=1;i<=k;i++){
if(q.empty()) break;
data temp=q.top();q.pop();
// D(temp.id1);D(temp.id2);D(temp.d);E;
ans=(ans+temp.d)%mod;
if(temp.id2<cnt) q.push((data){temp.id1,temp.id2+1,_hash[temp.id1]*_hash[temp.id2+1]});
}
cout<<ans;
}
int main() {
ios::sync_with_stdio(false);
//freopen("BZOJ3131.in","r",stdin);
cin>>n>>k;
pre(n);
solve();
return 0;
}
#endif
BZOJ4521
题意
求到间,不含前导零。至少存在个相邻的相同数字,且不能同时出现和的正整数的个数。
题解
状态定义:表示处理到第位,位为,位为,是否出现了连续三个数字的状态为,是否出现状态为,是否出现状态为,是否与上限相等为的答案
目标:
边界:
转移方程:
PS:
通过如下方式可避开第一位上的前导:枚举到第一位上的数字,从下一位开始搜索。
代码如下
/*
*/
#define method_1
#ifdef method_1
/*
*/
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cmath>
#include<set>
#include<map>
#include<queue>
#include<stack>
#include<vector>
#include<cstring>
#include<cstdlib>
#include<iomanip>
#include<ctime>
#include<string>
#include<bitset>
#define D(x) cout<<#x<<" = "<<x<<" "
#define E cout<<endl
using namespace std;
typedef long long ll;
typedef pair<int,int>pii;
const ll maxn=1e11+5;
const int maxl=11+2;
const int INF=0x3f3f3f3f;
ll A,B;
vector<int>v;
ll f[maxl][maxl][maxl][2][2][2][2];
//f[i,st1,st2,st,op1,op2,op3]表示处理到第i位,i-2位为st1,i-1位为st2,是否出现了连续三个数字的状态为st,4是否出现状态为op1,8是否出现状态为op2,是否与上限相等为op3的答案
ll dfs(int x,int st1,int st2,int st,int op1,int op2,int op3){
if(op1&&op2) return 0ll;
if(!x) return (ll)st;
if(~f[x][st1][st2][st][op1][op2][op3]) return f[x][st1][st2][st][op1][op2][op3];
ll res=0;
int maxx=op3?v[x]:9;
for(int i=0;i<=maxx;i++){
// if(op1==1&&i==8) continue;
// if(op2==1&&i==4) continue;
res+=dfs(x-1,st2,i,st||(i==st2&&i==st1),op1||(i==4),op2||(i==8),op3&&(i==maxx));
}
return f[x][st1][st2][st][op1][op2][op3]=res;
}
ll solve(ll x){
if(x<1e10) return 0;
memset(f,-1,sizeof(f));
v.clear();
v.push_back(-1);
while(x){
v.push_back(x%10);
x/=10;
}
ll ans=0ll;
//问:如何避开第一位上的前导0
//答:枚举1到第一位上的数字,从下一位开始搜索
for(int i=1;i<=v[v.size()-1];i++){
ans+=dfs(v.size()-2,-1,i,0,i==4,i==8,i==v[v.size()-1]);
}
return ans;
}
int main() {
ios::sync_with_stdio(false);
//freopen("BZOJ4521.in","r",stdin);
cin>>A>>B;
cout<<solve(B)-solve(A-1);
return 0;
}
#endif
#ifdef method_2
/*
*/
#endif
#ifdef method_3
/*
*/
#endif
BZOJ3598
题意
给出,求将与之间的数进行操作的最小代价。
操作指将一个数转化为进制,表示有几堆石块,每堆石块恰有该数位上的数个石子,相邻两堆距离为。
现要将一个数对应的若干石块堆合并为一堆,单次代价为移动石块数两堆石块间的距离。
题解
首先考虑计算一个数的合并代价,则需要确定最终合并的位置。
实现上,可以先计算出将所有石块都合并到最右边(即数字最低位)的代价。
然后不断尝试将最终合并点左移,通过判断前缀和的正负来确定移动调整后是否会使得结果更优。
(之所以考虑计算前缀和正负,是因为每向左移动一次,对答案的贡献是右边的数字和-左边的数字和)
而要计算间所有数字的情况,只需要在上述过程中加入一个数位dp即可。
状态定义:
表示处理到第位,代价和为,是否与上限相等为的答案
表示处理到第位,前缀和为,是否与上限相等为的答案
目标:
边界:
转移方程:
题解链接 https://blog.csdn.net/yzyyylx/article/details/79488044
代码如下
/*
*/
#define method_1
#ifdef method_1
/*
*/
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cmath>
#include<set>
#include<map>
#include<queue>
#include<stack>
#include<vector>
#include<cstring>
#include<cstdlib>
#include<iomanip>
#include<ctime>
#include<string>
#include<bitset>
#define D(x) cout<<#x<<" = "<<x<<" "
#define E cout<<endl
using namespace std;
typedef long long ll;
typedef pair<int,int>pii;
const ll maxn=1e15+5;
const int maxl=15*5+5; //log2(1e15)
const int maxs=250+5; //前缀和最大值
const int maxp=2000+5; //代价和最大值
const int INF=0x3f3f3f3f;
ll A,B;
int K;
vector<int>v;
ll f[maxl][maxp][2];
ll d[maxl][maxs][2];
//f[i,st,op]表示处理到第i位,代价和为st,是否与上限相等为op的答案
//d[i,st,op]表示处理到第i位,前缀和为st,是否与上限相等为op的答案
ll dfs(int x,int st,int op){
if(!x) return (ll)st;
if(~f[x][st][op]) return f[x][st][op];
ll res=0;
int maxx=op?v[x]:K-1;
for(int i=0;i<=maxx;i++){
res+=dfs(x-1,st+i*(x-1),op&&(i==maxx));
}
return f[x][st][op]=res;
}
int mid;
ll dfs2(int x,int st,int op){
if(st<0) return 0ll;
if(!x) return (ll)st;
if(~d[x][st][op]) return d[x][st][op];
ll res=0;
int maxx=op?v[x]:K-1;
for(int i=0;i<=maxx;i++){
if(x>=mid) res+=dfs2(x-1,st+i,op&&(i==maxx));
else res+=dfs2(x-1,st-i,op&&(i==maxx));
}
return d[x][st][op]=res;
}
ll solve(ll x){
ll ans=0ll;
v.clear();
v.push_back(-1);
while(x){
v.push_back(x%K);
x/=K;
}
memset(f,-1,sizeof(f));
ans+=dfs(v.size()-1,0,1); //计算将所有位数字都集中到最右端/最低位的最小代价
for(mid=2;mid<=v.size()-1;mid++){ //枚举最终合并点
memset(d,-1,sizeof(d));
ll temp=dfs2(v.size()-1,0,1);
ans-=temp;
}
return ans;
}
int main() {
ios::sync_with_stdio(false);
//freopen("BZOJ3598.in","r",stdin);
cin>>A>>B>>K;
cout<<solve(B)-solve(A-1);
return 0;
}
#endif
#ifdef method_2
/*
*/
#endif
#ifdef method_3
/*
*/
#endif