区间DP练习

环形石子合并

题目链接:环形石子合并
分析:和石子合并相比就多了一个条件,我们想,就算是环形的,它也是合并n-1次,最后肯定能等价为从某一点断开后的石子合并,因此我们只需把数组开为原来的两倍,也就是把所有石子的值再接在原数组后,然后对2*n长度的数组进行区间DP,从中找出长度为n的合并总分的最大值和最小值即可。
代码实现:

#include<iostream>
using namespace std;
const int N = 420, INF = 0x3f3f3f3f;
int dph[N][N], dpl[N][N], n, a[N];
int main() {
	cin >> n;
	for (int i = 1; i <= n; i++) cin >> a[i], a[i + n] = a[i];//接在后面
	for (int i = 1; i <= 2 * n; i++) a[i] += a[i - 1];//处理成前缀和数组
	for (int len = 2; len <= n; len++) {//枚举所有长度
		for (int l = 1; l + len - 1 < 2 * n; l++) {//枚举开始,这里2*n不用枚举,它和到n的值是一样的
			int r = l + len - 1;
			dpl[l][r] = INF;//求最小时初始为很大的数,求最大的初始为很小的数,因为全是整数,默认的0就可以了
			for (int k = l; k < r; k++) {//枚举分界点
				dph[l][r] = max(dph[l][r], dph[l][k] + dph[k + 1][r] + a[r] - a[l - 1]);
				dpl[l][r] = min(dpl[l][r], dpl[l][k] + dpl[k + 1][r] + a[r] - a[l - 1]);
			}
		}
	}
	int h = -INF, l = INF;//找出最小和最大值
	for (int i = 1; i <= n; i++) {
		h = max(h, dph[i][i + n - 1]);
		l = min(l, dpl[i][i + n - 1]);
	}
	cout << l << endl << h;
	return 0;
}

能量项链

题目链接:能量项链
和上题基本一模一样,就是长度需要从3开始,到n+1,其它都一样。
代码实现:

#include<iostream>
using namespace std;
const int N=210;
int n;
int w[N];
int dp[N][N];
int main(){
    cin>>n;
    for(int i=1;i<=n;i++){
        cin>>w[i];
        w[i+n]=w[i];
    }
    for(int len=3;len<=n+1;len++){
        for(int i=1;i+len-1<=2*n;i++){
            int j=i+len-1;
            for(int k=i+1;k<j;k++){
                dp[i][j]=max(dp[i][j],dp[i][k]+dp[k][j]+w[i]*w[k]*w[j]);
            }
        }
    }
    int res=0;
    for(int i=1;i<=n;++i)   res=max(res,dp[i][i+n]);
    cout<<res<<endl;
    return 0;
}

加分二叉树

题目链接:加分二叉树
分析:看似有二叉树的题目,实际上如果我们按根节点进行分类,我们就能把这题变成一个区间DP,根节点左边是左子树,右边是右子树。详细讲解看注释。
代码实现:

#include<iostream>
using namespace std;
const int N=35;
int dp[N][N];//dp[i][j]表示把i到j合并的最大分数
int pre[N][N];//pre[i][j]表示从i到j的子树的根节点
int w[N];
void dfs(int l,int r){//递归打印
    if(l>r) return ;
    printf("%d ",pre[l][r]);//输出前序遍历,则先输出根节点
    dfs(l,pre[l][r]-1);//左子树
    dfs(pre[l][r]+1,r);//右子树
}
int main(){
    int n;
    cin>>n;
    for(int i=1;i<=n;++i){
        cin>>w[i];
    }
    for(int len=1;len<=n;len++){
        for(int l=1;l+len-1<=n;l++){
            int r=l+len-1;
            if(len==1){//子树的长度为1时,根结点是自己,合并的权值也是自己,因为左右子树权值都为1
                dp[l][l]=w[l];
                pre[l][l]=l;
            }
            else{//如果长度大于1
                for(int k=l;k<=r;k++){//枚举根节点
                    int left= k==l? 1:dp[l][k-1];//若根节点是最左边的,则左子树权值为1,否则就是dp[l][k-1]
                    int right= k==r? 1:dp[k+1][r];//同理
                    int s=left*right+w[k];//分值
                    if(dp[l][r]<s){//如果比原分值高
                        dp[l][r]=s;//更新分值
                        pre[l][r]=k;//记录新的根节点
                    }
                }
            }
        }
    }
    cout<<dp[1][n]<<endl;
    dfs(1,n);
    return 0;
}

