作者: KSkun

[AHOI2001]多项式乘法 题解

[AHOI2001]多项式乘法 题解

题目地址:洛谷:【P2553】[AHOI2001]多项式乘法 – 洛谷 题目描 

[ZJOI2014]力 题解

[ZJOI2014]力 题解

题目地址:洛谷:【P3338】[ZJOI2014]力 – 洛谷、BZOJ:Pr 

[洛谷4245]【模板】MTT 题解 & 任意模数卷积算法(MTT)原理与实现

[洛谷4245]【模板】MTT 题解 & 任意模数卷积算法(MTT)原理与实现

题目地址:洛谷:【P4245】【模板】MTT – 洛谷

题目描述

多项式乘法。

输入输出样例

输入样例#1:

5 8 28
19 32 0 182 99 95
77 54 15 3 98 66 21 20 38

输出样例#1:

7 18 25 19 5 13 12 2 9 22 5 27 6 26

说明

数据范围1e5。

题解

由于NTT对模数有要求,我们需要使用任意模数卷积算法,也就是俗称的MTT。具体做法是,把一个数拆成32768x+y的形式,对拆出来的四组数分别卷积后拼起来。这样可以避免计算过程中的越界问题,一般的数据能够满足精度。
单位根最好预处理出来,这样才不会有精度问题。

代码

// Code by KSkun, 2018/3
#include <cstdio>
#include <cmath>

#include <algorithm>

typedef long long LL;

inline char fgc() {
    static char buf[100000], *p1 = buf, *p2 = buf;
    return p1 == p2 && (p2 = (p1 = buf) + fread(buf, 1, 100000, stdin), p1 == p2) ? EOF 
        : *p1++;
}

inline LL readint() {
    register LL res = 0, neg = 1;
    char c = fgc();
    while(c < '0' || c > '9') {
        if(c == '-') neg = -1;
        c = fgc();
    }
    while(c >= '0' && c <= '9') {
        res = res * 10 + c - '0';
        c = fgc();
    }
    return res * neg;
}

const int MAXN = 1 << 20;
const double PI = acos(-1);
int MO;

struct Complex {
    double real, imag;
    Complex(double real = 0, double imag = 0) : real(real), imag(imag) {}
    inline Complex conj() {
        return Complex(real, -imag);
    }
    inline Complex operator+(Complex rhs) const {
        return Complex(real + rhs.real, imag + rhs.imag);
    }
    inline Complex operator-(Complex rhs) const {
        return Complex(real - rhs.real, imag - rhs.imag);
    }
    inline Complex operator*(Complex rhs) const {
        return Complex(real * rhs.real - imag * rhs.imag, imag * rhs.real + real * rhs.imag);
    }
    inline Complex operator*=(Complex rhs) {
        return (*this) = (*this) * rhs;
    }
    friend inline Complex operator*(double x, Complex cp) {
        return Complex(x * cp.real, x * cp.imag);
    }
    inline Complex operator/(double x) const {
        return Complex(real / x, imag / x);
    }
    inline Complex operator/=(double x) {
        return (*this) = (*this) / x;
    }
    friend inline Complex operator/(double x, Complex cp) {
        return x * cp.conj() / (cp.real * cp.real - cp.imag * cp.imag);
    }
    inline Complex operator/(Complex rhs) const {
        return (*this) * rhs.conj() / (rhs.real * rhs.real - rhs.imag * rhs.imag);
    }
    inline Complex operator/=(Complex rhs) {
        return (*this) = (*this) / rhs;
    }
    inline double length() {
        return sqrt(real * real + imag * imag);
    }
};

int n, m, len, rev[MAXN];
LL x[MAXN], y[MAXN], z[MAXN];
Complex a[MAXN], b[MAXN], c[MAXN], d[MAXN], e[MAXN], f[MAXN], g[MAXN], h[MAXN], wn[MAXN];

inline void fft(Complex *arr, int f) {
    for(int i = 0; i < n; i++) {
        if(i < rev[i]) std::swap(arr[i], arr[rev[i]]);
    }
    for(int i = 1; i < n; i <<= 1) {
        for(int j = 0; j < n; j += i << 1) {
            for(int k = 0; k < i; k++) {
                Complex w = Complex(wn[n / i * k].real, f * wn[n / i * k].imag), 
                    x = arr[j + k], y = w * arr[j + k + i];
                arr[j + k] = x + y;
                arr[j + k + i] = x - y;
            }
        }
    }
    if(f == -1) {
        for(int i = 0; i < n; i++) {
            arr[i] /= n;
        }
    }
}

