首先我们考虑直接搞
考虑每个元素的贡献,得表达式:
$ans=sum_{i=1}^{n}w_{i}sum_{j=1}^{n}jC_{n-1}^{j-1}S(n-j,k-1)$
即枚举每个元素所在集合中元素个数及划分方案数
这个玩意显然是$O(n^{2})$的
有大佬把它化简之后变成了可以直接递推的东西,但是...太恶心了好吗
因此我们换个思想:
我们知道,对于一种划分方式,元素$i$的贡献是$|S|w_{i}$
那么我们可以理解为在这种划分方式下,这个集合中每个元素都产生了$w_{i}$的贡献
那么我们分成两部分来考虑这个问题
首先,一个元素本身无论在哪个集合里都会产生$w_{i}$的贡献,这个贡献是一定的,只与集合划分方法有关,因此这个元素本身对$w_{i}$的贡献是$w_{i}S(n,k)$
接下来,我们考虑其他元素对这个$w_{i}$的贡献:去掉第$i$个元素之后,其他元素仍然可以划分成$k$个集合,划分方案数为$S(n-1,k)$
在每种划分方式下,我们都可以把第$i$个元素放进任意一个集合里面去,产生的贡献等于集合大小*$w_{i}$(注意这个集合大小显然不包含$i$这个元素,因为他本身的贡献我们已经统计过了)
这样的话,对每种划分方式,对$w_{i}$产生的贡献其实都是$(n-1)$,因为每个集合大小加起来就是$(n-1)$
那么表达式就变成了$sum_{i=1}^{n}w_{i}[S(n,k)+(n-1)S(n-1,k)]$
也就是$[S(n,k)+(n-1)S(n-1,k)]sum_{i=1}^{n}w_{i}$
注意到第二类斯特林数可以$O(klog_{2}k)$递推,后面那个东西读入的时候累个前缀和即可
第二类斯特林数的递推式:$S(n,m)=frac{1}{m!}sum_{i=0}^{m}(-1)^{i}C_{m}^{i}(m-i)^{n}$
贴代码:
#include <cstdio> #include <cmath> #include <cstring> #include <cstdlib> #include <iostream> #include <algorithm> #include <queue> #include <stack> #define ll long long using namespace std; const ll mode=1000000007; ll minv[200005]; ll inv[200005]; ll mul[200005]; ll n,k,s; ll w[200005]; void init() { minv[0]=minv[1]=inv[0]=inv[1]=mul[0]=mul[1]=1; for(int i=2;i<=200000;i++) { inv[i]=(mode-mode/i)*inv[mode%i]%mode; minv[i]=minv[i-1]*inv[i]%mode; mul[i]=mul[i-1]*i%mode; } } ll pow_mul(ll x,ll y) { ll ret=1; while(y) { if(y&1)ret=ret*x%mode; x=x*x%mode,y>>=1; } return ret; } ll C(ll x,ll y) { if(x<y)return 0; return mul[x]*minv[y]%mode*minv[x-y]%mode; } ll get_S(ll x,ll y) { ll ret=0,f=1; for(int i=0;i<=y;i++)ret=(ret+f*C(y,i)*pow_mul(y-i,x)%mode+mode)%mode,f=-f; return ret*minv[y]%mode; } int main() { init(); scanf("%lld%lld",&n,&k); for(int i=1;i<=n;i++)scanf("%lld",&w[i]),s=(s+w[i])%mode; printf("%lld ",s*((get_S(n,k)+(n-1)*get_S(n-1,k)%mode)%mode)%mode); return 0; }