区间DP(模板)

引入:

区间类的DP不同于线性DP,常常以区间设状态,用合并小区间的办法状态转移。

例题1:

石子合并(弱化版)

题目描述

设有 N ( N ≤ 300 ) N(N \le 300) N(N300) 堆石子排成一排,其编号为 1 , 2 , 3 , ⋯   , N 1,2,3,\cdots,N 1,2,3,,N。每堆石子有一定的质量 m i   ( m i ≤ 1000 ) m_i\ (m_i \le 1000) mi (mi1000)。现在要将这 N N N 堆石子合并成为一堆。每次只能合并相邻的两堆,合并的代价为这两堆石子的质量之和,合并后与这两堆石子相邻的石子将和新堆相邻。合并时由于选择的顺序不同,合并的总代价也不相同。试找出一种合理的方法,使总的代价最小,并输出最小代价。

输入格式

第一行,一个整数 N N N

第二行, N N N 个整数 m i m_i mi

输出格式

输出文件仅一个整数,也就是最小代价。

样例 #1

样例输入 #1

4
2 5 3 1

样例输出 #1

22

思路:

我们设 f [ i ] [ j ] f[i][j] f[i][j] 表示 [ i , j ] [i, j] [i,j] 的最小代价。

状态转移:

我们枚举一个中间点 k k k ,把大区间 [ i , j ] [i, j] [i,j] 划分成两个小区间 [ i , k ] [i, k] [i,k] [ k + 1 , j ] [k+1, j] [k+1,j] ,再合并,取最小值即可。

合并 [ i , k ] [i, k] [i,k] 的代价为 f [ i ] [ k ] f[i][k] f[i][k]

合并 [ k + 1 , j ] [k+1, j] [k+1,j] 的代价为 f [ k + 1 ] [ j ] f[k+1][j] f[k+1][j]

把两个小区间合并的代价其实就是整个区间的和,用前缀和加速。

得到方程: f [ i ] [ j ] = m i n ( f [ i ] [ k ] + f [ k + 1 ] [ j ] + s u m ( i , j ) ) f[i][j] = min(f[i][k]+f[k+1][j]+sum(i, j)) f[i][j]=min(f[i][k]+f[k+1][j]+sum(i,j))

结果就是 f [ 1 ] [ n ] f[1][n] f[1][n]

注意:我们是合并小区间得到的,所以要先枚举区间长度,再枚举左端点,算出右端点,再转移。

左端点为 l l l,区间长度为 l e n len len,求右端点 r r r:

有: l e n = r − l + 1 len = r-l+1 len=rl+1

r − l = l e n − 1 r-l = len-1 rl=len1

r = l + l e n − 1 r = l+len-1 r=l+len1

代码:
#include <iostream>
using namespace std;
// 设 f[i][j] 表示合并 i~j 的结果

// 初始化 f[i][j] = 正无穷, f[i][i] = 0

// 状态转移:
// 可以枚举一个点 k,把区间分成 [i, k]和[k+1, j]
// 该次合并的代价为这个区间所有的石子和,用前缀和算出 
// 最后取最小值就可以了 
// f[i][j] = min(f[i][k]+f[k+1][j]+(sum[j]-sum[i-1]))

// 最后的结果就是 f[1][n]  
int n, a[305], f[305][305], sum[305];
int main()
{
	cin >> n;
	for (int i=1;i<=n;i++)
		for (int j=1;j<=n;j++)
			f[i][j] = 1000000000;
	for (int i=1;i<=n;i++)
	{
		cin >> a[i];
		sum[i] = sum[i-1]+a[i];
		f[i][i] = 0;
	} 
	for (int len=2;len<=n;len++) // 枚举区间的长度,先处理小区间 
	{
		for (int l=1;l+len-1<=n;l++) // 枚举左端点 
		{
			int i = l, j = l+len-1; // 计算左右端点 
			for (int k=i;k<j;k++) // 枚举中间点 k,状态转移 
			{
				f[i][j] = min(f[i][j], f[i][k]+f[k+1][j]+sum[j]-sum[i-1]);
			}
		}
	}
	cout << f[1][n]; // 输出结果 
	return 0;
} 

AC记录


例题2:

[NOI1995] 石子合并

题目描述

在一个圆形操场的四周摆放 N N N 堆石子,现要将石子有次序地合并成一堆,规定每次只能选相邻的 2 2 2 堆合并成新的一堆,并将新的一堆的石子数,记为该次合并的得分。

试设计出一个算法,计算出将 N N N 堆石子合并成 1 1 1 堆的最小得分和最大得分。

