ACM总结 - 区间DP

本文探讨了区间动态规划在解决环形石子儿合并与L型难题中的应用,通过扩展区间和巧妙利用前缀和解决了环形结构带来的挑战。此外,还介绍了涉及先手优势的博弈问题,通过寻找最优解确定先手最多能领先对手的分数。总结了动态规划在解决这类问题中的关键策略。
摘要由CSDN通过智能技术生成

区间动态规划

区间动态规划框架

...
memset(dp, 0, sizeof(dp));
for(len = 2; len <= n; len++)//区间动态规划是把一个大区间划分成若干个小区间的方式。这句代表小区间长度
    for(i = 1, j = len; j <= n; i++, j++)//枚举区间起点,划分[i, j]
        for(k = 1; k < j; k++){
            dp[i][j] = func(dp[i][j],...)
        }
...

秒解变式石子儿

在圆形操场上摆放着一行共n堆的石子。现要将石子有序地合并成一堆。规定每次只能选相邻的两堆合并成新的一堆,并将新的一堆石子数记为该次合并的得分。请编辑计算出将n堆石子合并成一堆的最小得分和将n堆石子合并成一堆的最大得分。

这一题和直线石子儿的区别是:直线石子儿是首尾不相连的,这个形成了一个闭环。起初,我想的解决这个题的方法是:把上面框架里的k增加一个(这种思想来源于高中生物必修三基因工程)。要把一个环切开,需要切两刀。因为之前对直线时设置k也就相当于设置一个断点。现在,切一下只能断开两端,是远远不够的。于是我尝试设置两个端点,划分成两个直线,再对每条直线求dp。
但随之而来的问题是,我根本写不出这样的实现。我推测可能是我思路错误或太复杂,于是换了一种思路。虽然这是一个环,但求得分后会产生一个计数缺口(原因是选用区间时就已经默认设置了首尾)。于是,应该可以把一个区间扩展成两个,比如一个以 3 , 1 , 2 3,1,2 3,1,2组成的环,可以像下面这样展开:
3 , 1 , 2 , 3 , 1 , 2 3,1,2,3,1,2 3,1,2,3,1,2
于是,得到的前缀和就是这样:
3 , 4 , 6 , 9 , 10 , 12 3,4,6,9,10,12 3,4,6,9,10,12
这样的前缀和,对环形是没有影响的。
这样,一个长度为n的数组会扩展成2n长度。因此,在框架中间选用 [ i , j ] [i, j] [i,j]的时候,要把 j < = n j<=n j<=n该换成 j < = 2 ∗ n j<=2*n j<=2n,同时,最后也不是cout出 d p [ 1 ] [ n ] dp[1][n] dp[1][n],而是循环查出 [ 1 , n ] [1, n] [1,n]区间里的最大/最小值。这是第二行的改变产生的改变。第二行其实相当于一个“单向等距双指针”(我自己起的),就是检查[1, n]后再检查[2, n + 1]一直检查到[n + 1, 2n]。而单纯是输出dp[1][2 * n]会产生错误,因为[i, j]再进行区间选择的时候会卡在n的位置。因此,完整代码如下:

#include <iostream>
#include <cstdio>
#include <cstring>

using namespace std;

#define Max 1000
#define INF 0x3f

int n, sum[Max], dp1[Max][Max], dp2[Max][Max];
int main(){
	while(~scanf("%d", &n)){
		int minx = INF, maxx = 0;
		sum[0] = 0;
		memset(dp1, INF, sizeof(dp1));
		memset(dp2, 0, sizeof(dp2));
		for(int i = 1; i <= n; i ++){
			scanf("%d", &sum[i]);
			sum[i + n] = sum[i];
			sum[i] += sum[i - 1];
			dp2[i][i] = 0;
			dp1[i][i] = 0;
		}
		for(int i = n + 1; i <= 2 * n; i++){
			sum[i] += sum[i - 1];
			dp1[i][i] = 0;
			dp2[i][i] = 0;
		}
		for(int len = 2; len <= n; len++){
			for(int i = 1, j = len; j <= 2 * n; i++, j++){
				for(int k = i; k < j; k++){
					dp1[i][j] = min(dp1[i][j], dp1[i][k] + dp1[k + 1][j] + sum[j] - sum[i - 1]);
					dp2[i][j] = max(dp2[i][j], dp2[i][k] + dp2[k + 1][j] + sum[j] - sum[i - 1]);
				}
			}
		}
		for(int i = 1; i <= n; i++){
			maxx = max(maxx, dp2[i][i + n - 1]);
			minx = min(minx, dp1[i][i + n - 1]);
		}
		cout << minx << " " << maxx << endl;
	}	
	return 0;
}

鲜有人做的L题

我看L题只有两个人做。也许在最中间,大家都以为“强迫症”去做两边儿的题。于是脑回路清奇的我偏挑这一题做。
题目的意思就是,有一组数字,每次可以从左边或者从右边取走连续的若干个数字,然后两个取,A先取,B后取,两个人都是尽量使自己比对方取的分数多,问最后A比B多多少。
这个题刚开始我还真的没思路。于是我最后只能去网上寻题解。主要是这还是个PDF格式的题,不好找。
不过我最终还是知道了方法:
首先我们用f[i][j]表示在取 i 到 j 范围内的数字,先取的人最大取到几,我们每次计算时就找它这个区间中间的连续区间f[i][k] f[k][j] (k从i到j) 然后把最小的值找出来,那么f[i][j]就等于这个区间所有数字的和减去这个最小值。
实现代码:

#include <iostream>
#include <cstdio>
#include <cmath>
#include <cstring>

using namespace std;

const int N = 105;

int n, num[N], sum[N], f[N][N];

int main() {
	while(scanf("%d", &n) && n != 0) {
        sum[0] = 0;
		for (int i = 1 ; i <= n ;i++) {
			scanf("%d",&num[i]);
			sum[i] = sum[i - 1] + num[i];
		}
		memset(f, 0, sizeof(f));
		for (int i = 1; i <= n; i++) f[i][i] = num[i];
		for (int i = 1; i <= n; i++)
			for(int j = 1; i + j <= n; j++){
				int m = 0, r = i + j, s = sum[i + j] - sum[j -1];
				for (int k = j; k <= r ; k++) {
					m = m < f[j][k] ? m : f[j][k];
					m = m < f[k][r] ? m : f[k][r];
				}
				f[j][r] = s - m;
            }
		printf("%d\n", f[1][n]  - (sum[n] - f[1][n]));
	}
    return 0;
}

总结

这周开了背包问题。此类的背包比贪心算法的背包问题在一定条件下更精确。其状态转移方程为:
f [ i ] [ v ] = m a x ( f [ i − 1 ] [ v ] , f [ i − 1 ] [ v − c [ i ] ] + w [ i ] ) f[i][v]=max(f[i-1][v],f[i-1][v-c[i]]+w[i]) f[i][v]=max(f[i1][v],f[i1][vc[i]]+w[i])
还是很好理解的。
这周还做了一些区间DP的题,题少,但精炼,值得细细回味。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值