int main() {
    n = readint(); m = readint(); MO = readint();
    for(int i = 0; i <= n; i++) {
        x[i] = readint() % MO;
    }
    for(int i = 0; i <= m; i++) {
        y[i] = readint() % MO;
    }
    m += n;
    for(n = 1; n <= m; n <<= 1) len++;
    for(int i = 0; i < n; i++) {
        rev[i] = (rev[i >> 1] >> 1) | ((i & 1) << (len - 1));
    }
    for(int i = 1; i < n; i <<= 1) {
        for(int k = 0; k < i; k++) {
            wn[n / i * k] = Complex {cos(PI * k / i), sin(PI * k / i)};
        }
    }
    for(int i = 0; i < n; i++) {
        a[i].real = x[i] >> 15; b[i].real = x[i] & 0x7fff;
        c[i].real = y[i] >> 15; d[i].real = y[i] & 0x7fff;
    }
    fft(a, 1); fft(b, 1); fft(c, 1); fft(d, 1);
    for(int i = 0; i < n; i++) {
        e[i] = a[i] * c[i]; f[i] = b[i] * c[i];
        g[i] = a[i] * d[i]; h[i] = b[i] * d[i];
    }
    fft(e, -1); fft(f, -1); fft(g, -1); fft(h, -1);
    for(int i = 0; i < n; i++) {
        z[i] = (((LL(round(e[i].real)) % MO) << 30) % MO 
            + ((LL(round(f[i].real)) % MO) << 15) % MO
            + ((LL(round(g[i].real)) % MO) << 15) % MO 
            + LL(round(h[i].real)) % MO) % MO;
    }
    for(int i = 0; i <= m; i++) {
        printf("%lld ", z[i]);
    }
    return 0;
}
快速数论变换(NTT)原理及实现

快速数论变换(NTT)原理及实现

概述 上次写了一篇狗屎文章快速傅里叶变换(FFT)原理与实现 | KSkun’ 

[洛谷1919]【模板】A*B Problem升级版(FFT快速傅里叶) 题解

[洛谷1919]【模板】A*B Problem升级版(FFT快速傅里叶) 题解

题目地址:洛谷:【P1919】【模板】A*B Problem升级版(FFT快速傅里叶) & 

快速傅里叶变换(FFT)原理与实现

快速傅里叶变换(FFT)原理与实现

这篇文章写得很烂,而且大部分来自于算导,如有感兴趣者可以直接阅读算导或参考资料QAQ!本文省略大量证明,请各位谨慎食用QAQ!

概述

快速傅里叶变换(FFT)是一种快速计算序列的离散傅里叶变换(DFT)及其逆变换(IDFT)的算法,其复杂度为O(n \log n),比朴素的DFT计算更优。OI中常用它来计算多项式乘法等需要应用卷积的地方。
除了本文章中介绍的数学知识,你还需要知道:

多项式的表达法

系数表达

一个n次多项式可以写成A(x) = \sum_{i=0}^{n} a_ix^i的形式,每一项对应有系数,我们把这些系数组成的向量a = (a_0, a_1, \ldots, a_n)叫做这个多项式的系数表达。

点值表达

一个n次多项式A(x)的点值表达是一个由n+1个点值对组成的集合\{ (x_0, y_0), (x_1, y_1), \ldots, (x_n, y_n) \},其中,x值互不相同,且y_i = A(x_i)
对于n+1个点值能表达唯一的n次多项式的证明,参见[学习笔记] 多项式与快速傅里叶变换(FFT)基础 – rvalue – 博客园
有了点值表达,我们来想想多项式相乘的结果:A(x) \cdot B(x)应该怎么计算。假设A多项式的次数是n,B的次数是m,那么得到的结果一定是一个n+m次多项式,因此我们需要n+m+1个点值才能确定它。因此我们需要把A和B的点值表示扩展为n+m+1个点值,然后对它们的点值表示中的y值对应相乘,结果就是新多项式的点值表示。

单位根的使用与多项式乘法的步骤

