线段树/树状数组优化 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;
}
posted @ 2026-07-07 14:28  GCSG01  阅读(11)  评论(0)    收藏  举报