区间dp-

文章详细探讨了区间动态规划在解决字符串配对、回文保持、气球爆破和队形排序等问题中的应用,涉及端点讨论和范围划分点两种可能性展开方法,以及暴力递归和记忆化搜索的优化技巧。
摘要由CSDN通过智能技术生成

间dp就是在区间上进行动态规划,求解一段区间上的最优解。主要是通过合并小区间的最优解进而得出整个大区间上最优解的 dp 算法。

既然让我求解在一个区间上的最优解,那么我把这个区间分割成一个个小区间,求解每个小区间的最优解,再合并小区间得到大区间即可。 

所以在代码实现上,我可以枚举区间长度len为每次分割成的小区间长度(由短到长不断合并),内层枚举该长度下可以的起点,自然终点也就明了了。然后在这个起点终点之间枚举分割点,求解这段小区间在某个分割点下的最优解。

可能性展开的常见方式:

1.基于两侧端点讨论的可能性展开

2.基于范围上划分点的可能性展开

基于两侧端点讨论的可能性题目

题目一

暴力解法:

直接采用递归的方法,对两侧端点进行讨论,取min

int cmp(char* s, int r, int l)
{
	if (r == l) //如果只有一个字符
		return 0;
	if (r + 1 == l) //如果只有两个字符
	{
		if (s[r] == s[l])
			return 0;
		else 
			return 1;
	}
	if (s[r] == s[l]) // 如果两个端点的字符相同
		return cmp(s, l+1, r-1);
	else
		return min(cmp(s, l+1, r)+1, cmp(s, l, r-1)+1);
}

当两个端点的字符不相同时,例如aab,我们有两种方式进行插入,我们先满足 l 位置与后面的位置相同,也就是在 b 的后面插入 a 时,那下一步的 r 保持不变,l + 1,当我们也可以满足 r 位置与前面的位置相同,也就是在 a 的前面插入 b 时,则下一步的 l 保持不变,r - 1

两个操作都加一,取最小值。

暴力的递归的方法递归层数太多,那我们就可以通过记忆化搜索

优化一

int f2(char* s, int l, int r, int (*dp)[10])
{
	if (dp[l][r] != -1)
		return dp[l][r]
	int ans;
	if (l == r)
	{
		ans = 0;
	}
	else if (l + 1 == r)
	{
		if (s[l] = s[r])
		{
			ans = 0;
		}
		else
		{
			ans = 1;
		}
	}
	else
	{
		if (s[l] == s[r])
			ans = f2(s, l+1, r-1, dp);
		else
			ans = min(f2(s, l+1, r, dp), f2(s, l, r-1, dp)) + 1;
	}
	dp[l][r] = ans;
	return ans;
}

也可以分析其严格依赖位置的动态规划

(分析可变参数的范围)

假设这个字符串长度为 5

那么我们就可以做出它的二维dp表(dp[i][j] 表示从 i 到 j 保持回文的最小操作次数)

因为 l 一定小于 r

所以它的表为:

当l == r的时候,操作次数一定为0

当l + 1 == r时

我们也可以直接求出对应的dp值

最终,我们要求的就是最右上角的格子(也就是dp[4][4])

那我们分析dp[i][j] 的位置依赖

从递归可知:

dp[i][j] 是由 dp[i+1][j-1],dp[i+1][j],dp[i][j-1] 三个位置得到

因此我们可以发现要求出dp[ i ][ j ]就必须先求出dp[ i+1 ][ j-1 ],dp[ i+1 ][ j ],dp[ i ][ j-1 ]

所以我们的求解顺序:

所以我们就可以直接求出dp表

# include <stdio.h>



int main()
{
	char s[10];
	int dp[10][10]; //表示从i到j位置保持回文的最少操作次数 
	int n = 10; //字符串长度 
	for (int i=0; i<n-1; ++i)
	{
		if (s[i] == s[i+1])
			dp[i][i+1] = 0;
		else
			dp[i][i+1] = 1;
	}
	for (int l=n-3; l>=0; --l) //从小到上 
		for (int r = l+2; r<n; ++r) //从左到右
		{
			if (s[l] == s[r])
				dp[l][r] = dp[l+1][r-1];
			else
				dp[l][r] = min(dp[l][r-1], dp[l+1][r]);
		}
		
}

也可以用一维dp优化空间

代码类似

题目二

暴力递归:

# include <stdio.h>

