首页 > 其他分享 >矩阵优化 DP 以及全局平衡二叉树实现的动态 DP 学习笔记

矩阵优化 DP 以及全局平衡二叉树实现的动态 DP 学习笔记

时间:2024-07-15 21:19:38浏览次数:10  
标签:begin end int 矩阵 times 二叉树 bmatrix DP

矩阵乘法

求斐波那契数列的第 \(n\) 项,其中 \(n\le 10^{18}\),对数 \(m\) 取模。

写出转移方程,\(f_i=f_{i-1}+f_{i-2}\),直接算是 \(O(n)\) 的,似乎没什么办法优化。

定义大小分别为 \(n\times p\) 和 \(p\times m\) 的矩阵(分别为 \(a\) 和 \(b\))相乘的结果(矩阵 \(c\))是一个大小为 \(n\times m\) 的矩阵,其中 \(c_{i,j}=\sum_{k=1}^p a_{i,k}\times b_{k,j}\)。

把转移方程写成矩阵形式,得到:

\[\begin{bmatrix} f_i&f_{i-1} \end{bmatrix} = \begin{bmatrix} f_{i-1}&f_{i-2} \end{bmatrix} \times \begin{bmatrix} 1&1\\1&0 \end{bmatrix} \]

\(f_1\) 和 \(f_2\) 是已知的,后面乘的矩阵是相同的,所以可以用矩阵快速幂。

常见优化矩阵乘法的方法:

预处理出转移矩阵的 \(2\) 的若干次幂次方。

如果转移是 \(1\times n\) 初始矩阵乘以 \(n\times n\) 的转移矩阵的若干次方,可以用初始矩阵依次乘以转移矩阵的 \(2\) 的若干次幂次方,这样一次乘法就由 \(O(n^3)\) 变成 \(O(n^2)\)。

如果转移矩阵很多位置是空,可以把不是零的位置拎出来做乘法。

单位矩阵:只有从左上到右下的对角线的位置是 \(1\) 其它都是 \(0\) 的矩阵。

可以动态修改的矩阵优化 DP

Ex - 01? Queries (atcoder.jp)

[ABC246Ex] 01? Queries - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

给定长度为 \(N\) 的仅包含 01? 的字符串 \(S\),给定 \(Q\) 组询问 \((x_1, c_1), (x_2, c_2), \cdots, (x_q, c_q)\),每次将原字符串中 \(x_i\) 位置的字符改为 \(c_i\),然后输出 \(S\) 有多少种非空子序列,? 需任意替换为 01。对 \(998244353\) 取模。

\(1 \le N, Q \le 10^5, 1 \le x_i \le N\)。

设 \(f_{i,0/1}\) 表示处理到第 \(i\) 位,最后一位是 \(0\) 还是 \(1\) 的方案,得到方程:

\[f_{i,0}=f_{i-1,0}+[S_i\neq 1](f_{i-1,1}+1)\\ f_{i,1}=f_{i-1,1}+[S_i\neq 0](f_{i-1,0}+1) \]

注意到有常数项 \(1\),不是很好维护,于是再转移矩阵里加多一个常数项 \(1\)。

\[\begin{bmatrix} f_{i,0}&f_{i,1}&1 \end{bmatrix} = \begin{bmatrix} f_{i-1,0}&f_{i-1,1}&1 \end{bmatrix} \times \begin{bmatrix} 1&[S_i\neq0]&0\\ [S_i\neq1]&1&0\\ [S_i\neq1]&[S_i\neq0]&1 \end{bmatrix} \]

要求动态修改一个位置的值,相当于修改一个位置的转移矩阵,可以用线段树维护。

时间复杂度 \(O(27(N+Q)\log N)\),前面的 \(27\) 是矩阵乘法的常数。

动态 DP

P4719 【模板】"动态 DP"&动态树分治 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

P4751 【模板】"动态DP"&动态树分治(加强版) - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

给你一个 \(n\) 个点的有点权(点 \(i\) 的点权是 \(a_i\))的树,\(q\) 次操作,每次修正一个点的点权,每次修正后输出最大权独立集的权值大小。

设 \(f_{i,0/1}\) 表示考虑 \(i\) 的子树,选或不选点 \(i\) 形成的最大权独立集的权值大小,列出 DP 式子:

\[f_{i,0}=\sum _{j\in son_i} \max(f_{j,0},f_{j,1})\\ f_{i,1}=\sum _{j\in son_i} f_{j,0}+a_i \]

考虑树链剖分,设 \(mson_i\) 为点 \(i\) 的重儿子,\(lson_i\) 为点 \(i\) 的轻儿子,又设:

\[g_{i,0}=\sum _{j\in lson_i} \max(f_{j,0},f_{j,1})\\ g_{i,1}=\sum _{j\in lson_i} f_{j,0}+a_i \]

那么可以列出转移方程:

\[f_{i,0}=g_{i,0}+\max(f_{mson_i,0},f_{mson_i,1})\\ f_{i,1}=g_{i,1}+f_{mson_i,1} \]

重新定义矩阵乘法:定义大小分别为 \(n\times p\) 和 \(p\times m\) 的矩阵(分别为 \(a\) 和 \(b\))相乘的结果(矩阵 \(c\))是一个大小为 \(n\times m\) 的矩阵,其中 \(c_{i,j}=\max_{k=1}^p a_{i,k}+b_{k,j}\)。就是把 \(\sum\) 换成 \(\max\),把 \(\times\) 换成 \(+\),称这种矩阵乘法为 \(\max +\) 矩阵乘法。

那么化成矩阵就是:

\[\begin{bmatrix} f_{i,0}\\ f_{i,1} \end{bmatrix} = \begin{bmatrix} g_{i,0}&g_{i,0}\\ -\inf&g_{i,1} \end{bmatrix} \times \begin{bmatrix} f_{mson_i,0}\\ f_{mson_i,1} \end{bmatrix} \]

叶子(LeaF)节点没有儿子,所以后面乘的是 \(\begin{bmatrix}0\\0\end{bmatrix}\)。

树链剖分,用线段树维护一条重链上转移矩阵的值,跳轻边就把 \(f\) 贡献到父亲的 \(g\) 里面,时间复杂度是 \(O(n\log^2 n)\)。

但你会被卡树剖,而且树剖难写

全局平衡二叉树

LCT 由于有 splay 的良好性质,所以高度是 \(\log n\) 级别的,它可以很好地在 \(O(\log n)\) 的时间内维护一条重链上转移矩阵的乘积。

但 LCT 常数又大有难写,注意到树的形态是固定的,用 LCT 会不会大材小用?

有一个叫全局平衡二叉树的一个东西,是一个静态的 LCT,给重链上的点附上一个权值(这个权值只被用来构建全局平衡二叉树),权值是当前点的子树大小减去重儿子的子树大小。一条重链的一段缩成一颗 splay 的时候,找到它的带权中点,以它为根。这样构建出的单棵 splay 可能不一定特别平衡,但从整体来看,整棵树是平衡的,深度不会超过 \(\log n\) 级别。

修改的时候,跳重边可以直接更改父亲的转移矩阵乘积,跳轻边可以更改父亲的转移矩阵。

附上全局平衡二叉树实现上面例题的代码:

点击开 D
const int N=1000099; const int inf=1047483646;
struct ju { int a[2][2]={}; friend ju operator * (const ju a,const ju b) { ju ans; int i,j; for(i=0;i<2;++i) for(j=0;j<2;++j) ans.a[i][j]=max(a.a[i][0]+b.a[0][j],a.a[i][1]+b.a[1][j]); return ans; } } val[N]={},sum[N]={};
int n,q,a[N]={},w[N]={},sz[N]={},lsz[N]={};
int mson[N]={},type[N]={},stype=0; vector<int> lian[N]={};
int fa[N]={},ch[N][2]={},f[N][2]={},g[N][2]={};
int em=0,e[N*2]={},ls[N]={},nx[N*2]={};
void insert(int x,int y) { e[++em]=y,nx[em]=ls[x],ls[x]=em; e[++em]=x,nx[em]=ls[y],ls[y]=em; return ; }
#define lson (ch[x][0])
#define rson (ch[x][1])
#define get(x) (ch[fa[x]][1]==(x))
#define isroot(x) (ch[fa[x]][0]!=(x)&&ch[fa[x]][1]!=(x))
int makebst(int tp,int l,int r) {
	int i,tot=0,s=0,x; for(i=l;x=lian[tp][i],i<=r;++i) tot+=lsz[x];
	for(i=l;x=lian[tp][i],s+=lsz[x];++i)
		if(s*2>=tot) {
			if(l<i) fa[lson=makebst(tp,l,i-1)]=x;
			if(i<r) fa[rson=makebst(tp,i+1,r)]=x;
			sum[x]=sum[lson]*val[x]*sum[rson];
			return x;
		}
}
int make(int rt) {
	int i,tp=type[rt],y,oldf=fa[rt];
	for(auto x:lian[tp]) {
		for(i=ls[x];y=e[i],i;i=nx[i])
			if(y!=fa[x]&&y!=mson[x])
				y=make(y),g[x][0]+=max(f[y][0],f[y][1]),g[x][1]+=f[y][0];
		g[x][1]+=a[x],val[x]=(ju){g[x][0],g[x][0],g[x][1],-inf};
	}
	fa[rt=makebst(tp,0,lian[tp].size()-1)]=oldf;
	f[rt][0]=max(sum[rt].a[0][0],sum[rt].a[0][1]);
	f[rt][1]=max(sum[rt].a[1][0],sum[rt].a[1][1]);
	return rt;
}
void change(int x) {
	sum[x]=sum[lson]*val[x]*sum[rson];
	if(fa[x]&&isroot(x))
        g[fa[x]][0]-=max(f[x][0],f[x][1]),g[fa[x]][1]-=f[x][0];
	if(isroot(x))
        f[x][0]=max(sum[x].a[0][0],sum[x].a[0][1]),f[x][1]=max(sum[x].a[1][0],sum[x].a[1][1]);
	if(fa[x]&&isroot(x))
        g[fa[x]][0]+=max(f[x][0],f[x][1]),g[fa[x]][1]+=f[x][0],
		val[fa[x]]=(ju){g[fa[x]][0],g[fa[x]][0],g[fa[x]][1],-inf};
	if(fa[x]) change(fa[x]);
	return ;
}
int main()
{
//	usefile("ddp");
	int i,x,y,l,r,root,lstans=0; ju nxt;
	read(n,q); for(i=1;i<=n;++i) read(a[i]); for(i=1;i<n;++i) read(x,y),insert(x,y);
	w[l=r=1]=1; while(l<=r) { x=w[l]; for(i=ls[x];y=e[i],i;i=nx[i]) if(y!=fa[x]) fa[y]=x,w[++r]=y; ++l; }
	for(i=n;x=w[i],y=fa[x],i;--i) { ++sz[x],++lsz[x],sz[y]+=sz[x],lsz[y]+=sz[x]; if(!mson[y]||sz[mson[y]]<sz[x]) lsz[y]+=sz[mson[y]]-sz[x],mson[y]=x; }
	for(i=1;x=w[i],y=mson[x],i<=n;++i) { if(!type[x]) type[x]=++stype,lian[stype].push_back(x); if(y) type[y]=type[x],lian[type[x]].push_back(y); }
	val[0]=sum[0]={0,-inf,-inf,0},root=make(1);
	loop : --q; read(x,y),x^=lstans,val[x].a[1][0]+=y-a[x],g[x][1]+=y-a[x],a[x]=y,change(x),printf("%d\n",lstans=max(f[root][0],f[root][1])); if(q) goto loop; return 0;
}

其它技巧

P6021 洪水 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

容易写出转移方程:\(f_i=\min(\sum_{j\in son_i} f_j + a_i)\)。

设 \(s_i=\sum_{j\in lson_i} f_j\),可以列成矩阵形式,这里的 \(\times\) 是 \(\min+\) 矩阵乘法:

\[\begin{bmatrix} f_i&0 \end{bmatrix} = \begin{bmatrix} f_{mson_i}&0 \end{bmatrix} \times \begin{bmatrix} s_i&\inf\\ a_i&0 \end{bmatrix} \]

用全局平衡二叉树维护。

这里的转移和上一题不一样,上一题的初始矩阵是列向量,是竖着的,用根节点的转移矩阵乘以重儿子的矩阵,从上乘到下;而这里的初始矩阵是行向量,是横着的,用重儿子的矩阵乘以根节点的转移矩阵,从下乘到上。

但,题目要求询问子树,这相当于询问一条重链的后缀乘积,可以在全局平衡二叉树的 splay 中实现。

时间复杂度 \(O(n\log n)\)。

你还可以继续卡常,注意到:

\[\begin{bmatrix} a&\inf\\ b&0 \end{bmatrix} \times \begin{bmatrix} c&\inf\\ d&0 \end{bmatrix} = \begin{bmatrix} a+c&\inf\\ \min(b+c,d)&0 \end{bmatrix} \]

所以只需要记录矩阵的左上角和左下角两个量即可。

