区间DP

简单介绍:区间DP就是在区间上进行动态规划,求解一段区间上的最优解, 通过合并小区间的最优解进而得出大区间上的最优解。
定义状态时,定义的是一段区间

本文章,主要通过几道经典例题的分析来试着阐述区间dp。初次接触,可能很是浅显,欢迎指正和建议@^@

石子合并

题目链接
简单概述:
在这里插入图片描述
分析:

  1. 题目特点:我们每次只能合并相邻两堆

  2. 最后合并的两堆,一堆是左边连续的一部分,另一堆是右边连续的一部分。

  3. 状态表示:
    【(1)化零为正所有不同合并方式一定是有限的,(2)枚举必超时。】
    集合:所有将[i,j]合并成一堆的方案的集合f[i,j] 我们要求的是最小值。

  4. 状态计算:f[i, j]
    想要试图找到状态转移的方程时,我们可以根据最后一步来确定划分依据。比如左边最后一堆是谁。在这里,左边最后一堆我们可以是i; i + 1; …j - 1;我们把这些情况都求出来,取最小值,即为[i, j]区间上所求最小值。我们看这个区间的第k类取法:这时的两大区间[i, k], [k + 1, j]两区间毫无影响。左边取最小,右边取最小。S[n]意为从1-n的所有元素的和。
    f[i][j] = f[i][k] + f[k + 1, j] + S[j]- S[i - 1]

  5. 时间复杂度: n ^3;

关于代码解释

  1. 我们写区间DP时,一般先写区间长度,再枚举区间左端点,区间右端点
  2. f[1][N]带入定义,意为:将1-n堆的石子合并成一堆花费的最小代价。即为所求。
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 310;
int S[N]; //前缀和
int f[N][N]; 
int main(void){
	int n;
	cin >> n;
	for(int i = 1; i <= n; i ++) cin >> S[i], S[i] += S[i-1];
	for(int len = 2; len <= n; len ++){
		for(int i = 1; i +  len - 1 <= n; i ++){ 
		/*关于这里循环条件的一点解释:i+len-1正好为右端点,小于等于n*/
			int j = i + len - 1;
			f[i][j] = 1e9 + 10;
			for(int k = i; k <= j - 1; k ++){
				f[i][j] = min(f[i][j], f[i][k] + f[k + 1][j] + S[j] - S[i - 1]);
			}
		}
	}
	cout << f[1][n];
	return 0;
}

环形石子合并

题目链接

在这里插入图片描述
分析:

  1. 环形区间可以变成一条链
    (1)朴素版本,非常朴素,以致会超时。
    我们把该题作为上道基本石子合并的一个拓展。n堆石子可以看成组成环的n个点,当我们将两堆合并时,相当于在两点之间画一条线。当把n堆合并完成后,一定会有一个缺口,(此时,这两点间没有连线,而n堆石子已然合并成一堆了),我们可以由此受到启发,不妨枚举缺口位置,转化成上题石子合并问题。
    时间复杂度:n ^ 4, emmmnn, 会超时
    (2)优化:本质上是要求n个长度为n的链上的石子合并问题。先将整个环形展开,复制一遍,即再来相同的八个点,变成长度为2*n的链。当在1和8间断开,得到的如图中上面的那条链表示;当1和2断开,即下面那条红色链。

在这里插入图片描述
我们对长度为2*n的链做一遍上面那道石子合并的处理,我们处理出来所有在这段长度上的f[i][j] ,那么在所有长度为n的区间里取一个最小值即可。时间复杂度:(2*n) ^ 3

代码:
注意:我们需要给f,g数组初始化最大最小,且在区间长度为1时,更新为1

