区间DP总结

区间类动态规划是线性动态规划的扩展,它在分阶段地划分问题时,与阶段中元素出现的顺序和由前一阶段的哪些元素合并而来有很大的关系。 ----OI Wiki

两端扩展型区间 DP 讲解

核心思想:处理区间问题时,状态转移时从区间的两端进行扩展。通常使用二维 DP 数组 dp[i][j] 表示区间 [i, j] 的最优解或方案数,通过枚举区间长度,从小区间向大区间递推求解。转移时考虑从区间左端或右端进行扩展,结合问题特性设计状态转移方程。

特点

  1. 状态定义:以区间 [i, j] 为状态
  2. 转移方向:从小区间向大区间扩展
  3. 转移方式:考虑从左边扩展 (i → i+1) 或右边扩展 (j → j-1)
  4. 初始化:处理长度为 1 的区间

T1: P2858 [USACO06FEB] Treats for the Cows G/S

题目简述\(n\) 个零食排成一行,每天从两端取一个卖出,第 \(k\) 天卖出零食 \(i\) 的价值为 \(v_i \times k\),求最大总价值。

思路

  1. 状态定义dp[i][j] 表示卖出区间 [i, j] 的零食能获得的最大价值。
  2. 初始化:单个零食在第 \(n\) 天卖出,dp[i][i] = v[i] * n
  3. 状态转移:考虑从左边或右边取零食,取的价值为当前零食乘以卖出天数(区间长度越小,卖出越晚,天数越大):
    • 取左边:dp[i][j] = dp[i+1][j] + v[i] * (n - len + 1)
    • 取右边:dp[i][j] = dp[i][j-1] + v[j] * (n - len + 1)
  4. 答案dp[1][n]

代码

#include<bits/stdc++.h>
#define int long long
#define endl "\n"
using namespace std;
const int maxn=2e3+5,mod=1e9+7,inf=1e18;
int read(){int x=0,f=1;char ch=getchar();while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}while(ch>='0' && ch<='9')x=x*10+ch-'0',ch=getchar();return x*f;}
void write(int x){if(x<0){putchar('-'),x=-x;}if(x>9){write(x/10);}putchar(x%10+'0');return;}
int fpow(int a,int b,int p){if(b==0){return 1;}int res=fpow(a,b/2,p)%p;if(b%2==1){return((res*res)%p*a)%p;}else{return(res*res)%p;}}
int dp[maxn][maxn];
int n,v[maxn];
signed main(){
	cin>>n;
	for(int i=1;i<=n;i++){
		cin>>v[i];
	}
	for(int i=1;i<=n;i++){
		dp[i][i]=v[i]*n;
	}	
	for(int len=2;len<=n;len++){
		for(int l=1;l+len-1<=n;l++){
			int r=l+len-1;
			dp[l][r]=max(dp[l+1][r]+v[l]*(n-len+1),dp[l][r-1]+v[r]*(n-len+1));
		}
	}
	cout<<dp[1][n];
	return 0;
}

T2: P3205 [HNOI2010] 合唱队

题目简述:给定理想队形,求有多少初始队形能通过规则(比前一个人高插右边,否则插左边)得到理想队形。

思路

  1. 状态定义:增加状态维度记录最后插入位置:
    • dp[i][j][0]:区间 [i, j] 最后插入位置在 \(i\)(左端)的方案数
    • dp[i][j][1]:区间 [i, j] 最后插入位置在 \(j\)(右端)的方案数
  2. 初始化:单个元素方案数为 1,dp[i][i][0] = 1(注意:实际中左右等价,但转移需要区分)。
  3. 状态转移
    • 扩展左端点 \(i\):需满足 \(a_i\) 小于子区间左端点或右端点
      • \(a_i < a_{i+1}\):可从子区间 [i+1, j] 左端转移,dp[i][j][0] += dp[i+1][j][0]
      • \(a_i < a_j\):可从子区间 [i+1, j] 右端转移,dp[i][j][0] += dp[i+1][j][1]
    • 扩展右端点 \(j\):需满足 \(a_j\) 大于子区间左端点或右端点
      • \(a_j > a_{j-1}\):可从子区间 [i, j-1] 右端转移,dp[i][j][1] += dp[i][j-1][1]
      • \(a_j > a_i\):可从子区间 [i, j-1] 左端转移,dp[i][j][1] += dp[i][j-1][0]
  4. 答案(dp[1][n][0] + dp[1][n][1]) % mod

代码