//考虑num[l …r]范围上的数字进行游戏,轮到玩家1
//返回玩家1最终能获得多少分数,玩家1和玩家2都绝顶聪明 
int cmp(int* num, int l, int r)
{
	if (l == r)
		return num[l];
	if (l + 1 == r )
		return max(num[l], num[r]);
	
	//当玩家1,拿num[l]后,轮到玩家2了,他考虑num[l+1, r],因为两个人都是 
	//顶聪明,所以玩家2一定拿max(num[l+1], num[r]),然后轮到玩家1面对的情况
	//一定是 min(cmp(num, l+2, r), cmp(num, l+1, r-1))
	int p1 = num[l] + min(cmp(num, l+2, r), cmp(num, l+1, r-1));
	//当玩家1,拿num[r]后 … 
	int p2 = num[r] + min(cmp(num, l+1, r-1), cmp(num, l, r-2));
	return max(p1, p2);
}

int main()
{
	int sum = 0;
	int num[10];
	for (int i=0; i<10; ++i)
	{
		scanf("%d", &num[i]);
		sum = sum + num[i];
	}
	int n = 10;
	int first = cmp(num, 0, n-1); //玩家1 
	int second = sum - first; //玩家2 
	
 } 

记忆化搜索

# include <stdio.h>
int dp[10][10]; 
//考虑num[l …r]范围上的数字进行游戏,轮到玩家1
//返回玩家1最终能获得多少分数,玩家1和玩家2都绝顶聪明 
int cmp(int* num, int l, int r)
{
	int ans;
	if (dp[l][r] != -1)
		return dp[l][r];
	if (l + 1 == r )
		ans = max(num[l], num[r]);
	
	else
	{
		int p1 = num[l] + min(cmp(num, l+2, r), cmp(num, l+1, r-1));
		int p2 = num[r] + min(cmp(num, l+1, r-1), cmp(num, l, r-2));
		ans = max(p1, p2);
	}
	return ans;
}

int main()
{
	memset(dp, -1, sizeof(dp));
	int sum = 0;
	int num[10];
	for (int i=0; i<10; ++i)
	{
		scanf("%d", &num[i]);
		sum = sum + num[i];
	}
	int n = 10;
	int first = cmp(num, 0, n-1); //玩家1 
	int second = sum - first; //玩家2 
	
 } 

也可以分析其严格依赖位置的动态规划

(分析可变参数的范围)

dp[i][j] 是由 dp[i + 2][j], dp[l+1][r - 1], dp[l][r-2]得到

代码于上题类似

基于范围上划分点的可能性展开

题目一

假设有6个顶点

它的values数组为:

最左顶点为0, 最右顶点为5

那么我们就可以直接选择划分点进行枚举

以上的三角形(0,1,5)+  三角形(1 ~ 5)的情况

以上的三角形(0,2,5)+ 三角形(0 ~ 2)+ 三角形(2 ~ 5)的情况

以上为三角形(0,3,5)+ 三角形(0 ~ 3)+ 三角形(3 ~ 5)的情况

还有一种情况就是三角形(0,4,5)+ 三角形(0 ~ 4)的情况

因此我们可以用记忆化搜索

代码:

# include <stdio.h>
int n;
int v[100];
int dp[100][100];
int cmp(int l, int r)
{
	int a;
	if (dp[l][r] != -1)
		return dp[l][r];
	if (l == r || l + 1 == r)
		a = 0;
	else
	{
		for (int i=l+1; i<r; ++i)
			a = min(a, cmp(0, i) + cmp(i, r) + a[l]*a[r]*a[i]);
	}
	dp[l][r] = a;
	return a;
}

int main()
{
	scanf("%d", &n);
	for (int i=0; i<n; ++i)
		scanf("%d", &v[i]);
	memset(dp, -1, sizeof(dp));
	int ans = cmp(0, n-1);
}

然后我们就可以查看它严格依赖位置的动态规划

假设n = 4

我们做出它的dp表:

情况一,当以1为划分点时

dp[0][4] = dp[0][1]+dp[1][4]

情况二,当以2为划分点时

dp[0][4] = dp[0][2]+dp[2][4]

情况三,当以3为划分点时

dp[0][4] = dp[0][3]+dp[3][4]

因此我们发现,一个点的dp值是由该点的左方格子和下方的格子构成

所以代码:

# include <stdio.h>
int n;
int v[100];
int dp[100][100];

int main()
{
	scanf("%d", &n);
	for (int i=0; i<n; ++i)
		scanf("%d", &v[i]);
	memset(dp, -1, sizeof(dp));
	for (int l=n-3; l>=0; --l)
		for (int r = l+2; r<n; ++r)
		{
			dp[l][r] = 100000;
			for (int m = l+1; m<r; ++m)
				dp[l][r] = min(dp[l][r], dp[l][m]+dp[m][r] + a[l]*a[r]*a[m]);
		}
}

题目二

首先我们要先预处理

在首尾都添加 1 

这样我们就处理1~5位置的气球,1位置和6位置的气球永远不打爆

方便我们处理边界问题

本题和上题找划分点不一样

本题不能以先打爆某个气球作为划分点

例如:

因此如果我们以先打爆某个气球作为划分点时,我们没有足够的信息去结算下一个状态

