zoukankan      html  css  js  c++  java
  • 区间 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 ;
    }
    

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

  • 相关阅读:
    Security headers quick reference Learn more about headers that can keep your site safe and quickly look up the most important details.
    Missing dollar riddle
    Where Did the Other Dollar Go, Jeff?
    proteus 与 keil 联调
    cisco router nat
    router dhcp and dns listen
    配置802.1x在交换机的端口验证设置
    ASAv931安装&初始化及ASDM管理
    S5700与Cisco ACS做802.1x认证
    playwright
  • 原文地址:https://www.cnblogs.com/Zmonarch/p/14829326.html
Copyright © 2011-2022 走看看