#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
const int N = 400;
int S[N], w[N]; //前缀和
int f[N][N], g[N][N]; 
int main(void){
	int n;
	cin >> n;
	for(int i = 1; i <= n; i ++){
		cin >> w[i];
		w[i + n] = w[i];
	}
	for(int i = 1; i <= 2 * n; i ++){
		S[i] = S[i - 1] + w[i];
	}
	memset(f, 0x3f, sizeof(f));//记录最小值 
	memset(g, 0, sizeof(g));//记录最大值 
	for(int len = 1; len <= n; len ++){
		for(int l = 1; l + len - 1 <= 2 * n; l ++){
			int r = l + len - 1;
			if(len == 1) g[l][r] = f[l][r] = 0;
			for(int k = l; k <= r - 1; k ++){
				f[l][r] = min(f[l][r], f[l][k] + f[k + 1][r] + S[r] - S[l-1]);
				g[l][r] = max(g[l][r], g[l][k] + g[k + 1][r] + S[r] - S[l-1]);
			}
		}
	}
	int maxn = 0, minn = 1e9 + 10;
	for(int i = 1; i <= n; i ++){
		maxn = max(maxn, g[i][i+n-1]);
		minn = min(minn, f[i][i+n-1]);
	}
	cout << minn << endl;
	cout << maxn << endl;
	return 0;
}

能量项链

题目链接

在这里插入图片描述
输入样例:
4
2 3 5 10
输出:710.

分析:

  1. f[L][R]表示将[L,R]合并成一个珠子的方式。取max

(1)我们不妨先把它想成一条链(具体怎么样一条链,且听我娓娓道来。后文再说@^@)
以样例为例,我们不妨把它看成 2 3 5 10 2
共5个数,每次合并相邻的俩。那么f[L,R]便有左分界点:L+1, L+2…R-2, R-1, 对应的关系:以左分界点为k的说:f[l][r] = f[l][k] + f[k][r] + w[l]*w[k]*w[r]那样例对应的所求为f[1][5]
(2)与上题类似,我们把它展成长度为2*n的链,求区间为n+1长度的最大值即可。
为何是n+1?以样例为例我们不妨给每个珠子的两个编号(1, 2), (2, 3), (3, 4), (4, 5)(注意输入的2、3、5、10只是与其价值有关),我们模拟从左到右依次合并的场景:
第一步:(1,3)区间长度3-1+1=3;第二步:(1,4)区间长度:4-1+1=3; 第三步:(1, 5)5-1+1=5.(区间包含左右端点)
那这也就解释了为何区间长度我们从3开始枚举。

代码

#include <bits/stdc++.h>
using namespace std;
const int N = 210;
int w[N];
int f[N][N];
int main(void){
    int n;
    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 l = 1; l + len - 1 <= 2 * n; l ++){
            int r = l + len - 1;
            for(int k = l + 1; k <= r - 1; k ++){
                f[l][r] = max(f[l][r], f[l][k] + f[k][r] + w[l] * w[k] * w[r]);
            }
        }
    }
    int maxn = 0;
    for(int l = 1; l <= n; l ++){
        maxn = max(maxn, f[l][l+n]);
    }
    cout << maxn << endl;
    return 0;
}

凸多边形的划分

和高精度的一个结合版

题目链接

题目描述:
在这里插入图片描述

分析:

  1. f[L][R] = K含义: 所有将(L, L+1), (L+1, L + 2), …(R-1, R),(R, L)这个多边形划分成三角形的方案中,最小值为K。

在这里插入图片描述

  1. 状态计算:f[L][K] = min(f[L][K] + f[K][R] + w[L] * w[K] * w[R])
  2. 与上道题相差不大,但是结合了高精度。

代码一(数组做法):

