Live2D

Note/Solution - 转置原理 & 多点求值

Defining LATEX Macros...

  我并没有透彻理解涉及知识点的严谨描述形式,所以本文大量用语是基于让读者理解而非让读者以此为研究依据的,烦请注意。


  设现有一个算法 A,它接受向量 x 为输入,以向量 y 为输出,满足 y=Ax,其中 A 是常矩阵,则 A 为线性算法。转置原理指出,我们可以据此找到一个算法 B 用以计算 x=ATy,且所用乘法次数不变,加法次数增加至多 (mn) 次。即,当已有足够优秀的算法 A,我们“几乎”得到了同样优秀的算法 B。(本段并不严谨,若有严重误导性请指正。)

  转置原理基于这样的事实:(AB)T=BTAT,我们将 A 分解为若干初等矩阵的乘积,令 A=AkAk1A1,那么 AT=A1TA2TAkT,而初等矩阵的转置是显然的,因此能实现算法间的转化。具体地,从后往前考虑算法 A 中的指令:

  • output qi rj,即将变量 rj 的值作为输出的 qi。将其改写为 input qi rj,即将变量 rj 的值作为输入的 qi
  • swap ri rj,即交换变量 ri,rj 的值,保持不变;
  • mul ri k,即将变量 ri 的值乘上常数 k,保持不变;
  • add ri rj k,即将变量 ri 的值加上 k 倍的 rj。将其改写为 add rj ri k
  • input pi rj,将其改写为 output pi rj

  值得一提的是,实际的算法往往难以表示为如此初等语句的顺序结构。若某个子算法的转置是显然的,我们无需再将其拆开,而可以直接取它的转置。典型的例子是 DFT,其插值的矩阵是对称的,所以 DFT 的转置就是 DFT 自身。


  举一个经典例子:多点求值。

  对于 n 次多项式 f(x),令

f=[f0f1fn1],

并给定 a0,,am1,令

A=[1a0a0n11a1a1n11am1am1n1],

r=Af=[f(a0)f(a1)f(am1)].

  考虑取转置,转置问题形如求 q=ATp。研究 q 的形式

qi=j=0m1ajipj.

可见

q(x)i=0m1pi1aix(modxn).

不妨令 q(x)=i=0m1pi1aix,整理其形式得

q(x)=[y]i(piy+1aix)i(1aix).

可以分治 O(nlog2n) 求解。到此,我们对这一算法再次转置即可得到同复杂度,求解原问题的算法。

  设计算法:

Algorithm 1: input p, output q=ATp.1function solve(l,r)2if l=r then3return pl4midl+r25Lsolve(l,mid)6Rsolve(mid+1,r)7FlL×A[mid+1,r]8FrR×A[l,mid]9FFl+Fr10return F11end function1213input p14qsolve(0,m1)×A[0,m1]115output q

其中多项式 A[l,r]=i=lr(1aix),多项式系数序列与向量不作区分。进行转置得到

Algorithm 2: input q, output p=Aq.1function solveT(l,r,F)2if l=r then3plF04return null5midl+r26LF×TA[mid+1,r]7RF×TA[l,mid]8solveT(l,mid,L)9solveT(mid+1,r,R)10return null11end function1213input q14solveT(0,m1,q×TA[0,m1]1)15output p

注意到递归的底层仅需要 F0 的值,所以可以保持 degF=rl+1,那么复杂度亦为 O(nlog2n)。鉴于我初次理解的困难,这里给出详细的转化步骤。先看 A1 中的 solve(l,r) 函数,我们把它转置成 A2 中的 solveT(l,r,F),过程如下:

  • 10return F 这个“返回值”可以看做函数的“输出”,它应当变为函数的“输入”,所以 solveT 额外增加了参数 F
  • 9FFl+Fr 实际上跳步了。应当是初始 F0,后 FF+1×Fl,再 FF+1×Fr,取转置后,得到 FlF,FrF,所以 A2 中直接用了 F 而并没有添加变量 FlFr
  • 8FrR×A[l,mid] 注意 A[l,mid] 是常量,取转置得到 RF×TA[l,mid],其中 ×T× 的转置。第 7 行同理。
  • 6Rsolve(mid+1,r) “输出”变“输入”,转置得 solveT(mid+1,r,R)。第 5 行同理。
  • 3return pl 形象地说,想想你自己写代码的时候,可能在这里才读入 pl 的值。所以这个实际上是输入 pl(作为 F0),转置为输出 pl(值为 F0)。

  主过程就三行,不讲啦。

  对于 ×T,写出暴力多项式卷积的算法 A(x)×B(x),将 B 视为常量得到一个关于 A(x) 的线性算法。取转置,发现就是 A(x)B(x) 在做差卷积。所以卷积的转置是差卷积。


  先到这里叭,请一定去看看 Tiw 的讲解 w!

  下面是多点求值的代码,挺快的√

/*+Rainybunny+*/

#include <bits/stdc++.h>

