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; }