因此我们不能以先打爆某个气球作为划分点

我们要以最后打爆某个气球作为划分点

代码:

# include <stdio.h>
int dp[100][100];
memset(dp, -1, sizeof(dp));
int num[100];
int n;

//num[l ... r]表示这些气球决定一个顺序,获得最大得分返回
//一定有:num[r+1]一定没爆
//一定有:num[l-1]一定没爆
//如果没有上述条件,就不要调用com函数 
//尝试每个气球最后打爆 
int com(int l, int r)
{
	if (dp[l][r] != -1)
		return dp[l][r];
	int ans;
	if (l == r)
	//我们保证了l - 1 和 r + 1 的气球都没爆 
		ans = num[l]*num[l-1]*num[r+1];
	else
	{
		ans = max(num[l-1]*num[r+1]*num[l]+com(l+1, r), num[l-1]*num[r+1]*num[r]+com(l, r-1));
		for (int k=l+1; k<r; ++k)
			ans = max(ans, num[l-1]*num[r+1]*num[k] + com(l, k-1) + com(k+1, r));
	}
}

int main()
{
	scanf("%d", &n);
	for (int i=1; i<=n; ++i)
		scanf("%d", &num[i]);
	num[0] = 1;
	num[n+1] = 1;
	int ans = com(1, n);
}

更多

题目一:

我们分析出所有可能的情况:

当然,这两种情况可以结合在一起

因此我们在对字符串进行 dp 时,只用考虑这两种情况

代码:

# include <stdio.h>
# include <string.h>
char a[10];
int n = 10;
int dp[10][10];

int cmp(int l, int r)
{
	if (l == r)
		return 1;
	if (l+1 == r)
	{
		if (a[l] == '(' && a[r] == ')' || a[l] == '[' && a[r] == ']')
			return 0;
		else
			return 2;
	}
	if (dp[l][r] != -1)
		return dp[l][r];
	//情况一 : [l],[r]本来就是配对的 
	int p1 = 99999;
	if (a[l] == a[r])
		p1 = cmp(l+1, r-1);
	//情况二 : 基于每一个点进行左右划分 
	int p2 = 99999;
	for (int m=l; m<=r; ++m) 
	{
		p2 = min(p2, cmp(l, m) + cmp(m+1, r));
	}
	int ans = min(p1, p2);
	dp[l][r] = ans;
	return ans;
}

int main()
{
	memset(dp, -1, sizeof(dp));
	scanf("%s", a);
	printf("%d", cmp(0, n-1));
}

题目二

分析:

代码:

# include <stdio.h>
# include <string.h>
char a[10];
int n = 10;
int dp[10][10];

int cmp(int l, int r)
{
	if (dp[l][r] != -1)
		return dp[l][r];
	int ans;
	if (l == r)
	 	ans = 1;
	else if (l+1 = r)
	{
		if (a[l] == a[r])
			ans = 1;
		else
			ans = 2;
	}
	else
	{
		if (a[l] == a[r])
			ans = cmp(l, r-1);
		else
		{
			for (int m=l, m<=r; ++m)
				ans = min(ans, cmp(l, m)+cmp(m+1, r));
		}
	}
	dp[l][r] = ans;
	return ans;
}

int main()
{
	memset(dp, -1, sizeof(dp));
	scanf("%s", a);
	printf("%d", cmp(0, n-1));
}

题目三

我们假设有一个理想队形【4,6,2,8】

我们要以最后一个进入作为划分点进行dp

我们假设有一个理想队形【2,6,4,1】

因此我们要设一个三维dp数组

dp[l][r][0]表示:a[l]最后进来

dp[l][r][1]表示:a[r]最后进来

代码:

//严格按照位置的dp

# include <stdio.h>
# include <string.h>
int num[10];
int n = 10;
int dp[10][10][2];

int main()
{
	memset(dp, -1, sizeof(dp));
	for(int i=0; i<n; ++i)
		scanf("%d", &num[i]);
	for (int i=1; i<n; ++i)
	{
		if (num[i] < num[i+1])
		{
			dp[i][i+1][0] = 1;
			dp[i][i+1][1] = 1;
		}
	}
	for (int l=n-2; l>=1; ++l)
	{
		for (int r=l+2; r<=n; ++r)
		{
			if (num[l] < num[l+1])
				dp[l][r][0] = dp[l][r][0] + dp[l+1][r][0];
				
			if (num[l] < num[r])
				dp[l][r][0] = dp[l][r][0] + dp[l+1][r][0];
				
			if (num[l] < num[r])
				dp[l][r][1] = dp[l][r][1] + dp[l][r-1][1];
				
			if (num[r-1] < num[r])
				dp[l][r][1 = dp[l][r][1] + dp[l][r-1][1];
		}
	}
	
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值