SCOI2007 排列

传送门

这道题竟然可以使用全排列暴力模拟水过……

不过我们还是说一下正解。既然数据范围这么小,所以我们考虑状压DP。

用dp[i][j]表示状态为i时,当前选取的所有数的排列,其对d取模后结果为j有多少种情况。其中i是一个二进制数字串,每一个二进制位对应原数组中的数字有没有被选中。

简单的解释一下,假设原数组中是1246,那么当状态为0011时,我们相当于求4,6这两个数字组成的全排列,对d取模后结果为j有多少种情况。

DP方程如下,若(i & 1<<k) == 0 ,则dp[i | (1 << k)][(j * 10 + f[k]) % d] += dp[i][j]

这里解释一下,其实就相当于我们在每次dp转移的时候又取了一个数,并且把取得这个数加到末尾,计算一共有多少种排列对d取模之后结果为j。

比如说(原数组还是用上面的),从状态1010转移至1011就相当于是把1,4的全排列末尾加上6,之后计算。

有人可能会有疑问,你要算的是当前选取的所有数(1,4,6)的全排列可能产生的方案数,而你当前只计算了6在末尾的情况,它在中间的情况你并没有计算。

其实并不是这样。对于状态1011,它可以从不止一个状态中转移过来。比如0011,1010,1001.而这三种状态其实恰好就对应了排列1,4,排列4,6,排列1,6,把新加入的数分别放在他们的后面,当前选取的三个数的全排列还是会被完全考虑到的。

同理,对于任意一种已经被转移的状态,其必然已经计算过当前选取的所有数字的全排列的情况,所以也就必然能保证所有情况都被枚举到。

再说的通俗一点,拿上面的举例。比如状态1010,他可以由1000和0010转移,所以状态1010必然已经包含过前面两种状态构成的所有情况,当他继续向后DP的时候亦然。

还有一点比较显然,我们直接把上一次取模的结果*10加上当前数再取模即可,因为他和原数肯定是同余的。

这样dp方程的正确性就很显然了。

注意应该怎么dp,初始值dp[0][0] = 1.然后注意dp的时候要从0开始,不要取错。

还有就是遇到了一个有重复元素的情况。比如说122.122被转移的时候,可以从状态110,101,011转移过来。而前两种状态计算的是重复的。再类推一下,可以得到,每个重复的元素会贡献其出现次数的阶乘倍的多余答案,应该被除去。

这样就可以了。

#include<iostream>
#include<cstdio>
#include<cmath>
#include<algorithm>
#include<queue>
#include<cstring>
#define rep(i,a,n) for(int i = a;i <= n;i++)
#define per(i,n,a) for(int i = n;i >= a;i--)
#define enter putchar('\n')
using namespace std;
typedef long long ll;
const int M = 15;
ll n,L,f[M],dp[2000][2000],num[M],t,d,len,sum,cur,ans; 
ll read()
{
    ll ans = 0,op = 1;
    char ch = getchar();
    while(ch < '0' || ch > '9')
    {
    if(ch == '-') op = -1;
    ch = getchar();
    }
    while(ch >='0' && ch <= '9')
    {
    ans *= 10;
    ans += ch - '0';
    ch = getchar();
    }
    return ans * op;
}
char s[M];
int main()
{
    t = read();
    while(t--)
    {
    memset(dp,0,sizeof(dp));
    memset(f,0,sizeof(f));
    memset(s,0,sizeof(s));
    memset(num,0,sizeof(num));
    scanf("%s",s+1);
    len = strlen(s+1);
    rep(i,1,len) f[i] = s[i] - '0',num[f[i]]++;
    d = read();
    dp[0][0] = 1;
    rep(i,0,(1<<len)-1)
        rep(j,0,d-1)
        rep(k,0,len-1)
        if(!(i & (1<<k))) dp[i|(1<<k)][(j*10+f[k+1])%d] += dp[i][j];
    ans = dp[(1<<len)-1][0];
    rep(i,0,9)
        while(num[i] > 1) ans /= num[i],num[i]--;
    printf("%lld\n",ans);
    }
    return 0;
}

 

posted @ 2018-08-26 23:55  CaptainLi  阅读(241)  评论(0编辑  收藏  举报