我们知道,朴素进行多项式乘法需要O(n^2)时间。如果我们能快速进行系数转点值(DFT)及其逆运算,通过点值表达做乘法只需要O(n)的时间,就能够实现加速这一过程的目的。我们可以通过选取精妙的点值来实现这一加速。
还记得复数中的单位根吗?它有一个折半引理,也就证明了 (\omega_n^k)^2 = \omega_{\frac{n}{2}}^k 。这个结论会在FFT中用到。这决定了我们可以分治实现O(n \log n)的系数转点值及其逆运算。这种加速后的算法被称为FFT。
那么我们计算多项式乘法步骤就是:

  1. 系数转点值,一次FFT,O(n \log n)
  2. 计算乘积的点值,O(n)
  3. 点值转系数,一次FFT的逆运算,O(n \log n)

离散傅里叶变换(DFT)

离散傅里叶变换是一种完成系数表达转换为点值表达的变换。这一变换是对多项式A在n个n次单位根处求点值,也就是说,是在完成下面这样一个事情:
多项式的系数表达给出为a = (a_0, a_1, \ldots, a_n),求y = (y_0, y_1, \ldots, y_n),其中y_i = A(\omega_{n+1}^i)
但是这样看起来其实也是一个O(n^2)的算法啊,教练加速去哪里了?往下看↓

快速傅里叶变换(FFT)

下面的叙述中,n代表多项式的次数界,次数界是指严格大于一个多项式的次数的正整数。为了方便,我们默认n是2的某次幂。
上面我们提到了要把这个问题分治解决,那么就要构造出适合分治解决的形式。首先是怎么构造分治的分支。我们考虑把系数按奇偶性分开,构造下面这样两个多项式
\begin{aligned} A_1(x) &= a_0 + a_2x + a_4x^2 + \cdots \\ A_2(x) &= a_1 + a_3x + a_5x^2 + \cdots \end{aligned}
那么就有A(x) = A_1(x^2) + A_2(x^2)x
那么我们考虑那个折半引理的式子 (\omega_n^k)^2 = \omega_{\frac{n}{2}}^k ,要求的一堆平方实际上变成了\frac{n}{2}个数字,那么我们就分别计算这\frac{n}{2}个数处的点值,然后合并到当前点值上就可以了。这样我们成功实现了缩小问题规模的目的。
具体而言,我们需要对\frac{n-1}{2}之前和之后的点值分别讨论
y_k = A(\omega_n^k) = A_1(\omega_n^{2k}) + \omega_n^kA_2(\omega_n^{2k}) = y^{[1]}_k + \omega_n^ky^{[2]}_k \ (k = 0, 1, \ldots, \frac{n}{2} - 1)
y_{k+\frac{n}{2}} = A(\omega_n^{k+\frac{n}{2}}) = A_1(\omega_n^{2k+n}) + \omega_n^{k+\frac{n}{2}}A_2(\omega_n^{2k+n}) = A_1(\omega_n^{2k}) - \omega_n^kA_2(\omega_n^{2k}) = y^{[1]}_k - \omega_n^ky^{[2]}_k \ (k = 0, 1, \ldots, \frac{n}{2} - 1)
对于递归边界,因为\omega_1^0 = 1,所以y_0 = a_0\omega_1^0 = a_0,直接不做处理即可。

逆变换:点值转系数(IDFT)

我们发现我们可以用下面这张矩阵来表示DFT的过程
\begin{bmatrix} y_0 \\ y_1 \\ \vdots \\ y_{n-1} \end{bmatrix} = \begin{bmatrix} 1 & 1 & 1 & \cdots & 1 \\ 1 & \omega_n & \omega_n^2 & \cdots & \omega_n^{n-1} \\ \vdots & \vdots & \vdots & \ddots & \vdots \\ 1 & \omega_n^{n-1} & \omega_n^{2(n-1)} & \cdots & \omega_n^{(n-1)^2} \end{bmatrix} \begin{bmatrix} a_0 \\ a_1 \\ \vdots \\ a_{n-1} \end{bmatrix}
其实中间那个大块头矩阵就是一个范德蒙矩阵V_n,实际上我们还有一个定理来求这个矩阵的逆矩阵: (V_n^{-1})_{j, k} = \frac{\omega_n^{-kj}}{n} 。那么我们就可以把逆矩阵往点值上一乘,点值就乖乖地变回系数啦~具体而言,变化是这样的a_j = \frac{1}{n} \sum_{k=0}^{n-1} y_k\omega_n^{-kj}。我们发现这个玩意和FFT在做的事情很相似,差就差在一个除以n还有单位根指数上面的负号。负号我们可以通过函数参数传进去,除以n可以在函数外部做,那么直接用FFT的过程不就好了。

