洛谷4163 [SCOI2007]排列

题解 动态规划 动态规划-状压DP 提高+/省选-
题目链接 编辑文章

题意

给一个数字串 $s(|s|\le 10)$ 和正整数 $d(\le 1000)$ , 统计 $s$ 有多少种不同的排列能被 $d$ 整除(可以有前导 $0$)。

题解

$|s|$ 较小,考虑状压。用 $f(S,i)$ 表示下标集合 $S$ 内的数都用过了,当前数 $\equiv i\pmod d$ 。每次枚举一个不在 $S$ 的数 $j$ 加入转移,方程式为:

$$f(S,i)\rightarrow f(S\cup j,(i\times 10+s_j)\mod d)$$

但还有一个问题,如果有多个相同的数,答案中就会包含重复的排列。可以用排列组合原理去重,如果数字 $i$ 有 $cnt[i]$ 个,那么答案就需要 $\div cnt[i]!$ 。

#include<bits/stdc++.h>

using namespace std;

inline int read()
{
    char ch=getchar(); int f=1,x=0;
    while (ch<'0' || ch>'9') { if (ch=='-') f=-1; ch=getchar(); }
    while (ch>='0' && ch<='9') { x=x*10+ch-'0'; ch=getchar(); }
    return f*x;
}

char s[15];
int t,d,n,fac[15],cnt[10],f[1<<11][1005];

signed main()
{
    fac[0]=1;
    for (int i=1;i<=10;i++) fac[i]=fac[i-1]*i;
    t=read();
    while (t--)
    {
        memset(f,0,sizeof(f));
        memset(cnt,0,sizeof(cnt));
        scanf("%s",s); d=read();
        n=strlen(s);
        for (int i=0;i<n;i++) s[i]-='0',cnt[int(s[i])]++;
        const int N=1<<n;
        f[0][0]=1;
        for (int i=0;i<N;i++)
        {
            for (int j=0;j<n;j++)
            {
                if (i>>j&1) continue;
                for (int k=0;k<d;k++) f[i|(1<<j)][(k*10+s[j])%d]+=f[i][k];
            }
        }
        int ans=f[N-1][0];
        for (int i=0;i<=9;i++) ans/=fac[cnt[i]]; //去重
        printf("%d\n",ans);
    }
    return 0;
}

新评论

称呼不能为空
邮箱格式不合法
网站格式不合法
内容不能为空