现在来学习DP中的区间DP~
线性区间DP的状态转移方程为:
例题是经典的石子合并问题:
N堆石子摆成一条线。现要将石子有次序地合并成一堆。规定每次只能选相邻的2堆石子合并成新的一堆,并将新的一堆石子数记为该次合并的代价。计算将N堆石子合并成一堆的最小代价。
石子是有顺序的,不是无序的,要是无序的话直接用贪心,每一次选取数值最少的一对合并就行了
- 石子摆成一条线的解法,没有用平行四边形优化,时间复杂度为o(N^3):
/**
* TODO Auther: by SR
* Date: 2020-01-24 17:02:01
* LastEditTime: 2020-01-27 15:17:18
* ! 今日运势: 吉,无bug
* *石子合并问题
* TODO 先用思路最直接的递归算法
* ?这里石子呈一条直线
**/
#include<iostream>
#include<cmath>
#include<algorithm>
#include<cstdio>
#include<string>
#include<memory.h>
#include<
#define INF 0x3f3f3f3f
using namespace std;
int solute();
int que[102],n,ans[102][102]={0},sum[102]={0};
int main(){
cin >> n;
for(int i = 1; i <= n; i++){
cin >> que[i];
}
sum[0] = 0;
for(int i = 1; i <= n; i++){
sum[i] = sum[i-1] + que[i];
}
memset(ans, INF, sizeof(ans));//求最大值,要先值为无穷大
for(int i = 0; i <= n; i++){
ans[i][i] = 0;//边界需要初始化为0
}
cout<< solute();
return 0;
}
int solute(){
for(int v = 1; v <= n; v++){//计算相邻的v个区间合并的最小值,需要从小到大
for(int i = 1; i <= n-v; i++){//从ans[1][2]开始
int j = i + v;
int temp = sum[j] - (i > 0 ? sum[i-1]:0);
for(int k = i; k < j; k++){
ans[i][j] = min(ans[i][j], ans[i][k]+ans[k+1][j]+temp);
}
}
}
return ans[1][n];
}
在看四边形不定式的证明时找到的几篇比较详细的文章:
平行四边形不等式优化DP
四边形不等式优化讲解(详解)
这篇是洛谷的题解,但我个人感觉更好理解: P1880 [NOI1995]石子合并 题解
最后才发现这不定式的意思是:如果一个函数w满足w(i,j)+w(i’,j’)≤w(i’,j)+w(i,j’)这个不等式,那么这个函数满足四边形不定式。
我搞错了这里的因果关系,一直在找这个不定式的证明🤔
太笨了我。。。
看完这定理,感觉这处处是玄学啊。。
愚笨如我决定直接死记规律。
没看资料直接打优化的代码,太累了,调来调去的,我决定下次卡了十来分钟以上就去找资料:
- 用平行四边形进行优化后的代码,时间复杂度为o(N^2)
/**
* TODO Auther: by SR
* Date: 2020-01-24 17:02:01
* LastEditTime: 2020-01-27 23:36:39
* ! 今日运势: 吉,无bug
* *石子合并问题
* TODO 这里用平行四边形进行了优化,优化后的时间复杂度为//!o(N^2)
* *先用线型熟悉下定理
**/
#include<iostream>
#include<cmath>
#include<algorithm>
#include<cstdio>
#include<string>
#include<memory.h>
#define INF 0x3f3f3f3f
using namespace std;
int solute();
int que[102],n,ans[102][102]={0},sum[102]={0};
int best_coordinate[102][102]={0};//!用于标记最优点的下标
int main(){
cin >> n;
for(int i = 1; i <= n; i++){
cin >> que[i];
}
for(int i = 1; i <= n; i++){
best_coordinate[i][i] = i;
}
/*
for(int i = 1; i < n; i++){
best_coordinate[i][i+1] = i;
}
*/
sum[0] = 0;
for(int i = 1; i <= n; i++){
sum[i] = sum[i-1] + que[i];
}
memset(ans, INF, sizeof(ans));//求最大值,要先值为无穷大
for(int i = 0; i <= n; i++){
ans[i][i] = 0;//边界需要初始化为0
}
cout<< solute();
/*
cout << "\n";
for(int i = 0; i <= n; i++){
for(int j = 0; j <= n; j++){
cout << best_coordinate[i][j]<<" ";
}
cout <<"\n";
}
cout << "\n";
for(int i = 0; i <= n; i++){
for(int j = 0; j <= n; j++){
if(ans[i][j] != INF){
cout <<" "<< ans[i][j]<<" ";
}
else{
cout << ans[i][j]<<" ";
}
}
cout <<"\n";
}
*/
return 0;
}
int solute(){
for(int i = n; i >= 1; i--){
for(int j = i + 1; j <= n; j++){
int temp = sum[j] - sum[i-1];
if(!best_coordinate[i][j-1]){
best_coordinate[i][j-1] = i;
}
if(!best_coordinate[i+1][j]){
best_coordinate[i+1][j] = j-1;
}
for(int k = best_coordinate[i][j-1]; k <= best_coordinate[i+1][j]; k++){
if(ans[i][k]+ans[k+1][j]+temp < ans[i][j]){
ans[i][j] = ans[i][k]+ans[k+1][j]+temp;
best_coordinate[i][j] = k;
}
}
}
}
return ans[1][n];
}
A完题后,接着考虑石子成环状的情况。
思路不难,只要把石子“展开”就行,因为不知道是在环的哪里分割可以取得最大值,所以就把所有情况都算一遍。比如说,1 2 3 4 的环状石堆,展开后就是 1 2 3 4 1 2 3。展开后遍历n次就行了,其中就包含了所有的分割情况:1 2 3 4,2 3 4 1,3 4 1 2,4 1 2 3。
- 石子成环状时的代码,要注意虽然要算n个分隔点下的最优解,但ans数组依然只需要遍历一次,如果n个分割点都分别计算一次的话,时间复杂度就又是o(N^3)了。:
/**
* TODO Auther: by SR
* Date: 2020-01-24 17:02:01
* LastEditTime: 2020-01-28 21:28:32
* ! 今日运势: 吉,无bug
* *石子合并问题,环状,用了四边形不等式进行优化
**/
#include<iostream>
#include<cmath>
#include<algorithm>
#include<cstdio>
#include<string>
#include<memory.h>
#define INF 0x3f3f3f3f
#define MAXN 1003
using namespace std;
void solute();
int que[MAXN*2], ans[MAXN*2][MAXN*2]={0}, sum[MAXN*2]={0}, n;
int best_coordinate[MAXN*2][MAXN*2]={0};//!用于标记最优点的下标
int main(){
cin >> n;
for(register int i = 1; i <= n; i++){
cin >> que[i];
}
//初始化
for(register int i = 1; i <= n-1; i++){
que[i+n] = que[i];
}
sum[0] = 0;
int less = INF;
for(register int i = 1; i <= n*2-1; i++){
sum[i] = sum[i-1] + que[i];
}
memset(ans, INF, sizeof(ans));
memset(best_coordinate,0,sizeof(best_coordinate));
for(int i = 1; i <= n*2; i++){
ans[i][i] = 0;
best_coordinate[i][i] = i;
}
solute();//!注意虽然要枚举n个分隔点的情况,但整个ans数组只用记录一次就行
for(register int i = 1; i <= n; i++){//选出最小值
less = min(less, ans[i][i+n-1]);
}
cout<< less;
/*
for(int i = 0; i <= n; i++){
for(int j = 0; j <= n; j++){
cout << best_coordinate[i][j]<<" ";
}
cout <<"\n";
}
cout << "\n";
for(int i = 0; i <= n; i++){
for(int j = 0; j <= n; j++){
if(ans[i][j] != INF){
cout <<" "<< ans[i][j]<<" ";
}
else{
cout << ans[i][j]<<" ";
}
}
cout <<"\n";
}
*/
return 0;
}
void solute(){
for(register int i = n*2-1; i >= 1; i--){//i从大到小
for(register int j = i + 1; j <= n*2-1; j++){//j从小到大
if(!best_coordinate[i][j-1]){
best_coordinate[i][j-1] = i;//min(i+1, j-1)
}
if(!best_coordinate[i+1][j]){
best_coordinate[i+1][j] = j-1;
}
int temp = sum[j] - sum[i-1];
for(int k = best_coordinate[i][j-1]; k <= best_coordinate[i+1][j]; k++){
if(ans[i][k] + ans[k+1][j] + temp < ans[i][j]){
ans[i][j] = ans[i][k] + ans[k+1][j] + temp;
best_coordinate[i][j] = k;
}
}
}
}
return;
}
我有一个理解不太对的地方:在用了平行四边形优化后,best_coordinate[i][j]的值依然是都有存进去的,平行四边形优化不是减少了需要找的best_coordinate[i][j]的数量,而是减少了找best_coordinate[i][j]的时间。
用register,可以运行地更快一些。