【DP计划】10.17——[BZOJ]淘金(数位DP) HARD

Description

小Z在玩一个叫做《淘金者》的游戏。游戏的世界是一个二维坐标。X轴、Y轴坐标范围均为1…N。初始的时候,所有的整数坐标点上均有一块金子,共N*N块。
一阵风吹过,金子的位置发生了一些变化。细心的小Z发现,初始在(i,j)坐标处的金子会变到(f(i),fIj))坐标处。其中f(x)表示x各位数字的乘积,例如f(99)=81,f(12)=2,f(10)=0。如果金子变化后的坐标不在1…N的范围内,我们认为这块金子已经被移出游戏。同时可以发现,对于变化之后的游戏局面,某些坐标上的金子数量可能不止一块,而另外一些坐标上可能已经没有金子。这次变化之后,游戏将不会再对金子的位置和数量进行改变,玩家可以开始进行采集工作。
小Z很懒,打算只进行K次采集。每次采集可以得到某一个坐标上的所有金子,采集之后,该坐标上的金子数变为0。
现在小Z希望知道,对于变化之后的游戏局面,在采集次数为K的前提下,最多可以采集到多少块金子?
答案可能很大,小Z希望得到对1000000007(10^9+7)取模之后的答案。

Input

共一行,包含两介正整数N,K。

Output

一个整数,表示最多可以采集到的金子数量。

Sample Input

1 2  5

Sample Output

18

HINT

N < = 10^12 ,K < = 100000

对于100%的测试数据:K < = N^2


最近这几道数位DP都比较难,这道题我们要求出对于每一个i,有多少f(x)=if(x)=if(x)=i,我们将这个记为sum(i)sum(i)sum(i),那么我们就需要用DP来求这个sum。
f[i][j][k]表示dp到第i位,每一位数字乘起来是j,是否卡到上界(k)。那么就可以进行转移。
这里转移有一个小tip,就是可以从已知最终状态往前推,就像这里枚举每一个j往前推,这样能减小复杂度。
由于数字j是很大的,但是它只能由10以内的数乘起来,而十以内的质数只有2,3,5,7。所以我们先离散枚举出所以可能的j,这样这个dp就显得可行了。
    ll x=c?9:a[p],res=0;
    for(ll i=1;i<=x;i++){
        if(num[j]%i) continue;
        res+=dp(p-1,B[num[j]/i],c|(i<x));
    }
当我们求出所有的c之后,就用一个桶的操作即可。把它们全部丢到一个桶里,然后从大到小贪心选即可。
本题还需注意的一点就是f数组刚开始不能赋值位0。因为有很多的f值本来就是0,赋值为0那么当之后dp转移到这一步时不会直接弹回答案,而会继续dfs下去,导致TLE。
#include<bits/stdc++.h>
#define MAXN 15005
#define ll long long
using namespace std;
const ll MD=1e9+7;
ll read(){
    char c;ll x;while(c=getchar(),c<'0'||c>'9');x=c-'0';
    while(c=getchar(),c>='0'&&c<='9') x=x*10+c-'0';return x;
}
ll n,k,len,top,ans,num[MAXN],a[20],sum[MAXN],f[20][MAXN][2],t[MAXN];
map<ll,int> B;
struct node{
    ll sum,id;
    friend bool operator<(node a,node b){
        return a.sum<b.sum;
    } 
};
priority_queue<node> q;
ll dp(int p,int j,int c){
    if(!p) return j==1;
    if(f[p][j][c]!=-1) return f[p][j][c];
    ll x=c?9:a[p],res=0;
    for(ll i=1;i<=x;i++){
        if(num[j]%i) continue;
        res+=dp(p-1,B[num[j]/i],c|(i<x));
    }
    f[p][j][c]=res;
    return res;
}
ll cmp(ll a,ll b){
    return a>b;
}
int main()
{
    n=read();k=read();
    memset(f,-1,sizeof(f));
    for(ll x=n;x;x/=10)a[++len]=x%10;
    for(ll i=0,a=1;i<=39&&a<=n;i++,a*=2)
     for(ll j=0,b=1;j<=25&&a*b<=n;j++,b*=3)
      for(ll p=0,c=1;p<=17&&a*b*c<=n;p++,c*=5)
       for(ll q=0,d=1;q<=14&&a*b*c*d<=n;q++,d*=7)
        if(!B[a*b*c*d]) B[a*b*c*d]=1,num[++top]=a*b*c*d;
    sort(num+1,num+1+top);
    for(int i=1;i<=top;i++) B[num[i]]=i;
    for(int i=1;i<=top;i++){
        dp(len,i,1);dp(len,i,0);
    }
    for(int i=1;i<=top;i++){
        for(int j=1;j<len;j++) 
          sum[i]+=f[j][i][1];
        sum[i]+=f[len][i][0];sum[i]%=MD;
    }
    sort(sum+1,sum+1+top,cmp);
    for(int i=1;i<=top;i++) q.push((node){sum[1]*sum[i],i}),t[i]=1;
    for(int i=1;i<=k;i++){
        if(q.empty()) break;
        node e=q.top();q.pop();
        ans=(ans+e.sum)%MD;q.push((node){sum[e.id]*sum[t[e.id]+1],e.id});t[e.id]++;
    }
    printf("%lld",ans);
    return 0;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值