FFT的优化方式

蝴蝶操作

我们发现上面的代码中\omega_n^ky_k^{[2]}的计算似乎是重复的,我们可以临时存起来而不是计算两次。

迭代实现

我们来观察FFT的调用树:
FFT调用树
(图片来自《算法导论(第3版)》,参见参考资料)
我们发现我们可以从最下层逐对合并结果,而需要把最下层的顺序处理出来。通过观察我们发现神奇的事:最下层第i个位置的下标刚好是它的二进制倒过来。那么交换一个位置和它二进制倒过来位置的元素即可。二进制倒过来的数可以预处理好。

不要用STL

STL比手写complex慢一些。

代码

本代码可以通过【P3803】【模板】多项式乘法(FFT) – 洛谷一题。

// Code by KSkun, 2018/3
#include <cstdio>
#include <cmath>

#include <algorithm>

typedef long long LL;

inline char fgc() {
    static char buf[100000], *p1 = buf, *p2 = buf;
    return p1 == p2 && (p2 = (p1 = buf) + fread(buf, 1, 100000, stdin), p1 == p2) ? EOF : *p1++;
}

inline LL readint() {
    register LL res = 0, neg = 1;
    char c = fgc();
    while(c < '0' || c > '9') {
        if(c == '-') neg = -1;
        c = fgc();
    }
    while(c >= '0' && c <= '9') {
        res = res * 10 + c - '0';
        c = fgc();
    }
    return res * neg;
}

const int MAXN = 1 << 22;
const double PI = acos(-1);

struct Complex {
    double real, imag;
    Complex(double real = 0, double imag = 0): real(real), imag(imag) {}
    inline Complex operator+(const Complex &rhs) const {
        return Complex(real + rhs.real, imag + rhs.imag);
    }
    inline Complex operator-(const Complex &rhs) const {
        return Complex(real - rhs.real, imag - rhs.imag);
    }
    inline Complex operator*(const Complex &rhs) const {
        return Complex(real * rhs.real - imag * rhs.imag, real * rhs.imag + imag * rhs.real);
    }
    inline Complex& operator*=(const Complex &rhs) {
        Complex old = *this;
        real = old.real * rhs.real - old.imag * rhs.imag;
        imag = old.real * rhs.imag + old.imag * rhs.real;
        return *this;
    }
};

int n, m, len, rev[MAXN];
Complex a[MAXN], b[MAXN];

inline void fft(Complex *arr, int f) {
    for(int i = 0; i < n; i++) {
        if(i < rev[i]) std::swap(arr[i], arr[rev[i]]);
    }
    for(int i = 1; i < n; i <<= 1) {
        Complex wn(cos(PI / i), f * sin(PI / i));
        for(int j = 0; j < n; j += i << 1) {
            Complex w(1, 0);
            for(int k = 0; k < i; k++) {
                Complex x = arr[j + k], y = w * arr[j + k + i];
                arr[j + k] = x + y;
                arr[j + k + i] = x - y;
                w *= wn;
            }
        }
    }
}

int main() {
    n = readint(); m = readint();
    for(int i = 0; i <= n; i++) {
        a[i].real = readint();
    }
    for(int i = 0; i <= m; i++) {
        b[i].real = readint();
    }
    m += n;
    for(n = 1; n <= m; n <<= 1) len++;
    for(int i = 0; i < n; i++) {
        rev[i] = (rev[i >> 1] >> 1) | ((i & 1) << (len - 1));
    }
    fft(a, 1);
    fft(b, 1);
    for(int i = 0; i <= n; i++) {
        a[i] *= b[i];
    }
    fft(a, -1);
    for(int i = 0; i <= m; i++) {
        printf("%d ", int(a[i].real / n + 0.5));
    }
    return 0;
}

参考资料

数学笔记:矩阵、行列式

数学笔记:矩阵、行列式

矩阵(Matrix) 一个的矩阵是一个由行列元素排列成的矩形阵列。元素可以是数字、符号或数 

数学笔记:复数

数学笔记:复数

虚数单位(Imaginary unit) 虚数单位定义为二次方程式的两个解答中的一个解答。 

[CF364D]Ghd 题解

[CF364D]Ghd 题解

题目地址:Codeforces:Problem – 364D – Codeforces、洛谷:【CF364D】Ghd – 洛谷

题目描述

