今日 Floyd 矩阵大学习
Floyd 矩阵
我在网上搜了很久这个名字,但是我只能搜到 Floyd 算法的讲解……
不过,在有些题解里提到过这个名字。不管怎样,我第一次听见这种定义时它的名字叫 Floyd 矩阵,那我就跟着这么叫吧。
定义
对于矩阵 \(A(N\times K)\) 和 \(B(K \times M)\),可以定义矩阵乘法 $C=A\times B $ :
即将一般的矩阵乘法中的“乘法”替换为加法,将“加法”替换为“max”。其也拥有和矩阵乘法类似的代数性质(结合律)。
具体证明懒得写了,但可以感性理解一下:在某种结构上,max 可以看做“ 0 级运算”,而加法是“ 1 级运算”,乘法是“ 2 级运算”。注意到,max、加法和乘法都有交换律、结合律,并且,高一级运算对低一级运算满足分配律,有着整体的相似性。
因此,可以将这种 Floyd 矩阵运算看做常规矩阵运算的降级。
稍稍拓展下,不难注意到,指数也满足这种分配率:$(a\times b)c=ac\times b^c $。但是指数不具备交换律和结合律,因此很遗憾矩阵乘法应该没有上级拓展了。
组合意义
既然叫 Floyd 矩阵,自然会联想到那个算法。我们将那个算法的公式写出来(为了统一使用最长路):
随后,我们不难发现,这不就等价于 $f \leftarrow f^2 $ 吗?
由此可见,Floyd 矩阵的组合意义之一,就是多源最短路。或者说,一个类似于 DFA 中,两点间经过固定边的路径的最长路的东西。举个例子,\(f^3\) 就可以表示所有经过了三条边的路径中,最长路的大小。
这里也可以后普通矩阵乘做一个对照:普通矩阵乘求的是方案数,而 Floyd 矩阵求的是最长路。
当然,和矩阵乘法一样,有些时候 Floyd 矩阵只是单纯用来加速某种递推,比如说:
例题一 P4719
题目描述
给定一棵 \(n\) 个点的树,点带点权。
有 \(m\) 次操作,每次操作给定 \(x,y\),表示修改点 \(x\) 的权值为 $y4。
你需要在每次操作之后求出这棵树的最大权独立集的权值大小。
\(1\le n,m\le 10^5\)
Solution
注意到,如果没有修改,就是没有上司的舞会。存在 dp 式:
但是,这里是有修改的动态动态规划,所以我们要考虑如何修改。注意到,修改只对其祖先的状态有影响,也就是说每次修改的影响是一条链。这启示我们进行树剖。
但是,树剖的前提是区间操作,我们如何进行区间操作呢?首先,根据树剖的性质,尝试修改上述 dp 式。将重儿子单拉出来。定义 \(g\):
这样,\(f\) 就会好看很多(定义 \(son\) 为重儿子):$f_{i,0}=g_{i,0}+max(f_{son,0},f_{son,1}),f_{i,1}=g_{i,1}+f_{son,0} $。
不难想到将重、轻儿子单独处理,重儿子直接序列维护,轻儿子点内维护。一条链只经过 \(O(\log n)\) 个轻儿子,复杂度目测 \(O(n \log^2 n)\),我们做完了……吗?
这个东西如何区间维护?
仔细观察这个式子,我们目前的目标是已知区间内一点的 \(f\),以及区间内所有点的 \(g\),如何查找一个另一个 \(f\)?联想一下我们的标题,有没有感觉像什么。比如说——矩阵递推?
现在,\(f_i\) 之和 \(f_{son}\) 与 \(g_i\) 有关,简直就是一个转移矩阵啊。不过,一般的矩阵乘法是加法和乘法,但是我们现在需要 max 和加法,怎么办呢好难猜啊。
我们定义一个矩阵\(\begin{bmatrix} f_{i,0} \\ f_{i,1} \end{bmatrix}\),然后根据上面的定义,写出转移矩阵$ \begin{bmatrix} g_{i,0} & g_{i,0} \ g_{i,1} & -\infin \end{bmatrix} $,这样,我们只需要在线段树中维护转移矩阵的乘积,然后直接用右端点的 \(f\) 就可以了。
注意合并顺序哦。
struct Matrix{
// vector <vector <int> > v; //警惕 vector 大常数!!!
int v[3][3];
Matrix(){For(i,0,2) For(j,0,2) v[i][j]=0;n=2,m=2;}//局部变量一定要赋值!!!
int n,m;
int* operator [] (const int &x){return v[x];}
const int* operator [] (const int &x) const{return v[x];}
friend Matrix operator * (const Matrix &a,const Matrix &b){
Matrix c;
For(i,0,a.n-1)
For(j,0,b.n-1)
For(k,0,b.m-1)//记得小常数矩阵乘,一定要顺序访问!
tomax(c[i][k],a[i][j]+b[j][k]);
return c;
}
};
int a[N];
vector <int> e[N];
int f[N][2];//id is dfn
Matrix val[N];
struct Tr{
int tl,tr,lc,rc;
Matrix sum;
};
struct Seg{//基础的线段树
Tr t[N*4];
#define unzip(root) int &tl=t[root].tl,&tr=t[root].tr,&lc=t[root].lc,&rc=t[root].rc;Matrix &sum=t[root].sum;
void push_up(int root){
t[root].sum=t[t[root].lc].sum*t[t[root].rc].sum;
}
void build(int root,int l,int r)
{
unzip(root);
tl=l;tr=r;lc=root<<1;rc=lc|1;
if(tl==tr) return sum=val[tl],void();
int mid=tl+tr>>1;
build(lc,tl,mid);
build(rc,mid+1,tr);
push_up(root);
}
void update(int root,int k)
{
unzip(root);
if(tl==tr) return sum=val[tl],void();
if(k<=t[lc].tr) update(lc,k);
else update(rc,k);
push_up(root);
}
Matrix query(int root,int l,int r)
{
unzip(root);
if(l<=tl&&tr<=r) return t[root].sum;
else if(l<=t[lc].tr)
if(r<t[rc].tl) return query(lc,l,r);
else return query(lc,l,r)*query(rc,l,r);
else return query(rc,l,r);
}
}t1;
int sz[N],son[N],top[N],tal[N],fa[N];
void dfs1(int x,int fa)
{
sz[x]=1;
for(int v:e[x]){
if(v==fa) continue;
dfs1(v,x);
sz[x]+=sz[v];//警惕 sz[x]+=v 随机链剖分
if(sz[son[x]]<=sz[v]) son[x]=v;
}
if(son[x]==0) tal[x]=x;
else tal[x]=tal[son[x]];
}
int dfn[N],timestap;
void dfs2(int x,int ff,int tp)
{
fa[x]=ff;int fa=ff;
dfn[x]=++timestap;
top[x]=tp;
int dx=dfn[x];
f[dx][0]=0;f[dx][1]=a[x];
if(son[x]){
dfs2(son[x],x,tp);
for(int v:e[x]){
if(v==fa||v==son[x]) continue;
dfs2(v,x,v);
int dv=dfn[v];
f[dx][0] += max(f[dv][0],f[dv][1]);
f[dx][1] += f[dv][0];
}
}
val[dx][0][1]=val[dx][0][0]=f[dx][0];
val[dx][1][0]=f[dx][1];val[dx][1][1]=-INF;
if(son[x]){
int dv=dfn[son[x]];
f[dx][0]+=max(f[dv][0],f[dv][1]);
f[dx][1]+=f[dv][0];
}
}
void update(int x,int y)
{
val[dfn[x]][1][0]+=y-a[x];
a[x]=y;
while(x){
Matrix A=t1.query(1,dfn[top[x]],dfn[tal[x]]);
t1.update(1,dfn[x]);
Matrix B=t1.query(1,dfn[top[x]],dfn[tal[x]]);
x=fa[top[x]];
int dx=dfn[x];
val[dx][0][1]=val[dx][0][0]+=max(B[0][0],B[1][0])-max(A[0][0],A[1][0]);
val[dx][1][0]+=B[0][0]-A[0][0];
}
}
int Test=1;
void mian()
{
read(n);read(m);
For(i,1,n) read(a[i]);
For(i,2,n){
int u,v;
read(u),read(v);
e[u].push_back(v);
e[v].push_back(u);
}
dfs1(1,0);
dfs2(1,0,0);
t1.build(1,1,n);
while(m--){
int x,y;
read(x),read(y);
update(x,y);
Matrix Res=t1.query(1,1,dfn[tal[1]]);
printf("%d\n",max(Res[0][0],Res[1][0]));
}
}
例题二 [CSP-S 2022] 数据传输
这个破题非常没有素质,因为动态 dp 一般和树剖绑定,但是树剖是 NOI 级算法,所以这玩意理论上不在考纲。但是,出题人声称这玩意可以倍增(和树剖相比会多个 \(\log\) ),然后给端上来了……
题面
你有一个 \(n\) 个点的树,编号 \(1 \sim n\),点有点权 \(v_i\),边长度为 \(1\)。称两个点是近的当且仅当两点间的简单路径的长度不超过 \(k\)。
我们定义长度为 \(p\) 的传输路径 $c_1,c_2,\dots,c_p $ 合法,当且仅当路径中相邻的点都是近的。这个序列的权值定义为 $\sum {i=1} ^p v $
现在总共有 \(q\) 次询问,第 \(i\) 次请求会从询问 \(s_i\) 到 \(t_i\) 的近的传输路径中,权值最小的是路径的权值是多少。
$ n,q \le 2 \times 10^5 , k \le 3 $
题解略,因为好久以前写的了,不想重写了……
例题三 [NOI Online #1 入门组] 魔法
题面
C 国由 \(n\) 座城市与 \(m\) 条有向道路组成,城市与道路都从 \(1\) 开始编号,经过 \(i\) 号道路需要 \(t_i\) 的费用。
现在你要从 \(1\) 号城市出发去 \(n\) 号城市,你可以施展最多 \(k\) 次魔法,使得通过下一条道路时,需要的费用变为原来的相反数,即费用从 \(t_i\) 变为 \(-t_i\)。请你算一算,你至少要花费多少费用才能完成这次旅程。
注意:使用魔法只是改变一次的花费,而不改变一条道路自身的 \(t_i\);最终的费用可以为负,并且一个城市可以经过多次(包括 \(n\) 号城市)。
$ n \le 100 ,m\le 2500 ,k\le 10^6 $
Solution
如果 \(k=0\),就是白板的 Floyd。
如果 \(k \ge 1\),那么直接给 Floyd 的数组后面上一个使用了魔法次数的下标即可。转移 belike $ f_{i,j,k}=f_{i,l,t}+f_{l,j,k-t} $
如果用分层图,每下一层图表示用了一次魔法,好像能拿接近正解的分。
如果我们熟知 Floyd 矩阵的组合意义,那我们可以转化题意。注意到,上面那个东西非常类似于(就是)Floyd 算法的转移,唯一的问题是多了个系数。但是,这个系数的转移非常类似于完全就是矩阵乘法的次方。
我们记录 \(T^i\) 表示使用了 \(i\) 次魔法的最短路,然后发现这个东西满足 $ T^{i+j} = T^i \times T^j $,然后就过去了。
感性证明一下:这矩阵在分层图中相当于从上层的 \(i\) 点到下一层 \(j\) 点的距离,而这个东西显然满足 Floyd 矩阵的组合意义,并且也满足上面的 dp 式子。

浙公网安备 33010602011771号