点击开 D
const int N=200099;
int n,q,fa[N]={},sz[N]={},mson[N]={},tag[N]={};
int type[N]={},stype=0,num[N]={},leng[N]={},root[N]={};
vector<int> lian[N]={};
int em=0,e[N*2]={},ls[N]={},nx[N*2]={};
ll a[N]={};
void insert(int x,int y) { e[++em]=y,nx[em]=ls[x],ls[x]=em ;e[++em]=x,nx[em]=ls[y],ls[y]=em; return ; }
struct ju {
	ll a,b;
	ju(ll a_=0,ll b_=0) { a=a_,b=b_; return ; }
	friend ju operator * (const ju a,const ju b) {
		return ju(a.a+b.a,min(a.b+b.a,b.b));
	}
} v[N]={},sum[N]={}; int lson[N]={},rson[N]={};
void setup(int x) {
	int i,y; sz[x]=1;
	for(i=ls[x];y=e[i],i;i=nx[i])
		if(y!=fa[x]) {
			fa[y]=x,setup(y);
			sz[x]+=sz[y];
			if(sz[mson[x]]<sz[y])
				mson[x]=y;
		}
	tag[x]=sz[x]-sz[mson[x]];
	return ;
}
void getlian(int x) {
	if(!type[x])
		type[x]=++stype,
		num[x]=0,leng[stype]=1,
		lian[type[x]].push_back(x);
	if(!mson[x]) return ;
	type[mson[x]]=type[x];
	num[mson[x]]=leng[type[x]]++;
	lian[type[x]].push_back(mson[x]);
	getlian(mson[x]);
	int i,y;
	for(i=ls[x];y=e[i],i;i=nx[i])
		if(y!=fa[x]&&y!=mson[x])
			getlian(y);
	return ;
}
void update(int x) { sum[x]=sum[rson[x]]*v[x]*sum[lson[x]]; return ; }
int makebst(int tp,int l,int r) {
	int i,s=0,tot=0;
	for(i=l;i<=r;++i) tot+=tag[lian[tp][i]];
	for(i=l;i<=r;++i) {
		s+=tag[lian[tp][i]];
		if(s*2>=tot) {
			int x=lian[tp][i]; v[x].b=a[x];
			if(l<i) fa[lson[x]=makebst(tp,l,i-1)]=x;
			if(i<r) fa[rson[x]=makebst(tp,i+1,r)]=x;
			update(x);
			return x;
		}
	}
}
int make(int x) {
	int i,y;
	for(auto p:lian[type[x]])
		for(i=ls[p];y=e[i],i;i=nx[i])
			if(y!=fa[p]&&y!=mson[p])
				y=make(y),fa[y]=p,v[p].a+=sum[y].b;
	return root[type[x]]=makebst(type[x],0,leng[type[x]]-1);
}
int getopt() { static char chart; do chart=getchar(); while(chart!='Q'&&chart!='C'); return chart=='Q'; }
ju query(int x,int p) {
	if(!x) return ju(0,1e15);
	else if(num[x]==p)
		return sum[rson[x]]*v[x];
	else if(num[x]<p)
		return query(rson[x],p);
	else return sum[rson[x]]*v[x]*query(lson[x],p);
}
void change(int x) {
	if(lson[fa[x]]==x||rson[fa[x]]==x)
		update(x),change(fa[x]);
	else if(!fa[x])
		update(x);
	else
		v[fa[x]].a-=sum[x].b,
		update(x),
		v[fa[x]].a+=sum[x].b,
		change(fa[x]);
	return ;
}
int main()
{
//	usefile("flood");
	int i,x,y,opt;
	read(n);
	for(i=1;i<=n;++i)
		read(a[i]);
	for(i=1;i<n;++i)
		read(x,y),insert(x,y);
	setup(1),getlian(1);
	v[0]=sum[0]=ju(0,1e15),fa[make(1)]=0;
	read(q);
	loop : --q;
	opt=getopt();
	if(opt) read(x),printf("%lld\n",query(root[type[x]],num[x]).b);
	else read(x,y),a[x]+=y,v[x].b+=y,change(x);
	if(q) goto loop;
	return 0;
}

容易写错的地方

这里讲述的是个人经验,你可能没有容易写错的地方。

  • 注意构建全局平衡二叉树的时候对父亲的更改,有三个地方要注意:全局平衡二叉树内左右儿子的父亲重新设为根节点;splay 的父亲要设为对应的重链最顶上的节点的父亲;整颗全局平衡二叉树的根节点的父亲要设为 0。
  • 更改的时候要分三类:跳重边,直接更改 splay 子树内转移矩阵的乘积,并更新父亲;跳轻边,先把对父亲转移矩阵的贡献删去,再更改 splay 子树内转移矩阵的乘积以求出 DP 值,然后再把对父亲转移矩阵的贡献加回来,并更新父亲;根节点,直接更改乘积,不需要也不可以更新父亲。
  • \(0\) 号节点的转移矩阵和转移矩阵乘积要设为单位矩阵,普通矩阵乘法是 \(\begin{bmatrix}1&0\\0&1\end{bmatrix}\),\(\min +\) 矩阵乘法是 \(\begin{bmatrix}0&\inf\\\inf&0\end{bmatrix}\),若是 \(\max +\) 矩阵乘法则把 \(\inf\) 改为 \(-\inf\)。