John Doe offered his sister Jane Doe find the gcd of some set of numbers a.
Gcd is a positive integer g, such that all number from the set are evenly divisible by g and there isn’t such g’ (g’ > g), that all numbers of the set are evenly divisible by g’.
Unfortunately Jane couldn’t cope with the task and John offered her to find the ghd of the same subset of numbers.
Ghd is a positive integer g, such that at least half of numbers from the set are evenly divisible by g and there isn’t such g’ (g’ > g) that at least half of the numbers from the set are evenly divisible by g’.
Jane coped with the task for two hours. Please try it, too.
有一个数列,求最大的数,使得该数是数列中一半以上的数的因数。

输入输出格式

输入格式:
The first line contains an integer n (1 ≤ n ≤ 10^6) showing how many numbers are in set a. The second line contains space-separated integers a1, a2, …, an (1 ≤ ai ≤ 10^12). Please note, that given set can contain equal numbers.
Please, do not write the %lld specifier to read or write 64-bit integers in С++. It is preferred to use the %I64d specifier.

输出格式:
Print a single integer g — the Ghd of set a.

输入输出样例

输入样例#1:

6
6 2 3 4 5 6

输出样例#1:

3

输入样例#2:

5
5 5 6 10 15

输出样例#2:

5

题解

参考资料:CFR 364 D. Ghd ( Random, Math ) – 0w1
随机化的题目,直接求显然不好办,n的数据范围看着就吓人,直观感觉像O(n \log n)
我们知道这个数字一定是这个数列中某个数的因数,因此我们需要选择一个数求它与其他数的最大公因数。这些公因数中的一个就是答案。求一遍公因数是O(n \log n)的。
对于求出来的公因数,我们去从大到小找一个会成为超过一半数的因数的数字。具体做法是,选择一个因数,去找比它大的因数,如果它能整除大因数,说明大因数对应的数字也可以被这个数整除,应当把加到这个数的计数上。这个过程直观看上去是O(n^2)的,但是实际上我们不会对比当前最优解还小的因数计算,并且当计数超过n的一半的时候就可以停止计数并计入答案,在这样的处理下我们可以让这个计数的时间变得可接受。
答案肯定是这些数字中超过一半数的因数,因此每一次随机找到正确解的概率超过0.5。我们考虑重复找解的过程多次,这里我使用10次(超过10次似乎会TLE 18)。

代码

// Code by KSkun, 2018/3
#include <cstdio>
#include <ctime>
#include <cstdlib>

#include <map>

typedef long long LL;

inline char fgc() {
    static char buf[100000], *p1 = buf, *p2 = buf;
    return p1 == p2 && (p2 = (p1 = buf) + fread(buf, 1, 100000, stdin), p1 == p2) ? EOF 
        : *p1++;
}

inline LL readint() {
    register LL res = 0, neg = 1;
    char c = fgc();
    while(c < '0' || c > '9') {
        if(c == '-') neg = -1;
        c = fgc();
    }
    while(c >= '0' && c <= '9') {
        res = res * 10 + c - '0';
        c = fgc();
    }
    return res * neg;
}

const int MAXN = 1000005;

inline LL gcd(LL a, LL b) {
    LL t;
    while(b) {
        t = a % b;
        a = b;
        b = t;
    }
    return a;
}

int n;
LL a[MAXN];

int main() {
    srand(time(NULL));
    n = readint();
    for(int i = 1; i <= n; i++) {
        a[i] = readint();
    }
    LL ans = 1;
    for(int rep = 1; rep <= 10; rep++) { // bad rand
        int rnd = (rand() * RAND_MAX + rand()) % n + 1;
        std::map<LL, int> fact;
        for(int i = 1; i <= n; i++) {
            LL t = gcd(a[i], a[rnd]);
            if(!fact.count(t)) fact[t] = 1;
            else fact[t]++;
        }
        std::map<LL, int>::iterator it = fact.end();
        do {
            it--;
            if((*it).first <= ans) continue;
            int cnt = 0;
            for(std::map<LL, int>::iterator it1 = it; 
                it1 != fact.end() && cnt << 1 < n; it1++) {
                if(!((*it1).first % (*it).first)) {
                    cnt += (*it1).second;
                }
            }
            if(cnt << 1 >= n) ans = (*it).first;
        } while(it != fact.begin());
    }
    printf("%I64d", ans);
    return 0;
}
[WF2015]Cutting Cheese 题解

[WF2015]Cutting Cheese 题解

题目地址:UVa:UVa Online Judge、洛谷:【UVA1712】Cutting