qaq(曾经の NOIP 模考)

Tomoyuki Mizuyama 要出 \(n\) 道题,他现在有 \(m\) 个不同的乱码,他非常的阴间,所以他决定先决定先用这群乱码起好第一个题目的名字,之后每个题目都直接从上一个题目名字中找一个子序列当做自己的名字。
现在他想知道,在第 \(i\) 道题目名字长度为 \(a_i\) 的情况下(\(a_1\ge a_2\ge\cdots\ge a_n\)),总共会有多少组不同的题目名字呢?(输出对 \(10^9+7\) 取模的结果)

这道字符串其实是数论。
典型的逆向思维,既然每一个名字都是上一个的(非空)子序列,不妨倒过来看,变成先确定一个字符串,之后往这个串里面插入字符,一共搞 \(n\) 个。
显然字符插入的位置不同就是不同的方案,而我们正好要求方案数,这个时候其实就会想到组合数了。
怎么用组合数?设当前的串长是 \(a_i\),下一个串长是 \(a_{i+1}\)。我们可以用组合数辅助求出根据已知 \(a_i\) 可以求出的 \(a_{i+1}\) 的种类数。
当然很有可能会计算重复,栗子:原串是 \(bxxxb\) 我们可以在第一、二、三个 \(x\) 前面插入 \(x\)(按理说我们会计算 \(3\) 次),但是实际上最后的结果都是 \(bxxxxb\),所以只能算一次,于是重复计算了 \(2\) 次。
那么如何避免这个问题?其实很简单:对于当前串 \(i\) 的每个字符,我们在它前面插入的字符不能与这个字符相同,就可以避免重复的情况(认真体会,真的很巧妙)。
当然,我们在一个串的结尾插入字符就没有限制,因为我们已经保证了前面不同,那么即使结尾一样整个字符串也必然不可能相同。
所以我们有了这张图:

我们固定住字符串的结尾 \(p\),那么我们前面还有 \(j-1\) 个位置,现在我们相当于把原字符串前面 \(a_i-1\) 个字符散布到这 \(j-1\) 个位置中(这一步可以把原来的字符串变成现在的子序列),而剩下的每个空的位置上都有 \(m-1\) 种字符可供插入(因为不能与前一个相同),那么一共就有

\[{j-1\choose a_i-1}\times (m-1)^{(j-1)-(a_i-1)}={j-1\choose a_i-1}\times(m-1)^{j-a_i} \]

种不同的构造方法,当然这只是结尾的前面,我们还没有考虑向结尾插入字符,考虑到结尾剩下 \(a_{i+1}-j\) 个位置,而每个位置可以插入 \(m\) 种乱码的任何一个,那么一共有 \(m^{a_{i+1}-j}\) 种构造方法,根据乘法原理 ,这一个字符串总的不同构造数就是在前面插字符的可能数乘在结尾插字符的可能数,也就是 $${j-1\choose a_i-1}\times(m-1)^{j-a_i}\times m^{a_{i+1}-j}$$
但是显然 \(j\) 的长度不定,它的值域是 \([a_i,a_{i+1}]\),那么我们可以枚举所有的 \(j\),根据加法原理,我们这一个字符串总的组数就等于 \(j\) 取各个值时数量的和。

pow1[0] = pow2[0] = 1 ;
f (i ,1 ,maxx ,1) {
  pow1[i] = pow1[i - 1] * (m - 1) % mod ; // 维护一个 m - 1 的幂次数组
  pow2[i] = pow2[i - 1] * m % mod ; // m 的幂次数组
}
sort (a + 1 ,a + n + 1) ; // 原来 a 是从大到小,变成从小到大就可以加字符了
ans = qpow (m ,a[1]) ; // 先处理好第一个(最短的)串的情况
f (i ,2 ,n ,1) {
  sum = 0 ;
  f (j ,a[i - 1] ,a[i] ,1) {
    sum = (sum + C (j - 1 ,a[i - 1] - 1) * pow1[j - a[i - 1]] % mod * pow2[a[i] - j] % mod) % mod ;
  }
  ans = ans * sum % mod ;
}

那么我们现在唯一的问题就变成了如何统计组合数。
先上公式:

\[{n\choose m}=\frac{n!}{m!\times (n-m)!} \]

所以我们维护一个阶乘数组。

f (i ,1 ,maxx ,1) {
  qq[i] = qq[i - 1] * i % mod ;
}