#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 55, M = 35;
typedef long long LL;
int n;
int w[N];
LL f[N][N][M];
//数据在数组中的存储为逆序存储,例如123, 在数组中为321 
void add(LL a[], LL b[]){
	LL c[M];
	memset(c, 0, sizeof c);
	for(int i = 0, t = 0; i < M; i ++){
		t += a[i] + b[i];
		c[i] = t % 10;
		t /= 10;
	}
	memcpy(a, c, sizeof c);
}
void mul(LL a[], LL b){
	LL c[M];
	memset(c, 0, sizeof c);
	LL t = 0;
	for(int i = 0; i < M; i ++){
		t += a[i] * b; 
		c[i] = t % 10;
		t /= 10;
	}
	memcpy(a, c, sizeof c);
}
int cmp(LL a[], LL b[]){
	for(int i = M - 1; i >= 0; i --){
		if(a[i] > b[i]) return 1;
		else if(a[i] < b[i]) return -1;
	}
	return 0;
}
void print(LL a[]){
	int k = M - 1;
	while(k && !a[k]) k --;
	while(k >= 0) cout << a[k --];
	cout << endl;
}
int main(void){
	cin >> n;
	for(int i = 1; i <= n; i ++) cin >> w[i];
	LL temp[M];
	for(int len = 3; len <= n; len ++){
		for(int i = 1; i + len - 1 <= n; i ++){
			int l = i, r = i + len - 1;
			f[l][r][M - 1] = 1; //开始初始化为大于最大值的一个值,因为我们要求最小值。 
			
			for(int k = l + 1; k < r; k ++){
				memset(temp, 0, sizeof temp);
				temp[0] = w[l];
				mul(temp, w[k]), mul(temp, w[r]);
				add(temp, f[l][k]), add(temp, f[k][r]);
				if(cmp(f[l][r], temp) > 0){
					memcpy(f[l][r], temp, sizeof temp);
				} 
			}
		}
	}
	print(f[1][n]);
	return 0;
}

代码二:(完全是手写的高精度模板)
高精度模板链接 戳我

#include <iostream>
#include <cstring>
#include <vector>
#include <algorithm>
using namespace std;
const int N = 55, M = 35;
typedef long long LL;
int n;
int w[N];
vector<int>f[N][N];
vector<int> add(vector<int> &a, vector<int> &b){
	vector<int>c;
	int t = 0;
	for(int i = 0; i < a.size() || i < b.size(); i ++){
		if(i < a.size()) t += a[i];
		if(i < b.size()) t += b[i];
		c.push_back(t % 10);
		t /= 10;
	}
	if(t) c.push_back(1);
	return c;
}
vector<int> mul(vector<int>&a, int b){
	vector<int> c;
	LL t = 0;
	for(int i = 0; i < a.size() || t; i ++){
		if(i < a.size()) t += (LL) a[i] * b;
		c.push_back(t % 10);
		t /= 10;
	}
	return c;
}
bool cmp(vector<int> &a, vector<int> &b){
	if(a.size() != b.size()) return a.size() > b.size();
	for(int i = a.size() - 1; i >= 0; i --){
		if(a[i] != b[i]) return a[i] > b[i];
	}
}
int main(void){
	cin >> n;
	for(int i = 1; i <= n; i ++) cin >> w[i];
	vector<int>temp;
	for(int len = 3; len <= n; len ++){
		for(int l = 1; l + len - 1 <= n; l ++){
			int r = l + len - 1;
			f[l][r] = vector<int>(M, 1);
			for(int k = l + 1; k < r; k ++){
				temp.clear();
				temp.push_back(w[l]);
				temp = mul(temp, w[k]), temp = mul(temp, w[r]);
				temp = add(temp, f[l][k]), temp = add(temp, f[k][r]);
				if(cmp(f[l][r], temp)) f[l][r] = temp;
			}
		}
	}
	for(int i = f[1][n].size() - 1; i >= 0; i --){
		cout << f[1][n][i];
	}
	cout << endl;
	return 0;
}

加分二叉树

题目链接:戳我
题目描述:

在这里插入图片描述
在这里插入图片描述