输入格式

数据的第 1 1 1 行是正整数 N N N,表示有 N N N 堆石子。

2 2 2 行有 N N N 个整数,第 i i i 个整数 a i a_i ai 表示第 i i i 堆石子的个数。

输出格式

输出共 2 2 2 行,第 1 1 1 行为最小得分,第 2 2 2 行为最大得分。

样例 #1

样例输入 #1

4
4 5 9 4

样例输出 #1

43
54

提示

1 ≤ N ≤ 100 1\leq N\leq 100 1N100 0 ≤ a i ≤ 20 0\leq a_i\leq 20 0ai20


思路:

我们设 f [ i ] [ j ] [ 0 ] f[i][j][0] f[i][j][0] 表示最小值, f [ i ] [ j ] [ 1 ] f[i][j][1] f[i][j][1] 表示最大值。

方程和转移同上,只相差了 m a x ( ) max() max() m i n ( ) min() min() 两个函数:

f [ i ] [ j ] [ 0 ] = m i n ( f [ i ] [ k ] [ 0 ] + f [ k + 1 ] [ j ] [ 0 ] + s u m ( i , j ) ) f[i][j][0] = min(f[i][k][0]+f[k+1][j][0]+sum(i, j)) f[i][j][0]=min(f[i][k][0]+f[k+1][j][0]+sum(i,j))

f [ i ] [ j ] [ 1 ] = m a x ( f [ i ] [ k ] [ 1 ] + f [ k + 1 ] [ j ] [ 1 ] + s u m ( i , j ) ) f[i][j][1] = max(f[i][k][1]+f[k+1][j][1]+sum(i, j)) f[i][j][1]=max(f[i][k][1]+f[k+1][j][1]+sum(i,j))

可以发现,这样只是多了环形的条件,我们考虑如何解决环形的问题。

例如,题中的环如下图:

数据:

4
1 2 3 4

我们把原本的数组延长一倍,让 a [ 1 ] = a [ n + 1 ] a[1] = a[n+1] a[1]=a[n+1], a [ 2 ] = a [ n + 2 ] a[2] = a[n+2] a[2]=a[n+2], a [ i ] = a [ n + i ] a[i] = a[n+i] a[i]=a[n+i]

延长后的数组:1 2 3 4 1 2 3 4

该数组的下标:1 2 3 4 5 6 7 8

我们要把环拆成链:

  1. 可以从 1 1 1 处拆,得到 1 2 3 4,可以发现这正是延长后的数组中第 1 1 1 项到第 1 + 4 − 1 = 4 1+4-1 = 4 1+41=4 项。

  2. 可以从 2 2 2 处拆,得到 2 3 4 1,可以发现这正是延长后的数组中第 2 2 2 项到第 2 + 4 − 1 = 5 2+4-1 = 5 2+41=5 项。

可以发现规律:从第 i i i 项拆开得到的序列在延长后的数组对应区间 [ i , i + n − 1 ] [i, i+n-1] [i,i+n1]

利用这个,我们可以对延长后的数组进行 DP,取每一个区间 [ i , i + n − 1 ] [i, i+n-1] [i,i+n1]的最大和最小值即可。

代码:
#include <iostream>
using namespace std;
#define int long long
int f[205][205][2];
int a[205], sum[205];
// f[i][j][0] -> 最小
// f[i][j][1] -> 最大
signed main()
{
	int n;
	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++)
	{
		for (int j=1;j<=2*n;j++)
		{
			f[i][j][0] = 1000000000;
			f[i][j][1] = 0;
		}
	}
	for (int i=1;i<=2*n;i++)
		f[i][i][0] = f[i][i][1] = 0;
	for (int i=1;i<=2*n;i++)
		sum[i] = sum[i-1]+a[i];
	for (int len=2;len<=2*n;len++)
	{
		for (int i=1;i+len-1<=2*n;i++)
		{
			int l = i, r = i+len-1;
			for (int k=l;k<r;k++)
			{
				f[l][r][0] = min(f[l][r][0], f[l][k][0]+f[k+1][r][0]+(sum[r]-sum[l-1]));
				f[l][r][1] = max(f[l][r][1], f[l][k][1]+f[k+1][r][1]+(sum[r]-sum[l-1]));
			}
		}
	}
	int Max = 0, Min = 10000000000;
	for (int i=1;i+n-1<=2*n;i++)
	{
		Max = max(Max, f[i][i+n-1][1]);
		Min = min(Min, f[i][i+n-1][0]);
	}
	cout << Min << endl << Max;
	return 0;
}

AC记录

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值