一、题意
题目传送门:Problem - 401D - Codeforces
拙劣翻译:问给出的数位的全排列组成的数当中,模m等于0的数的个数,但是不能有前导0。
看到这个数据范围,我是直摇头qaq,和队友阅读理解了很久其他ac代码,下面谈谈合理的思路。
二、状态压缩
一点预备:(来源于状态压缩_*Slime*的博客-CSDN博客_状态压缩)
而关于状态压缩,想着把一个状态表示成二进制形态。这个题,n的每一位都能表示成0或1(取或是不取)。有一个看了很久才明白的点:我们标记的取数字只是用于标记这个位置的数被拿走了,取出来的数字每一次都加在我们排列的数字个位,这样也方便计算余数。
比如对于n=140,010表示我们取第二位的数字4,现在的排列就是4;从010转移到110就是再取第一位的数字1,排列就是41;
三、注意的问题
我们用dp[i][j]来表示当前数字的状态为i,余数为j的方案数,最后的答案就为dp[(1<<n)-1][0]。
转移方程dp[i][(k*10+num[j])%m]+=dp[i^(1LL<<(j-1))][k]
状态i可以从i^(1LL<<(j-1))转移过来,比如101可以从001转移过来,转移后的余数=(k*10+num[j])%m。
前导零问题我们在选择第一位时保证不为0就行。
重复情况,比如n=221,那么100转移到110和010转移到110是重复的,我们记录每一个数字(数值)的使用情况,保证不重复。
#include<bits/stdc++.h>
using namespace std;
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
ll dp[(1<<18)+1][101];
int a[20],num[20],vis[20];
int cnt=0;
int main()
{
ll n,m;
cin>>n>>m;
while(n)
{
a[++cnt]=n%10;
n/=10;
}
for(int i=cnt,j=1;i>=1;i--,j++)
num[j]=a[i];//cout<<num[j]<<endl;
//初始化
dp[0][0]=1;
for(ll i=0;i<(1<<cnt);i++) //遍历每一种状态
{
memset(vis,0,sizeof(vis));
for(ll j=1;j<=cnt;j++)//遍历每一位数字
{
//排除前导0
if(i==(1LL<<(j-1)) && !num[j])
break;//i==(1ll<<(j-1))表示目前只选了一个j位数字(现在在个位,以后就成前导0了)
//j位的数字已经被拿走了(与数字本身是什么无关,只是说位置)
if(!( i & (1LL<<(j-1)))|| vis[num[j]])
continue;
//为了避免重复状态的产生
//用了这一个数字(本身),标记防止重复
vis[num[j]]=1;
for(ll k=0;k<m;k++)
{
dp[i][(k*10+num[j])%m]+=dp[i^(1LL<<(j-1))][k];
}
}
}
printf("%lld",dp[(1<<cnt)-1][0]);
}