码迷,mamicode.com
首页 > 其他好文 > 详细

区间 DP

时间:2021-06-02 18:09:37      阅读:0      评论:0      收藏:0      [点我收藏+]

标签:http   noi   还需要   避免   假设   大会   hnoi   四边形不等式   长度   

【前序】

本来是打算写完背包再写区间 \(DP\) 的,但是发现,好像区间 \(DP\) 耗费的时间不是太长,在机房的时间也不是十分充沛,所以先写一波区间 \(DP\)

你能学到什么

  • \(1.\) \(DP\) 的浅显的理解。
  • \(2.\) \(DP\) 的一些简单套路

【主要思想】

区间 \(DP\) 显然是以区间作为动态规划的阶段。去分成多个区间来搞。
这类大部分是有一个十分套路化的做法 :

  • 设状态转移的时候,没有什么特殊条件,一般设为 \(f_{l,r}\) , 意为区间 \([l,r]\) 的信息。
  • 枚举左右端点,再在 \(l , r\) 中枚举断点更新。
  • 更新的时候,取断点两端的信息进行合并。
for(qwq int len = 2 ; len <= n ; len++ ) //枚举区间长度
{
	for(qwq int l = 1 ; l + len - 1 <= n ; l++) // 枚举右端点
	{
		int r = l + len - 1 ; // 右端点
		for(qwq int k = l ; k < r ; k++) 
		{
			f[l][r] = … …
		}
	}
}

这种方法的更新则是显然的。

那一定是需要枚举断点吗? 显然是不一定的。
技术图片

我们考虑一下 \([l,r]\) 这个区间怎么扩展,或者说,怎么更新。我们可以换一种思路,这个区间可能是由 下图扩展而来的。使蓝色变成红色
技术图片

那么很简单的意思,就是你的这个区间不一定非要是 \([l,r]\) 的断点转移你才满意,虽然说 \(l+1\) 也是一个断点,但是这种方法就避免了你去遍历断点,从而在两个断点间实现最优的转移。本文中 【只取一端的套路】 可以说明这种的正确性。

这里提这个作用是为了能够开阔一下脑袋瓜,省的学死了。

【一个方法】:

做个题就知道的方法,断环成链,就是将一个本质上是环的 \(DP\) ,将其从 \(1,n\) 再进行复制一遍,我们能够得到一个长度为 \(2n\) 的一条链,我们知道这条链可以代替那个环,其正确性就是环能够顺时针或逆时针找到一个节点,那么这条链也能够到达。正确性没什么问题。

【一个简单的理解】

我们将其划分为区间的合并的时候,有时候是比较像递归的,但是递归是重复次数特别多的,记忆化搜索就不会,所以有时候在实行区间 \(DP\) 的时候,思想,动态规划用的区间 \(DP\),但是实现的时候用的是记忆化搜索之类的。


【合并套路】


【 (NOI1995)石子合并】

老题了,老生常谈的板子题。

【description】:

现有 \(n\) 个数组成一个环,求解将其合并为 \(1\) 个数的最大得分和最小得分。 只能合并相邻的两数,并且合并两数的得分为两个数之和。

【solution】:

这题本来是需要四边形不等式的,但是因为数据水,就直接暴力区间 \(DP\) 过了,为了防止在 \(T1\) 就死了,这里不给出四边形不等式的做法。并且这里直接给出区间 \(DP\) , 就不说错误的贪心了。

我们设 \(f_{i,j}\) 表示从 \(i\to j\) 的最小得分,那么我们考虑一下其中的断点会怎么给他转移。
那么显然我们是有 \(f_{i,j} = \min_{k\in[i,j]}f_{i,k}+f_{k+1,j} + Sum(i\to j)\)

【code】

int n ;  
int a[kmaxn] , f1[kmaxn][kmaxn] , f2[kmaxn][kmaxn] , sum[kmaxn]; // max,min 
signed main() {
	n = read() ; 
	for(qwq int i = 1 ; i <= n ; i++) a[i] = a[i + n] = read() ; 
	for(qwq int i = 1 ; i <= n + n ; i++) sum[i] = sum[i - 1] + a[i] ; 
	for(qwq int i = 2 ; i <= n ; i++) 
	{
		for(qwq int l = 1 , r ; l + i - 1 < n + n ; l++) 
		{
			r = l + i - 1 ; f1[l][r] = - inf ; f2[l][r] = inf ; 
			for(qwq int k = l ; k < r ; k++)
			{
				f1[l][r] = std::max(f1[l][k] + f1[k + 1][r] + sum[r] - sum[l - 1] , f1[l][r]) ; 
				f2[l][r] = std::min(f2[l][k] + f2[k + 1][r] + sum[r] - sum[l - 1] , f2[l][r]) ; 
			}
		}
	}
	int ans1 = inf , ans2 = - inf ; 
	for(qwq int i = 1 ; i <= n ; i++) 
	{
		ans1 = std::min(f2[i][i + n - 1] , ans1) ; 
		ans2 = std::max(f1[i][i + n - 1] , ans2) ;
	}
	printf("%lld\n%lld" , ans1 , ans2) ;
	return 0 ; 
}

