区间DP入门学习

区间DP,区间上做动态规划。一个大区间必然由长度不等的小区间合并而来,在合并过程中,最基本也是动态规划的必须原则:满足最优化原理和无后效性原则,所以在确定状态转移方程时,得特别留心这两点。

区间DP区别于其它线性DP题的特点是:区间DP通过枚举区间分界点实现状态转移。

词穷了,还是写题来感受吧~~,有坑自己再补上。

入门题1:凸多边形三角划分

题意:给定一个具有N(N<=50)个顶点的凸多边形,每个顶点的权值均已知。问如何把这个凸多边形划分成N-2个互不相交的三角形,使得这些三角形顶点的权的乘积之和最小。

样例:

5
121
122
123
245
231
这是一个几何模型,我们可以将其转化为实际模型,给定N个数,首尾成环,每拿走一个数的代价是该数乘以左右相邻两个的数,问拿走N-2个数后的最小代价。

思路:1)确定子问题。一个凸多边形划分为两个小的凸多边形,再进行划分......最后合并,可以确定子问题相同,都是求每个小凸多边形的三角形顶点的权的乘积。

            2)确定状态。将1-N个顶点的凸多边形划分为(A1,A2,A3..Ak) + (Ak+1,Ak+2,Ak+3......An)两部分,只要前后两部分都是最优策略,那么必然合并后的凸多边形也是最优策略,且这两部分单独求值,互不影响。所以确定状态dp[i,j]为从顶点i到顶点j的凸多边形三角划分后得到的最优值,显示区间最优状态时dp[1,n]。

            3)列出状态转移方程式。dp[i][j] = min(dp[i][k]+dp[k][j]+vis[i]*vis[k]*vis[j]).(i<k<j)。

其实自己对矩阵链乘那道题还是比较感兴趣滴,不过现在还是没有懂,虽然感觉都大同小异,去看看再回来写总结。

#include<stdio.h>
#include<string.h>
#include<algorithm>
using namespace std;
const int maxn = 210;
#define inf 0x3f3f3f3f
int dp[maxn][maxn],vis[maxn];
int main()
{
	int n,len,j,k,i;
//	freopen("in.txt","r",stdin);
	scanf("%d",&n);
	for(i = 1; i <= n; i ++)
		scanf("%d",&vis[i]);
	memset(dp,0,sizeof(dp));
	for(len = 2; len <= n-1; len++)//区间长度 
	{
		for(i = 1; i+len <= n; i ++)//枚举每次区间起点 
		{
			j = i+len;//区间终点 
			dp[i][j] = inf;//存储区间[i,j]的最优值 
			for(k= i+1; k < j; k ++)//枚举每一个分界点K 
			{
				dp[i][j] = min(dp[i][j],dp[i][k]+dp[k][j]+vis[i]*vis[k]*vis[j]);
			}
		}
	}
	printf("%d\n",dp[1][n]);//显然最优值在区间[1,n] 
	return 0;
}

入门题2:矩阵链乘(MCM)

给出n个矩阵组成的序列,设计一种方法把它们依次相乘,使得总的运算量最小。假设第i个矩阵Ai=pi-1*pi。

样例

3
10 30 5 60
3
10 20 5 4

输出

4500
((A1A2)A3)
1200
(A1(A2A3))

思路:矩阵相乘满足结合律,运算量随着运算顺序改变而改变。个人理解,要使运算量最小等价于先找到最优运算顺序,平时我们都是用括号划分运算顺序,因此在此题中,我们需要找到括号划分的最优位置,即分界点K的最优位置。一条有N个矩阵的矩阵链,可以划分为[1,k]和[k+1,n]两条次矩阵链,即最大区间问题转移为求两小区间最优值合并问题,在合并过程过,要找到最优分界点,需要遍历整个矩阵链位置,假设dp[i,j]为矩阵链第i个矩阵到第j个矩阵的最优运算量,可列得状态转移方程:

    dp[i][j] = min(dp[i][k]+dp[k+1][j]+vis[i-1]*vis[k]*vis[j]) (i<=k<j)

最后加上的是最后一次矩阵乘法的运算量,比如((A*B)*(C*D)),以B作为分界点,在合并最后一步,我们必然得加上(A*B)矩阵*(C*D)矩阵的运算量。

学习过程中的几个自己想到的思考点:

1.为什么例题1枚举矩阵区间长度从2开始,而此题从1开始?因为三角形顶点最少需要3个顶点才能进行状态转移,而此题中两个矩阵也可进行状态转移。

2.为什么例题1分界点可以重合,而此题不可以?因为划分凸多边形时,只要三角形不相交,必然存在顶点重合,此题矩阵相乘时,分界点矩阵只能位于一个矩阵链区间,否则会重复多乘,影响正确的状态转移。

3.为什么例题1分界点i<k<j,而此题i<=k<j?实质原因和1的思考点一样,如果只有两个顶点时,无法完成状态转移,而此题可行。

输出最优解就不用再写了,自己明白就行。