凸多边形的划分

题目链接:凸多边形的划分
分析:非常恶心的一题,看到数据范围就知道要用高精度。先不想高精的事,我们发现,我们可以枚举所有区间(长度为3~n),然后再在这个区间的左右端点之间选取一个数作为三角形顶点,那么所有三角形的顶点权值乘积之和就是这个区间左边的答案加上这个区间右边的答案再加上这个区间的答案。就是高精度太恶心了。
代码实现:

#include<iostream>
#include<vector>
using namespace std;
typedef vector<long long> VLL;
const int N=110;
int w[N];
VLL dp[N][N];
VLL tmp;
long long t;
bool cmp(VLL x,VLL y){//比较两数大小,若第一个数大于第二个数,返回true,否则返回false
    if(x.size()!=y.size()){
        return x.size()>y.size();
    }
    else{
        for(int i=x.size()-1;i>-1;i--){
            if(x[i]!=y[i]){
                return x[i]>y[i];
            }
        }
    }
    return false;
}
VLL add(VLL a,VLL b){//高精度加法
    t=0;
    VLL ans;
    int n=max(a.size(),b.size());
    for(int i=0;i<n;i++){
        if(i<a.size())t+=a[i];
        if(i<b.size())t+=b[i];
        ans.push_back(t%10);
        t/=10;
    }
    if(t)   ans.push_back(t);
    return ans;
}
VLL mul(VLL a,int b){//高精乘低精
    t=0;
    VLL ans;
    for(int i=0;i<a.size();i++){
        t+=a[i]*b;
        ans.push_back(t%10);
        t/=10;
    }
    while(t){
        ans.push_back(t%10);
        t/=10;
    }    
    return ans;
}
int main(){
    int n;
    cin>>n;
    for(int i=1;i<=n;i++){
        cin>>w[i];
    }
    for(int len=3;len<=n;len++){//枚举区间长度
        for(int l=1;l+len-1<=n;l++){//枚举开始点
            int r=l+len-1;//枚举结束点
            for(int i=1;i<100;i++) dp[l][r].push_back(1);//初始化为一个很大的数,方便后面取最小值
            for(int k=l+1;k<r;k++){//选三角形的顶点
                tmp.clear();
                tmp.push_back(w[l]);
                tmp=mul(mul(tmp,w[k]),w[r]);//中间三角形的权值
                tmp=add(tmp,add(dp[l][k],dp[k][r]));//左右两边的权值
                if(cmp(dp[l][r],tmp)){//如果比初始化的值小,就更新答案
                    dp[l][r]=tmp;
                }
            }
        }
    }
    for(int i=dp[1][n].size()-1;i>-1;i--){//输出答案
        cout<<dp[1][n][i];
    }
    return 0;
}

棋盘分割

题目链接:棋盘分割
分析:这题说是一个区间DP,也有记忆化搜索的味道,对于一个棋盘,我们用一个5维状态来搜索,具体解析见代码。
代码实现:

