• 区间 DP


    【前序】

    本来是打算写完背包再写区间 (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 o j) 的最小得分,那么我们考虑一下其中的断点会怎么给他转移。
    那么显然我们是有 (f_{i,j} = min_{kin[i,j]}f_{i,k}+f_{k+1,j} + Sum(i o 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
    %lld" , ans1 , ans2) ;
    	return 0 ; 
    }
    

    P3146 248 G

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

    【description】

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

    【solution】

    这个题很显然的比较简单,我们仍是设 (f_{i,j}) 表示能够合并区间 (i o 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
    " , ans) ; 
    	return 0 ;
    }
    

    【You Are the One】

    【description】:

    因为题目是英文的,所以这里给出大体意思:
    就是有 (n) 个男生去寻找伴侣,每一个男生有自己固定的指数 (d_i),寻找伴侣的过程是有序的,假设第 (i) 个男生找伴侣时,他的愤怒值为 ((i - 1) imes d_i) , 现在求解所有人的最小的总愤怒值。 (nleq 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 imes (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
    " , oi , f[1][n]) ; 
    	} 
    	return 0 ; 
    }
    

    【CF149D Coloring Brackets】

    先咕一下。


    【只取一端的套路】

    P2858 [USACO06FEB]Treats for the Cows G/S

    【description】:

    给定长度为 (n) 的序列 (a) ,求解按照规则取完这个序列的最大值。规则 : 假设当前取为第 (k) 次取,取的时候只能够从最前面或者最后面取,并且当前取的贡献为 (a_i imes k) , (nleq 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
    " , 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
    " , 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
    " , (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
    " , 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
    " , std::min(f[1][n + 1][1] , f[1][n + 1][0])) ; 
    	return 0 ;
    }
    

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

  • 相关阅读:
    cookie、 Session Storage 、 Local Storage
    json 常用的方法
    ssm 框架 使用ajax异步,实现登陆
    ssm框架整合,配置文件中的配置内容
    ipv4和ipv6的区别
    分析域名的解析过程
    网络体系结构总结
    线程同步和异步
    博客目录
    [STM32H743]按键控制LED
  • 原文地址:https://www.cnblogs.com/Zmonarch/p/14829326.html
Copyright © 2020-2023  润新知