Note/Solution - 转置原理 & 多点求值
\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)\),令
f_0\\
f_1\\
\vdots\\
f_{n-1}
},
\]
并给定 \(\lang a_0,\cdots,a_{m-1}\rang\),令
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}
},
\]
求
f(a_0)\\
f(a_1)\\
\vdots\\
f(a_{m-1})
}.
\]
考虑取转置,转置问题形如求 \(\vct q=A^T\vct p\)。研究 \(\vct q\) 的形式
\]
可见
\]
不妨令 \(q(x)=\sum_{i=0}^{m-1}\frac{p_i}{1-a_ix}\),整理其形式得
\]
可以分治 \(\mathcal O(n\log^2n)\) 求解。到此,我们对这一算法再次转置即可得到同复杂度,求解原问题的算法。
设计算法:
\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)\),多项式系数序列与向量不作区分。进行转置得到
\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 - 转置原理 & 多点求值的更多相关文章
- 洛谷P5282 【模板】快速阶乘算法(多项式多点求值+MTT)
题面 传送门 前置芝士 \(MTT\),多项式多点求值 题解 这题法老当初好像讲过--而且他还说这种题目如果模数已经给定可以直接分段打表艹过去 以下是题解 我们设 \[F(x)=\prod_{i=0} ...
- 多项式的各类计算(多项式的逆/开根/对数/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 ...
- luogu P5667 拉格朗日插值2 拉格朗日插值 多项式多点求值 NTT
LINK:P5667 拉格朗日插值2 给出了n个连续的取值的自变量的点值 求 f(m+1),f(m+2),...f(m+n). 如果我们直接把f这个函数给插值出来就变成了了多项式多点求值 这个难度好像 ...
- 洛谷P5050 【模板】多项式多点求值
传送门 人傻常数大.jpg 因为求逆的时候没清零结果调了几个小时-- 前置芝士 多项式除法,多项式求逆 什么?你不会?左转你谷模板区,包教包会 题解 首先我们要知道一个结论\[f(x_0)\equiv ...
- 【洛谷P5050】 【模板】多项式多点求值
code: #include <bits/stdc++.h> #define ll long long #define ull unsigned long long #define set ...
- Note/Solution -「洛谷 P5158」「模板」多项式快速插值
\(\mathcal{Description}\) Link. 给定 \(n\) 个点 \((x_i,y_i)\),求一个不超过 \(n-1\) 次的多项式 \(f(x)\),使得 \(f(x ...
- php实现用短路求值原理求1+2+3+...+n(短路求值是什么)(仔细分析题干)
php实现用短路求值原理求1+2+3+...+n(短路求值是什么)(仔细分析题干) 一.总结 1.仔细分析题干,找出要点:该递归还是得递归啊 2.短路求值原理:&&就是逻辑与,逻辑与有 ...
- 左求值表达式,堆栈,调试陷阱与ORM查询语言的设计
1,表达式的求值顺序与堆栈结构 “表达式” 是程序语言一个很重要的术语,也是大家天天写的程序中很常见的东西,但是表达式的求值顺序一定是从左到右么? C/C++语言中没有明确规定表达式的运算顺序(从左到 ...
- 数据结构算法C语言实现(八)--- 3.2栈的应用举例:迷宫求解与表达式求值
一.简介 迷宫求解:类似图的DFS.具体的算法思路可以参考书上的50.51页,不过书上只说了粗略的算法,实现起来还是有很多细节需要注意.大多数只是给了个抽象的名字,甚至参数类型,返回值也没说的很清楚, ...
随机推荐
- 利用栈实现括号匹配(python语言)
原理: 右括号总是与最近的左括号匹配 --- 栈的后进先出 从左往右遍历字符串,遇到左括号就入栈,遇到右括号时,就出栈一个元素与其配对 当栈为空时,遇到右括号,则此右括号无与之匹配的左括号 当最终右括 ...
- Jetpack—LiveData组件的缺陷以及应对策略 转至元数据结尾
一.前言 为了解决Android-App开发以来一直存在的架构设计混乱的问题,谷歌推出了Jetpack-MVVM的全家桶解决方案.作为整个解决方案的核心-LiveData,以其生命周期安全,内存安全等 ...
- Spark-寒假-实验4
1.spark-shell 交互式编程 (1)该系总共有多少学生: 执行命令: var tests=sc.textFile("file:///home/hadoop/studata/chap ...
- jQuery ajax get与post后台交互中的奥秘
这两天在做关注功能模块(类似于Instagram).多处页面都需要通过一个"关注"按钮进行关注或者取消该好友的操作.一个页面对应的放一个按钮,进行操作.效率低维护性差.因此想通过j ...
- Windows和Linux(Centos7)下安装Nginx
安装Nginx 这篇记录只不过做了一个简单总结,如果对这块没什么概念的话可以看一下知乎的这篇文章 https://zhuanlan.zhihu.com/p/83890573 window下安装 win ...
- 程序员必备的编程助手!SmartCoder助你轻松集成HMS Core
当开发者在集成HMS Core遇到一些疑问时,需要翻阅官网文档,反复查看集成说明或者API调用说明,或者研究GitHub上的开源示例代码,花费较多的时间,在IDE环境和网页浏览器之间反复切换也会耗费很 ...
- 集合框架-Vector集合
1 package cn.itcast.p1.vector.demo; 2 3 import java.util.Enumeration; 4 import java.util.Iterator; 5 ...
- JS、jQuery 刷新 iframe 的方法
1.JavaScript 刷新 iframe 可以使用以下方法: document.getElementById('some_frame_id').contentWindow.location.rel ...
- 010 Linux 文本统计与去重 (wc 和 uniq)
wc 命令一般是作为组合命令的一员与其他命令一同起到统计的作用.而一般情况下使用wc -l 命令较多. uniq 可检查文本文件中重复出现的行,一般与 sort 命令结合使用.一起组合搭配使用完成统计 ...
- 洛谷P2240 【深基12.例1】部分背包问题(经典模板)
首先应该注意几个关键信息:装走尽多的金币.所有金币可以随意分割并且分割后金币的价值比不变; 我们可以抽象成一个买东西的场景,给你一定数目的钱,让你在这些钱内尽量在超市买更多的小零食,这就牵扯到了一个性 ...