P3146 248 G

这个问题和上面的有点像,准确的来说,这个更为简单一点,但还是决定将其放在第二位上,而非作为第一个题。

【description】

在一个序列里玩合并,只有相邻的两个并且相等才可以合并,合并造成的影响就是将数 \(+ 1\) , 求解在整个合并过程中最大数。和 \(2048\) 不一样, \(2048\) 直接翻倍。

【solution】

这个题很显然的比较简单,我们仍是设 \(f_{i,j}\) 表示能够合并区间 \(i\to j\) 的数。我们的状态转移方程也就是

\[f_{l,r} = max(f_{k+1,r} + 1) [ a_k = a_{k+1}] \]

就是相等(能够合并),取最大值,因为 \(1,n\) 不一定能够完全合并完 , 所以用一个变量取出来就好。

【code】

int f[kmaxn][kmaxn] , ans; 
signed main() {
	int n = read() ; 
	for(qwq int i = 1 ; i <= n ; i++) f[i][i] = read() ; 
	for(qwq int i = 2 ; i <= n ; i++) 
	{
		for(qwq int l = 1 , r ; l + i - 1 <= n ; l++)
		{
			r = l + i - 1 ; 
			for(qwq int k = l ; k < r ; k++) 
			{
				if(f[l][k] == f[k + 1][r] && f[l][k]) // 能够合并,也就是数目相同,并且需要保证这两个区间是能够合并的,也就是非 0 即可。 
				{
					f[l][r] = std::max(f[l][r] , f[l][k] + 1) ;
					ans = std::max(ans , f[l][r]) ;	
				} 
			}
		}
	}
	printf("%lld\n" , ans) ; 
	return 0 ;
}

【You Are the One】

【description】:

因为题目是英文的,所以这里给出大体意思:
就是有 \(n\) 个男生去寻找伴侣,每一个男生有自己固定的指数 \(d_i\),寻找伴侣的过程是有序的,假设第 \(i\) 个男生找伴侣时,他的愤怒值为 \((i - 1) \times d_i\) , 现在求解所有人的最小的总愤怒值。 \(n\leq 100 , d_i \leq 100\)

【solution】:

我们设 \(f_{i,j}\) 表示从第 \(i\) 个开始,到第 \(j\) 个结束,寻找伴侣的最小愤怒值。在状态转移的时候,我们还是枚举断点 \(k\) ,但是这里有些不同,我在这里卡了点时间。

首先我们是清楚的,对于 \(l,k\) 这个区间,\([k+ 1 ,r]\) 是需要等待的,也就是 \(cost(k+1 , r)\) 的怒气值,那么对于前面的区间来说 , 我们从 \([l ,k]\) 的小的区间来考虑,那么也就是 \([l+1 ,k]\) ,在上文中提到,区间 \(DP\) 我们可以将其理解是递归子序列对吧,那么我们在求解 \([l+1,k]\) 这一个序列完成后我们需要将 \(l\) 加上,那么 \(l\) 加上的代价是什么? 那必然是 \(l\) 一直等完这一个区间完成之后才行,所以我们需要加上 \(d_l \times (k - l)\)。同时我们考虑为什么后面的不加呢?实际上后面的已经算是加了,我们这里合并区间,对于 \(k\) 往后的已经是递归处理掉了,所以这里只需要考虑 \(l,k\) 这一个区间对其的影响就好。

如没有看明白,则请移居代码,因为笔者一开始想歪了,后来才转过来的,所以比较讲不是特别的清晰,内容中可能还含有之前的残余。

【Code】

