先把问题转化成更形象的版本:你可以任意建一棵k叉树,并且需要满足恰有n+m叶子,其中有n个权值为1(黑点),m个权值为0(白点),其余节点的权值等于儿子节点的均值。问根节点有多少种可能的取值。
这棵树形象地模拟了合并的过程,并且我们可以看到:对于每一个黑色叶子结点i,记其深度为,那么它对根的贡献就是。由于这些贡献都是k的幂的形式,所以这启发我们在k进制下去想问题。我们安排一下每个黑点的深度,这就形成了一个k进制小数,其中,。现在问题就变成了:有多少个k进制小数,满足它能够对应一棵合法的“合并树”。
假设所有叶子结点都是黑色,那么最后根节点权值必为1,我们假设白色的节点权值也是1,它们的贡献之和是k进制小数,则有,因此合法的必要条件是存在,使得。反过来看就不难发现,这个条件也是充分的,因为将黑白叶子结点按照深度从大到小排序后,最深的点数一定是k的倍数(否则最后不可能是整数),那我们将它们合并起来,就得到了若干深度-1的点,这样重复进行下去,一定可以合并成1个点,这棵树就构造好了。
现在已经知道了能够对应一棵树的充要条件,但我们还有一个头疼的问题就是如何求的个数。首先,我们的并不是随随便便的一个k进制数,它必须能够被n个形如的数凑出才行,也就是k进制下给n个位置加1得到。同理,必须由k进制下给m个位置加1得到。我们讨论一下的约束(同理):
一个最显然的约束就是(约束1),因为就算不考虑进位,也最多只能贡献n个1。
但进位的话就复杂了。仔细观察,我们看到,一次进位相当于将k个1变成了更高位的一个1,也就是1的个数减少了(k-1),所以最终一定满足(约束2),反过来,满足了这个式子,我们显然可以找到构造的方案。
从下图可以看到,记x0的每一位为,则时有 ,时有。为了方便,我们钦定最后一位已经放了一个1,这样m--,然后最后一位就也是了。
综上,我们的合法的充要条件是:
我们最后显然只关心和,因此用表示状态,进行一个简单DP即可统计出所有合法的个数。复杂度,已经近似了,但是我们其实也可以加个前缀和优化转移,这样更快:复杂度。
说了那么多,有点啰嗦了,但是代码短到不可思议:
#include <cstdio>
#include <algorithm>
#define ll long long
#define rep(i,j,k) for (i=j;i<=k;i++)
using namespace std;
const int N=2e3+5,mod=1e9+7;
int n,m,l,k,i,j,p,q,ans,g[2][N],s[N];
int main()
{
scanf("%d%d%d",&n,&m,&k);
l=(n+m-1)/(k-1);
g[0][0]=1; p=0; q=1;
rep(i,1,l)
{
swap(p,q);
s[0]=g[q][0];
rep(j,1,n) s[j]=(g[q][j]+s[j-1])%mod;
rep(j,0,min(n,k-1)) g[p][j]=s[j];
rep(j,k,n) g[p][j]=(s[j]-s[j-k]+mod)%mod;
rep(j,1,n)
if ( j%(k-1)==n%(k-1) && (k-1-j%(k-1))%(k-1)==(m-1)%(k-1) && (k-1)*i-j<=m-1)
ans=((ll)ans+mod+g[p][j]+s[j-1]-s[j])%mod;
}
printf("%d\n",ans);
return 0;
}