分析

  1. f[L][R]=k 表示中序遍历是[L,R]这一段的二叉树的集合。k为这个集合分值最大值
  2. 根据根节点的位置,划分成若干类:根节点在L, L-1, …R-1, R, 如果说根节点在第k个点, f[L][R] = max(f[L][k-1]) +max(f[k+1][r]) + w[k])
  3. 问题是:如何记录方案?
  4. 开一个新的数组g[l][r],记录更新最优解时,[l, r]区间内根节点是谁。
  5. 要求最优解对应的二叉树不唯一时,输出前序遍历字典序最小的那种。即要求根节点尽可能靠左,
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 50;
int f[N][N], g[N][N];
int w[N];
int n;

void dfs(int l, int r){
    if(l > r) return ;
    int k = g[l][r];
    printf("%d ", k);
    dfs(l, k - 1);
    dfs(k + 1, r);
}

int main(void){
    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;
            for(int k = l; k <= r; k ++){
                int left = k == l ? 1 : f[l][k-1];
                int right = k == r ? 1 : f[k + 1][r];
                int score = left * right + w[k];
//该处特判不能省略,你品品这个边界情况。
                if(l == r) score = w[k];
                if(f[l][r] < score){
                    f[l][r] = score;
                    g[l][r] = k;
                }
            }
        }
    }
    printf("%d\n", f[1][n]);
    dfs(1, n);
    return 0;
}

棋盘分割

题目链接:戳我

题目描述:
在这里插入图片描述
在这里插入图片描述

分析:

  1. 我们从方差公式入手,进行化简:

在这里插入图片描述
目标L最小化,所有部分平方和的最小值。

  1. 不做化简,直接用公式进行处理也可。
  2. 状态表示:f[x1, y1, x2, y2, k] = m子矩阵(x1, y1),(x2, y2)(左上角和右下角)切分成k部分的所有方案。m表示方差平方的最小值。
  3. 所有状态大可分为两类:横切、纵切。这两大类中也可横切、纵切,很多种方法,每种均可选上边、下边。求出每一类的最小值,取最小值即可 。
  4. 如图所示,我们横着在x下标为i的地方横切一刀,并选择上边一部分时,这时可以看成两部分,i上部分,i下部分。想让整个最小,左边:f[x1, y1, i, y2],右部分是固定的,我们可用二维前缀和求出(二位前缀和相关知识,推荐参考这篇博客,博客链接:戳我)
  5. 循环来写的话,可能需要写好机重循环,在这里我们用记忆化搜索来操作。
  6. double数组,memset全为1或-1, double数组中的每个数均小于0.
#include <iostream>
#include <cstring>
#include <cmath>
#include <algorithm>
using namespace std;
const int N = 9, M = 15;
const double INF = 1e9;
int n, m;
int s[N][N];
double f[N][N][N][N][M];
double X;//平均数

int get_sum(int x1, int y1, int x2, int y2){
    return s[x2][y2] - s[x2][y1 - 1] - s[x1 - 1][y2] + s[x1 - 1][y1 - 1];
}

double get(int x1, int y1, int x2, int y2){
    double sum = get_sum(x1, y1, x2, y2) - X;
    return (double) sum *sum / n;
}

double dp(int x1, int y1, int x2, int y2, int k){
    double &v = f[x1][y1][x2][y2][k];
    if(v >= 0) return v;
    if(k == 1) return v = get(x1, y1, x2, y2);
    v = INF;
    for(int i = x1; i < x2; i ++){
        v = min(v, get(x1, y1, i, y2) + dp(i + 1, y1, x2, y2, k - 1));
        v = min(v, get(i + 1, y1, x2, y2) + dp(x1, y1, i, y2, k - 1));
    }
    for(int i = y1; i < y2; i ++){
        v = min(v, get(x1, y1, x2, i) + dp(x1, i + 1, x2, y2, k - 1));
        v = min(v, get(x1, i + 1, x2, y2) + dp(x1, y1, x2, i, k - 1));
    }
    return v;
}


int main(void){
    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;
    memset(f, -1, sizeof f); //负数
    printf("%.3lf\n", sqrt(dp(1, 1, 8, 8, n)));
    return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Xuhx&

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

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

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

打赏作者

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

抵扣说明:

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

余额充值