void clear() {
	memset(f , 0 , sizeof(f)) ; 
	memset(d , 0 , sizeof(d)) ; 
	memset(sum , 0 , sizeof(sum)) ; 
}
signed main() {
	int T = read() ; 
	for(qwq int oi = 1 ; oi <= T ; oi++) 
	{
		n = read() ; clear() ; 
		for(qwq int i = 1 ; i <= n ; i++) 
		d[i] = read() , sum[i] = sum[i - 1] + d[i] ;
		for(qwq int len = 2 ; len <= n ; len++) 
		 for(qwq int l = 1 ; l + len - 1 <= n ; l++) 
		  {
			int r = l + len - 1 ; 
			f[l][r] = inf ; 
			for(qwq int k = l ; k <= r ; k++) 
			 f[l][r] = std::min(f[l][r] , f[l + 1][k] + f[k + 1][r] + d[l] * (k - l) + (sum[r] - sum[k]) * (k - l + 1)) ;
		  }
		printf("Case #%lld: %lld\n" , oi , f[1][n]) ; 
	} 
	return 0 ; 
}

【CF149D Coloring Brackets】

先咕一下。


【只取一端的套路】

P2858 [USACO06FEB]Treats for the Cows G/S

【description】:

给定长度为 \(n\) 的序列 \(a\) ,求解按照规则取完这个序列的最大值。规则 : 假设当前取为第 \(k\) 次取,取的时候只能够从最前面或者最后面取,并且当前取的贡献为 \(a_i \times k\) , \(n\leq 2000 , a_i \leq 1000\)

【solution】:

非常简单的一个题,状态不必说为 \(f_{l,r}\) 为区间 \([l,r]\) 的最优解,但是我们发现贡献和当前取的次数有关,那么我们是否还需要再开一维维护第几次取?

不必,我们直接记忆化搜索即可,记忆化搜索可以直接让我们进入下一次进行乱搞。

状态转移方程 : \(f_{l,r,k} = max(f_{l+1,r,k+1}+k*a_l , f_{l,r-1,k+1} +k *a_r)\)

这里的 \(k\) 就是第几次取了,直接记忆化搜索里面维护就好了。

【code】:

int n , a[kmaxn] ; 
int f[kmaxn][kmaxn] ; 
qaq int dp(int l , int r , int num) {
	if(num == n + 1) return 0 ; 
	if(f[l][r]) return f[l][r] ; 
	int ret = std::max(dp(l + 1 , r , num + 1) + a[l] * num , dp(l , r - 1 , num + 1) + a[r] * num) ;
	return f[l][r] = ret ;
}
signed main() {
	n = read() ; 
	for(qwq int i = 1 ; i <= n ; i++) a[i] = read() ; 
	printf("%lld\n" , dp(1 , n , 1)) ;
	return 0 ;
}

P2890 [USACO07OPEN]Cheapest Palindrome G

【description】:

给定一个长度为 \(m\) 的字符串,希望通过增加或者删除某一个字符从而使得该字符串为回文字符串,但是增加或者删除某一个字符都会有一个贡献,现在求解最小贡献。

\(m \leq 2000\)

【solution】:

一开始想坑里去了

我们仍是设 \(f_{i,j}\) 表示将区间 \([i,j]\) 转变为回文字符串的最小贡献。我们考虑一下就是我们什么样的叫做回文字符串。是不是长度不为 \(1\) 的回文字符串必然关于其对称中心对称?

那么我们类比马拉车算法,枚举对称中心 ? 啥也能类比,我服我自己。

那么我们如果找到 \(s_i = s_j\) 说明什么,这个这个区间的左右端点就是对称的,不管这个区间如何,那么我们显然是不是就不需要管这两个节点了,我们最后不都是要将其转变为对称嘛,我们只要动就会添加贡献,显然这时候什么都不做就是最优的,那么这个时候就是 \(f_{i,j} = f_{i+1,j-1}\)

那么我们找不到怎么办?我们分四种情况来搞一下就好。

  • \(1.\)\(i\) 之前插入 \(s_j\) , 那么我们就知道 \(s_{i - 1} = s_j\) , 我们根据上面的,我们直接找 \(f_{i , j - 1}\)
  • \(2.\) 我直接删掉 \(s_l\),那么我们去寻找 \(f_{i + 1 , j}\)
  • \(3.\) 我在 \(j\) 之后插入 \(s_i\) , 去寻找 \(f_{i + 1 , j}\)
  • \(4.\) 我直接删掉 \(s_j\) , 我们继续去找 \(f_{i , j - 1}\)
    最后的时候,我们直接加上每一步操作的贡献就好了。

【code】