#include<bits/stdc++.h>
#define int long long 
#define endl "\n"
using namespace std;
const int maxn=2e3+5,mod=19650827,inf=1e18;
int read(){int x=0,f=1;char ch=getchar();while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}while(ch>='0' && ch<='9')x=x*10+ch-'0',ch=getchar();return x*f;}
void write(int x){if(x<0){putchar('-'),x=-x;}if(x>9){write(x/10);}putchar(x%10+'0');return;}
int fpow(int a,int b,int p){if(b==0){return 1;}int res=fpow(a,b/2,p)%p;if(b%2==1){return((res*res)%p*a)%p;}else{return(res*res)%p;}}
int dp[maxn][maxn][2];
int n,a[maxn];
signed main(){
	cin>>n;
	for(int i=1;i<=n;i++){
		cin>>a[i];
	}
	for(int i=1;i<=n;i++){
		dp[i][i][0]=1;
	}	
	for(int len=2;len<=n;len++){
		for(int l=1;l+len-1<=n;l++){
			int r=l+len-1;
			if(a[l]<a[l+1]){
				dp[l][r][0]+=dp[l+1][r][0];
				dp[l][r][0]%=mod;
			}
			if(a[l]<a[r]){
				dp[l][r][0]+=dp[l+1][r][1];
				dp[l][r][0]%=mod;
			}
			if(a[r]>a[l]){
				dp[l][r][1]+=dp[l][r-1][0];
				dp[l][r][1]%=mod;
			}
			if(a[r]>a[r-1]){
				dp[l][r][1]+=dp[l][r-1][1];
				dp[l][r][1]%=mod;
			}
			
		}
	}
	cout<<(dp[1][n][0]+dp[1][n][1])%mod;
	return 0;
}

T3: P1220 关路灯

题目简述:在一条直线上有 \(n\) 盏路灯,给出位置和功率。老张从初始位置 \(c\) 出发,以 \(1m/s\) 移动,关灯不耗时,求最小总功耗(所有灯在关闭前的耗电总和)。

思路

  1. 状态定义dp[i][j][0/1] 表示关闭区间 [i, j] 的灯后,老张停在左端点/右端点时的最小功耗。
  2. 初始化
    • 老张初始位置 \(c\)dp[c][c][0] = dp[c][c][1] = 0
    • 其他位置 \(i\):从 \(c\) 移动到 \(i\) 的功耗为 abs(a[i]-a[c]) * (总功率 - c灯功率)
  3. 状态转移:考虑从左边或右边扩展,移动时间 = 距离,功耗 = 移动时间 × 未关灯的总功率
    • 停在左端点 \(i\)
      • \(i+1\) 移动:dp[i][j][0] = min(dp[i][j][0], dp[i+1][j][0] + (a[i+1]-a[i]) * (sum[i] + sum[n]-sum[j]))
      • \(j\) 移动:dp[i][j][0] = min(dp[i][j][0], dp[i+1][j][1] + (a[j]-a[i]) * (sum[i] + sum[n]-sum[j]))
    • 停在右端点 \(j\)
      • \(i\) 移动:dp[i][j][1] = min(dp[i][j][1], dp[i][j-1][0] + (a[j]-a[i]) * (sum[i-1] + sum[n]-sum[j-1]))
      • \(j-1\) 移动:dp[i][j][1] = min(dp[i][j][1], dp[i][j-1][1] + (a[j]-a[j-1]) * (sum[i-1] + sum[n]-sum[j-1]))
  4. 答案min(dp[1][n][0], dp[1][n][1])

代码

#include<bits/stdc++.h>
#define int long long
#define endl "\n"
using namespace std;
const int maxn=1e3+5,mod=1e9+7,inf=1e18;
int read(){int x=0,f=1;char ch=getchar();while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}while(ch>='0' && ch<='9')x=x*10+ch-'0',ch=getchar();return x*f;}
void write(int x){if(x<0){putchar('-'),x=-x;}if(x>9){write(x/10);}putchar(x%10+'0');return;}
int fpow(int a,int b,int p){if(b==0){return 1;}int res=fpow(a,b/2,p)%p;if(b%2==1){return((res*res)%p*a)%p;}else{return(res*res)%p;}}
int n,dp[maxn][maxn][2],a[maxn],sum[maxn];
int x,c; 
signed main(){
	cin>>n>>c;
	for(int i=1;i<=n;i++){
		cin>>a[i]>>x;
		sum[i]=sum[i-1]+x;
	}
	memset(dp,0x3f3f3f3f,sizeof(dp));
	for(int i=1;i<=n;i++){
		dp[i][i][0]=dp[i][i][1]=abs(a[i]-a[c])*sum[n];
	}
	for(int len=2;len<=n;len++){
		for(int i=1;i+len-1<=n;i++){
			int j=i+len-1;
			dp[i][j][0]=min(dp[i][j][0],dp[i+1][j][0]+(a[i+1]-a[i])*(sum[n]-sum[j]+sum[i]));
			dp[i][j][0]=min(dp[i][j][0],dp[i+1][j][1]+(a[j]-a[i])*(sum[n]-sum[j]+sum[i]));
			
			dp[i][j][1]=min(dp[i][j][1],dp[i][j-1][0]+(a[j]-a[i])*(sum[n]-sum[j-1]+sum[i-1]));
			dp[i][j][1]=min(dp[i][j][1],dp[i][j-1][1]+(a[j]-a[j-1])*(sum[n]-sum[j-1]+sum[i-1]));
		}
	}
	cout<<min(dp[1][n][0],dp[1][n][1]);
	return 0;
}
posted @ 2025-08-15 20:31  KK_SpongeBob  阅读(12)  评论(0)    收藏  举报