20251014 区间DP总结
引子
有一些题目会要你求一些关于区间的问题,就比如什么最多什么最少之类的。由一个长区间可以由多个小区间组成这种特征的区间问题,一般可以用区间DP来解。
区间DP
区间DP一般的做法就是合并两个小区间或一次增加一个上去,常见的题目就是合并石子堆求最小体力,修改字符串成回文串求最小花费等。
区间DP的DP数组一般是二维的,两个维度分别是左端点和右端点,那么dp[i][j]dp[i][j]dp[i][j]就等于区间[i,j][i,j][i,j]的最大(小)花费什么的,答案显而易见就是dp[1][n]dp[1][n]dp[1][n]。
那么什么时候需要增维呢?当我们发现推着推着条件不够的时候,就需要增维,提高一个维度,这个维度就可以恰到好处的去补充条件。
第一类区间DP模板(合并小区间)
for(int len=2;len<=n;len++){for(int l=1;l<=n-len+1;l++){int r=l+len-1;for(int k=l;k<r;k++){dp[l][r]=max(,);}}
}
第二类区间DP模板(一次加一个)
for(int len=2;len<=n;len++){for(int l=1;l<=n-len+1;l++){int r=l+len-1;dp[l][r]=max(,);}
}
例题 石子合并(弱化版)
区间DP的模板题。dp[i][j]dp[i][j]dp[i][j]在这题显然表示第iii堆石子到第jjj堆石子合并后的最小代价,并且属于合并小区间类的做法。
首先初始状态dp[i][i]=0(1≤i≤n)dp[i][i]=0(1\leq i\leq n)dp[i][i]=0(1≤i≤n),其余无穷大;然后,从小到大枚举区间,枚举中断点kkk,动态转移方程为dp[l][r]=min(dp[l][r],dp[l][k]+dp[k+1][r]+s[r]−s[l−1])dp[l][r]=min(dp[l][r],dp[l][k]+dp[k+1][r]+s[r]-s[l-1])dp[l][r]=min(dp[l][r],dp[l][k]+dp[k+1][r]+s[r]−s[l−1]);最后答案为dp[1][n]dp[1][n]dp[1][n]。
memset(dp,0x3f,sizeof(dp));
for(int i=1;i<=n;i++){dp[i][i]=0;
}
for(int len=2;len<=n;len++){//区间长度for(int l=1;l<=n-len+1;l++){//左端点int r=l+len-1;//右端点for(int k=l;k<r;k++){dp[l][r]=min(dp[l][r],dp[l][k]+dp[k+1][r]+s[r]-s[l-1]);}}
}
cout<<dp[1][n];
例题 石子合并
跟上一题没啥区别,就多了个环而已,破环成链就行了。比如一个环1,2,3,4,破环成链就是1,2,3,4,1,2,3。
剩余也就多求了个最大值。
例题 调整队形
显然是第二种区间DP,其次,我们会发现前三种操作其实可以结合到一起去,因为队伍里剔掉一个人就是在另一边加上一个人,没有区别。那么结合之后就是剔掉一个人,没了。
首先,初始值全为0;然后dp[i][j]dp[i][j]dp[i][j]表示第iii个同学到第jjj个同学最少的调整次数;接着枚举每一个区间后有了几种情况:1.a[i]==a[j]a[i]==a[j]a[i]==a[j],那么什么也不用干,直接等于dp[i+1][j−1]dp[i+1][j-1]dp[i+1][j−1] 2.a[i]!=a[j]a[i]!=a[j]a[i]!=a[j],没办法,只能操作一次了,要么选择剔掉一个左边的,要么剔掉一个右边的,要么考虑把两端的人换成同样颜色的衣服,那么动态转移方程dp[i][j]=min(dp[i+1][j],min(dp[i][j−1],dp[i+1][j−1]))+1dp[i][j]=min(dp[i+1][j],min(dp[i][j-1],dp[i+1][j-1]))+1dp[i][j]=min(dp[i+1][j],min(dp[i][j−1],dp[i+1][j−1]))+1。
for(int len=2;len<=n;len++){for(int i=1;i<=n-len+1;i++){int j=i+len-1;if(a[i]==a[j]){dp[i][j]=dp[i+1][j-1];}else{dp[i][j]=min(dp[i+1][j],min(dp[i][j-1],dp[i+1][j-1]))+1;}}
}
cout<<dp[1][n];
例题 关路灯
依然是区间DP,但是一段区间,他可能是从左端点走到右端点,也有可能恰恰相反,怎么办呢?这时就需要增维了,第三个维度表示是否从左边走到右边。
这就好办了,dp[i][j][0]dp[i][j][0]dp[i][j][0] 的推导有两种可能情况:从折返状态关闭 i,ji,ji,j 的灯(即从 i+1i+1i+1 返回),或者继续关闭第 iii 盏灯进而扩展到 (i,j)(i,j)(i,j) 状态。
dp[i][j][0]dp[i][j][0]dp[i][j][0] 的推导有两种可能情况:从折返状态关闭 i,ji,ji,j 的灯(即从 i+1i+1i+1 返回),或者继续关闭第 iii 盏灯进而扩展到 (i,j)(i,j)(i,j) 状态。
于是动态转移方程及初始值及答案:
for(int i=1;i<=n;i++){//s是前缀和数组dp[i][i][0]=dp[i][i][1]=abs(d[i]-d[c])*(s[n]-w[c]);
}
dp[c][c][0]=dp[c][c][1]=0;//从c点出发,自然不用任何代价
for(int len=2;len<=n;len++){for(int i=1;i<=n-len+1;i++){int j=i+len-1;dp[i][j][0]=min(dp[i+1][j][0]+(d[i+1]-d[i])*(s[i]+s[n]-s[j]),dp[i+1][j][1]+(d[j]-d[i])*(s[i]+s[n]-s[j]));//这样走能节省时间吗?dp[i][j][1]=min(dp[i][j-1][0]+(d[j]-d[i])*(s[i-1]+s[n]-s[j-1]),dp[i][j-1][1]+(d[j]-d[j-1])*(s[i-1]+s[n]-s[j-1]));//是否从j点折返关闭i灯会更高效?(当前状态:[i+1,j]区间关闭,i灯亮着,需从j端点返回关闭i灯)}
}
cout<<min(dp[1][n][0],dp[1][n][1]);//可以从左端点走到右端点,或从右端点走到左端点