int n , m ;
char s1[kmaxn] ;
int c[kmaxn][2] , s[kmaxn] , f[kmaxn][kmaxn]; 
signed main() {
	n = read() , m = read() ; 
	std::cin >> s1 + 1 ; 
	for(qwq int i = 1 ; i <= m ; i++) s[i] = s1[i] - ‘0‘ ; 
	for(qwq int i = 1 ; i <= n ; i++)  
	{
		char a ; std::cin >> a ; 
		c[a - ‘0‘][1] = read() ; // 加 
		c[a - ‘0‘][0] = read() ; // 删 
	}
	// 发现一开始想了两种不可行的做法
	for(qwq int len = 2 ; len <= m ; len++) 
	{
		for(qwq int l = 1 , r ; l + len - 1 <= m ; l++ ) 
		{
			r = l + len - 1 ; 
			if(s[l] == s[r]) f[l][r] = f[l + 1][r - 1] ; 
			else 
			{
				f[l][r] = inf ; 
				f[l][r] = std::min(f[l][r] , f[l][r - 1] + c[s[r]][1]) ; // 1 
				f[l][r] = std::min(f[l][r] , f[l + 1][r] + c[s[l]][0]) ; // 2
				f[l][r] = std::min(f[l][r] , f[l + 1][r] + c[s[l]][1]) ; // 3
				f[l][r] = std::min(f[l][r] , f[l][r - 1] + c[s[r]][0]) ; // 4
			}
		}
	} 
	printf("%lld\n" , f[1][m]) ; 
	return 0 ;
}

【P3205 [HNOI2010]合唱队】

【description】:

给定一串序列,问有多少种初始序列经过如题操作可以得到此序列。

【solution】:

这里的状态稍微有所变化, \(f_{l,r,0|1}\) , \(f_{i,j,0}\) 表示第 \(i\) 个人从左边来的方案数,\(f_{i,j,1}\) 表示第 \(j\) 个人从右边来的方案数。

在转移的时候我们还是需要判断一下:
从左边进来肯定前1个人比他高,前 \(1\) 个人有 \(2\) 种情况,要么在 \(i+1\) 号位置,要么在 \(j\) 号位置。

从右边进来肯定前 \(1\) 个人比他矮,前 \(1\) 个人有 \(2\) 种情况,要么在 \(j-1\) 号位置,要么在 \(i\) 号位置。

状态转移涉及到一个判断,我以代码块的形式给出 :

		if(a[l] < a[l + 1]) f[l][r][0] += f[l + 1][r][0] ; 
		if(a[l] < a[r]) f[l][r][0] += f[l + 1][r ][1] ;
		if(a[l] < a[r]) f[l][r][1] += f[l][r - 1][0] ; 
		if(a[r] > a[r - 1]) f[l][r][1] += f[l][r - 1][1] ; 

【code】

signed main()
{
	n = read() ; 
	for(int i = 1 ; i <= n ; i++) a[i] = read() ; 
	for(int i = 1 ; i <= n ; i++) f[i][i][0] = 1 ;
	for(int len = 1 ; len <= n ; len++) 
	{
		for(int l = 1 ; l + len <= n ; l++)
		{
			int r = l + len ;
			if(a[l] < a[l + 1]) f[l][r][0] += f[l + 1][r][0] ; 
			if(a[l] < a[r]) f[l][r][0] += f[l + 1][r ][1] ;
			if(a[l] < a[r]) f[l][r][1] += f[l][r - 1][0] ; 
			if(a[r] > a[r - 1]) f[l][r][1] += f[l][r - 1][1] ; 
			f[l][r][0] %= kmod ; 
			f[l][r][1] %= kmod ; 
		}
	}
	printf("%lld\n" , (f[1][n][0] + f[1][n][1] ) %kmod ) ; 
	return 0 ;
}

【关路灯套路】

这类问题因为某些特殊情况,从而导致我一下子取整个区间会更优秀,所以这时候我们就将整个区间取完,这个时候,我们就会发现,我们取完区间后,只可能在两个位置 ,左端点和右端点,那么我们一般将状态设为 \(f_{l,r,1|0}\) 表示我取完 \([l,r]\) 这一整个区间后,在右|左端点的最优解。

【P1220 关路灯】

【description】 :

直接见题面,笔者这里不大会简化了。

【solution】:

