动态 dp 学习笔记
目录
一、矩阵乘法
普通矩阵乘法
广义矩阵乘法
常用卡常方法
二、动态 概述
例1、
三、相关例题
例2、
例3、
例4、
例5、
例6、
一、矩阵乘法
普通矩阵乘法
相信大家对矩阵乘法都不陌生,普通的矩阵乘法定义如下:
对于 的矩阵 和 的矩阵 ,定义 ,其中:
矩阵 的大小为 ,单次矩阵乘法的时间复杂度为 。
可以简记为: 中第 行第 列的元素,等于 的第 行与 的第 列对应相乘再相加。
mat operator*(const mat &a,const mat &b)///传参O(1),传矩阵O(n^2)
{
static mat c;
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
{
c.v[i][j]=0;
for(int k=1;k<=n;k++) c.v[i][j]=(c.v[i][j]+1ll*a.v[i][k]*b.v[k][j])%mod;
}
return c;
}
广义矩阵乘法
不过在动态 中,更常见的是 广义矩阵乘法:
常用卡常方法
设矩阵阶数为 ,由于**矩阵乘法时间复杂度 **,而题目中 一般为常数,所以配上数据结构后很容易成为卡常重灾区。
矩阵乘法有两种常见卡常方法:
-
循环展开。
原理是手动展开从而避免使用
for
循环,仅适用于 很小的情形(一般)。struct mat { int a,b,c,d; }; mat operator*(const mat &x,const mat &y) { return {min(x.a+y.a,x.b+y.b),min(x.a+y.b,x.b+y.d),min(x.c+y.a,x.d+y.c),min(x.c+y.b,x.d+y.d)}; }
-
减少取模。
如果模数 ,那么
long long
可以承受 的数据量。mat operator*(const mat &a,const mat &b) { static mat c; for(int i=1;i<=n;i++) for(int j=1;j<=n;j++) { ll res=0; for(int k=1;k<=n;k++) res+=1ll*a.v[i][k]*b.v[k][j]; c.v[i][j]=res%mod; } return c; }
二、动态 概述
动态 用于解决带修树形 问题。
话不多说,先上模板题。
约定 表示对 的所有子节点 求和, 表示对 的所有轻儿子 求和。
例1、
题目描述
给定一棵 个节点的树,点有点权 。
次单点修改点权的操作,每次操作后询问最大权独立集。
数据范围
- 。
时间限制 ,空间限制 。
分析
先考虑链怎么做。
表示仅考虑前 个数,不选/选第 个节点的最大收益。
写成 广义矩阵乘法:
修改点权等价于修改单个矩阵,线段树维护单点修改区间乘积即可。
再考虑树的情况。
表示仅考虑 子树,不选选 的最大收益。
树链剖分转化为链上的问题,发现重儿子比较特殊,我们把轻儿子放在一起考虑。
表示仅考虑 自身及其轻子树,不选选 的最大收益。
看起来好像变复杂了,但我们可以反过来用 化简 。
终于转化成熟悉的 广义矩阵乘法!
然后考虑怎么带修。
有一个显然的性质:只有 到根的路径上的点的 值会改变。
求 是简单的,线段树查询重链( 区间)的矩阵乘积即可。
然而我们更关心哪些节点的 值会改变,因为 的变化会引起矩阵的变化。
结论是,只有 自身及所有 fa[top[u]]
的 值会改变!
从 top[u]
跳到 fa[top[u]]
的过程中,我们要先减掉原本对 fa[top[u]]
的贡献,更新信息后再加入新的对 fa[top[u]]
的贡献。
时间复杂度 ,本题 。
至此本题的大致思路就讲完了,还有一些代码实现上的细节问题。
-
初始的 需要预处理。
考虑到 需要遍历所有儿子, 需要遍历所有重儿子,因此,在树剖第一遍 中预处理 ,第二遍 中预处理 ,写起来会比较简洁。
-
线段树的
pushup
要用右边乘左边。为什么呢?矩阵乘法不满足交换律,又因为一条重链是在从底向上更新 值,所以体现在 序上就是从右往左乘。
-
树剖时预处理每条重链的链底,查询时需要
query
整条链。修改时
query
的区间为[dfn[top[u]],ed[u]]
(ed[u]
和ed[top[u]]
本质相同),注意不要把右端点写成u
!
#include<bits/stdc++.h>
using namespace std;
const int maxn=1e5+5,inf=1e9;
int m,n,u,v,cnt;
int a[maxn];
int d[maxn],fa[maxn],sz[maxn],son[maxn];
int ed[maxn],id[maxn],dfn[maxn],top[maxn];
int f[maxn][2],g[maxn][2];
vector<int> h[maxn];
struct mat
{
int v[2][2];
};
mat operator*(const mat &a,const mat &b)
{
static mat c;
for(int i=0;i<=1;i++)
for(int j=0;j<=1;j++)
{
c.v[i][j]=-inf;
for(int k=0;k<=1;k++) c.v[i][j]=max(c.v[i][j],a.v[i][k]+b.v[k][j]);
}
return c;
}
namespace sgmt
{
#define ls p<<1
#define rs p<<1|1
struct node
{
int l,r;
mat x;
}f[4*maxn];
void pushup(int p)
{
f[p].x=f[rs].x*f[ls].x;
}
void build(int p,int l,int r)
{
f[p].l=l,f[p].r=r;
if(l==r)
{
int x=id[l];
return f[p].x={g[x][0],g[x][1],g[x][0],-inf},void();
}
int mid=(l+r)/2;
build(ls,l,mid);
build(rs,mid+1,r);
pushup(p);
}
void modify(int p,int pos,mat x)
{
if(f[p].l==f[p].r) return f[p].x=x,void();
int mid=(f[p].l+f[p].r)/2;
if(pos<=mid) modify(ls,pos,x);
else modify(rs,pos,x);
pushup(p);
}
mat query(int p,int l,int r)
{
if(l<=f[p].l&&f[p].r<=r) return f[p].x;
int mid=(f[p].l+f[p].r)/2;
if(r<=mid) return query(ls,l,r);
if(l>=mid+1) return query(rs,l,r);
return query(rs,l,r)*query(ls,l,r);///注意这里是从右往左乘
}
}
void dfs1(int u,int father)
{
sz[u]=1,f[u][1]=a[u];
for(auto v:h[u])
{
if(v==father) continue;
d[v]=d[u]+1,fa[v]=u;
dfs1(v,u),sz[u]+=sz[v];
if(sz[v]>=sz[son[u]]) son[u]=v;
f[u][0]+=max(f[v][0],f[v][1]),f[u][1]+=f[v][0];
}
}
void dfs2(int u,int topf)
{
dfn[u]=++cnt,id[cnt]=u,top[u]=topf,ed[u]=dfn[u],g[u][1]=a[u];
if(son[u]) dfs2(son[u],topf),ed[u]=ed[son[u]];
for(auto v:h[u])
{
if(v==fa[u]||v==son[u]) continue;
dfs2(v,v);
g[u][0]+=max(f[v][0],f[v][1]),g[u][1]+=f[v][0];
}
}
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++) scanf("%d",&a[i]);
for(int i=1;i<=n-1;i++)
{
scanf("%d%d",&u,&v);
h[u].push_back(v),h[v].push_back(u);
}
d[1]=1,dfs1(1,0),dfs2(1,1);
sgmt::build(1,1,n);
while(m--)
{
scanf("%d%d",&u,&v);
g[u][1]+=v-a[u],a[u]=v;///更新u自身的信息
while(u)
{
mat lst=sgmt::query(1,dfn[top[u]],ed[u]);
sgmt::modify(1,dfn[u],{g[u][0],g[u][1],g[u][0],-inf});
mat now=sgmt::query(1,dfn[top[u]],ed[u]);
u=fa[top[u]];
g[u][0]-=max(lst.v[0][0],lst.v[0][1]),g[u][1]-=lst.v[0][0];///减掉旧的贡献
g[u][0]+=max(now.v[0][0],now.v[0][1]),g[u][1]+=now.v[0][0];///加入新的贡献
}
mat res=sgmt::query(1,dfn[1],ed[1]);
printf("%d\n",max(res.v[0][0],res.v[0][1]));
}
return 0;
}
总结
动态 的操作流程:
-
在树剖过程中预处理 。
这里 表示考虑 子树的贡献, 表示考虑 及其所有轻子树的贡献。
注意 的转移依赖于 ,而不是 自身封闭转移。
一般来说, 仅在预处理时会用到,修改后可以通过线段树查询得到真实的 ;而 会在转移矩阵有所体现,修改需要实时维护。
-
线段树维护转移矩阵。
注意
pushup
是从右往左乘。void pushup(int p) { f[p].x=f[rs].x*f[ls].x; }
-
修改时先减掉原来的贡献,再加入更新之后的贡献。
下面是修改操作的伪代码:
///单点修改g[u],注意不需要上线段树 while(u) { mat lst=/**初始矩阵**/ * sgmt::query(1,dfn[top[u]],ed[u]); sgmt::modify(1,dfn[u],/**u的转移矩阵**/); mat now=/**初始矩阵**/ * sgmt::query(1,dfn[top[u]],ed[u]); u=fa[top[u]]; ///在g[u]中减掉lst的贡献 ///在g[u]中加入now的贡献 }
-
查询时 的答案为初始矩阵乘上自底向上整条链的转移矩阵。
如果初始矩阵刚好为矩阵乘法的单位元,那么可以偷懒不乘初始矩阵。比如非负整数域上 广义矩阵乘法的单位元为全零矩阵,模板题就是典型例子。
但是多数情况下初始矩阵不是单位元,也不可省略。
ans[u]=/**初始矩阵**/ * sgmt::query(1,dfn[u],ed[u]);
-
如果没有修改操作,或者所有查询操作在修改操作之后,然后询问多个点(多条路径)的答案,可以用倍增代替树剖,时间复杂度少一只 。
三、相关例题
例2、
题目描述
给定一个长为 的数字串 。
次询问,对于 的某个子串 ,至少要删去几个字符,才能使其包含序列 "2017"
,但不包含序列 "2016"
,无解输出 -1
。
数据范围
- 。
时间限制 ,空间限制 。
分析
严格来说,本题不算动态 ,只能算线段树维护矩阵乘法。
注意认真审题,要求包含的是子序列而不是子串。
考虑在子序列自动机上 ,状态设计如下:
- 表示走到状态 ,至少需要删几个字符。
- 表示走到状态
"2"
,至少需要删几个字符。 - 表示走到状态
"20"
,至少需要删几个字符。 - 表示走到状态
"201"
,至少需要删几个字符。 - 表示走到状态
"2017"
,至少需要删几个字符。
记 ,可以写出转移方程:
注: 要求 时才能转移, 将 视为一个布尔函数。
至此已经做到 ,考虑继续优化。
把转移方程看成 广义矩阵乘法,那么每个 都可以预处理出转移矩阵。
每次询问用初始矩阵 乘上区间 的所有矩阵,线段树维护即可。
时间复杂度 ,本题 。
可以用向量乘矩阵的 优化到,但本题 同阶,所以意义不大。
#include<bits/stdc++.h>
#define ls p<<1
#define rs p<<1|1
using namespace std;
const int maxn=2e5+5,inf=1e9;
int l,n,q,r;
char s[maxn];
struct mat
{
int v[5][5];
mat()
{
for(int i=0;i<=4;i++)
for(int j=0;j<=4;j++)
v[i][j]=inf;
}
}c[10];
mat operator*(const mat &a,const mat &b)
{
static mat c;
for(int i=0;i<=4;i++)
for(int j=0;j<=4;j++)
{
c.v[i][j]=inf;
for(int k=0;k<=4;k++) c.v[i][j]=min(c.v[i][j],a.v[i][k]+b.v[k][j]);
}
return c;
}
struct node
{
int l,r;
mat x;
}f[4*maxn];
void build(int p,int l,int r)
{
f[p].l=l,f[p].r=r;
if(l==r) return f[p].x=c[s[l]-'0'],void();
int mid=(l+r)/2;
build(ls,l,mid);
build(rs,mid+1,r);
f[p].x=f[ls].x*f[rs].x;
}
mat query(int p,int l,int r)
{
if(l<=f[p].l&&f[p].r<=r) return f[p].x;
int mid=(f[p].l+f[p].r)/2;
if(r<=mid) return query(ls,l,r);
if(l>=mid+1) return query(rs,l,r);
return query(ls,l,r)*query(rs,l,r);
}
int main()
{
scanf("%d%d%s",&n,&q,s+1);
for(int i=0;i<=9;i++)
for(int j=0;j<=4;j++)
c[i].v[j][j]=0;
c[2].v[0][0]=1,c[2].v[0][1]=0;
c[0].v[1][1]=1,c[0].v[1][2]=0;
c[1].v[2][2]=1,c[1].v[2][3]=0;
c[7].v[3][3]=1,c[7].v[3][4]=0;
c[6].v[3][3]=1,c[6].v[4][4]=1;
build(1,1,n);
while(q--)
{
scanf("%d%d",&l,&r);
mat res;
res.v[0][0]=0,res=res*query(1,l,r);
printf("%d\n",res.v[0][4]!=inf?res.v[0][4]:-1);
}
return 0;
}
例3、
题目描述
给定一棵 个节点的树,你可以花费 的代价删掉第 个点。
接下来 次操作:
Q x
:询问如果要使 与其子树中所有叶节点不连通,花费代价的最小值。C x y
:将 加上 。
数据范围
- 。
- 保证任意时刻 。
时间限制 ,空间限制 。
分析
记 为让 与其子树所有叶子不连通的最小代价,容易写出转移方程:
记 ,则 。
然后写成 广义矩阵乘法:
动态 维护即可,时间复杂度 ,本题 。
注意叶子节点 不存在,因此 query
时需要用初始矩阵 乘上整条链的贡献。
#include<bits/stdc++.h>
#define ll long long
#define ls p<<1
#define rs p<<1|1
using namespace std;
const int maxn=2e5+5;
const ll inf=1e18;
int m,n,u,v,cnt;
int d[maxn],fa[maxn],sz[maxn],son[maxn];
int ed[maxn],id[maxn],dfn[maxn],top[maxn];
ll f[maxn],g[maxn],w[maxn];
char ch[2];
vector<int> h[maxn];
struct mat
{
ll v[2][2];
};
mat operator*(const mat &a,const mat &b)
{
static mat c;
for(int i=0;i<=1;i++)
for(int j=0;j<=1;j++)
{
c.v[i][j]=inf;
for(int k=0;k<=1;k++) c.v[i][j]=min(c.v[i][j],a.v[i][k]+b.v[k][j]);
}
return c;
}
namespace sgmt
{
struct node
{
int l,r;
mat x;
}f[4*maxn];
void pushup(int p)
{
f[p].x=f[rs].x*f[ls].x;
}
void build(int p,int l,int r)
{
f[p].l=l,f[p].r=r;
if(l==r)
{
int u=id[l];
return f[p].x={g[u],inf,w[u],0},void();
}
int mid=(l+r)/2;
build(ls,l,mid);
build(rs,mid+1,r);
pushup(p);
}
void modify(int p,int pos,mat x)
{
if(f[p].l==f[p].r) return f[p].x=x,void();
int mid=(f[p].l+f[p].r)/2;
if(pos<=mid) modify(ls,pos,x);
else modify(rs,pos,x);
pushup(p);
}
mat query(int p,int l,int r)
{
if(l<=f[p].l&&f[p].r<=r) return f[p].x;
int mid=(f[p].l+f[p].r)/2;
if(r<=mid) return query(ls,l,r);
if(l>=mid+1) return query(rs,l,r);
return query(rs,l,r)*query(ls,l,r);
}
}
using sgmt::modify;
using sgmt::query;
void dfs1(int u,int father)
{
sz[u]=1;
for(auto v:h[u])
{
if(v==father) continue;
d[v]=d[u]+1,fa[v]=u;
dfs1(v,u),sz[u]+=sz[v];
if(sz[v]>=sz[son[u]]) son[u]=v;
f[u]+=f[v];
}
f[u]=sz[u]==1?w[u]:min(f[u],w[u]);
}
void dfs2(int u,int topf)
{
dfn[u]=++cnt,id[cnt]=u,top[u]=topf,ed[u]=cnt;
if(son[u]) dfs2(son[u],topf),ed[u]=ed[son[u]];
for(auto v:h[u])
{
if(v==fa[u]||v==son[u]) continue;
dfs2(v,v);
g[u]+=f[v];
}
}
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;i++) scanf("%lld",&w[i]);
for(int i=1;i<=n-1;i++)
{
scanf("%d%d",&u,&v);
h[u].push_back(v),h[v].push_back(u);
}
dfs1(1,0),dfs2(1,1);
sgmt::build(1,1,n);
scanf("%d",&m);
while(m--)
{
scanf("%s",ch);
if(ch[0]=='Q')
{
scanf("%d",&u);
mat res=(mat){inf,0,0,0}*query(1,dfn[u],ed[u]);
printf("%lld\n",res.v[0][0]);
}
else
{
scanf("%d%d",&u,&v),w[u]+=v;
while(u)
{
mat lst=(mat){inf,0,0,0}*query(1,dfn[top[u]],ed[u]);
modify(1,dfn[u],{g[u],inf,w[u],0});
mat now=(mat){inf,0,0,0}*query(1,dfn[top[u]],ed[u]);
u=fa[top[u]];
g[u]+=now.v[0][0]-lst.v[0][0];
}
}
}
return 0;
}
例4、
题目描述
给定一棵 个节点的树,点有点权 。
次询问,每次分别钦定 必须选/不选,求最小权点覆盖,无解输出 -1
。
数据范围
- 。
时间限制 ,空间限制 。
分析
强制选点/不选点可以通过给权值加上/减去 实现。
熟知结论:最小权点覆盖等于总权值减去最大权独立集。
带修的最大权独立集就是模板题干的事情。
时间复杂度 ,本题 。
#include<bits/stdc++.h>
#define ll long long
#define ls p<<1
#define rs p<<1|1
using namespace std;
const int maxn=1e5+5;
const ll inf=1e18;
int a,b,m,n,u,v,cnt;
ll sum;
int d[maxn],fa[maxn],sz[maxn],son[maxn];
int ed[maxn],id[maxn],dfn[maxn],top[maxn];
ll f[maxn][2],g[maxn][2],w[maxn];
vector<int> h[maxn];
struct mat
{
ll v[2][2];
};
mat operator*(const mat &a,const mat &b)
{
static mat c;
for(int i=0;i<=1;i++)
for(int j=0;j<=1;j++)
{
c.v[i][j]=-inf;
for(int k=0;k<=1;k++) c.v[i][j]=max(c.v[i][j],a.v[i][k]+b.v[k][j]);
}
return c;
}
namespace sgmt
{
struct node
{
int l,r;
mat x;
}f[4*maxn];
void pushup(int p)
{
f[p].x=f[rs].x*f[ls].x;
}
void build(int p,int l,int r)
{
f[p].l=l,f[p].r=r;
if(l==r)
{
int u=id[l];
return f[p].x={g[u][0],g[u][1],g[u][0],-inf},void();
}
int mid=(l+r)/2;
build(ls,l,mid);
build(rs,mid+1,r);
pushup(p);
}
void modify(int p,int pos,mat x)
{
if(f[p].l==f[p].r) return f[p].x=x,void();
int mid=(f[p].l+f[p].r)/2;
if(pos<=mid) modify(ls,pos,x);
else modify(rs,pos,x);
pushup(p);
}
mat query(int p,int l,int r)
{
if(l<=f[p].l&&f[p].r<=r) return f[p].x;
int mid=(f[p].l+f[p].r)/2;
if(r<=mid) return query(ls,l,r);
if(l>=mid+1) return query(rs,l,r);
return query(rs,l,r)*query(ls,l,r);
}
}
void dfs1(int u,int father)
{
sz[u]=1,f[u][1]=w[u];
for(auto v:h[u])
{
if(v==father) continue;
d[v]=d[u]+1,fa[v]=u;
dfs1(v,u),sz[u]+=sz[v];
if(sz[v]>=sz[son[u]]) son[u]=v;
f[u][0]+=max(f[v][0],f[v][1]),f[u][1]+=f[v][0];
}
}
void dfs2(int u,int topf)
{
dfn[u]=++cnt,id[cnt]=u,ed[u]=cnt,top[u]=topf,g[u][1]=w[u];
if(son[u]) dfs2(son[u],topf),ed[u]=ed[son[u]];
for(auto v:h[u])
{
if(v==fa[u]||v==son[u]) continue;
dfs2(v,v);
g[u][0]+=max(f[v][0],f[v][1]),g[u][1]+=f[v][0];
}
}
void modify(int u,ll tag)
{
g[u][1]+=tag,sum+=tag;
while(u)
{
mat lst=sgmt::query(1,dfn[top[u]],ed[u]);
sgmt::modify(1,dfn[u],{g[u][0],g[u][1],g[u][0],-inf});
mat now=sgmt::query(1,dfn[top[u]],ed[u]);
u=fa[top[u]];
g[u][0]-=max(lst.v[0][0],lst.v[0][1]),g[u][1]-=lst.v[0][0];
g[u][0]+=max(now.v[0][0],now.v[0][1]),g[u][1]+=now.v[0][0];
}
}
int main()
{
scanf("%d%d%*s",&n,&m);
for(int i=1;i<=n;i++) scanf("%lld",&w[i]),sum+=w[i];
for(int i=1;i<=n-1;i++)
{
scanf("%d%d",&u,&v);
h[u].push_back(v),h[v].push_back(u);
}
dfs1(1,0),dfs2(1,1);
sgmt::build(1,1,n);
while(m--)
{
scanf("%d%d%d%d",&u,&a,&v,&b);
modify(u,a?-inf:inf),modify(v,b?-inf:inf);
mat cur=sgmt::query(1,1,ed[1]);
ll res=sum-max(cur.v[0][0],cur.v[0][1])+(a+b)*inf;
printf("%lld\n",res<inf?res:-1);
modify(u,a?inf:-inf),modify(v,b?inf:-inf);
}
return 0;
}
例5、
题目描述
给定一棵 个节点的树,点有点权 ,满足 ,初始全为 。
接下来 次操作:先单点修改点权,再询问为使所有权值为 的点不与权值为 的点连通,至少要删几条边。
数据范围
- 。
时间限制 ,空间限制 。
分析
目标等价于 , 不能既与 连通又不与 连通。
表示使点 不和子树内的 连通,至少要删几条边。
注:对于本题,中括号不满足时该项视为。
然后分离轻重子树的贡献。定义:
再用 化简 :
写成 转移矩阵:
然后动态 就可以了。
时间复杂度 ,本题 。
#include<bits/stdc++.h>
#include"catdog.h"
#define ls p<<1
#define rs p<<1|1
using namespace std;
const int maxn=1e5+5,inf=1e9;
int n,cnt;
int d[maxn],fa[maxn],sz[maxn],son[maxn];
int w[maxn],ed[maxn],id[maxn],dfn[maxn],top[maxn];
int f[maxn][2],g[maxn][2];
vector<int> e[maxn];
struct mat
{
int v[2][2];
};
mat operator*(const mat &a,const mat &b)
{
static mat c;
for(int i=0;i<=1;i++)
for(int j=0;j<=1;j++)
{
c.v[i][j]=inf;
for(int k=0;k<=1;k++) c.v[i][j]=min(c.v[i][j],a.v[i][k]+b.v[k][j]);
}
return c;
}
namespace sgmt
{
struct node
{
int l,r;
mat x;
void init(int u)
{
x={w[u]!=0?g[u][0]:inf,w[u]!=1?g[u][1]+1:inf,w[u]!=0?g[u][0]+1:inf,w[u]!=1?g[u][1]:inf};
}
}f[maxn<<2];
void pushup(int p)
{
f[p].x=f[rs].x*f[ls].x;
}
void build(int p,int l,int r)
{
f[p].l=l,f[p].r=r;
if(l==r) return f[p].init(id[l]);
int mid=(l+r)/2;
build(ls,l,mid);
build(rs,mid+1,r);
pushup(p);
}
void modify(int p,int pos)
{
if(f[p].l==f[p].r) return f[p].init(id[pos]);
int mid=(f[p].l+f[p].r)/2;
if(pos<=mid) modify(ls,pos);
else modify(rs,pos);
pushup(p);
}
mat query(int p,int l,int r)
{
if(l<=f[p].l&&f[p].r<=r) return f[p].x;
int mid=(f[p].l+f[p].r)/2;
if(r<=mid) return query(ls,l,r);
if(l>=mid+1) return query(rs,l,r);
return query(rs,l,r)*query(ls,l,r);
}
}
void dfs1(int u,int father)
{
sz[u]=1;
for(auto v:e[u])
{
if(v==father) continue;
d[v]=d[u]+1,fa[v]=u;
dfs1(v,u),sz[u]+=sz[v];
if(sz[v]>=sz[son[u]]) son[u]=v;
f[u][0]+=min(f[v][0],f[v][1]+1),f[u][1]+=min(f[v][1],f[v][0]+1);
}
if(w[u]!=2) f[u][w[u]]=inf;
}
void dfs2(int u,int topf)
{
dfn[u]=++cnt,id[cnt]=u,ed[u]=cnt,top[u]=topf;
if(son[u]) dfs2(son[u],topf),ed[u]=ed[son[u]];
for(auto v:e[u])
{
if(v==fa[u]||v==son[u]) continue;
dfs2(v,v);
g[u][0]+=min(f[v][0],f[v][1]+1),g[u][1]+=min(f[v][1],f[v][0]+1);
}
}
void initialize(int _n,vector<int> a,vector<int> b)
{
n=_n;
for(int i=0;i<n-1;i++)
{
int u=a[i],v=b[i];
e[u].push_back(v),e[v].push_back(u);
}
for(int i=1;i<=n;i++) w[i]=2;
d[1]=1,dfs1(1,0),dfs2(1,1);
sgmt::build(1,1,n);
}
int modify(int u,int x)
{
w[u]=x;
while(u)
{
mat lst=(mat){0,0,0,0}*sgmt::query(1,dfn[top[u]],ed[u]);
sgmt::modify(1,dfn[u]);
mat now=(mat){0,0,0,0}*sgmt::query(1,dfn[top[u]],ed[u]);
u=fa[top[u]];
g[u][0]-=min(lst.v[0][0],lst.v[0][1]+1),g[u][1]-=min(lst.v[0][1],lst.v[0][0]+1);
g[u][0]+=min(now.v[0][0],now.v[0][1]+1),g[u][1]+=min(now.v[0][1],now.v[0][0]+1);
}
mat res=(mat){0,0,0,0}*sgmt::query(1,1,ed[1]);
return min(res.v[0][0],res.v[0][1]);
}
int cat(int u)
{
return modify(u,0);
}
int dog(int u)
{
return modify(u,1);
}
int neighbor(int u)
{
return modify(u,2);
}
例6、
题目描述
给定一棵 个节点的树,点有点权 。
接下来 次操作:
Change x y
:将 改为 。Query k
:询问有多少个非空连通块,权值异或和为 。
数据范围
- ,保证
Change
操作个数 。 - ,保证 为 的方幂。
- 。
时间限制 ,空间限制 。
分析
表示以 为根的所有连通块中,权值异或和为 的连通块个数。
每次合并一棵 的子树 ,转移方程为。
这里 表示转移前的 值, 表示转移后的 值。
为节约篇幅,记 ,注意 。
两边同时取 :
记 的初始值 ,最终的转移方程为:
最后输出 即可,于是我们得到了一个 的算法。
由于询问要对所有的 求和,因此再记一个。
转移方程 。
两边同时取 :
接下来是动态 的基本操作,分离轻重子树的贡献。
定义:
转移方程为:
然后写成矩阵乘法:
矩阵乘法自带 倍常数,显然无法承受的。
注意到 做矩阵乘法时关于这个形式封闭:
我们只需维护 四个值,常数从 降为 。
询问时初始矩阵为 ,但代码实现没这么麻烦,因为 分别对应转移矩阵中的 位置。
具体可以看这篇blog中“基于变换合并的算法”一栏。
但本题并没有结束,动态 中修改需要删除原来的贡献。
其中 可以直接减,但 需要做除法,而零没有乘法逆元。
对 ,我们额外用一个数组 z[u]
维护 g[u]
中乘零的次数,这样乘零和除零可以直接在 z[u]
上修改。
时间复杂度 ,前面的 为矩阵乘法的常数。
#include<bits/stdc++.h>
#define poly array<int,128>
using namespace std;
const int maxn=3e4+5,mod=1e4+7,inv2=(mod+1)>>1;
int m,n,q,u,v,cnt;
int ed[maxn],fa[maxn],sz[maxn],dfn[maxn],son[maxn],top[maxn];
int w[maxn],inv[mod];
char ch[10];
poly f[maxn],g[maxn],h[maxn],l[maxn],z[maxn],p[128];
vector<int> e[maxn];
int qpow(int a,int k)
{
int res=1;
for(;k;a=a*a%mod,k>>=1) if(k&1) res=res*a%mod;
return res;
}
int add(int x,int y)
{
if((x+=y)>=mod) x-=mod;
return x;
}
int dec(int x,int y)
{
if((x-=y)<0) x+=mod;
return x;
}
void fwt(poly &f,int n,int op)
{
for(int k=2,m=1;k<=n;k<<=1,m<<=1)
for(int i=0;i<n;i+=k)
for(int j=i;j<i+m;j++)
{
int x=f[j],y=f[j+m];
f[j]=add(x,y),f[j+m]=dec(x,y);
if(op==-1) f[j]=f[j]*inv2%mod,f[j+m]=f[j+m]*inv2%mod;
}
}
poly operator+(poly a,poly b)
{
for(int i=0;i<m;i++) a[i]=add(a[i],b[i]);
return a;
}
poly operator-(poly a,poly b)
{
for(int i=0;i<m;i++) a[i]=dec(a[i],b[i]);
return a;
}
poly operator*(poly a,poly b)
{
for(int i=0;i<m;i++) a[i]=a[i]*b[i]%mod;
return a;
}
void operator+=(poly &a,poly b)
{
a=a+b;
}
void operator-=(poly &a,poly b)
{
a=a-b;
}
void operator*=(poly &a,poly b)
{
a=a*b;
}
void dfs1(int u,int _f)
{
sz[u]=1,f[u]=p[w[u]];
for(auto v:e[u])
{
if(v==_f) continue;
dfs1(v,u),fa[v]=u,sz[u]+=sz[v];
if(sz[v]>=sz[son[u]]) son[u]=v;
f[u]*=f[v]+p[0],h[u]+=h[v];
}
h[u]+=f[u];
}
void dfs2(int u,int topf)
{
dfn[u]=++cnt,top[u]=topf,ed[u]=cnt;
for(int i=0;i<m;i++) g[u][i]=1;
if(son[u]) dfs2(son[u],topf),ed[u]=ed[son[u]];
for(auto v:e[u])
{
if(v==fa[u]||v==son[u]) continue;
dfs2(v,v);
l[u]+=h[v];
poly tmp=f[v]+p[0];
for(int i=0;i<m;i++) tmp[i]?g[u][i]=g[u][i]*tmp[i]%mod:z[u][i]++;
}
}
struct mat
{
poly a,b,c,d;
mat operator*(const mat &x)
{
return {a*x.a,a*x.b+b,c*x.a+x.c,c*x.b+d+x.d};
}
};
mat get_mat(int u)
{
static poly tmp;
for(int i=0;i<m;i++) tmp[i]=z[u][i]?0:p[w[u]][i]*g[u][i]%mod;
return {tmp,tmp,tmp,l[u]+tmp};
}
namespace sgmt
{///博主为了偷懒写了zkw线段树,但是直接导致用时翻倍,大家不要学我qwq
int p;
mat val[3*maxn];
void build()
{
p=1<<(__lg(n+1)+1);
for(int i=1;i<=n;i++) val[p+dfn[i]]=get_mat(i);
for(int i=(p+n)>>1;i;i--) val[i]=val[i<<1|1]*val[i<<1];
}
void modify(int u)
{
val[p+dfn[u]]=get_mat(u);
for(int i=(p+dfn[u])>>1;i;i>>=1) val[i]=val[i<<1|1]*val[i<<1];
}
mat query(int l,int r)
{
static int st[20];
int top=0;
mat res={::p[0],f[0],f[0],f[0]};///f[0]指代常多项式0,p[0]指代常多项式1经过FWT后的结果
for(l=p+l-1,r=p+r+1;l^r^1;l>>=1,r>>=1)
{
if(~l&1) st[++top]=l^1;
if(r&1) res=res*val[r^1];
}
for(int i=top;i>=1;i--) res=res*val[st[i]];
return res;
}
}
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++) scanf("%d",&w[i]);
for(int i=1;i<=n-1;i++)
{
scanf("%d%d",&u,&v);
e[u].push_back(v),e[v].push_back(u);
}
for(int i=1;i<mod;i++) inv[i]=qpow(i,mod-2);
for(int i=0;i<m;i++) p[i][i]=1,fwt(p[i],m,1);
dfs1(1,0),dfs2(1,1);
sgmt::build();
for(scanf("%d",&q);q--;)
{
scanf("%s",ch);
if(ch[0]=='C')
{
scanf("%d%d",&u,&v),w[u]=v;
while(u)
{
mat lst=sgmt::query(dfn[top[u]],ed[u]);
sgmt::modify(u);
mat now=sgmt::query(dfn[top[u]],ed[u]);
u=fa[top[u]];
l[u]=l[u]-lst.d+now.d;
for(int i=0;i<m;i++)
{
int x=add(lst.c[i],p[0][i]),y=add(now.c[i],p[0][i]);
x?g[u][i]=g[u][i]*inv[x]%mod:z[u][i]--;
y?g[u][i]=g[u][i]*y%mod:z[u][i]++;
}
}
}
else
{
scanf("%d",&u);
poly tmp=sgmt::query(dfn[1],ed[1]).d;
fwt(tmp,m,-1),printf("%d\n",tmp[u]);
}
}
return 0;
}
本文来自博客园,作者:peiwenjun,转载请注明原文链接:https://www.cnblogs.com/peiwenjun/p/17070870.html
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构
· AI与.NET技术实操系列(六):基于图像分类模型对图像进行分类