[bzoj4314] 倍数?倍数! 解题报告

来源:互联网 发布:mac 播放器 字幕 编辑:程序博客网 时间:2024/04/28 05:41

感觉完全不会做。。看讨论区里有大爷用母函数做的。。完全不会母函数,所以想看看官方题解。。但是官方题解要登录topcoder,注册还得翻墙,然后还是英文,搞了好久终于看懂了。感觉每一步都非常神。。
我们要求的是集合的个数,集合是无序的,这并不好求。我们可以变无序为有序,先求有序集合的数量然后除以k!。
考虑一个元素互不相同的长为k-1的序列,如果说我们要求长为k的序列mod n=0的话,实际上最后一个元素就已经被确定了。所以我们就可以写出:长为k的序列所有元素互不相同和mod n=0的序列方案数=长为k-1的序列所有元素互不相同的序列方案数-长为k的序列有且仅有一对元素相同且mod n=0的方案数。
但问题是,后者依然不易求。不过,按照类似的思路,我们考虑一下有且仅有一对元素相同且mod n=0的序列,我们把这一对元素放到最后,设前k-2个元素的和为S,则最后一对元素就是S+2x=0 mod n的解。这就是一个经典的丢番图方程问题了。显然,当且仅当(2,n)|S时有解,且解有(2,n)个,所以问题就被神奇的化归到了mod (2,n)=0时的情况!
所以我们可以设f(k,d,a)表示在[0,n)中选k个不同的数构成序列,要求它们mod d=0,且最后一个数在序列的最后重复了a遍的方案数。考虑转移,前k-1个数的和为S,则S+ax=0 mod d当且仅当S mod (a,d)=0,且x的循环节长度是d(a,d),所以每一种S对应的x的数量为nd(a,d),那么我们就可以算出重复a或a+1遍的方案数是nd(a,d)f(k1,(a,d),1),可能会出现重复了a+1遍的情况是因为我们在后面添的数可能和前面的数重复了,所以我们要减去这种讨厌的情况,这种情况的方案数是(k1)f(k1,d,a+1)。注意到我们的状态表示的是最后一个数重复a遍(因为我们转移的时候总是试图在最后加一个数),因为考虑每种非法情况,它实际上可以被每一种重复的数在最后的情况交换位置得到,而一共有(k-1)个位置可供交换,所以要乘以k-1。
再考虑一下边界情况,当k=1时,答案当然就是nd;当d=1时,答案就是Akn
所以总的转移就是

f(k,d,a)=ndAknnd(a,d)f(k1,(a,d),1)(k1)f(k1,d,a+1),k=1,d=1,k>1&d>1

时间复杂度是O(k3)的。因为a随着k减1只会增加1,而d除了等于n就只会等于小于等于a的数,所以它们的范围都是O(k)的。
不过注意到实际上这个上界是很松的,因为光a、k这两维实际上就应该是k(k+1)2之类的东西,而d实际上更远远不足k,因为d必须要满足是n的因子,而n在1001内的因子显然不会太多。所以这个时间复杂度实际上非常虚。
但是空间呢?注意到实际上我们只需要保存f(k,d,1)即可,因为f(k,d,a)(a>1)只会被调用一次。所以空间只需要O(k2)

#include<cstdio>#include<iostream>using namespace std;#include<algorithm>#include<cstring>const int Mod=1000000007;typedef long long LL;const int N=1e9+5,K=1000+5;LL pow(LL prod,int x){    LL ans=1;    for(;x;x>>=1){        if(x&1)ans=ans*prod%Mod;        prod=prod*prod%Mod;    }    return ans;}int gcd[K][K],gn[K];int n;LL f[K][K];LL array[K];LL dfs(int k,int d,int a){    int g=d==n?gn[a]:gcd[a][d];    if(k==1)return n/d*g;    if(d==1)return array[k];    if(a==1&&d<=1000){        if(f[k][d]==-1)f[k][d]=((n/d*g*dfs(k-1,g,1)-(k-1)*dfs(k-1,d,a+1))%Mod+Mod)%Mod;        //printf("f(%d,%d,%d)=%I64d\n",k,d,a,f[k][d]);        return f[k][d];    }    LL ans=((n/d*g*dfs(k-1,g,1)-(k-1)*dfs(k-1,d,a+1))%Mod+Mod)%Mod;    //printf("f(%d,%d,%d)=%I64d\n",k,d,a,ans);    //cout<<n/d*g*dfs(k-1,g,1)<<endl;    return ans;}int main(){    int k;    scanf("%d%d",&n,&k);    if(n<k){        puts("0");        return 0;    }    for(int i=1;i<=k;++i){        gcd[i][0]=gcd[0][i]=i;        for(int j=1;j<=i;++j)gcd[i][j]=gcd[j][i]=gcd[i%j][j];        gn[i]=gcd[i][n%i];    }    array[0]=1;    for(int i=1;i<=k;++i)array[i]=array[i-1]*(n-i+1)%Mod;    memset(f,-1,sizeof(f));    LL ans=dfs(k,n,1);    //7cout<<ans<<endl;    for(int i=k;i;--i)ans=ans*pow(i,Mod-2)%Mod;    cout<<ans<<endl;}

总结:
①无序与有序之间的相互转化经常非常有用!
②dp时如果感觉状态不够用可以尝试大胆的扩展。

0 0
原创粉丝点击