CF401D Roman and Numbers

发布时间 2023-09-08 10:34:00作者: NBest

2023-07-25 20:41:07 solution

原题链接

思路

看到这个数据和范围,果断数位 dp。

因为同一个数字交换顺序是一样的,所以我们直接把它们合并即可。

设计状态,观察到每个数字的个数不一定相同,今天集训的时候学到了一个小技巧,

对于一个哈密顿回路(每个点恰好经过一次),我们可以用二进制表示其状态。

那对于一个广义哈密顿回路(好像没有这个概念,我自己编的),每个点恰好经过 \(a_i\) 次。

通常来说,状压是指的二进制状压,因为二进制状压可以方便进行 and,or,xor 等位运算。但是也会有像本题这样非二进制的情况,有些甚至要爆搜出所有情况进行 dp。

但是其实是和二进制同样的状态压缩方式,只是把每个位进位的阈值改了,参考了一下上面巨佬的题解,就叫它变进制数吧。

对于一个二进制数 \(x\),我们可以将它表示为:

\(x=x_0\times 2^0+x_1\times 2^1+...+x_n\times 2^{n-1}\)

那么设 \(n\) 中各个数字出现的次数分别为 \(A[i]\),那么可以把第 \(i\) 为看为一个 \(A[i]+1\) 进制数。

那么我们可以将状态压缩为 \(s\),则:

\(s=x_0\times 1+x_1\times (A[0]+1)+x_2\times (A[0]+1)\times(A[1]+1)+...\)

然后就可以状态压缩加数位 dp 了。

\(Code1\)

int A[10],m,p[12];
ll n,f[100][60004];
ll dfs(int zt,int mv){
    if(!zt)return !mv;//没数了就返回
    if(~f[mv][zt])return f[mv][zt];
    ll ans=0;
    for(int i=9;~i;--i){//从大到小枚举数
        if(zt==p[10]-1&&!i)continue;//首位不能为0
        if(zt%p[i+1]<p[i])continue;//去掉前面的位判断是否还有数
        ans+=dfs(zt-p[i],(mv*10+i)%m);//转移
    }
    return f[mv][zt]=ans;
}
int main(){
    cin>>n>>m;
    memset(f,-1,sizeof(f));
    while(n){
        A[n%10]++;
        n/=10;
    }
    p[0]=1;
    for(int i=1;i<=10;i++){
        p[i]=p[i-1]*(A[i-1]+1);//前缀积预处理
    }
    cout<<dfs(p[10]-1,0);//p[10]-1是一开始状态
}

相似题 P4163 排列

只需要把去前导 \(0\) 的步骤删除,然后加点细节即可。

\(Code2\)

int A[10],m,p[12],T;
int f[1000][1<<10];
int dfs(int zt,int mv){
    if(!zt)return !mv;
    if(~f[mv][zt])return f[mv][zt];
    int ans=0;
    for(int i=9;~i;--i){
        if(zt%p[i+1]<p[i])continue;
        ans+=dfs(zt-p[i],(mv*10+i)%m);
    }
    return f[mv][zt]=ans;
}
int main(){
    scanf("%d",&T);
    while(T--){
        string n;
        cin>>n;
        scanf("%d",&m);
        memset(f,-1,sizeof(f));
        memset(A,0,sizeof(A));
        for(int i=0;i<n.length();i++){
            A[n[i]^48]++;
        }
        p[0]=1;
        for(int i=1;i<=10;i++){
            p[i]=p[i-1]*(A[i-1]+1);
        }
        printf("%d\n",dfs(p[10]-1,0));
    }
}