高级算法指北——浅谈整体dp
简述整体dp
整体 dp 是一种优化 dp 的方式。它通过类似数据结构维护序列的方式将 dp 状态中的一维压入数据结构,并通过少量单点或区间的操作达到对该维所有状态进行转移的目的,从而将一维
不难发现,整体 dp 是基于
一般地,使用整体 dp 的题目有以下几个步骤:
-
写出朴素的 dp
-
将朴素 dp 使用前缀和等方式优化至
转移(如果本来是 的一对一转移就不需要了) -
找出 dp 状态中转移具有共性的一维,使用数据结构维护这一维。具体地,随着其它维的变化,在数据结构上执行各种修改操作,动态维护此时此刻,当压进数据结构一维的下标为某个值时的 dp 值。
下面用若干例题说明整体 dp 的过程。
经典例题
谈到数据结构,首先想到的应该是线段树。下面先看几道用线段树维护整体 dp 的例子。
P01 整体 dp 板子题:Luogu P9400 「DBOI」Round 1 三班不一般
简要题意:构造一个长度为
解法:
考虑 dp,设
处可以填的数的数量,可以简单求出。则有:
对于
发现已经无法继续优化转移,且每个
下面是 AC 代码。可以结合代码细节理解。
int n,a,b;
struct seg{
int t[4*N],tag[4*N];
void pushdown(int x){
if(tag[x]==-1)return;
if(tag[ls(x)]==-1)tag[ls(x)]=1;
if(tag[rs(x)]==-1)tag[rs(x)]=1;
t[ls(x)]*=tag[x],t[rs(x)]*=tag[x];tag[ls(x)]*=tag[x],tag[rs(x)]*=tag[x];//利用乘法结合律与分配律
t[ls(x)]%=mo,t[rs(x)]%=mo,tag[ls(x)]%=mo,tag[rs(x)]%=mo,tag[x]=-1;
}
void pushup(int x){
t[x]=(t[ls(x)]+t[rs(x)])%mo;
}
void modify(int x,int le,int ri,int p,int v){
if(le==ri){
t[x]=v,tag[x]=-1;
return;
}
pushdown(x);
int mid=(le+ri)>>1;
if(p<=mid)modify(ls(x),le,mid,p,v);
else modify(rs(x),mid+1,ri,p,v);
pushup(x);
}
void mult(int x,int le,int ri,int ql,int qr,int v){
if(ql<=le&&qr>=ri){
if(tag[x]==-1)tag[x]=1;
t[x]=t[x]*v%mo,tag[x]=tag[x]*v%mo;
return;
}
pushdown(x);
int mid=(le+ri)>>1;
if(ql<=mid)mult(ls(x),le,mid,ql,qr,v);
if(qr>mid)mult(rs(x),mid+1,ri,ql,qr,v);
pushup(x);
}
int query(int x,int le,int ri,int ql,int qr){
if(ql<=le&&qr>=ri)return t[x];
pushdown(x);
int mid=(le+ri)>>1,res=0;
if(ql<=mid)res+=query(ls(x),le,mid,ql,qr);
if(qr>mid)res+=query(rs(x),mid+1,ri,ql,qr);
return res%mo;
}
}T;
signed main(){
memset(T.tag,-1,sizeof(T.tag));
read(n),read(a),read(b);
int le=n+1,ri=n+a;
T.modify(1,1,n+a,n+1,1);
rep(i,1,n){
int x,y;
read(x),read(y);
int sum=T.query(1,1,n+a,le,ri);
le--,ri--;//区间移动
int val=max(0ll,y-max(b,x-1));
if(le+1<=ri)T.mult(1,1,n+a,le+1,ri,val);//更新 dp[i][1]~dp[i][a-1]
val=max(0ll,min(b,y)-x+1)*sum%mo;
T.modify(1,1,n+a,le,val);//更新 dp[i][0]
}
printf("%lld\n",T.query(1,1,n+a,le,ri));
return 0;
}
P02 线段树的复杂操作:Luogu P8476 「GLR-R3」惊蛰
简要题意:给定长度为
解法:
考虑 dp。设
接下来考虑如何把第二维搬上数据结构。首先滚完后缀 min 之后的 dp 数组是自己向自己位置转移,没有位移操作,可以直接用线段树简单维护。不难发现
int n,c,a[N],lsh[N],cntl;
struct seg{
int t[4*N],tx[4*N],tag[4*N],cntt[4*N],tagv[4*N];
void pushup(int x){
t[x]=min(t[ls(x)],t[rs(x)]);
tx[x]=max(tx[ls(x)],tx[rs(x)]);
}
void pushdown(int x,int le,int ri){
if(tagv[x]!=-1){
t[ls(x)]=t[rs(x)]=tx[ls(x)]=tx[rs(x)]=tagv[ls(x)]=tagv[rs(x)]=tagv[x],tagv[x]=-1;
cntt[ls(x)]=cntt[rs(x)]=tag[ls(x)]=tag[rs(x)]=0;//pushdown的时候,赋值标记依然会影响其他标记
}
int mid=(le+ri)>>1;
if(cntt[x]){
cntt[ls(x)]+=cntt[x],cntt[rs(x)]+=cntt[x],t[ls(x)]+=cntt[x]*lsh[le],t[rs(x)]+=cntt[x]*lsh[mid+1];
tx[ls(x)]+=cntt[x]*lsh[mid],tx[rs(x)]+=cntt[x]*lsh[ri],cntt[x]=0;
}
if(tag[x])tag[ls(x)]+=tag[x],tag[rs(x)]+=tag[x],t[ls(x)]+=tag[x],t[rs(x)]+=tag[x],tx[ls(x)]+=tag[x],tx[rs(x)]+=tag[x],tag[x]=0;
}
void add(int x,int le,int ri,int ql,int qr,int v){//区间加
if(ql<=le&&qr>=ri){
t[x]+=v,tx[x]+=v,tag[x]+=v;
return;
}
pushdown(x,le,ri);
int mid=(le+ri)>>1;
if(ql<=mid)add(ls(x),le,mid,ql,qr,v);
if(qr>mid)add(rs(x),mid+1,ri,ql,qr,v);
pushup(x);
}
void addi(int x,int le,int ri,int ql,int qr){//区间加下标
if(ql<=le&&qr>=ri){
t[x]+=lsh[le],tx[x]+=lsh[ri],cntt[x]++;
return;
}
pushdown(x,le,ri);
int mid=(le+ri)>>1;
if(ql<=mid)addi(ls(x),le,mid,ql,qr);
if(qr>mid)addi(rs(x),mid+1,ri,ql,qr);
pushup(x);
}
void modify(int x,int le,int ri,int ql,int qr,int v){//区间赋值
if(ql<=le&&qr>=ri){
tagv[x]=t[x]=tx[x]=v,cntt[x]=tag[x]=0;
return;
}
pushdown(x,le,ri);
int mid=(le+ri)>>1;
if(ql<=mid)modify(ls(x),le,mid,ql,qr,v);
if(qr>mid)modify(rs(x),mid+1,ri,ql,qr,v);
pushup(x);
}
int query(int x,int le,int ri,int ql,int qr){//区间最小值
if(ql<=le&&qr>=ri)return t[x];
pushdown(x,le,ri);
int mid=(le+ri)>>1,res=inf;
if(ql<=mid)res=min(res,query(ls(x),le,mid,ql,qr));
if(qr>mid)res=min(res,query(rs(x),mid+1,ri,ql,qr));
return res;
}
int querypos(int x,int le,int ri,int v){//线段树上二分
if(tx[x]<v)return ri+1;
if(le==ri)return le;
pushdown(x,le,ri);
int mid=(le+ri)>>1;
if(tx[ls(x)]>=v)return querypos(ls(x),le,mid,v);
else return querypos(rs(x),mid+1,ri,v);
}
}T;
signed main(){
read(n),read(c);
rep(i,1,n)
read(a[i]),lsh[i]=a[i];
sort(lsh+1,lsh+n+1),cntl=unique(lsh+1,lsh+n+1)-lsh-1;
rep(i,1,n)
a[i]=lower_bound(lsh+1,lsh+cntl+1,a[i])-lsh;
memset(T.tagv,-1,sizeof(T.tagv));//注意赋值有可能赋成0,所以tagv初始值为-1.
rep(i,1,n){
if(a[i]!=1)T.add(1,1,cntl,1,a[i]-1,c);
T.add(1,1,cntl,a[i],cntl,-lsh[a[i]]),T.addi(1,1,cntl,a[i],cntl);
int rmin=T.query(1,1,cntl,a[i],cntl),targ=T.querypos(1,1,cntl,rmin);
if(targ<a[i])T.modify(1,1,cntl,targ,a[i]-1,rmin);
}
printf("%lld\n",T.t[1]);
return 0;
}
P03 用线段树合并支持树上整体dp:Luogu P6773 [NOI2020] 命运
简要题意:给定一棵
解法:
不难发现对于从同一个点出发的若干个限制,若
考虑用线段树维护第二维,合并时直接线段树合并。对于第一部分的
int n,m;
struct edge{
int to,nxt;
}e[2*N];
int fir[N],np,dep[N];
vector<int>op[N];
void add(int x,int y){
e[++np]=(edge){y,fir[x]};
fir[x]=np;
}
int sl,sr;
struct seg{//注意在线合的过程中,若一边有一边没有,会访问不到叶子节点,因而我们需要一个乘法tag.
int t[30*N],lson[30*N],rson[30*N],rt[N],cnt=0,tag[30*N];
void pushup(int x){
t[x]=(t[ls(x)]+t[rs(x)])%mo;
}
void pushdown(int x){
if(tag[x]==1)return;
tag[ls(x)]=tag[ls(x)]*tag[x]%mo,tag[rs(x)]=tag[rs(x)]*tag[x]%mo;
t[ls(x)]=t[ls(x)]*tag[x]%mo,t[rs(x)]=t[rs(x)]*tag[x]%mo;
tag[x]=1;
}
void modify(int &x,int le,int ri,int p,int v){
if(!x)x=++cnt,tag[x]=1;
if(le==ri){
t[x]=v,tag[x]=1;
return;
}
pushdown(x);
int mid=(le+ri)>>1;
if(p<=mid)modify(ls(x),le,mid,p,v);
else modify(rs(x),mid+1,ri,p,v);
pushup(x);
}
int query(int x,int le,int ri,int ql,int qr){
if(!x)return 0;
if(ql<=le&&qr>=ri)return t[x];
pushdown(x);
int mid=(le+ri)>>1,ret=0;
if(ql<=mid)ret+=query(ls(x),le,mid,ql,qr);
if(qr>mid)ret+=query(rs(x),mid+1,ri,ql,qr);
return ret%mo;
}
int merge(int p,int q,int le,int ri){
if(!p&&!q)return 0;
if(!p){
sl=(sl+t[q])%mo,t[q]=t[q]*sr%mo,tag[q]=tag[q]*sr%mo;
return q;
}
if(!q){
sr=(sr+t[p])%mo,t[p]=t[p]*sl%mo,tag[p]=tag[p]*sl%mo;
return p;
}
if(le==ri){
sl=(sl+t[q])%mo,t[q]=t[q]*sr%mo,sr=(sr+t[p])%mo,t[p]=t[p]*sl%mo;
t[p]=(t[p]+t[q])%mo;
return p;
}
pushdown(p),pushdown(q);
int mid=(le+ri)>>1;
ls(p)=merge(ls(p),ls(q),le,mid),rs(p)=merge(rs(p),rs(q),mid+1,ri);
pushup(p);
return p;
}
}T;
void dfs(int x,int f){
dep[x]=dep[f]+1;
int maxd=0;
rep(i,0,(int)op[x].size()-1)
maxd=max(maxd,dep[op[x][i]]);
T.modify(T.rt[x],0,n,maxd,1);
for(int i=fir[x];i;i=e[i].nxt){
int j=e[i].to;
if(j==f)continue;
dfs(j,x);
sl=T.query(T.rt[j],0,n,0,dep[x]),sr=0;
T.rt[x]=T.merge(T.rt[x],T.rt[j],0,n);
}
}
signed main(){
// freopen("destiny.in","r",stdin);
// freopen("destiny.out","w",stdout);
read(n);
rep(i,1,n-1){
int x,y;
read(x),read(y),add(x,y),add(y,x);
}
read(m);
rep(i,1,m){
int x,y;
read(x),read(y),op[y].push_back(x);
}
dfs(1,0);
printf("%lld\n",T.query(T.rt[1],0,n,0,0));
return 0;
}
其他数据结构,如平衡树、可并堆等,在整体 dp 过程中也有独特的优势,如平衡树支持插入和删除,可并堆空间占用小......
P04 用平衡树维护复杂的插入、删除、位移操作:CF809D Hitchhiking in the Baltic States
简要题意:构造一个长度为
解法:
模仿 LIS 的 dp 状态,设
-
当
时, ,找到最后一个满足 的位置 ,不难发现仅有 ,其余部分都是 。 -
当
时, 。由于 单增,因而更新一定不劣。故有 。 -
当
时,不能更新,有 。
观察上面的转移,不难发现我们实际上是维持了
下面给出用 FHQ 实现的代码。
int n;
mt19937 rd(time(NULL));
struct FHQ{
int hp[N],val[N],lson[N],rson[N],sz[N],tag[N],cnt=0,rt=0;
int addnode(int x){
val[++cnt]=x,sz[cnt]=1,hp[cnt]=rd();
return cnt;
}
void pushup(int x){
sz[x]=sz[ls(x)]+sz[rs(x)]+1;
}
void pushdown(int x){
if(ls(x))tag[ls(x)]+=tag[x],val[ls(x)]+=tag[x];
if(rs(x))tag[rs(x)]+=tag[x],val[rs(x)]+=tag[x];
tag[x]=0;
}
int merge(int x,int y){//x<y
if(!x)return y;
if(!y)return x;
if(hp[x]>hp[y]){
pushdown(x),rs(x)=merge(rs(x),y),pushup(x);
return x;
}
else{
pushdown(y),ls(y)=merge(x,ls(y)),pushup(y);
return y;
}
}
void split(int nw,int v,int &x,int &y){//按值分裂:小于等于v的在x
if(!nw){
x=y=0;
return;
}
pushdown(nw);
if(val[nw]<=v)x=nw,split(rs(nw),v,rs(x),y),pushup(x);
else y=nw,split(ls(nw),v,x,ls(y)),pushup(y);
}
void insert(int v){//插入一个值为v的节点
int x,y;
split(rt,v,x,y),rt=merge(merge(x,addnode(v)),y);
}
void modify(int l,int r){//区间修改
int x,y,z;
split(rt,l-1,x,y),split(y,r,y,z),tag[y]++,val[y]++,rt=merge(merge(x,y),z);
}
int getk(int nw,int k){//找第k小的值
if(sz[nw]<k)return inf;
pushdown(nw);
if(sz[ls(nw)]>=k)return getk(ls(nw),k);
else if(sz[ls(nw)]==k-1)return val[nw];
else return getk(rs(nw),k-sz[ls(nw)]-1);
}
}T;
int main(){
read(n);
while(n--){
//先删,再平移,最后插入,是一个腾位置的过程,免得平移过去的被删了,插入的被一起平移了.
int l,r;
read(l),read(r);
int x,y;
T.split(T.rt,r-1,x,y);
if(y){
int val=T.getk(y,1),z;
if(val!=inf){
T.split(y,val,y,z);//删除1个节点,就删除这个根,将左右儿子合并即可.
y=T.merge(T.lson[y],T.rson[y]),y=T.merge(y,z);
}
}
T.rt=T.merge(x,y);
T.modify(l,r-1);
T.insert(l);
}
printf("%d\n",T.sz[T.rt]);
return 0;
}
P05 用堆维护取最值转移的操作:CF671D Roads in Yusland
简要题意:给定一棵有
解法:
套路化地考虑提前钦定,设
但我们可以考虑空间花费更小的做法。不难发现取
int n,m,dep[N];
struct edge{
int to,nxt;
}e[2*N];
int fir[N],np;
struct work{
int to,val;
};
vector<work>w[N];
int main();
struct node{
int upper;
ll co;
friend bool operator<(node x,node y){
return x.co>y.co;
}
};
priority_queue<node>q[N];
void add(int x,int y){
e[++np]=(edge){y,fir[x]};
fir[x]=np;
}
ll ans=0,tag[N];
bool ok=1;
void dfs(int x,int f){
dep[x]=dep[f]+1;
for(int i=fir[x];i;i=e[i].nxt){
int j=e[i].to;
if(j==f)continue;
dfs(j,x);
if(!ok)return;
if(q[x].size()<q[j].size())swap(tag[x],tag[j]),swap(q[x],q[j]);
ll del=0,minx=0;
if(!q[j].empty())del=q[j].top().co;
if(!q[x].empty())minx=q[x].top().co;
while(!q[j].empty()){
node nw=q[j].top();
q[j].pop();
nw.co+=minx-del;
q[x].push(nw);
}
tag[x]+=del+tag[j];
}
ll minn=0;
if(!q[x].empty())minn=q[x].top().co;
rep(i,0,(int)w[x].size()-1)
q[x].push((node){w[x][i].to,(ll)w[x][i].val+minn});
if(q[x].empty()){
ok=0;
return;
}
if(x==1)ans=q[x].top().co+tag[x];
else{
while(!q[x].empty()&&dep[q[x].top().upper]>=dep[x])
q[x].pop();
if(q[x].empty())ok=0;
}
}
int main(){
read(n),read(m);
rep(i,1,n-1){
int x,y;
read(x),read(y),add(x,y),add(y,x);
}
rep(i,1,m){
int x,y,v;
read(x),read(y),read(v);
w[x].push_back((work){y,v});
}
if(n==1){
printf("0\n");
return 0;
}
dfs(1,0);
if(!ok)printf("-1");
else printf("%lld\n",ans);
return 0;
}
完结撒花~❀
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 25岁的心里话
· 按钮权限的设计及实现