区间动态规划专题

例1:
P1880 [NOI1995] 石子合并
这应该算是区间DP最典型的题了吧。
首先说一下这道题的小技巧吧 , 因为这个题是一个环上的DP , 因此我们可以断环成链 , 从而可以降低问题的复杂度。具体操作就是复制一段一摸一样的序列放到该序列的后面。
然后再说一下dp的事情 :
1.对于区间间断点的枚举 :倘若我们枚举左端点 , 然后枚举右端点 , 然后再从左端点到右端点中枚举间断点 , 这样就会使得答案不能完全覆盖(意思就是有一些状态在更新时用到的之前的状态没有更新过), 这样就造成了错误解。
其实我们可以枚举区间长度 , 然后枚举左端点 , 右端点就确定了 , 再在左端点到右端点中枚举间断点 , 这个就可以使得答案能完全覆盖。
2.关于初始化的问题:求最小值的dp数组要初始化为无穷大 , 求最大值的dp数组要初始化为无穷小 , 区间长度为1的初始值答案都为0。
Code:

#include <bits/stdc++.h>
using namespace std ;
int a[202] , sum[202] , dp_max[202][202] , dp_min[202][202] ; // 环上dp转链上dp
int n ;
int main() {    
    cin >> n ;
    for(int i = 1 ; i <= n ; i ++) cin >> a[i] , a[i+n] = a[i] ;
    for(int i = 1 ; i <= 2 * n ; i ++) sum[i] = sum[i-1] + a[i] ;
    memset(dp_min , 0x3f3f3f3f , sizeof(dp_min)) ; // 初始化 此处省略了dp_max的初始化
    for(int i = 1 ; i <= 2 * n ; i ++) dp_min[i][i] = 0 ;
    for(int lenth = 2 ; lenth <= n ; lenth ++) { // 按照区间长度枚举
        for(int i = 1 ; i + lenth - 1 <= 2 * n ; i ++) {
            int j = i + lenth - 1 ;
            for(int k = i ; k < j ; k ++) {
                dp_max[i][j] = max(dp_max[i][j] , dp_max[i][k] + dp_max[k+1][j] + sum[j] - sum[i-1]) ;
                dp_min[i][j] = min(dp_min[i][j] , dp_min[i][k] + dp_min[k+1][j] + sum[j] - sum[i-1]) ;
            }
        }
    }
    int ans_min = 0x7f7f7f7f , ans_max = 0 ;
    for(int i = 1 ; i + n - 1 <= 2 * n ; i ++) ans_min = min(ans_min , dp_min[i][i+n-1]) , ans_max = max(ans_max , dp_max[i][i+n-1]) ;
    cout << ans_min << endl << ans_max ;
    return 0 ;
}

例2:
P3146 [USACO16OPEN]248 G
这题和石子合并差不多 , 相对简单一些 , 代码只要在石子合并的代码上改动一些就行了 , 没什么要说的。
Code:

#include <bits/stdc++.h>
using namespace std ;
int n ;
int dp[500][500] ;
int ans ;
int main() {
    cin >> n ;
    for(int i = 1 ; i <= n ; i ++) cin >> dp[i][i] ;
    for(int lenth = 2 ; lenth <= n ; lenth ++) {
        for(int i = 1 ; i + lenth - 1 <= n ; i ++) {
            int j = i + lenth - 1 ;
            for(int k = i ; k < j ; k ++) 
                if(dp[i][k] == dp[k+1][j]) dp[i][j] = max(dp[i][j] , dp[i][k] + 1) ;
                ans = max(ans , dp[i][j]) ;
        }
    }
    cout << ans ;
    return 0 ;
}

例3:
P1063 [NOIP2006 提高组] 能量项链
这题简直是石子合并的翻版。。。
同样是断环成链 , 不同的只是合并出来的价值计算方法不太一样 , 套过来就行啦。
Code:

#include <bits/stdc++.h>
using namespace std ;
int dp[210][210] ;
int a[210] , n ;
int main() {
    cin >> n ;
    for(int i = 1 ; i <= n ; i ++) cin >> a[i] , a[i+n] = a[i] ;
    a[2*n+1] = a[1] ;
    for(int lenth = 2 ; lenth <= n ; lenth ++) {
        for(int i = 1 ; i + lenth - 1 <= 2 * n ; i ++) {
            int j = i + lenth - 1 ;
            for(int k = i ; k < j ; k ++) {
                dp[i][j] = max(dp[i][j] , dp[i][k] + dp[k+1][j] + a[i] * a[k+1] * a[j+1]) ;
            }
        }
    }
    int ans = 0 ;
    for(int i = 1 ; i + n - 1 <= 2 * n ; i ++) ans = max(ans , dp[i][i+n-1]) ;
    cout << ans ;
    return 0 ;
}

例4:
P1005 [NOIP2007 提高组] 矩阵取数游戏
通过观察可以发现 , 每一行的选数是各自独立的 , 所以我们只需要考虑每一行怎么求得最大值就可以了。
在每一行中 , 该问题就是一个区间DP了!
很容易想到状态转移方程:

dp[i][j] = max(dp[i-1][j] + a[row][i-1] * p[m-lenth] , dp[i][j+1] + a[row][j+1] * p[m-lenth]) ;

需要注意的是,之前做的DP都是区间长度递增的dp , 并且都要枚举中间点 , 然而这个题的dp是区间长度递减的 , 并且不需枚举中间点 , 需要注意的是当区间长度为1时 , 该问题是还没有处理完的(最后一个数也要选走) , 所以最后还要特殊处理一下 。

最最最重要的一点 , 会了区间DP你只能得到60parts , because 另外 40parts需要用到高精 , 本专题不做探讨 。
Code:

#include <bits/stdc++.h>
using namespace std ;
int n , m ;
long long p[32] ;
long long dp[32][32] ;
long long a[32][32] ;
long long ans ; 
int main() {
    cin >> n >> m ;
    for(int i = 1 ; i <= 30 ; i ++) p[i] = pow(2 , i) ;
    for(int i = 1 ; i <= n ; i ++)
    for(int j = 1 ; j <= m ; j ++)
    cin >> a[i][j] ;
    for(int row = 1 ; row <= n ; row ++) {
        memset(dp , 0 , sizeof(dp)) ;
        for(int lenth = m - 1 ; lenth >= 1 ; lenth --) {
            for(int i = 1 ; i + lenth - 1 <= m ; i ++) {
                int j = i + lenth - 1 ;
                dp[i][j] = max(dp[i-1][j] + a[row][i-1] * p[m-lenth] , dp[i][j+1] + a[row][j+1] * p[m-lenth]) ;
            }
        }
        long long maxx = 0 ;
        for(int i = 1 ; i <= m ; i ++) maxx = max(maxx , dp[i][i] + a[row][i] * p[m]) ;
        ans += maxx ;
    }
    cout << ans ;
    return 0 ;
}

例5:
P4170 [CQOI2007]涂色
这道题也算是个比较典型的区间DP题了。
状态挺好想的 , dp[i][j]表示将区间[i,j]修改为指定颜色所需要的最少操作数。
难点在转移方程上 , 应该怎么写。
首先 , 区间长度为1时 , 毫无疑问 , dp = 1 ;
当区间长度大于一时我们该考虑什么呢 ?
我们想一下一般的区间dp是枚举间断点的呀 , 所以这里我们也尝试通过枚举间断点来更新答案 ,显然可以 , 转移方程为:

dp[i][j] = min(dp[i][j] , dp[i][k] + dp[k+1][j]) ; 

但是我们忽略了一个问题 , 如果区间的两端要涂的颜色一样 , 我们这样是不是就浪费了一次涂色(我们可以直接将两端涂成同一种颜色 , 通俗的讲就是在涂一个端点的时候顺便把另一个端点也涂了 ,这样就充分利用了每一次涂色)。
因此 , 区间两端颜色相同的dp转移方程有点不同 :

dp[i][j] = min(dp[i+1][j] , dp[i][j-1]) ;

Code:

#include <bits/stdc++.h>
using namespace std ;
int dp[51][51] ;
char s[52] ;
int main() {
    cin >> s + 1 ;
    memset(dp , 0x7f7f7f7f , sizeof(dp)) ;
    for(int i = 1 ; i <= strlen(s + 1) ; i ++) dp[i][i] = 1 ;
    for(int lenth = 2 ; lenth <= strlen(s + 1) ; lenth ++) {
        for(int i = 1 ; i + lenth - 1 <= strlen(s + 1) ; i ++) {
            int j = i + lenth - 1 ;
            if(s[i] == s[j]) dp[i][j] = min(dp[i+1][j] , dp[i][j-1]) ;
            else for(int k = i ; k < j ; k ++) 
                dp[i][j] = min(dp[i][j] , dp[i][k] + dp[k+1][j]) ; 
        }
    }
    int lenth = strlen(s + 1) ;
    cout << dp[1][lenth] ;
    return 0 ;
}

例6:
P4302 [SCOI2003]字符串折叠
第一道字符串类的区间dp。
老套路 , 枚举区间长度然后枚举区间间断点 , 注意一下初始化。
问题是区间该如何合并呢 , 即状态转移方程怎么写 ?
假设我们不考虑左右两个区间的合并 , 那么转移方程就应该为:

dp[i][j] = min(dp[i][j] , dp[i][k] + dp[k+1][j]) ;

合并两个区间的前提是一个区间是另一个区间的子串。
因此我们只需要通过枚举每一位即可判断上述条件是否成立 。
成立了之后该怎么进行状态转移呢?
显然 :

dp[i][j] = min(dp[i][j] , dp[i][k] + get((j - i + 1) / (k - i + 1)) + 2) ;

函数get()是得到数字的长度 , 2 是左右两个括号的长度 。
这里注意是dp[i][k] , 而不是 i - k + 1 , 想想为什么 ?

注 :
这里只考虑了左边的区间长度小于等于右边区间长度的情况 , 其实想想 , 另外一种情况和这些情况是对称的 , 因此只处理一种情况即可。
Code:

#include <bits/stdc++.h>
using namespace std ;
string s ;
int dp[101][101] ;
bool check(int l , int mid , int r) {
    int p = mid - l + 1 ;
    for(int i = mid + 1 ; i <= r ; i ++)
        if(s[(i-mid-1)%p+l] != s[i]) return  0 ;
    return 1 ;
}
int get(int x) {
    if(x == 0) return 1 ;
    int ans = 0 ;
    while(x) {
        ans ++ ;
        x = x / 10 ;
    }
    return ans ;
}
int main() {
    cin >> s ;
    int n = s.size() ;
    memset(dp , 0x7f7f7f7f , sizeof dp) ;
    for(int i = 0 ; i < n ; i ++) dp[i][i] = 1 ;
    for(int lenth = 2 ; lenth <= n ; lenth ++) {
        for(int i = 0 ; i + lenth - 1 < n ; i ++) {
            int j = i + lenth - 1 ; 
            for(int k = i ; k < j ; k ++) {
                dp[i][j] = min(dp[i][j] , dp[i][k] + dp[k+1][j]) ;
                if((j - i + 1) % (k - i + 1) != 0) continue ;
                if(check(i , k , j)) dp[i][j] = min(dp[i][j] , dp[i][k] + get((j - i + 1) / (k - i + 1)) + 2) ;
            }
        }
    }
    cout << dp[0][n-1] ;
    return 0 ;
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值