\[\newcommand{\vct}[1]{\boldsymbol{#1}}
\newcommand{\mat}[1]{\begin{bmatrix}#1\end{bmatrix}}
\newcommand{\opn}[1]{\operatorname{#1}}
\mathscr{\text{Defining }\LaTeX\text{ Macros...}}
\]

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


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

  转置原理基于这样的事实:\((AB)^T=B^TA^T\),我们将 \(A\) 分解为若干初等矩阵的乘积,令 \(A=A_{k}A_{k-1}\cdots A_1\),那么 \(A^T=A_1^TA_2^T\cdots A_k^T\),而初等矩阵的转置是显然的,因此能实现算法间的转化。具体地,从后往前考虑算法 \(\mathcal A\) 中的指令:

  • \(\textbf{output }q_i~r_j\),即将变量 \(r_j\) 的值作为输出的 \(q_i\)。将其改写为 \(\textbf{input}~q_i~r_j\),即将变量 \(r_j\) 的值作为输入的 \(q_i\);
  • \(\textbf{swap }r_i~r_j\),即交换变量 \(r_i,r_j\) 的值,保持不变;
  • \(\textbf{mul }r_i~k\),即将变量 \(r_i\) 的值乘上常数 \(k\),保持不变;
  • \(\textbf{add }r_i~r_j~k\),即将变量 \(r_i\) 的值加上 \(k\) 倍的 \(r_j\)。将其改写为 \(\textbf{add }r_j~r_i~k\);
  • \(\textbf{input }p_i~r_j\),将其改写为 \(\textbf{output }p_i~r_j\)。

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


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

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

\[\vct f=\mat{
f_0\\
f_1\\
\vdots\\
f_{n-1}
},
\]

并给定 \(\lang a_0,\cdots,a_{m-1}\rang\),令

\[A=\mat{
1&a_0&\cdots&a_0^{n-1}\\
1&a_1&\cdots&a_1^{n-1}\\
\vdots&\vdots&\ddots&\vdots\\
1&a_{m-1}&\cdots&a_{m-1}^{n-1}
},
\]

\[\vct r=A\vct f=\mat{
f(a_0)\\
f(a_1)\\
\vdots\\
f(a_{m-1})
}.
\]

  考虑取转置,转置问题形如求 \(\vct q=A^T\vct p\)。研究 \(\vct q\) 的形式

\[q_i=\sum_{j=0}^{m-1}a_j^ip_j.
\]

可见

\[q(x)\equiv \sum_{i=0}^{m-1}\frac{p_i}{1-a_ix}\pmod{x^n}.
\]

不妨令 \(q(x)=\sum_{i=0}^{m-1}\frac{p_i}{1-a_ix}\),整理其形式得

\[q(x)=\frac{[y]\prod_i(p_iy+1-a_ix)}{\prod_i(1-a_ix)}.
\]

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

  设计算法:

\[\text{Algorithm 1: input }\vct p\text{, output }\vct q=A^T\vct p.\\
\begin{array}{}
1& \textbf{function }\opn{solve}(l,r)\\
2& \quad \textbf{if }l=r\textbf{ then}\\
3& \quad \quad \textbf{return }p_l\\
4& \quad \textit{mid}\leftarrow\left\lfloor\frac{l+r}{2}\right\rfloor\\
5& \quad L\leftarrow\opn{solve}(l,\textit{mid})\\
6& \quad R\leftarrow\opn{solve}(\textit{mid}+1,r)\\
7& \quad F_l\leftarrow L\times A_{[\textit{mid}+1,r]}\\
8& \quad F_r\leftarrow R\times A_{[l,\textit{mid}]}\\
9& \quad F\leftarrow F_l+F_r\\
10& \quad \textbf{return }F\\
11& \textbf{end function}\\
12\\
13& \textbf{input }\vct p\\
14& \vct q\leftarrow \opn{solve}(0,m-1)\times A_{[0,m-1]}^{-1}\\
15& \textbf{output }\vct q
\end{array}
\]

其中多项式 \(A_{[l,r]}=\prod_{i=l}^r(1-a_ix)\),多项式系数序列与向量不作区分。进行转置得到

\[\text{Algorithm 2: input }\vct q\text{, output }\vct p=A\vct q.\\
\begin{array}{}
1& \textbf{function }\opn{solveT}(l,r,F)\\
2& \quad \textbf{if }l=r\textbf{ then}\\
3& \quad \quad p_l\leftarrow F_0\\
4& \quad \quad \textbf{return null}\\
5& \quad \textit{mid}\leftarrow\left\lfloor\frac{l+r}{2}\right\rfloor\\
6& \quad L\leftarrow F\times^T A_{[\textit{mid+1,r}]}\\
7& \quad R\leftarrow F\times^T A_{[l,\textit{mid}]}\\
8& \quad \opn{solveT}(l,\textit{mid},L)\\
9& \quad \opn{solveT}(\textit{mid}+1,r,R)\\
10& \quad \textbf{return null}\\
11& \textbf{end function}\\
12\\
13& \textbf{input }\vct q\\
14& \opn{solveT}(0,m-1,\vct q\times^T A_{[0,m-1]}^{-1})\\
15& \textbf{output }\vct p
\end{array}
\]

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

  • \(\begin{array}{}10&\textbf{return }F\end{array}\) 这个“返回值”可以看做函数的“输出”,它应当变为函数的“输入”,所以 \(\opn{solveT}\) 额外增加了参数 \(F\)。
  • \(\begin{array}{}9&F\leftarrow F_l+F_r\end{array}\) 实际上跳步了。应当是初始 \(F\leftarrow0\),后 \(F\leftarrow F+1\times F_l\),再 \(F\leftarrow F+1\times F_r\),取转置后,得到 \(F_l\leftarrow F,F_r\leftarrow F\),所以 \(\text{A2}\) 中直接用了 \(F\) 而并没有添加变量 \(F_l\) 和 \(F_r\)。
  • \(\begin{array}{}8&F_r\leftarrow R\times A_{[l,\textit{mid}]}\end{array}\) 注意 \(A_{[l,\textit{mid}]}\) 是常量,取转置得到 \(R\leftarrow F\times^T A_{[l,\textit{mid}]}\),其中 \(\times^T\) 是 \(\times\) 的转置。第 \(7\) 行同理。
  • \(\begin{array}{}6&R\leftarrow\opn{solve}(\textit{mid+1},r)\end{array}\) “输出”变“输入”,转置得 \(\opn{solveT}(\textit{mid}+1,r,R)\)。第 \(5\) 行同理。
  • \(\begin{array}{}3&\textbf{return }p_l\end{array}\) 形象地说,想想你自己写代码的时候,可能在这里才读入 \(p_l\) 的值。所以这个实际上是输入 \(p_l\)(作为 \(F_0\)),转置为输出 \(p_l\)(值为 \(F_0\))。

  主过程就三行,不讲啦。

  对于 \(\times^T\),写出暴力多项式卷积的算法 \(A(x)\times 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 了,新年快乐吖!

Note/Solution - 转置原理 & 多点求值的更多相关文章

  1. 洛谷P5282 【模板】快速阶乘算法(多项式多点求值+MTT)

    题面 传送门 前置芝士 \(MTT\),多项式多点求值 题解 这题法老当初好像讲过--而且他还说这种题目如果模数已经给定可以直接分段打表艹过去 以下是题解 我们设 \[F(x)=\prod_{i=0} ...

  2. 多项式的各类计算(多项式的逆/开根/对数/exp/带余除法/多点求值)

    预备知识:FFT/NTT 多项式的逆 给定一个多项式 F(x)F(x)F(x),请求出一个多项式 G(x)G(x)G(x),满足 F(x)∗G(x)≡1(mod xn)F(x)*G(x) \equiv ...

  3. luogu P5667 拉格朗日插值2 拉格朗日插值 多项式多点求值 NTT

    LINK:P5667 拉格朗日插值2 给出了n个连续的取值的自变量的点值 求 f(m+1),f(m+2),...f(m+n). 如果我们直接把f这个函数给插值出来就变成了了多项式多点求值 这个难度好像 ...

  4. 洛谷P5050 【模板】多项式多点求值

    传送门 人傻常数大.jpg 因为求逆的时候没清零结果调了几个小时-- 前置芝士 多项式除法,多项式求逆 什么?你不会?左转你谷模板区,包教包会 题解 首先我们要知道一个结论\[f(x_0)\equiv ...

  5. 【洛谷P5050】 【模板】多项式多点求值

    code: #include <bits/stdc++.h> #define ll long long #define ull unsigned long long #define set ...

  6. Note/Solution -「洛谷 P5158」「模板」多项式快速插值

    \(\mathcal{Description}\)   Link.   给定 \(n\) 个点 \((x_i,y_i)\),求一个不超过 \(n-1\) 次的多项式 \(f(x)\),使得 \(f(x ...

  7. php实现用短路求值原理求1+2+3+...+n(短路求值是什么)(仔细分析题干)

    php实现用短路求值原理求1+2+3+...+n(短路求值是什么)(仔细分析题干) 一.总结 1.仔细分析题干,找出要点:该递归还是得递归啊 2.短路求值原理:&&就是逻辑与,逻辑与有 ...

  8. 左求值表达式,堆栈,调试陷阱与ORM查询语言的设计

    1,表达式的求值顺序与堆栈结构 “表达式” 是程序语言一个很重要的术语,也是大家天天写的程序中很常见的东西,但是表达式的求值顺序一定是从左到右么? C/C++语言中没有明确规定表达式的运算顺序(从左到 ...

  9. 数据结构算法C语言实现(八)--- 3.2栈的应用举例:迷宫求解与表达式求值

    一.简介 迷宫求解:类似图的DFS.具体的算法思路可以参考书上的50.51页,不过书上只说了粗略的算法,实现起来还是有很多细节需要注意.大多数只是给了个抽象的名字,甚至参数类型,返回值也没说的很清楚, ...

随机推荐

  1. LC 只出现一次的数字

    Given a non-empty array of integers nums, every element appears twice except for one. Find that sing ...

  2. 互联网医疗行业PEST分析实践

    前言 今年开始逐步切入产品与数据工作,完全脱离了原来的舒适区,确实有一些挑战.开始以为只做数仓建设的事情,就仓促的学习了一些数仓相关的知识,但没搞多久,还要负责公司BI的工作,又开始补习数分相关的知识 ...

  3. 【C++】类-基础知识

    类-基础知识 目录 类-基础知识 1. 语法定义 2. 类的实现 3. 三个基本的函数 3.1 构造函数 功能 形式 调用时机 默认构造函数 3.2 复制构造函数 功能 形式 调用时机 3.3 析构函 ...

  4. 一文读懂 HTTP/1HTTP/2HTTP/3

    转自 https://zhuanlan.zhihu.com/p/102561034

  5. Java 面试题史上最强整理

    https://mp.weixin.qq.com/s/kJpRgfI3zT77XqMeRfmmQQ

  6. 学习鸟哥linux私房菜--安装中文输入法fcitx

    首先需要卸载前面安装的scim,查阅命令,参考网址如下 http://www.cnblogs.com/propheteia/archive/2012/06/26/2563383.html 链接中博主采 ...

  7. 使用Hot Chocolate和.NET 6构建GraphQL应用(2) —— 实体相关功能实现

    系列导航 使用Hot Chocolate和.NET 6构建GraphQL应用文章索引 需求 在本文中,我们将会准备好用于实现GraphQL接口所依赖的底层数据,为下一篇文章具体实现GraphQL接口做 ...

  8. 第08讲:Flink 窗口、时间和水印

    Flink系列文章 第01讲:Flink 的应用场景和架构模型 第02讲:Flink 入门程序 WordCount 和 SQL 实现 第03讲:Flink 的编程模型与其他框架比较 第04讲:Flin ...

  9. 微服务架构 | 7.1 基于 OAuth2 的安全认证

    目录 前言 1. OAuth2 基础知识 1.1 安全性的 4 个组成部分 1.2 OAuth2 的工作原理 1.3 OAuth2 规范的 4 种类型的授权 1.4 OAuth2 的优势 1.5 OA ...

  10. ApacheCN 数据科学译文集 20211109 更新ApacheCN 数据科学译文集 20211109 更新

    计算与推断思维 一.数据科学 二.因果和实验 三.Python 编程 四.数据类型 五.表格 六.可视化 七.函数和表格 八.随机性 九.经验分布 十.假设检验 十一.估计 十二.为什么均值重要 十三 ...