生成函数是组合计数中的一个重要工具,总结一下吧~
在数学中,某个序列 an 的母函数(又称生成函数)是一种形式幂级数,其每一项的系数可以提供关于这个序列的信息。使用母函数解决问题的方法称为母函数方法。
母函数可分为很多种,包括普通母函数、指数母函数、L级数、贝尔级数和狄利克雷级数。对每个序列都可以写出以上每个类型的一个母函数。构造母函数的目的一般是为了解决某个特定的问题,因此选用何种母函数视乎序列本身的特性和问题的类型。
普通母函数就是最常见的母函数,一般来说,序列 (an) 的母函数是: ∑∞n=0an∗xn。
设 ak,bk,ck 是已知的序列,它们的普通型生成函数分别为 A(x),B(x),C(x) 。
(本段摘抄至离散数学教材。。)
1) 若 bk=αak ,α为常数,则 B(x)=αA(x) 称为 A(x) 的常数倍。
2) 若 ck=ak+bk ,则 C(x)=A(x)+B(x) 称为 A(x) 与 B(x) 的和。
3) 若 ck=∑ki=0aibk−i ,则 C(x)=A(x)B(x) 称为 A(x) 与 B(x) 的积。
(3)的形式就是两个多项式的相乘,使用FFT我们就可以 O(nlogn) 得出c序列。
普通母函数常用于多重集组合问题。
考虑如下的经典问题:对于非负整数 x1,x2,x3,...xk,求x1+x2+x3+..+xk=n 有多少组非负整数解。
我们可以为每个变量构造一个生成函数,这里每个的权值都是1,所以每个变量得到的生成函数均为 1+x+x2+...+xn+...+ (不选为1,选i个为 xi ,这里变量的取值没有限制,所以生成函数有无穷多项),变量的组合相加对应于生成函数的乘积,这样我们将这些多项式相乘得到 f(x)=(1+x+x2+...+xn+..)k ,将 f(x) 展开,则其中 xn 这一项的系数即为此方程解的个数。
这里给出一个常见的化简公式:
1+x+x2+...+xn+...=11−x 。
上式之所以成立,是因为我们假设 −1<x<1 ,而不关注具体x的取值,这是没有意义的。
由上式可以很容易得出接下来的式子:
∑∞i=0xki=11−xk 。
11−ax=1+ax+a2x2+a3x3+....+anxn+...+.. .
还有一个这样的式子: 11+x−x2=1(1+5√+12x)(1+1−5√2x) ,这里化成多项式后 xn 这一项的系数为 ∑ni=0(5√+12)i∗(1−5√2)n−i.
继续上面的问题,这样我们得到了 f(x)=(1−x)−k ,使用广义二项式定理展开得到
上述例子给出了使用普通母函数来解决组合问题的一个常见思路,为每个数构造一个生成函数,组合得到n的方案数即为 xn 这一项的系数。
如果没办法像上面的例子一样通过公式化简怎么办?那么我们可以考虑使用背包dp来计数。
说到普通母函数,不得不提另外一个经典的问题:整数划分问题。
问题如下:使用正整数相加组合成n的方案有多少种。
比如4有以下几种:1+1+1+1, 2+2,4,1+3,1+1+2。
初看这个问题不是很简单嘛: dp[i][j] 表示使用不大于j的正整数来组成i的方案数,那么 dp[i][j]=dp[i−j][j]+dp[i][j−1] ,如果 j>i ,那么 dp[i][j]=dp[i][i] 。
如果需要求1到n(n<=1e5)内每个数呢?
dp复杂度太高,无法承受。这里就要用到五边形数定理了~。
五边形数:组成刚好n个五边形的点的数量称为五边形数,第n个五边形数为 3n2−n2 ,那么五边形数序列为1,5,12,22,35,51…..
广义五边形数组成的序列是当上式中n取0,1,-1,2,-2,…x,-x…时得到的数值,即0,1,2,5,7,12,15….
我们定义将n拆分为一些正整数的和的方案数为p(n)。
有如下结论:
欧拉函数 φ(x) 的展开为
#include
using namespace std;
typedef long long LL;
const int N = 1e5 + 5, mod = 1e9 + 7;
int f1[270], f2[270];
LL ans[N];
void upd(LL& x) {
x %= mod;
if (x < 0) x += mod;
}
void init() {
for (int i = 1; ; i++) {
f1[i] = (3 * i * i - i) >> 1;
if (f1[i] > 100000) break;
f2[i] = (3 * i * i + i) >> 1;
if (f2[i] > 100000) break;
}
ans[0] = 1;
for (int i = 1; i <= 100000; i++) {
for (int j = 1; ; j++) {
if (f1[j] <= i) ans[i] += j & 1 ? ans[i-f1[j]] : -ans[i-f1[j]];
else break;
if (f2[j] <= i) ans[i] += j & 1 ? ans[i-f2[j]] : -ans[i-f2[j]];
else break;
}
upd(ans[i]);
}
}
int main() {
init();
int t;
scanf("%d", &t);
while (t--) {
int n;
scanf("%d", &n);
printf("%lld\n", ans[n]);
}
return 0;
}
序列 (an) 的指数型母函数是: ∑∞n=0an∗xnn!。
序列 an 的指数型生成函数与 bn 的指数型生成函数相乘,得到的新序列的系数 ck=∑ki=0(ki)aibk−i ,只要使用FFT,我们同样可以得出两个指数型生成函数多项式的乘积。
指数型母函数适用于解决多重集排列问题。
这里给出指数型生成函数常见的化简公式:
∑∞i=0xii!=ex ;
1−x1!+x22!−...+(−1)nxnn+..=e−x 。
所以只取偶数项的指数型生成函数为 ex+e−x2 ,只取奇数项的指数型生成函数为 ex−e−x2 。
①数列 rn 的指数型生成函数为 erx=∑∞i=0(rx)ii! 。
②数列{0,1,0,-1,0,1,0,-1….}的指数型生成函数为sin(x)。
③数列{1,0,-1,0,1,0,-1,0….}的指数型生成函数为cos(x)。
④设 A=a1,a2,a3….an 是n元集,从中可重复地选取r个元作排列,那么作成的排列数 er 是 E(t)=∏ni=1∑xtt! 展开式中 xrr! 的系数,式中t是 ai 这个元可以选取的次数。
有了上述定理,我们就可以解决一些复杂的多重集排列问题了~
考虑如下的一个简单问题:
把n个彼此相异的球放到4个不同的盒子 A1,A2,A3,A4 中,求使得A_1 A1 中含有奇数个球,A_2 A2 含有偶数个球的不同的放球方法数g_n gn 。
解:任意一种方案对应于A_1,A_2,A_3,A_4 A1,A2,A3,A4 的一个多重集排列.
A1 的生成函数为
除上述两种外,还存在其他类型的生成函数,但是在此就不多介绍了。
(没出现过题目而且我不会-_-)
下面来看一些题目吧~
先来看个水题压压惊。。。
hdu 2082 找单词
传送门
题意:给出26个字母的个数,字母c的价值为c-‘A’+1,求求能够组成的总价值小于50的单词个数(单词只与选的字母有关,与排列顺序无关)。
分析:非常水的普通母函数,这里每个字母个数有限制,我们暴力dp算系数就可以。
#include
using namespace std;
const int N = 30;
int dp[2][55], num[N];
int solve() {
memset(dp[0], 0, sizeof(dp[0]));
dp[0][0] = 1;
for (int i = 1, p = 1; i <= 26; i++, p = !p) {
memset(dp[p], 0, sizeof(dp[p]));
for (int j = 0; j <= num[i] && j * i <= 50; j++)
for (int k = 0; k + j * i <= 50; k++) dp[p][k + j * i] += dp[!p][k];
}
int ans = 0;
for (int i = 1; i <= 50; i++) ans += dp[0][i];
return ans;
}
int main() {
int t;
scanf("%d", &t);
while (t--) {
for (int i = 1; i <= 26; i++) scanf("%d", num + i);
printf("%d\n", solve());
}
return 0;
}
poj 3734 Blocks
传送门
题意:N块砖排成一行,每块砖可以被涂成红、蓝、绿、黄四种颜色,求最后涂为红、绿的砖的数目均为偶数的方案数。
分析:很水的指数型生成函数。可以知道 E(t)=e2t∗(ex+e−x2)2 ,化简得到第n项的系数即为 4n+2n+14 。
当然这题还可以矩阵快速幂。
#include
using namespace std;
const int mod = 10007, inv = 2502;
int qpow(int x, int n) {
n %= mod - 1;
int res = 1;
while (n) {
if (n & 1) res = res * x % mod;
x = x * x % mod;
n >>= 1;
}
return res;
}
int main() {
int t;
scanf("%d", &t);
while (t--) {
int n;
scanf("%d", &n);
printf("%d\n", (qpow(4, n) + qpow(2, n + 1)) % mod * inv % mod);
}
return 0;
}
poj 1322
传送门
题意:包裹里有无限个分布均匀且刚好c种颜色的巧克力,现在要依次拿n个出来放到桌子上,每次如果桌子上面有两种相同颜色的巧克力就会把这两个巧克力给吃掉,求最后桌子上面还有m个巧克力的概率。
分析:概率dp是可行的,令dp[i][j]表示拿i个巧克力出来后桌子上面还剩j个巧克力的概率。这样复杂度就是 O(NC) 的,无法承受,但是注意到题目只要求输出三位小数,通过打表找规律可以发现在n大于一定数值的时候,后面的值都是与奇偶有关了,做1000次迭代就已经远超过精度要求了。
本题当然可以直接生成函数搞:
还剩m个巧克力,也就是说有m种颜色选了奇数次,有c-m种颜色选了偶数次。
取n个巧克力出来刚好就对应于c种颜色的一个多重集排列。总共可能有 cn 个排列,现在我们只需要算出有多少种可能的排列满足上述要求即可。
生成函数相乘得到 E(t)=(ex+e−x2)c−m∗(ex−e−x2)m. 如果分子当中 xnn! 这一项的系数为a,那么答案即为 (cm)a2c∗cn 。
这里分子为
#include
#include
using namespace std;
const int N = 105;
double C[N][N], d[N<<1];
void init() {
for (int i = 0; i <= 100; i++) C[i][0] = C[i][i] = 1;
for (int i = 2; i <= 100; i++) {
for (int j = 1; j < i; j++) C[i][j] = C[i-1][j-1] + C[i-1][j];
}
}
double qpow(double x, int n) {
double res = 1;
while (n) {
if (n & 1) res *= x;
x *= x;
n >>= 1;
}
return res;
}
int main() {
init();
int c, n, m;
while (scanf("%d", &c), c) {
scanf("%d %d", &n, &m);
if (m > c || m > n || ((n - m) & 1)) { puts("0.000"); continue; }
double ans = 0;
for (int i = 0; i <= m; i++) {
int s = c - m;
for (int j = 0; j <= s; j++) {
int k = 2 * (i + j) - c;
if ((m - i) & 1) ans -= qpow(1.0 * k / c, n) * C[m][i] * C[s][j];
else ans += qpow(1.0 * k / c, n) * C[m][i] * C[s][j];
}
}
printf("%.3f\n", ans / qpow(2.0, c) * C[c][m]);
}
return 0;
}
codeforces 451E Devu and Flowers
传送门
题意:有n(n<=20)种花,每种花的数量 fi(<=1e12) 已知,现在要取 si(si<=1e14) 朵,求方案数。
分析:数据范围明显没办法dp,但是注意到n很小。带有限制的计数问题我们都可以考虑容斥原理。
所以我们只需要这样算即可:没有任何限制的方案数-一种限制不满足的方案数+两种限制不满足的方案数-….
没有任何限制的方案数在最开始已经讨论过了,就是n个变量和为s的非负整数解的个数。有一些限制不满足的话,那么就说明某种花用的数量大于 fi 了,这样的话我们只需要令 s−=fi+1 ,将解的下界重新变为0就可以转化了相同的问题了。复杂度 O(2n)。
#include
using namespace std;
typedef long long LL;
const int mod = 1e9 + 7, N = 25;
LL f[N], sum, s, inv[N];
int n;
void init() {
inv[1] = 1;
for (int i = 2; i <= 20; i++) inv[i] = (mod - mod / i) * inv[mod % i] % mod;
}
int C(LL n, int m) {
if (n < m) return 0;
if (!m || n == m) return 1;
if (m > n - m) m = n - m;
int res = 1;
for (int i = 1; i <= m; i++) res = (LL)res * ((n - i + 1) % mod) % mod * inv[i] % mod;
return res;
}
int dfs(int i, LL s, bool fl) {
if (s < 0) return 0;
if (i == n) {
int res = C(s + n - 1, n - 1);
return fl ? res : -res;
}
int ans = dfs(i + 1, s, fl);
ans = (ans + dfs(i + 1, s - f[i] - 1, !fl)) % mod;
return ans < 0 ? ans + mod : ans;
}
int main() {
scanf("%d %I64d", &n, &s);
init();
for (int i = 0; i < n; i++) scanf("%I64d", f + i), sum += f[i];
printf("%d\n", sum < s ? 0 : dfs(0, s, 1));
return 0;
}
hdu 4658 Integer Partition
传送门
题意:求n的整数划分方案,其中没有任意一个整数出现超过k-1次。
分析:结论题。。如果是n的整数划分方案,那么可以直接用五边形数定理 O(nn√) 预处理。
不过这里同样是有结论的,定义定义将n拆分为一些正整数的和且任意一种正整数的个数不能大于等于m的方案数为pp(n)。
pp(n) = p(n) – p(n-m) – p(n-2m) + p(n-5m)+p(n-7m)-….这里也是广义五边数序列。
#include
using namespace std;
typedef long long LL;
const int N = 1e5 + 5, mod = 1e9 + 7;
int f1[270], f2[270];
LL ans[N];
void upd(LL& x) {
x %= mod;
if (x < 0) x += mod;
}
void init() {
for (int i = 1; ; i++) {
f1[i] = (3 * i * i - i) >> 1;
if (f1[i] > 100000) break;
f2[i] = (3 * i * i + i) >> 1;
if (f2[i] > 100000) break;
}
ans[0] = 1;
for (int i = 1; i <= 100000; i++) {
for (int j = 1; ; j++) {
if (f1[j] <= i) ans[i] += j & 1 ? ans[i-f1[j]] : -ans[i-f1[j]];
else break;
if (f2[j] <= i) ans[i] += j & 1 ? ans[i-f2[j]] : -ans[i-f2[j]];
else break;
}
upd(ans[i]);
}
}
void solve(int n, int k) {
LL res = ans[n];
for (int i = 1; ; i++) {
if (f1[i] * k <= n) res += i & 1 ? -ans[n-f1[i]*k] : ans[n-f1[i]*k];
else break;
if (f2[i] * k <= n) res += i & 1 ? -ans[n-f2[i]*k] : ans[n-f2[i]*k];
else break;
}
upd(res);
printf("%lld\n", res);
}
int main() {
init();
int t;
scanf("%d", &t);
while (t--) {
int n, k;
scanf("%d %d", &n, &k);
solve(n, k);
}
return 0;
}
hdu 6042 Journey with Knapsack
传送门
题意:一个体积为 2∗n 的背包,有 n(n<=5e4) 种食物,第i种食物的体积是i,数量是 ai(0<=a1<a2<...<an<=2n) ,还有m种装备,第i种装备的体积是 bi(1<=bi<=2∗n) ,求装一些食物和一件装备使得背包装满的方案数。
分析:很明显的组合问题,我们首先考虑装食物的方案数。
首先为每一种食物构造生成函数然后相乘得到 f(z)=∏ni=1(1+xi+x2i+...+xaii)=∏ni=11−xi(ai+1)1−xi。
我们尝试把上述式子分解然后合并:
#include
using namespace std;
typedef long long LL;
const int N = 1e5 + 5, mod = 1e9 + 7;
LL p[N];
int dp[N];
void add(int& x, int y) {
x += y;
if (x >= mod) x -= mod;
else if (x < 0) x += mod;
}
void init() {
int f1[270], f2[270];
for (int i = 1; ; i++) {
f1[i] = (3 * i * i - i) >> 1;
if (f1[i] > 100000) break;
f2[i] = (3 * i * i + i) >> 1;
if (f2[i] > 100000) break;
}
p[0] = 1;
for (int i = 1; i <= 100000; i++) {
for (int j = 1; ; j++) {
if (f1[j] <= i) p[i] += j & 1 ? p[i-f1[j]] : -p[i-f1[j]];
else break;
if (f2[j] <= i) p[i] += j & 1 ? p[i-f2[j]] : -p[i-f2[j]];
else break;
}
if ((p[i] %= mod) < 0) p[i] += mod;
}
}
int main() {
init();
int n, m, ca = 0;
while (~scanf("%d %d", &n, &m)) {
int s = n << 1;
for (int i = 0; i <= s; i++) dp[i] = p[i];
for (int i = 1, v; i <= n; i++) {
scanf("%d", &v);
LL k = (LL)(v + 1) * i;
for (LL j = s; j >= k; j--) add(dp[j], -dp[j-k]);
}
int sum = dp[0];
for (int i = 1; i <= n; i++) add(dp[i+n], -sum), add(sum, dp[i]);
int ans = 0, v;
while (m--) scanf("%d", &v), add(ans, dp[s-v]);
printf("Case #%d: %d\n", ++ca, ans);
}
return 0;
}
bzoj 3771 Triple
传送门
题意:给n个互不相同的数 ai ,现在可以任选一个、两个或者三个数,需要输出这些数所有可能的和sum,以及每个sum下组成sum的方案数。
分析:选一个数直接加上即可;
选两个数,那么这就是一个明显的FFT了,对权值为0或者1的序列做自卷积即可,需要减去一个数选两次的情况,然后除2即可。
选三个数,就是同样地序列做两次卷积。减掉一个数选超过一次的情况,然后除以6即可。
构造三个01序列,分别为 a[i],b[i∗2],c[i∗3] ,简单地容斥一下,那么选两个数的情况就是 (a[i]∗a[i]−b[i])/2 ,选三个数的情况就是 (a[i]∗a[i]∗a[i]−3∗a[i]∗b[i]+2∗c[i])/6 。
我们只需要在DFT意义下直接算,然后将结果IDFT回来就行了。
#include
using namespace std;
typedef long long LL;
const double PI = acos(-1.0);
struct Complex {//复数结构体
double x, y; //实部和虚部 x+yi
Complex(double _x = 0.0, double _y = 0.0) : x(_x), y(_y) {}
Complex operator -(const Complex &b)const { return Complex(x - b.x, y - b.y); }
Complex operator +(const Complex &b)const { return Complex(x + b.x, y + b.y); }
Complex operator *(const Complex &b)const { return Complex(x * b.x - y * b.y, x * b.y + y * b.x); }
};
//进行FFT和IFFT前的反转变换,位置i和 (i二进制反转后位置)互换,len必须取2的幂
void change(Complex y[], int len) {
int i, j, k;
for (i = 1, j = len / 2; i < len - 1; i++) {
if (i < j) swap(y[i], y[j]);//交换互为小标反转的元素,i
//i做正常的+1,j左反转类型的+1,始终保持i和j是反转的
k = len / 2;
while (j >= k) j -= k, k /= 2;
if (j < k) j += k;
}
}
//做FFT,len必须为2^k形式,on==1时是DFT,on==-1时是IDFT
void fft(Complex y[], int len, int on) {
change(y, len);
for (int h = 2; h <= len; h <<= 1) {
Complex wn(cos(-on * 2 * PI / h), sin(-on * 2 * PI / h));
for (int j = 0; j < len; j += h) {
Complex w(1, 0);
for (int k = j; k < j + h / 2; k++) {
Complex u = y[k];
Complex t = w * y[k + h / 2];
y[k] = u + t;
y[k + h / 2] = u - t;
w = w * wn;
}
}
}
if (on == -1) for (int i = 0; i < len; i++) y[i].x /= len;
}
const int N = 150003;
Complex a[N], b[N], c[N], d[N];
int main() {
int n, m = 0, va;
scanf("%d", &n);
for (int i = 0; i < n; i++) {
scanf("%d", &va);
a[va] = b[va << 1] = c[va*3] = Complex(1, 0);
}
m = va * 3;
int l = 1;
while (l <= m) l <<= 1;
fft(a, l, 1); fft(b, l, 1); fft(c, l, 1);
for (int i = 0; i <= l; i++) {
d[i] = d[i] + a[i];
d[i] = d[i] + (a[i] * a[i] - b[i]) * Complex(0.5, 0);
d[i] = d[i] + (a[i] * a[i] * a[i] - Complex(3, 0) * a[i] * b[i] + Complex(2, 0) * c[i]) * Complex(1.0 / 6, 0);
}
fft(d, l, -1);
for (int i = 0; i <= l; i++) {
int ans = (int)(d[i].x + 0.5);
if (ans) printf("%d %d\n", i, ans);
}
return 0;
}
bzoj 3028 食物
传送门
题意:明明出去旅游,他带的食物和它们的限制如下:
承德汉堡:偶数个
可乐:0个或1个
鸡腿:0个,1个或2个
蜜桃多:奇数个
鸡块:4的倍数个
包子:0个,1个,2个或3个
土豆片炒肉:不超过一个。
面包:3的倍数个
求带N个食物的方案数。
分析:明显的生成函数,我们考虑为每一种食物建立生成函数然后相乘。
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.Scanner;
public class Main {
static Scanner in = new Scanner(System.in);
public static void main(String[] args) {
BigInteger s = in.nextBigInteger(), t = s.add(BigInteger.ONE);
BigInteger L = s.multiply(t).multiply(t.add(BigInteger.ONE));
L = L.divide(BigInteger.valueOf(6));
System.out.println(L.mod(BigInteger.valueOf(10007)));
}
}
bzoj 4772 显而易见的数论
传送门
题意:给定整数 n(n<=2000) ,和一个长为 k(k<=1e5) 的序列 ai(ai<=1e7) ,求n的每一种划分方案的价值之和,假设n的一种划分方案的所有整数组成的序列为p,长为m,那么n的这一种划分方案的价值为 ∑mi=1∑mj=i+1g(aF(p[i],p[j])%k) 。这里 g(n)=∑ni=1(i−1,n)∗[(i,n)==1] 。
输入时会给定一个整数type,当 type=1,F(x,y)=1 ,当 type=2 时, F(x,y)=gcd(x,y) ,当 type=3 时, F(x,y)=xy+yx+x xor y 。
分析:看完题目有一种绕地球几圈的感觉。。。不过这个题还是值得一做的。。
这里枚举每一个划分方案显然是不现实的,我们可以计算每一对 pi,pj 的贡献。这样我们就可以知道每一个 ai 出现的次数了,那么最终 ans=∑k−1i=0cnt[i]∗g(a[i]) 。cnt[i]表示 ai 出现的次数。
那么问题就转化为每两个小于n的整数x,y在所有划分方案中出现的次数了。
如果x!=y,且x和y在一个划分方案数出现了a次和b次,那么x和y贡献的次数就是a*b次,我们可以换个角度,也就是说a和b出现的次数中每一对数(c,d)1<=c<=a,1<=d<=b)在这个划分方案都要被统计一次,两者刚好是等价的,这样我们直接枚举每一对数 x,y 和其出现的次数 c,d ,假设 F(x,y)%k==s ,那么 cnt[s]+=∑x∗c<=n∑x∗c+y∗d<=np(n−x∗c−y∗d) 。 p(n) 表示n的划分方案的个数。
如果 x==y 呢?那么我们是不能这么计算的,因为两者并不等价,如果一个划分方案中x出现了 d 次,那么(x,x)贡献的次数就是 d∗(d−1)2 。我们计算出刚好只有 d个x 的划分方案的个数,这个个数很容易计算-> p(n−x∗d)−p(n−x∗(d+1)) 。
所以问题变得清晰了,预处理p(n),gcd(x,y)、x^y以及g函数,然后计算cnt[i]即可。
现在我们来考虑g函数的性质。
gcd是积性函数,那么g函数也是积性函数。
g(x)=g(a)g(b) ,其中 (a,b)==1 。
对于函数g(x),对于多素因子的合数,我们不好在线性筛时计算,所以我们可以考虑将x转化为两个互质的数的函数之积,也就是如果 x=∏ti=1peii ,那么 g(x)=g(pe11)∗g(∏ti=2peii) 。
对于 pe 这样形式的数, g(pe) 就变得很容易计算了。
#include
using namespace std;
typedef long long LL;
const int N = 1e7, M = 2001, mod = 1e9 + 7, K = 1e5 + 1;
int gcd[M][M], pri[N+1], a[K], cnt[K], q[N+1], g[N+1], p[M][M], type, k, n;
bool vis[N+2];
LL sum[M];
void init() {
for (int i = 1; i <= n; i++) {
p[i][0] = 1;
for (int j = 1; j <= n; j++) p[i][j] = (LL)p[i][j-1] * i % k;
}
for (int i = 0; i <= n; i++) gcd[i][0] = gcd[0][i] = gcd[i][i] = i, gcd[i][1] = gcd[1][i] = 1;
for (int i = 2; i <= n; i++) {
for (int j = 2; j < i; j++) {
if (!gcd[i][j]) gcd[i][j] = gcd[j][i-j];
gcd[j][i] = gcd[i][j];
}
}
int f1[40], f2[40];
for (int i = 1; i <= 37; i++) {
f1[i] = (3 * i * i - i) >> 1;
f2[i] = (3 * i * i + i) >> 1;
}
sum[0] = 1;
for (int i = 1; i <= n; i++) {
for (int j = 1; ; j++) {
if (f1[j] <= i) sum[i] += j & 1 ? sum[i-f1[j]] : -sum[i-f1[j]];
else break;
if (f2[j] <= i) sum[i] += j & 1 ? sum[i-f2[j]] : -sum[i-f2[j]];
else break;
}
if ((sum[i] %= mod) < 0) sum[i] += mod;
}
g[1] = 1; int k = 0;
for (int i = 2; i <= N; i++) {
if (!vis[i]) pri[k++] = i, g[i] = 2 * i - 2, q[i] = i;
for (int j = 0; j < k; j++) {
int pr = pri[j], s = i * pr;
if (s > N) break;
vis[s] = 1;
if (i % pr == 0) {
q[s] = q[i] * pr;
if (q[i] != i) g[s] = (LL)g[i /q[i]] * g[q[i] * pr] % mod;
else g[s] = ((LL)g[i] * pr + i * pr - i) % mod;
break;
}
q[s] = pr; g[s] = (LL)g[i] * g[pr] % mod;
}
}
}
int f(int i, int j) {
if (type == 1) return k == 1 ? 0 : 1;
if (type == 2) return gcd[i][j] % k;
return (p[i][j] + p[j][i] + (i ^ j)) % k;
}
void add(int& x, int y) {
x += y;
if (x >= mod) x -= mod;
else if (x < 0) x += mod;
}
int main() {
scanf("%d %d %d", &type, &n, &k);
init();
for (int i = 0; i < k; i++) scanf("%d", a + i);
for (int i = 1; i <= n; i++)
for (int j = i + 1; i + j <= n; j++) {
int t = f(i, j);
for (int _i = 1; _i * i + j <= n; _i++)
for (int _j = 1; _i * i + _j * j <= n; _j++) add(cnt[t], sum[n - _i * i - _j * j]);
}
for (int i = 1; i <= n; i++) {
int t = f(i, i);
for (int _i = 1; _i * i <= n; _i++) {
int s = sum[n - _i * i];
if ((_i + 1) * i <= n) add(s, -sum[n - (_i + 1) * i]);
add(cnt[t], (LL)_i * (_i - 1) / 2 * s % mod);
}
}
int ans = 0;
for (int i = 0; i < k; i++) add(ans, (LL)g[a[i]] * cnt[i] % mod);
printf("%d\n", ans);
return 0;
}
hdu 6067 Big Integer
传送门
题意:Q 喜欢 k(k<=10) 进制下的大整数,这个整数不能包含0,且每个数字c出现的次数不能超过 n(n<=14000) 次。他有一个 (k−1)∗(n+1) 的矩阵 g , gi,j 为0代表这个他喜欢的大整数中i这个数字不能刚好出现j次。每天他的口味会变化,也就是矩阵中的某一个位置会翻转。总共有 m(m<=200) 天,求 ∑mi=0cnt[i] mod 786433。 cnt[i] 表示第i天后Q喜欢的整数的个数。
分析:首先我们可以计算出初始时Q喜欢的整数个数,这个可以通过生成函数求出,每个数i的生成函数为 f(i)=∑nj=0gijxjj! 。
那么我们只需要求 ∏k−1t=1f(i) 的系数即可。k比较小,这里我们可以直接NTT。
初始的答案我们可以直接先算出来;考虑变化,对于一个翻转u、v。它只会影响到第f(u)这个多项式DFT的过程。 Xk=∑N−1n=0an(gp−1N)nk 。这里p是模数,N是序列长度,g是原根。一次操作只会加上或者删除 1v! 这一项,那么对于f(u) DFT的过程会使得 X0,X1,...XN−1 加上或者减去 1v!,1v!(gp−1N)v,1v!(gp−1N)2v...1v!(gp−1N)(N−1)v 。这是一个等比数列。
令 V[i]=∏ti=1Xi(0<=i<N) ,这样一次修改操作我们可以直接 O(N) 的完成,在V[i]中除去这一项(乘上逆元)、修改 Xv ,然后在给V[i]乘上去即可。为了方便计算,我们考虑记录每一项中0的个数。
总时间复杂度 O(nk2log(nk)+mnk) 。做完这题,可以回顾一下FFT的原理了~
#include
using namespace std;
typedef long long LL;
const int MAX = 1 << 17, mod = 786433, g = 10;
int fac[MAX], inv[mod], invf[mod];
int qpow(int x, int n) {
int res = 1;
while (n) {
if (n & 1) res = (LL)res * x % mod;
x = (LL)x * x % mod; n >>= 1;
}
return res;
}
void init() {
fac[0] = fac[1] = inv[1] = invf[0] = invf[1] = 1;
for (int i = 1; i < MAX; i++) fac[i] = (LL)i * fac[i - 1] % mod;
for (int i = 2; i < mod; i++) {
inv[i] = mod - (LL)(mod / i) * inv[mod % i] % mod;
invf[i] = (LL)inv[i] * invf[i-1] % mod;
}
}
void ntt(int* y, int len, int on) {
for (int i = 1, j = len >> 1; i < len - 1; i++) {
if (i < j) swap(y[i], y[j]);
int k = len >> 1;
while (j >= k) j -= k, k >>= 1;
if (j < k) j += k;
}
for (int h = 2; h <= len; h <<= 1) {
int wn = qpow(g, (mod - 1) / h);
if (on == -1) wn = qpow(wn, mod - 2);
for (int j = 0; j < len; j += h) {
LL w = 1;
for (int k = j; k < j + h / 2; k++) {
int u = y[k], t = (LL)w * y[k + h / 2] % mod;
y[k] = u + t >= mod ? u + t - mod : u + t;
y[k + h / 2] = u - t < 0 ? u - t + mod : u - t;
w = w * wn % mod;
}
}
}
if (on == -1) {
LL t = qpow(len, mod - 2);
for (int i = 0; i < len; i++) y[i] = y[i] * t % mod;
}
}
const int N = 131075;
int f[N], X[11][N], cnt[N], ans[N];
char s[14002];
bool mp[11][14002];
void add(int& x, int y) {
x += y;
if (x >= mod) x -= mod;
else if (x < 0) x += mod;
}
int main() {
init();
int t;
scanf("%d", &t);
while (t--) {
int k, n, m, L;
scanf("%d %d %d", &k, &n, &m);
k--; L = 1;
int mu = k * n;
while (L <= mu) L <<= 1;
fill(f, f + L, 1);
for (int i = 1; i <= k; i++) {
scanf("%s", s);
for (int j = 0; j <= n; j++) if (s[j] == '1') X[i][j] = invf[j], mp[i][j] = 1;
ntt(X[i], L, 1);
for (int j = 0; j < L; j++) X[i][j] ? f[j] = (LL)f[j] * X[i][j] % mod : cnt[j]++;
}
for (int i = 0; i < L; i++) ans[i] = cnt[i] ? 0 : f[i];
int q = qpow(g, (mod - 1) / L), u, v;
while (m--) {
scanf("%d %d", &u, &v);
int tm = invf[v], s = qpow(q, v), i = 0;
if (mp[u][v]) tm = mod - tm;
mp[u][v] = !mp[u][v];
for (int* x = X[u]; i < L; i++, tm = (LL)tm * s % mod) {
x[i] ? f[i] = (LL)f[i] * inv[x[i]] % mod : cnt[i]--;
add(x[i], tm);
x[i] ? f[i] = (LL)f[i] * x[i] % mod : cnt[i]++;
if (!cnt[i]) add(ans[i], f[i]);
}
}
ntt(ans, L, -1);
int res = 0;
for (int i = 1; i <= mu; i++) add(res, (LL)ans[i] * fac[i] % mod);
printf("%d\n", res);
if (t) {
for (int i = 1; i <= k; i++) memset(X[i], 0, L << 2), memset(mp[i], 0, (n + 1) << 2);
memset(cnt, 0, L << 2);
}
}
return 0;
}
CodeForces - 438E The Child and Binary Tree
传送门
题意:给定长为 n(n<=1e5) 的 c 序列,以及 m(m<=1e5) ,构造二叉树,每个点的权值必须是 c 序列中的数,而一颗二叉树的总权值为树中所有节点的权值之和,输出m个数,第i个数代表总权值为i的二叉树的个数。
分析:做了就长姿势了。。
首先,我们可以很容易地得出递推式,令 fi 表示总权值为 i 的二叉树的个数,那么 fi=∑w∈{c1,c2,...cn}∑i−wj=0fj∗fi−w−j 。
很容易看出来后面的和式是个卷积。这里我们通过生成函数来化简求解。
令 F(x) 是 f 的生成函数, g(x)=∑xi=0fi∗fx−i ,那么 fi=∑w∈{c1,c2,...cn}gi−w ,实际上这里还是个卷积的形式。
我们知道,如果 f(x)=∑ni=0aixi,g(x)=∑ni=0bixi ,那么两个生成函数的乘积 f(x)g(x) 中每一项 xn 的系数是 ∑ni=0aibn−i 。
那么如果 c 序列的生成函数为 C(x) ,这里就有 F(x)=C(x)∗F2(x)+1 。后面的 +1 是 f0 。
所以我们可以得到 F(x)=21+1−4C(x)√ 。
那么现在到了问题的关键了,如何求F(x),我们只需要做一次多项式开根和一次多项式求逆,就可以得出f的生成函数,进而得到f序列了。
在这里就可以学会多项式求逆和多项式开根,2333。。
还可以看看picks大牛的博客。
这算是第一次做自己感觉高级的内容了。。哈哈,感觉挺有意思的。。
#include
using namespace std;
typedef long long LL;
const int mod = 998244353, g = 3, N = 262144, inv = 499122177;
int w[30];
int qpow(int x, int n) {
int res = 1;
while (n) {
if (n & 1) res = (LL)res * x % mod;
x = (LL)x * x % mod; n >>= 1;
}
return res;
}
void ntt(int y[], int len, int on) {
for (int i = 1, j = len >> 1; i < len - 1; i++) {
if (i < j) swap(y[i], y[j]);
int k = len >> 1;
while (j >= k) j -= k, k >>= 1;
if (j < k) j += k;
}
for (int h = 2, _i = 1; h <= len; h <<= 1, _i++) {
int wn = w[_i];
if (on == -1) wn = qpow(wn, mod - 2);
for (int j = 0; j < len; j += h) {
LL w = 1LL;
for (int k = j; k < j + h / 2; k++) {
int u = y[k], t = w * y[k + h / 2] % mod;
y[k] = u + t >= mod ? u + t - mod : u + t;
y[k + h / 2] = u - t < 0 ? mod + u - t : u - t;
w = w * wn % mod;
}
}
}
if (on == -1) {
int t = qpow(len, mod - 2);
for (int i = 0; i < len; i++) y[i] = (LL)y[i] * t % mod;
}
}
int t[N];
void pol_inv(int* a, int* b, int n) {
if (n == 1) { b[0] = qpow(a[0], mod - 2); return ; }
pol_inv(a, b, n >> 1);
memcpy(t, a, n << 2);
memset(t + n, 0, n << 2);
int len = 1;
while (len <= n) len <<= 1;
ntt(b, len, 1); ntt(t, len, 1);
for (int i = 0; i < len; i++) t[i] = (LL)b[i] * (mod + 2 - (LL)b[i] * t[i] % mod) % mod;
ntt(t, len, -1);
for (int i = 0; i < n; i++) b[i] = t[i];
memcpy(b, t, n << 2);
memset(b + n, 0, n << 2);
}
int tb[N], c[N], sqlc[N], invc[N];
void pol_sqr(int* a, int* b, int n) {
if (n == 1) { b[0] = 1; return ; }
pol_sqr(a, b, n >> 1);
memset(tb, 0, n << 2);
pol_inv(b, tb, n);
memcpy(t, a, n << 2);
memset(t + n, 0, n << 2);
int len = 1;
while (len <= n) len <<= 1;
ntt(t, len, 1); ntt(b, len, 1); ntt(tb, len, 1);
for (int i = 0; i < len; i++) t[i] = (LL)inv * ((LL)tb[i] * t[i] % mod + b[i]) % mod;
ntt(t, len, -1);
memcpy(b, t, n << 2);
memset(b + n, 0, n << 2);
}
int main() {
for (int i = 1; i <= 20; i++) w[i] = qpow(g, (mod - 1) >> i);
int n, m;
scanf("%d %d", &n, &m);
for (int i = 1, v; i <= n; i++) scanf("%d", &v), c[v]++;
c[0] = 1;
for (int i = 1; i <= m; i++) if (c[i]) c[i] = mod - 4;
int len = 1;
while (len <= m) len <<= 1;
pol_sqr(c, sqlc, len);
if (++sqlc[0] >= mod) sqlc[0] -= mod;
pol_inv(sqlc, invc, len);
for (int i = 1; i <= m; i++) printf("%d\n", invc[i] * 2 % mod);
return 0;
}