练习

Problem - 573D - Codeforces

[P3781 [SDOI2017] 切树游戏 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)](

标签:begin,end,int,矩阵,times,二叉树,bmatrix,DP
From: https://www.cnblogs.com/fydj/p/18303986/DDP

相关文章

  • 数据结构-二叉树
    引入图论中的树和现实生活中的树长得一样,只不过我们习惯于处理问题的时候把树根放到上方来考虑。这种数据结构看起来像是一个倒挂的树,因此得名。定义一个没有固定根结点的树称为无根树(unrootedtree)。无根树有几种等价的形式化定义:有n个结点,n-1条边的连通无向图无向无环......
  • 状压DP
    状压DP状压DP是动态规划的一种,通过将状态压缩为整数来达到优化转移的目的。例题https://www.luogu.com.cn/problem/P1896思路一行中所有放置的状态可以用二进制数表示:如01000111(1代表当前位置放置物品)因此,我们可以先找出所有的合法状态,(利用dfs进行递归)并用sit[i]记录第......
  • lamp基本架构+wordpress
    一、安装并设置MariaDB1、时钟同步先安装chrony,重启并设置enable,最后同步yum-yinstallchrony2、安装mariadb和httpdyum-yinstallhttpdmariadbmariadb-server将httpd和mariadb重新启动并enable3、设置mariadb数据库进入数据库进行初始化mysql_secure_......
  • TCP和UDP
    【1】TCP服务器#include<stdio.h>#include<sys/types.h>/*SeeNOTES*/#include<sys/socket.h>#include<netinet/in.h>#include<netinet/ip.h>#include<unistd.h>#include<arpa/inet.h>intmain(intargc,charcon......
  • 「代码随想录算法训练营」第十一天 | 二叉树 part1
    二叉树的基本知识链接:https://programmercarl.com/二叉树理论基础.html要点:深度优先遍历前序遍历(递归法,迭代法)中序遍历(递归法,迭代法)后序遍历(递归法,迭代法)广度优先遍历层次遍历(迭代法)由于栈就是递归的一种实现结构,因此前中后序遍历的逻辑可以借助栈使用递归的方式......
  • 代码随想录算法训练营第22天 |二叉树part07:235. 二叉搜索树的最近公共祖先、701.二叉
    代码随想录算法训练营第22天|二叉树part07:235.二叉搜索树的最近公共祖先、701.二叉搜索树中的插入操作、450.删除二叉搜索树中的节点235.二叉搜索树的最近公共祖先https://leetcode.cn/problems/lowest-common-ancestor-of-a-binary-search-tree/description/代码随想录:htt......
  • SAP ABAP 写更改记录到表CDHDR/CDPOS 下篇
    表更改记录上篇写入表更改记录下篇发布日期:2024/07/11继上一篇的内容,用户测试的过程中发现,还是查不到写入记录。最后发现,我使用的系统环境,更改表equp时,对象类是QUOTEN2。基于此,当一个通过表TCDOB能查出多个对象类时,我们最好通过标准功能更改任意一条目标表的数据。再去查......
  • 最喜欢dp动态规划的一次(暑期刷题)
    以积极的态度面对生活,才能感受到人生的美好!dp动态规划-第一天前言1、环绕字符串中唯一的子字符串2、最长递增子序列3、摆动序列4、最长递增子序列的个数5、最长数对链6、最长定差子序列7、最长的斐波那契子序列的长度8、总结前言所有的问题可能不止一种方法,但是由......
  • 合法方案数(dp)
    https://www.luogu.com.cn/problem/CF1606E第3题   合法方案数 查看测评数据信息有n个人,他们要进行下面的进程:每轮设存活i个人,那么每个存活的人会对其他人造成1点血量的代价,血量小于等于零就会被淘汰,现在需要你给他们每个人设置一个在[1,x]之间的初始血量,使得某轮游戏结......
  • WordPress:快速搭建站点,wp安装及模版介绍
    最近搭建个人站点比较多,都是想把业务做到国外,通过google来引流,那我们今年就来介绍一个比较受欢迎的站点平台wordPress。WordPress是使用PHP语言开发的博客平台,用户可以在支持PHP和MySQL数据库的服务器上架设属于自己的网站。也可以把WordPress当作一个内容管理系统(CMS)来使用......