#include<stdio.h>
#include<string.h>
#include<algorithm>
using namespace std;
const int maxn = 210;
#define inf 0x3f3f3f3f
int dp[maxn][maxn],path[maxn][maxn],vis[maxn];
void Print_path(int i,int j)
{
	if(i == j)//k初始等于i,故递归边界是i==j 
		printf("A%d");
	else
	{
		printf("(");
		Print_path(i,path[i][j]);//输出k边界左区间矩阵链 
		Print_path(path[i][j]+1,j);//输出k边界右区间矩阵链 
		printf(")");
	}
	return;
}
int main()
{
	int n;
	int i,j,k,len,m;
//	freopen("in.txt","r",stdin);
	while(scanf("%d",&n)!=EOF)
	{
		for(i = 0; i <= n; i ++)//存储n个矩阵的行列值,矩阵Ai = P(i-1)*Pi,故需要n+1个值 
			scanf("%d",&vis[i]);
		memset(dp,0,sizeof(dp));
		memset(path,0,sizeof(path));
		for(len = 1; len < n; len ++)//枚举区间长度 
		{
			for(i = 1; i+len <= n; i ++)//取到第n个矩阵 
			{
				j = i+len;
				dp[i][j] = inf;
				for(k = i; k < j; k ++)//i<=k<j
				{
					m = dp[i][k]+dp[k+1][j]+vis[i-1]*vis[k]*vis[j];
					//([i,k]区间+[k+1,j]区间)的矩阵乘积最优运算量+最后一次矩阵乘法运算 
					if(dp[i][j] > m)//如果分界点k能够降低运算量,则状态转移 
					{
						dp[i][j] = m;
						path[i][j] = k;//记录区间[i,j]的分界点k 
					}
				}
			}
		}
		printf("%d\n",dp[1][n]);//最优值区间 
		Print_path(1,n);
		printf("\n");
	}
	return 0;
}

### 区间动态规划的概念 区间动态规划是一种特殊的动态规划形式,主要用于解决涉及序列或数组中的某些区间的最优化问题。其核心思想是将整个区间划分为若干个小的子区间,在这些子区间上计算最优解并逐步扩展至更大的区间范围,最终得到全局最优解[^1]。 这种类型的动态规划通常用于处理具有某种“分治”性质的问题,即可以通过分割成更小子区间来解决问题,并且满足无后效性和最优子结构性质[^2]。 --- ### 区间动态规划的核心要素 #### 定义状态 设 `dp[l][r]` 表示从位置 `l` 到位置 `r` 的某个特定属性的最大值(或其他目标函数),其中 `l` 和 `r` 是区间的左端点和右端点。 #### 状态转移方程 对于任意一个区间 `[l, r]`,将其拆分成两部分:`[l, k]` 和 `[k+1, r]`,并通过枚举中间点 `k` 来更新当前区间的最优解: \[ dp[l][r] = \max(dp[l][r], f(dp[l][k], dp[k+1][r])) \] 这里的 `f()` 函数取决于具体问题的要求,可能是一个加法操作、乘法操作或者其他逻辑运算[^3]。 #### 初始化条件 初始状态下,当区间长度为 1 时(即 `l == r`),可以直接设定对应的值作为基础情况。 --- ### 示例:石子合并问题 假设有一排石头堆放在一条直线上,每堆都有一定的重量。现在要将所有的石子合并成一堆,每次只能选择相邻的两堆进行合并,合并后的总代价等于这两堆石子的重量之和。求最小化总的合并代价。 #### 输入样例 ``` stones = [4, 1, 1, 4] ``` #### 解决方案 以下是基于区间动态规划的思想实现的一个 Python 版本解决方案: ```python def stoneMerge(stones): n = len(stones) # 构造前缀和数组以便快速计算区间和 prefix_sum = [0] * (n + 1) for i in range(n): prefix_sum[i + 1] = prefix_sum[i] + stones[i] # 定义 dp 数组 dp = [[float('inf')] * n for _ in range(n)] # 当只有一个石子时,不需要任何代价 for i in range(n): dp[i][i] = 0 # 遍历所有可能的区间长度 for length in range(2, n + 1): # 子区间的长度从 2 开始逐渐增大 for l in range(n - length + 1): # 左边界 r = l + length - 1 # 右边界 # 尝试不同的分割点 k 进行合并 for k in range(l, r): cost = dp[l][k] + dp[k + 1][r] + (prefix_sum[r + 1] - prefix_sum[l]) if cost < dp[l][r]: dp[l][r] = cost return dp[0][n - 1] # 测试输入 stones = [4, 1, 1, 4] print(stoneMerge(stones)) # 输出应为 18 ``` 在这个例子中,我们利用了前缀和技巧加速计算区间内的权重总和,并通过遍历不同大小的子区间完成动态规划过程[^4]。 --- ### 总结 区间动态规划的关键在于如何合理地设计状态以及找到合适的递推关系。通过对小区间的不断组合与扩展,能够有效地构建出更大规模下的最优解结构。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值