然而这个除法很不好搞。
解释一下为什么不好搞,同余定理告诉我们 \((a\bmod c)\times(b\bmod c)=(a\times b)\bmod c\),注意到我们在所有运算中都及时取模(当然是为了避免炸 long long),所以对乘法运算来说我们及时取模没有关系,可以正常算。但是我们可不知道 \(\frac{a}{b}\bmod c=\frac{a\bmod c}{b\bmod c}\)(eg:\(\frac{10}{2}\bmod3\) 显然不等于 \(\frac{10\bmod3}{2\bmod3}\)),那么如果我们在中途及时取模,就会影响除法运算最终的结果,所以如果我们想正常地进行除法运算,就不能在中间取模,而不在中间取模就会爆 long long,所以除法不可行,这是第二个难点。
想办法转化成乘法,注意答案是在模 \(10^9+7\) 的意义下,而这个玩意是个大质数,所以我想到了两个东西,一个是乘法逆元,一个是费马小定理(其实用扩欧也不是不行)。
首先我们维护一个 \(back\) 数组,我们倒着计算,首先初始化 \(back_{10^7}=(10^7!)^{mod-2}\)(因为数据范围是 \(10^7\),我们为了方便设 \(10^9+7\)\(mod\)),之后的每个 \(back_i\) 都等于 \(back_{i+1}\times(i+1)\)。我们仔细观察,首先 \(back_{10^7-1}=back_{10^7}\times10^7=[(10^7-1)!]^{mod-2}\times(10^7)^{mod-1}\),很显然由于所有的数都小于 \(mod\),而 \(mod\) 是个质数,所以所有数都与 \(mod\) 互质,此时根据费马小定理(\(a^{p-1}\equiv 1\pmod p\)\(p\) 为质数且 \(a\)\(p\) 互质),\((10^7)^{mod-1}\equiv 1 \pmod {mod}\),那么 \(back_{10^7-1}\) 在模 \(mod\) 意义下等同于 \([(10^7-1)!]^{mod-2}\),以此类推,同理得 \(back_i\) 在模 \(mod\) 意义下等于 \((i!)^{mod-2}\)
那么这个数组有什么用呢?注意到我们上面的公式,那么 \(\frac{n!}{m!}\) 便可以用 \(back\) 数组和阶乘数组来维护。很明显 \(\frac{n!}{m!}\) 等于 \((m+1)\times\cdots\times n\),我们尝试将前缀和数组 \(qq_n\) 乘上 \(back_m\),得到 \(n!\times(m!)^{mod-2}=(m!)^{mod-1}\times(m+1)\times\cdots\times n\),又根据费马小定理,上面这个玩意就等于 \((m+1)\times\cdots\times n\),也就是 \(\frac{n!}{m!}\)
最后我们的公式里还剩一个 \(\frac{1}{(n-m)!}\),注意到 \(back_{n-m}\times(n-m)!=[(n-m)!]^{mod-1}\equiv1 \pmod {mod}\),也就是说 \(back_{n-m}\equiv\frac{1}{(n-m)!}\pmod {mod}\),那么在模 \(mod\) 意义下 \(back_{n-m}\) 就等价于 \(\frac{1}{(n-m)!}\)
那么我们的组合数公式被乱搞成了:\(qq_n\times back_m\times back_{n-m}\)
直接维护即可。
这一部分的代码:

f (i ,1 ,maxx ,1) {
  qq[i] = qq[i - 1] * i % mod ;
  pow1[i] = pow1[i - 1] * (m - 1) % mod;
  pow2[i] = pow2[i - 1] * m % mod ;
}
back[(int) 1e7] = qpow (qq[(int) 1e7] ,mod - 2) ;
for (ll i = (int) 1e7 - 1 ;i ;i --) {
  back[i] = back[i + 1] * (i + 1) % mod ;
}
inline ll C (ll x ,ll y) { 
  return qq[x] * back[y] % mod * back[x - y] % mod ;
}

那就放个总代码好了:

#include <bits/stdc++.h>
#define f(i ,m ,n ,x) for (int i = (m) ;i <= (n) ;i += (x))
#define mod 1000000007
#define maxx 10000005
typedef long long ll ;
using namespace std ;
template < typename T > inline void read (T &x) {
	x = 0 ;
	bool flag (0) ;
	register char ch = getchar () ;
	while (! isdigit (ch)) {
		flag = ch == '-' ;
		ch = getchar () ;
	}
	while (isdigit (ch)) {
		x = (x << 1) + (x << 3) + (ch ^ 48) ;
		ch = getchar () ;
	}
	flag ? x = -x : 0 ;
}
const int N = 1e6 + 7 ;
int n ;
ll m ,ans ,sum ,a[N] ,qq[maxx] ,pow1[maxx] ,pow2[maxx] ,back[maxx] ;
inline ll qpow (ll base ,ll p) {
	ll res = 1 ;
	while (p) {
		if (p & 1) {
			res = res * base % mod ;
		}
		base = base * base % mod ;
		p >>= 1 ;
	}
	return res ;
}
inline ll C (ll x ,ll y) { 
	return qq[x] * back[y] % mod * back[x - y] % mod ;
}
int main () {
	read (n) ,read (m) ;
	f (i ,1 ,n ,1) {
		read (a[i]) ;
	}
	sort (a + 1 ,a + n + 1) ;
	back[0] = qq[0] = pow1[0] = pow2[0] = 1 ;
	f (i ,1 ,maxx ,1) {
		qq[i] = qq[i - 1] * i % mod ;
		pow1[i] = pow1[i - 1] * (m - 1) % mod;
		pow2[i] = pow2[i - 1] * m % mod ;
	}
	back[(int) 1e7] = qpow (qq[(int) 1e7] ,mod - 2) ;
	for (ll i = (int) 1e7 - 1 ;i ;i --) {
		back[i] = back[i + 1] * (i + 1) % mod ;
	}
	// cout << C (5 ,2) << '\n' ;
	ans = qpow (m ,a[1]) ;
	f (i ,2 ,n ,1) {
		sum = 0 ;
		f (j ,a[i - 1] ,a[i] ,1) {
			sum = (sum + C (j - 1 ,a[i - 1] - 1) * pow1[j - a[i - 1]] % mod * pow2[a[i] - j] % mod) % mod ;
		}
		ans = ans * sum % mod ;
	}
	cout << ans << '\n' ;
	return 0;
}

upd:发现自己做繁了,其实只要快速幂求 \(mod-2\) 次方就可以了……毕竟那会还不会逆元呢嘛所以只能乱搞了。

posted @ 2024-07-23 20:31  tomzu  阅读(19)  评论(0)    收藏  举报