#define rep(i, l, r) for (int i = l, rep##i = r; i <= rep##i; ++i)
#define per(i, r, l) for (int i = r, per##i = l; i >= per##i; --i)

typedef std::vector<int> Poly;

const int MAXN = 1 << 17, MOD = 998244353;
int n, m, a[MAXN + 5], ans[MAXN + 5];
Poly A[MAXN << 2], Q;

inline int mul(const int u, const int v) { return 1ll * u * v % MOD; }
inline int sub(int u, const int v) { return (u -= v) < 0 ? u + MOD : u; }
inline int add(int u, const int v) { return (u += v) < MOD ? u : u - MOD; }
inline int mpow(int u, int v) {
    int ret = 1;
    for (; v; u = mul(u, u), v >>= 1) ret = mul(ret, v & 1 ? u : 1);
    return ret;
}

namespace PolyOper {

const int G = 3;
int omega[18][MAXN + 5];

inline void init() {
    rep (i, 1, 17) {
        int* wi = omega[i];
        wi[0] = 1, wi[1] = mpow(G, MOD - 1 >> i);
        rep (j, 2, (1 << i) - 1) wi[j] = mul(wi[j - 1], wi[1]);
    }
}

inline void ntt(Poly& u, const int tp) {
    static int rev[MAXN + 5]; int n = u.size();
    rep (i, 0, n - 1) rev[i] = rev[i >> 1] >> 1 | (i & 1) * n >> 1;
    rep (i, 0, n - 1) if (i < rev[i]) std::swap(u[i], u[rev[i]]);
    for (int i = 1, stp = 1; stp < n; ++i, stp <<= 1) {
        int* wi = omega[i];
        for (int j = 0; j < n; j += stp << 1) {
            rep (k, j, j + stp - 1) {
                int ev = u[k], ov = mul(wi[k - j], u[k + stp]);
                u[k] = add(ev, ov), u[k + stp] = sub(ev, ov);
            }
        }
    }
    if (!~tp) {
        int inv = mpow(n, MOD - 2);
        std::reverse(u.begin() + 1, u.end());
        for (int& a: u) a = mul(a, inv);
    }
}

inline Poly pmul(Poly u, Poly v) {
    int res = u.size() + v.size() - 1, len = 1;
    while (len < res) len <<= 1;
    u.resize(len), v.resize(len);
    ntt(u, 1), ntt(v, 1);
    rep (i, 0, len - 1) u[i] = mul(u[i], v[i]);
    ntt(u, -1);
    return u.resize(res), u;
}

inline Poly pmulT(Poly u, Poly v) {
    int n = u.size(), m = v.size();
    std::reverse(v.begin(), v.end()), v = pmul(u, v);
    rep (i, 0, n - 1) u[i] = v[i + m - 1];
    return u;
}

inline void pinv(const int n, const Poly& u, Poly& r) {
    if (n == 1) return void(r = { { mpow(u[0], MOD - 2) } });
    static Poly tmp; pinv(n >> 1, u, r);
    tmp.resize(n << 1), r.resize(n << 1);
    rep (i, 0, n - 1) tmp[i] = i < u.size() ? u[i] : 0;
    rep (i, n, (n << 1) - 1) tmp[i] = 0;
    ntt(r, 1), ntt(tmp, 1);
    rep (i, 0, (n << 1) - 1) r[i] = mul(r[i], sub(2, mul(tmp[i], r[i])));
    ntt(r, -1), r.resize(n);
}

} // namespace PolyOper.

inline void init(const int u, const int l, const int r) {
    if (l == r) return void(A[u] = { { 1, sub(0, a[l]) } });
    int mid = l + r >> 1;
    init(u << 1, l, mid), init(u << 1 | 1, mid + 1, r);
    A[u] = PolyOper::pmul(A[u << 1], A[u << 1 | 1]);
}

inline void solveT(const int u, const int l, const int r, Poly F) {
    F.resize(r - l + 1);
    if (l == r) return void(ans[l] = F[0]);
    int mid = l + r >> 1;
    solveT(u << 1, l, mid, PolyOper::pmulT(F, A[u << 1 | 1]));
    solveT(u << 1 | 1, mid + 1, r, PolyOper::pmulT(F, A[u << 1]));
}

int main() {
    scanf("%d %d", &n, &m), Q.resize(++n);
    for (int& u: Q) scanf("%d", &u);
    rep (i, 0, m - 1) scanf("%d", &a[i]);

    PolyOper::init();
    init(1, 0, m - 1);
    int len = 1; while (len < n) len <<= 1;
    Poly T; PolyOper::pinv(len, A[1], T);
    
    solveT(1, 0, m - 1, PolyOper::pmulT(Q, T));
    rep (i, 0, m - 1) printf("%d\n", ans[i]);
    return 0;
}

  嘛,2022 了,新年快乐吖!

posted @   Rainybunny  阅读(391)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 25岁的心里话
· 按钮权限的设计及实现
点击右上角即可分享
微信分享提示