#include<iostream>
#include<algorithm>
#include<cmath>
using namespace std;
const int N = 9, M = 15;
int n;
int s[N][N];
double x;
double dp[N][N][N][N][M];
double get(int x1, int y1, int x2, int y2) {//得到x1,y1~x2,y2的子矩阵的方差
    double sum = s[x2][y2] - s[x2][y1 - 1] - s[x1 - 1][y2] + s[x1 - 1][y1 - 1] - x;
    return sum * sum / n;
}
double dfs(int x1, int y1, int x2, int y2, int k) {
    double& tmp = dp[x1][y1][x2][y2][k];//写着更方便
    if (tmp >= 0)  return tmp;//如果大于0,说明已搜过,直接返回
    if (k == 1)    return tmp = get(x1, y1, x2, y2);//如果k=1,说明已经分割完毕,返回
    tmp = 1e9;//初始化为一个很大的值,方便后面取min
    for (int i = x1; i < x2; i++) {//枚举从哪一行分开,上边的棋盘包含这一行
        tmp = min(tmp, dfs(x1, y1, i, y2, k - 1) + get(i + 1, y1, x2, y2));//保留上边的
        tmp = min(tmp, dfs(i + 1, y1, x2, y2, k - 1) + get(x1, y1, i, y2));//保留下边的
    }
    for (int i = y1; i < y2; i++) {//枚举从哪一列分开,左边的棋盘包含这一列
        tmp = min(tmp, dfs(x1, y1, x2, i, k - 1) + get(x1, i + 1, x2, y2));//保留左边的
        tmp = min(tmp, dfs(x1, i + 1, x2, y2, k - 1) + get(x1, y1, x2, i));//保留右边的
    }
    return tmp;
}
int main() {
    cin >> n;
    for (int i = 1; i <= 8; i++) {
        for (int j = 1; j <= 8; j++) {
            cin >> s[i][j];
            s[i][j] += s[i - 1][j] + s[i][j - 1] - s[i - 1][j - 1];//这里处理成二维差分数组,因为后面我们会多次需要求子矩阵的值
        }
    }
    x = (double)s[8][8] / n;//n个棋盘的平均值
    fill(&dp[0][0][0][0][0], &dp[0][0][0][0][0] + N * N * N * N * M, -1e9);//初始化为很小的负数
    printf("%.3lf", sqrt(dfs(1, 1, 8, 8, n)));//打印均方差
    return 0;
}

区间DP其实套路性还是比较明显的,我感觉难的就是想出状态的正确表示和转移。

Sue的小球

题目链接:Sue的小球
分析:费用提前计算思想。具体的看代码就可以了

#include<iostream>
#include<algorithm>
using namespace std;
typedef long long LL;
int str,n;
const int N=1010;
struct pos{
	int x,y,t;
	bool operator<(const pos& tmp)const{
		return x<tmp.x;
	}
}p[N];
LL sum[N];
//本题中每经过一个彩蛋都能瞬间回收,因此很明显是区间DP, 我们还要多开一维记录走到了区间的左边还是右边 
//另外,对于每个菜单价值的损失,因为我们最后还是要全部回收,所以我们可以边dp边把后面的损失提前计算 
LL dp[N][N][2];//dp[i][j][0]表示回收了i到j的并且在左边时的最高分数 
int main(){
	cin>>n>>str;
	for(int i=1;i<=n;i++) cin>>p[i].x;
	for(int i=1;i<=n;i++) cin>>p[i].y;
	for(int i=1;i<=n;i++) cin>>p[i].t;
	sort(p+1,p+n+1);
	for(int i=1;i<=n;i++) sum[i]=sum[i-1]+p[i].t;
	for(int i=1;i<=n;i++)
		dp[i][i][1]=dp[i][i][0]=p[i].y-sum[n]*abs(p[i].x-str);
	for(int len=2;len<=n;++len){
		for(int l=1;l+len-1<=n;l++){
			int r=l+len-1;
			dp[l][r][0]=max(dp[l+1][r][0]+p[l].y-(p[l+1].x-p[l].x)*(sum[n]-sum[r]+sum[l]),
				dp[l+1][r][1]+p[l].y-(p[r].x-p[l].x)*(sum[n]-sum[r]+sum[l]));//dp[l][r][0]表示刚到l,说明l刚被收 
			dp[l][r][1]=max(dp[l][r-1][0]+p[r].y-(sum[n]-sum[r-1]+sum[l-1])*(p[r].x-p[l].x),
				dp[l][r-1][1]+p[r].y-(sum[n]-sum[r-1]+sum[l-1])*(p[r].x-p[r-1].x));
		}
	}
	printf("%.3lf",max(dp[1][n][0],dp[1][n][1])/1000.0);
	return 0;
}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

_bxzzy_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值