BZOJ 1072 排列perm (状压dp)

1072: [SCOI2007]排列perm

Time Limit: 10 Sec Memory Limit: 128 MB

Description
给一个数字串s和正整数d, 统计s有多少种不同的排列能被d整除(可以有前导0)。例如123434有90种排列能被2整除,其中末位为2的有30种,末位为4的有60种。

Input
输入第一行是一个整数T,表示测试数据的个数,以下每行一组s和d,中间用空格隔开。s保证只包含数字0, 1, 2, 3, 4, 5, 6, 7, 8, 9.

Output
每个数据仅一行,表示能被d整除的排列的个数。

Sample Input

7
000 1
001 1
1234567890 1
123434 2
1234 7
12345 17
12345678 29

Sample Output

1
3
3628800
90
3
6
1398

HINT

在前三个例子中,排列分别有1, 3, 3628800种,它们都是1的倍数。

【限制】

100%的数据满足:s的长度不超过10, 1<=d<=1000, 1<=T<=15

思路:
s的长度不超过10,算法就大概确定了。
f[i][j]表示选取i状态的数且此时的mod为j。
对于每个状态i,可能从它把的某一个二进制位置为1的位置变为零的状态转移过来(当前选了x个数有a种排列方式使得mod为k,那么在剩下的数字之中任选一个加在排列的最后,那么选x+1个数使得mod为k*10+这个数的值,就多了a种排列方式。代码注释中会解释。),这样不是特别容易操作,所以考虑逆向更新。
dp方程:f[(i|(1<<(k−1)))][(j∗10+a[k])%mod]+=f[i][j];条件是( i&(1<<(k-1)) )==0;
但是如果有重复的数字的话,我们会重复计算,所以除掉重复的数的个数的排列。

#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
#define N 15
#define M 1100
using namespace std;

int mod, lens, lim;
int a[N], cnt[N];
char s[N];
int f[1<<10][M];//f[i][j]表示选取i状态的数且此时的mod为j
//(eg:i=5时,选第1和第3个数,i=9时,选第1和第4个数,i=10时,选第2和第4个数) 
int J[]={0,1,2,6,24,120,720,5040,40320,362880,3628800};//阶乘 

void dfs(){
    for(int i=0; i<lim; i++)
        for(int k=0; k<mod; k++)
            if( f[i][k] )
                for(int j=0; j<lens; j++)
                    if( (i & (1<<j)) == 0 )
                        f[(i|(1<<j))][(k*10+a[j+1])%mod] += f[i][k];//a[j+1]放在最后 
    //对于状态i,排列的最后一位可能是选取了的数中的任意一个(前面的排列已经统计好了)
    //为什么只是加在最后而不是其他地方,因为加在其他地方的情况会在加上它的最后一位的计算中被统计 
}

int main(){
    int T;scanf("%d", &T);
    while( T-- ){
        memset(cnt, 0, sizeof(cnt));
        memset(f, 0, sizeof(f));
        scanf("%s", s);
        lens = strlen(s);
        for(int i=0; i<lens; i++){
            a[i+1] = s[i] - '0';
            cnt[a[i+1]]++;
        }
        scanf("%d", &mod);
        lim = (1<<lens);
        f[0][0] = 1;
        dfs();
        int ans = f[lim-1][0];//所有数字用完 
        for(int i=0; i<=9; i++) if( cnt[i] ) ans /= J[cnt[i]];//相同的数字调换位置本质不变 
        printf("%d\n", ans);
    }
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值