线段树/树状数组优化 DP
这没什么好讲的,反正就是注意力惊人吧。偶尔有一些 trick
树状数组优化
P3287 [SCOI2014] 方伯伯的玉米田 \(\text{(2026/07/06)}\)
注意到令每次区间操作的左端点 \(r=n\) 是不劣的,设 \(dp_{i,j}\) 表示进行 \(j\) 次操作,每次操作的左端点 \(l\le i\) 的最长不降序列的长度。容易得到转移式 \(dp_{i,j}=max\{dp_{x,y}+1\},x<i,y\le j,a_x+y\le a_i+j\),这个是很显然的,不过多赘述如何得到。
再次注意到当你按顺序转移时,\(x<i\) 的条件自然而然会满足,于是变成了一个二维前缀最大值,用树状数组维护一下即可。复杂度 \(O(nlogn \cdot klogk)\)。
Code
#include<bits/stdc++.h>
using namespace std;
const int N=1e4+5,K=505;
int f[N][K];
int tr[N][K];
int a[N];
int n,k;
inline int lowbit(int x){return x&-x;}
void update(int x,int y,int v){
while(x<=N-5){
int yy=y;
while(y<=k+1)tr[x][y]=max(tr[x][y],v),y+=lowbit(y);
y=yy,x+=lowbit(x);
}
return ;
}
int query(int x,int y,int s=0){
while(x){
int yy=y;
while(y)s=max(s,tr[x][y]),y-=lowbit(y);
y=yy,x-=lowbit(x);
}
return s;
}
int main(){
ios::sync_with_stdio(0);cin.tie(0);
cin>>n>>k;
for(int i=1;i<=n;i++)cin>>a[i];
f[1][0]=1;
for(int i=1;i<=n;i++)
for(int j=k;j>=0;j--)
f[i][j]=query(a[i]+j,j+1)+1,update(a[i]+j,j+1,f[i][j]);
cout<<query(N-5,k+1);
return 0;
}
线段树优化
P2605 [ZJOI2010] 基站选址 \(\text{(2026/07/06)}\)
考虑暴力的写法:\(dp_{i,j}\) 表示在保证第 \(i\) 个村庄建立了当前的第 \(j\) 个基站时(现有 \(j\) 个基站,并且 \(i\) 处有一个)时,总花费的最小值是多少。考虑枚举上一个基站 \(k\),则有 \(k\ge j-1\)(至少有建 \(j-1\) 个基站的空间)。得到最朴素的转移:\(dp_{i,j}=min\{dp_{k,j-1}+C_i+cost[k,i]\},j-1\le k < i\),其中 \(cost[k,i]\) 表示 \([k,i]\) 中未被基站覆盖的村庄的赔偿的总和。
暴力的复杂度为 \(O(n^2k)\),这个 \(k\) 一看就不好优化,所以我们把 \(j\) 提到外层,则转移可以写成 \(dp_{i}=min\{dp_{k}+cost[k,i]\}+C_i\),不难发现复杂度的瓶颈为对 \(cost[k,i]\) 的计算。回顾我们对于 \(cost\) 的定义:没有被覆盖的村庄的补偿的费用之和。当我们新建一个基站时,新增的被覆盖的村庄显然是一个区间,所以我们用线段树维护 \(cost\),在 dp 的过程中进行修改、查询。复杂度 \(O(nklogn)\)。
Code
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int inf=1e9+5,N=2e4+5;
int d[N],c[N],s[N],w[N],dp[N],L[N],R[N];
int n;
struct Segment{
int lzy[N<<2],w[N<<2];
#define ls p<<1
#define rs p<<1|1
void pushup(int p){
w[p]=min(w[ls],w[rs]);
return ;
}
void build(int p=1,int l=1,int r=n){
lzy[p]=0;
if(l==r)
return w[p]=dp[l],void();
int mid=(l+r)/2;
build(ls,l,mid),build(rs,mid+1,r);
pushup(p);
return ;
}
void maketag(int p,int x){
lzy[p]+=x,w[p]+=x;
return ;
}
void pushdown(int p){
maketag(ls,lzy[p]);
maketag(rs,lzy[p]);
lzy[p]=0;
return ;
}
void update(int l,int r,int x,int p=1,int L=1,int R=n){
if(r<L||R<l)return ;
if(l<=L&&R<=r)
return maketag(p,x);
pushdown(p);
int mid=(L+R)/2;
update(l,r,x,ls,L,mid);
update(l,r,x,rs,mid+1,R);
pushup(p);
return ;
}
int query(int l,int r,int p=1,int L=1,int R=n){
if(r<L||R<l)return inf;
if(l<=L&&R<=r)return w[p];
pushdown(p);
int mid=(L+R)/2;
return min(query(l,r,ls,L,mid),query(l,r,rs,mid+1,R));
}
}tr,seg;
vector<int>vc[N];
signed main(){
ios::sync_with_stdio(0);cin.tie(0);
int k,ans=0,sum=0;
cin>>n>>k;
for(int i=2;i<=n;i++)cin>>d[i];
for(int i=1;i<=n;i++)cin>>c[i];
for(int i=1;i<=n;i++)cin>>s[i];
for(int i=1;i<=n;i++)cin>>w[i];
n++,k++;
d[n]=w[n]=inf;s[n]=c[n]=0;
for(int i=1;i<=n;i++){
L[i]=lower_bound(d+1,d+1+n,d[i]-s[i])-d;
R[i]=lower_bound(d+1,d+1+n,d[i]+s[i])-d;
if(d[R[i]]>d[i]+s[i])
R[i]--;
vc[R[i]].push_back(i);
}
tr.build();
for(int i=1;i<=n;i++){
dp[i]=sum+c[i];
tr.update(1,L[i]-1,w[i]);
tr.update(R[i]+1,n,w[i]);
for(auto v:vc[i])
sum+=w[v];
}
ans=dp[n];
for(int j=2;j<=k;j++){
tr.build();
for(int i=1;i<=n;i++){
if(i!=1)
dp[i]=tr.query(1,i-1)+c[i];
else dp[1]=inf;
for(auto v:vc[i])
if(L[v]>1)
tr.update(1,L[v]-1,w[v]);
}
ans=min(ans,dp[n]);
}
cout<<ans;
return 0;
}
单调队列优化
对于一类 DP 状态转移形如:\(dp_i=min\{dp_j+a_i+b_j\},L(i)\le j\le R(i)\)。\(min\) 也可以替换为 \(max\),后面表示 \(j\) 的取值范围是由 \(i\) 决定的一段区间,一般将 \(a_i\) 提出来,则写为 \(dp_i=min\{dp_j+b_j\}+a_i\),则 \(min\) 里面相当于关于 \(j\) 的一个序列 \(B\),其中 \(B_j=dp_j+b_j\)(及 \(min\) 中的那一串东西),于是我们只需要快速求出序列 \(B\) 中的区间最值。注意到 \(i\) 是逐渐变大的,那么 \(j\) 的取值范围就会是一个滑动窗口,用单调队列维护最值就能优化掉一个 \(n\)。
P2627 [USACO11OPEN] Mowing the Lawn G \(\text{(2026/07/07)}\)
\(dp_i\) 表示前 \(i\) 只奶牛的最大贡献。枚举以 \(i\) 结尾的这一段奶牛的长度,得到转移:\(dp_i=max_{i-k\le j\le i}\{dp_{j-1}+sum_i-sum_j\}\),\(sum_i-sum_j\) 为这段奶牛的贡献。把 \(sum_i\) 提出来,相当于对 \(dp_{j-1}-sum_j\) 做单调队列,复杂度 \(O(n)\)。
Code
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=2e5+5;
int n,k;
int e[N],f[N],s[N];
int v[N];
int q[N],l,r=1;
signed main(){
ios::sync_with_stdio(0);cin.tie(0);
cin>>n>>k;
for(int i=1;i<=n;i++)cin>>e[i],s[i]=s[i-1]+e[i];
for(int i=1;i<=n;i++){
v[i]=f[i-1]-s[i];
while(l<=r&&v[q[r]]<v[i])r--;
q[++r]=i;
while(l<=r&&q[l]<i-k)l++;
f[i]=v[q[l]]+s[i];
}
cout<<f[n];
return 0;
}

浙公网安备 33010602011771号