我们这里根据上述的套路,我们就设状态为 \(f_{i,j,0|1}\) 表示我关闭掉 \([i,j]\) 这个区间在左右端点的最小耗电。那我们考虑一下状态转移。

  • 我们考虑 \(f_{i,j,0}\) 从何处而来,我们发现必然是从区间 \([l+1,r]\) 这一个区间来的,假如说,我们从区间 \([l , r- 1]\)来的,那就有一个问题了,我们 \([l+1,r-1]\) 这一段必然是已经走过了,再走回来显然不是最优的,所以我们就只会从 \([l + 1 ,r]\) 这个区间内转移。那么我们让老王去关 \(l\) 这个灯的时候发现,\(l\) 之前的灯会继续开着, \(r + 1 , n\) 这个也会继续开着, 我们加上他们花费。
  • 同理, \(f_{i,j,1}\) 也是同样的转移。

状态转移方程直接看一下代码吧。

【code】

int f[kmaxn][kmaxn][2] , pos[kmaxn] , b[kmaxn] , sum[kmaxn];  
signed main() {
	n = read() , c = read() ; 
	for(qwq int i = 1 ; i <= n ; i++) 
	pos[i] = read() , b[i] = read() , sum[i] = sum[i - 1] + b[i] ; 
	for(qwq int i = 1 ; i <= n ; i++) 
	 for(qwq int j = 1 ; j <= n ; j++) 
	  f[i][j][0] = f[i][j][1] = inf ; 
	f[c][c][1] = f[c][c][0] = 0 ; 
	for(qwq int len = 1 ; len <= n ; len++) 
	{
		for(qwq int l = 1 , r ; l + len - 1 <= n ; l++) 
		{
			r = l + len - 1 ; if(l == r) continue ; 
			f[l][r][0] = std::min(f[l][r][0] , f[l + 1][r][0] + (pos[l + 1] - pos[l]) * (sum[l] + sum[n] - sum[r])) ; 
			f[l][r][0] = std::min(f[l][r][0] , f[l + 1][r][1] + (pos[r] - pos[l]) * (sum[l] + sum[n] - sum[r])) ; 
			f[l][r][1] = std::min(f[l][r][1] , f[l][r - 1][0] + (pos[r] - pos[l]) * (sum[l - 1] + sum[n] - sum[r - 1])) ; 
			f[l][r][1] = std::min(f[l][r][1] , f[l][r - 1][1] + (pos[r] - pos[r - 1]) * (sum[l - 1] + sum[n] - sum[r - 1])) ;
		}
	}
	printf("%lld\n" , std::min(f[1][n][1] , f[1][n][0])) ; 
	return 0 ;
}

P3080 [USACO13MAR]The Cow Run G/S

【description】:

和关路灯差不多,建议直接看题面,这里不给出,因为是双倍经验。

【solution】:

这个题和关路灯就几乎一样的。就是加了一个离散化。

【code】:

signed main() {
	n = read() ; 
	for(qwq int i = 1 ; i <= n ; i++) pos[i] = read(); 
	std::sort(pos + 1 , pos + 1 + n) ; 
	c = std::lower_bound(pos + 1 , pos + n + 1 , 0) - pos ; 
	for(qwq int i = n ; i >= c ; i--) pos[i + 1] = pos[i] ; // 加入 0 点,0以后的向后移动 
	memset(f , 63 , sizeof(f)) ;
	f[c][c][0] = f[c][c][1] = 0 ;  pos[c] = 0 ; 
	for(qwq int len = 2 ; len <= n + 1 ; len++) 
	{
		for(qwq int l = 1 , r ; l + len - 1 <= n + 1 ; l++) 
		{
			r = l + len - 1 ;
			f[l][r][0] = std::min(f[l][r][0] , f[l + 1][r][0] + (pos[l + 1] - pos[l]) * (n - r + 1 + l)) ; 
			f[l][r][0] = std::min(f[l][r][0] , f[l + 1][r][1] + (pos[r] - pos[l]) * (n - r + 1 + l)) ; 
			f[l][r][1] = std::min(f[l][r][1] , f[l][r - 1][0] + (pos[r] - pos[l]) * (n - r + 1 + l)) ; 
			f[l][r][1] = std::min(f[l][r][1] , f[l][r - 1][1] + (pos[r] - pos[r - 1]) * (n - r + 1 + l)) ;
		}
	}
	printf("%lld\n" , std::min(f[1][n + 1][1] , f[1][n + 1][0])) ; 
	return 0 ;
}

如有认为可以补充,则可以私信笔者,予以添加,感谢。

区间 DP

标签:http   noi   还需要   避免   假设   大会   hnoi   四边形不等式   长度   

原文地址:https://www.cnblogs.com/Zmonarch/p/14829326.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!