2023-05-03 线性模型与区间DP

6 篇文章 0 订阅
5 篇文章 0 订阅

线性模型与区间DP

1 线性模型

基本概念

  • 这里的线性是指状态的排布是线性的
  • 线性模型是动态规划中最常用的模型

一般的代码模型是:

for(int i = 0; i < n; i++) {
    for(j = 0; j < i; j++) {
        // Todo: 更新dp的具体逻辑
    }
}

最典型的一个例题:最长上升子序列

第02章_动态规划简介.md#举例2从n个数的数组a中找出最长上升子序列的元素个数

另一个例题:POJ3486 买电脑

假设上一次买电脑是第j年,那么1~j-1年就是一个子问题,我们已经算出了f[j-1]是满足这个问题的最优解,后面我们就不用考虑前j-1年的情况,且他们也不会影响我们后面的决策,即满足最优子问题无后效性原则,所以可以用动态规划来做

题干概括

要求n年中每年你都要有电脑可用,每年你都可以选择购买一台新电脑,其固定费用是c。同时维护这台电脑从当前第x年到第y年的总费用是m(x,y) (也就是在第x年到第y年内不需要再花费别的钱维护或者购置)。问最少花费多少才能满足要求。

题目数据多组,先输入费用c,之后输入n,后面输入n行数据,假设行号为 0 ≤ x < n 0≤x<n 0xn,则每行的数据表示第x年买电脑后,后面[x, n)期间几年的累计维修费。

示例数据解析
3 # 买一台新电脑的花费
3 # 打算在3年间都保持有电脑可用
5 7 50 # 第0年(即一开始)就买电脑,在1、2、3年间时累计的维修费
6 8 # 第1年买电脑,在第2、3年间的维修费
10 # 第2年买电脑,在第3年的维修费
动态规划分析

dp[i]表示第i年产生的所有电脑花费(不管买多少电脑和花多少维修费)

  • 阶段:dp[i]即前i年。 0 ≤ i < n 0≤i<n 0in
  • 状态:状态变量为j,表示在前i年的第j年买了新电脑, 0 ≤ j < i 0≤j<i 0j<i
  • 决策:j所有的取值产生的花费情况

    第j年买电脑,则前i年的总花费分为两个阶段

    • [0, j)年:即前j-1年的电脑花费,从dp的定义可知公式为 d p [ j − 1 ] dp[j - 1] dp[j1]
    • [j, i)年:即此段时间内的维修费用+买一台新电脑的费用 m [ j ] [ i ] + c m[j][i] + c m[j][i]+c
  • 策略:求所有决策中花费最小的情况,即最优策略是对应闫氏DP分析法中min情况
  • 状态转移表达式:满足最优策略,则表达式为 d p [ i ] = m i n { d p [ i ] , d p [ j − 1 ] + m [ j ] [ i ] + c ∣ 0 ≤ i < n 并且 0 ≤ j < i } dp[i] = min\lbrace dp[i], dp[j-1] + m[j][i] + c \mid 0≤i<n 并且 0≤j<i \rbrace dp[i]=min{dp[i],dp[j1]+m[j][i]+c0in并且0j<i}
代码实现
/**
 * 买电脑
 * https://ac.nowcoder.com/acm/problem/108083
*/
#include <iostream>
#include <vector>

#define INF 0x3f
#define MAX_YEAR 10050

using namespace std;
int c, n;

int m[MAX_YEAR][MAX_YEAR], dp[MAX_YEAR];; // 数组尽量不要用vector,因为用例每次跑回累积上一次的结果的,用vector还得记得清理

int solve() {
    memset(dp, INF, sizeof(dp)); // 初始化为最大值
    dp[0] = 0; // 没有电脑花费自然为0
    for (int i = 1; i <= n; i++) { // 阶段i
        for (int j = 1; j <= i; j++) { // 状态j
            dp[i] = min(dp[i], dp[j - 1] + m[j][i] + c); // 状态j转移中更新最小值
        }
    }
    return dp[n];
}

int main() {
    // 用scanf和printf而不是cin和cout,性能会提高很多(从1563ms降到了344ms)
    while (scanf("%d%d", &c, &n) != EOF) { // 还没读到结尾就继续读,因为可能有多组数据
        for (int i = 1; i <= n; i++) { // 因为题目中说明了m[i][j]中的i和j都是从下标1开始
            for (int j = i; j <= n; j++) {
                scanf("%d", &m[i][j]); // 注意i和j的顺序
            }
        }
        printf("%d\n", solve());
    }
    return 0;
}

2 区间DP

基本概念

区间DP,顾名思义,就是在区间上的DP

一般是先算出小区间的DP得到最优解,再去得到大区间的最优解

一般的状态是假设f[i][j]是区间[i, j]的最优解,那么f[i][j]都可以由[i, j]的自区间的最优解更新得到

通用的模板如下:

 // 直接dp,要先计算小区间的答案
int cal() {
    for (int i = 1; i <= n; i++) f[i][i] = 0; // 区间长度为1,即只有一堆,不需要额外费体力
    for (int len = 2; len <= n; len++) { // 闭区间的区间长度
        for (int i = 1; i + len - 1 <= n; i++) { // 注意是闭区间,所以计算区间长度需要减1。i + len - 1即表示区间右侧端点j,其值要小于总的区间最右端点n
            int j = i + len - 1; // [i, j], j - i + 1 = len,可以推导出j = i + len - 1
            for (int k = i; k < j; k++) { // 枚举区间内的点作为分隔点
                // Todo:根据具体问题更新f的值
            }
        }
    }
    return f[1][n];
}

例题1:KOJ6023_合并果子-改

注意是相邻两堆才能合并,所以不能使用贪心(每次选择质量最小的两堆合并,合并完再加入到优先队列中…如此反复,直到只剩一堆)搞,148.合并果子就是这个思路

试题描述

在一个果园里,多多已经将所有的果子打了下来,而且按果子的不同种类分成了不同的堆。多多把这些果子堆排成一排,然后所有的果子合成一堆。

每一次合并,多多可以把相邻两堆果子合并到一起,消耗的体力等于两堆果子的重量之和。可以看出,所有的果子经过n-1次合并之后,就只剩下一堆了。多多在合并果子时总共消耗的体力等于每次合并所耗体力之和。

因为还要花大力气把这些果子搬回家,所以多多在合并果子时要尽可能地节省体力。假定每个果子重量都为1,并且已知果子的种类数和每种果子的数目,你的任务是设计出合并的次序方案,使多多耗费的体力最少,并输出这个最小的体力耗费值。

例如有3种果子,数目依次为1,2,9。可以先将1、2堆合并,新堆数目为3,耗费体力为3。接着,将新堆与原先的第三堆合并,又得到新的堆,数目为12,耗费体力为12。所以多多总共耗费体力=3+12=15。可以证明15为最小的体力耗费值。

输入

包括两行,第一行是一个整数n,表示果子的种类数。第二行包含n个整数,用空格分隔,第i个整数 a i ( 1 ≤ a i ≤ 20000 ) a_i(1≤a_i≤20000) ai(1ai20000)是第i种果子的数目。

输出

包括一行,这一行只包含一个整数,也就是最小的体力耗费值。输入数据保证这个值小于2^63。

输入示例
4
1 2 5 2
输出示例
20
分析

区间DP是上来先把问题缩小为区间 [ i , j ] [i, j] [i,j]上的问题,本问题中 f [ i ] [ j ] f[i][j] f[i][j]来表示合并[i, j]即第i堆到第堆果子合并产生的最小代价

  • 阶段:区间[i, j]组成的不同区间

    i和j的枚举通过固定区间长度,变化i和j来实现

  • 状态:状态变量为k,表示区间的分隔线, i ≤ k ≤ j i≤k≤j ikj,此时区间分割为两部分 [ i , k ] [i, k] [i,k] [ k + 1 , j ] [k + 1, j] [k+1,j]

  • 决策:区间分隔点k的不同,决定了 f [ i ] [ k ] → f [ i ] [ j ] f[i][k]→f[i][j] f[i][k]f[i][j] f [ k + 1 ] [ j ] → f [ i ] [ j ] 的不同转移方向 f[k + 1][j]→f[i][j]的不同转移方向 f[k+1][j]f[i][j]的不同转移方向

  • 策略:枚举所有的k,获取 f [ i ] [ k ] → f [ i ] [ j ] f[i][k]→f[i][j] f[i][k]f[i][j] f [ k + 1 ] [ j ] → f [ i ] [ j ] f[k + 1][j]→f[i][j] f[k+1][j]f[i][j]的最优转换策略(体力耗费值最小)

  • 状态转移表达式

    根据 f [ i ] [ j ] f[i][j] f[i][j]的定义,生成 [ i , k ] [i, k] [i,k] [ k + 1 , j ] [k + 1, j] [k+1,j]这两堆本身就会花费 f [ i ] [ k ] + f [ k + 1 ] [ j ] f[i][k] + f[k + 1][j] f[i][k]+f[k+1][j]的代价(经过这些代价花费,[i, j]区间内的果子才合并成了 [ i , k ] [i, k] [i,k] [ k + 1 , j ] [k + 1, j] [k+1,j]两堆)。

    而合并这两堆过程中新花费的代价与k没有关系,因为就是 a [ i ] + a [ i + 1 ] + a [ i + 2 ] + . . . . . . + a [ j ] a[i] + a[i + 1] + a[i + 2] + ...... + a[j] a[i]+a[i+1]+a[i+2]+......+a[j],用前缀和s表示就是 s [ j ] − s [ l − 1 ] s[j] - s[l - 1] s[j]s[l1]

    因此我们得到动态规划的状态转移公式是: f [ i ] [ j ] = m i n { f [ i ] [ j ] , f [ i ] [ k ] + f [ k + 1 ] [ j ] + s [ j ] − s [ i − 1 ] ∣ 1 ≤ l e n ≤ n , 1 ≤ i ≤ n − l e n + 1 , i ≤ k ≤ j } f[i][j] = min \lbrace f[i][j], f[i][k] + f[k + 1][j] + s[j] - s[i - 1] \mid 1 ≤ len ≤ n, 1 ≤ i ≤ n - len + 1, i ≤ k ≤ j \rbrace f[i][j]=min{f[i][j],f[i][k]+f[k+1][j]+s[j]s[i1]1lenn,1inlen+1,ikj}

代码实现
#include <iostream>
#include <climits>

using namespace std;
#define MAX_N 110
int n, a[MAX_N], s[MAX_N], f[MAX_N][MAX_N];

int solve()
{
    for (int i = 1; i <= n; i++) {
        f[i][i] = 0;  // 区间长度只有1,即只有一堆,不需要额外费体力
    }
    for (int len = 2; len <= n; len++) {  // 枚举闭区间的区间长度
        int j;
        for (int i = 1; (j = i + len - 1) <= n; i++) {  // 注意是闭区间,所以计算区间长度需要减去1. j-i+1=len所以j=i+len-1
            int j = i + len - 1;
            for (int k = i; k < j; k++) {
                f[i][j] = min(f[i][j], f[i][k] + f[k + 1][j] + s[j] - s[i - 1]);
            }
        }
    }
    return f[1][n];
}

int main()
{
    scanf("%d", &n);
    a[0] = 0, s[0] = 0;
    for (int i = 1; i <= n; i++) {
        scanf("%d", &a[i]);
        s[i] = s[i - 1] + a[i];
    }
    for (int i = 0; i <= n; i++) {
        for (int j = 0; j <= n; j++) {
            f[i][j] = INT_MAX;
        }
    }
    printf("%d\n", solve());
    return 0;
}

例题2:括号匹配

给定一个只有()[]四种字符的字符串S,取出一个最长的子序列(不一定连续)使得他们满足括号匹配,比如([]])结果是4,([][][)结果是6

区间DP是上来先把问题缩小为区间 [ i , j ] [i, j] [i,j]上的问题,本问题中 f [ i ] [ j ] f[i][j] f[i][j]来表示区间[i, j]中的最长匹配子序列长度

  • 阶段:区间[i, j]组成的不同区间

    i和j的枚举通过固定区间长度,变化i和j来实现

  • 状态:状态变量为k,表示区间的分隔线, i ≤ k ≤ j i≤k≤j ikj,此时区间分割为两部分 [ i , k ] [i, k] [i,k] [ k + 1 , j ] [k + 1, j] [k+1,j]
  • 决策:区间分隔点k的不同,决定了 f [ i ] [ k ] → f [ i ] [ j ] f[i][k]→f[i][j] f[i][k]f[i][j] f [ k + 1 ] [ j ] → f [ i ] [ j ] 的不同转移方向 f[k + 1][j]→f[i][j]的不同转移方向 f[k+1][j]f[i][j]的不同转移方向
  • 策略:枚举所有的k,获取 f [ i ] [ k ] → f [ i ] [ j ] f[i][k]→f[i][j] f[i][k]f[i][j] f [ k + 1 ] [ j ] → f [ i ] [ j ] f[k + 1][j]→f[i][j] f[k+1][j]f[i][j]的最优转换策略(长度最大max)
  • 状态转移表达式

    分两种情况,注意这两种情况不是互斥的关系

    • S[i]和S[j]匹配,那么 f [ i ] [ j ] = f [ i + 1 ] [ j − 1 ] + 2 f[i][j] = f[i + 1][j - 1] + 2 f[i][j]=f[i+1][j1]+2
    • 也可以由两个子区间的答案合并而来,即 f [ i ] [ j ] = m a x ( f [ i ] [ j ] , f [ i ] [ k ] + f [ k + 1 ] [ j ] ) f[i][j] = max(f[i][j], f[i][k] + f[k + 1][j]) f[i][j]=max(f[i][j],f[i][k]+f[k+1][j])

代码实现如下

#include <iostream>

using namespace std;

#define MAX_N 500

int f[MAX_N][MAX_N];

/**
 * ([]])   4
 * ([][][) 6
 */
int solve(string &s)
{
    int n = s.length();
    for (int len = 2; len <= n; len++) {
        int j;  // 区间[i, j]的长度为len = j - i + 1,移项可以得到j = i + len -1
        for (int i = 1; (j = i + len - 1) <= n; i++) {
            if ((s[i] == '(' && s[j] == ')') || (s[i] == '[' && s[j] == ']')) {
                // 如果区间左右端点可以配成一对的话,更新动态规划结果
                f[i][j] = f[i + 1][j - 1] + 2;
            }
            // 经典区间dp过程,枚举区间[i, j]内端点
            for (int k = i; k < j; k++) {  // 注意下面有k+1,所以k不能是k≤j
                f[i][j] = max(f[i][j], f[i][k] + f[k + 1][j]);
            }
        }
    }
    return f[1][n];
}

int main()
{
    string s;
    cin >> s;
    s = " " + s; // 加一个空格是为了下标从1开始
    cout << solve(s) << endl;
    return 0;
}

3~6 知识精练

POJ1159 Palindrome

一个字符串最少添加几个字符变成回文串。典型的区间问题,代码如下,注意下标从0开始.

f[i][j]表示s[i…j]子字符串最少添加几个字符变成回文串

  • 阶段:区间[i,j],i和j的不同形成不同的区间,对应不同的子字符串s[i…j]
  • 状态:s[i]是否等于s[j]
  • 决策:s[i]等于或者不等于s[j]决定了不同的状态迁移
  • 策略:枚举所有可能的i和j, 0 ≤ i < n 0≤i<n 0in and i < j < n i<j<n ijn,在每一对i和j都找到最小的f[i][j],最终f[0][n-1]就是最优策略对应的值(min:最少插入的字符个数)
  • 状态转移表达式:把当前阶段根据S[i]和S[j]是否相等来分情况讨论
    • s [ i ] = = s [ j ] s[i] == s[j] s[i]==s[j]时,显然有 f [ i ] [ j ] = f [ i + 1 ] [ j − 1 ] f[i][j] = f[i + 1][j - 1] f[i][j]=f[i+1][j1]

      s[i]==s[j]说明s[i]和s[j]已经对应,这两个字符可以不用管了,问题转换为区间 [ i + 1 , j − 1 ] [i + 1, j - 1] [i+1,j1]添加多少个字符变成回文串,根据 f [ i ] [ j ] f[i][j] f[i][j]的定义显然就是等效求 f [ i + 1 ] [ j − 1 ] f[i+1][j-1] f[i+1][j1]

    • s [ i ] ! = s [ j ] s[i] != s[j] s[i]!=s[j]时,在左侧补上和右端点相同的字符,或者在右侧补上和左端点相同的字符,都可以让新的左右端点满足回文串要求,添加了一个字符,所以最后 + 1。即 f [ i ] [ j ] = m i n ( f [ i ] [ j − 1 ] , f [ i + 1 ] [ j ] ) + 1 f[i][j] = min(f[i][j - 1], f[i + 1][j]) + 1 f[i][j]=min(f[i][j1],f[i+1][j])+1
      • 在左侧补上和右端点相同的字符,即补上的字符==s[j],这俩字符已经对称,问题转换成区间 [ i , j − 1 ] [i, j - 1] [i,j1]补多少个字符变成回文串即 f [ i ] [ j − 1 ] f[i][j - 1] f[i][j1]
      • 在右侧补上和左端点相同的字符,即补上的字符==s[i],这俩字符已经对称,问题转换成区间 [ i + 1 , j ] [i + 1, j] [i+1,j]补多少个字符变成回文串即 f [ i + 1 ] [ j ] f[i + 1][j] f[i+1][j]
      • 状态f[i][j]应该是取上面两种情况的较小值 + 补上的一个字符,即得到状态转移表达式: f [ i ] [ j ] = m i n ( f [ i ] [ j − 1 ] , f [ i + 1 ] [ j ] ) + 1 f[i][j] = min(f[i][j - 1], f[i + 1][j]) + 1 f[i][j]=min(f[i][j1],f[i+1][j])+1

区间DP实现如下,但是没有枚举len,是因为i是从大到小地,(j = i + len - 1) <= n;会导致提前退出

#include <iostream>

using namespace std;

#define MAX_N 5010
int n; // 字符串长度
string s; // 字符串
// f的初始化隐含了一步,i==j即字符串长度为1时自动初始化为0了,即不需要加字符已经是回文串了,对于下面的if判断很重要
short f[MAX_N][MAX_N];// 如果直接DP,需要开5001*5001的数组,用int会超内存,可以用滚动数组优化将第一维度降为2

int solve() {
    // i == j是表示只有一个字符,区间长度为1的已经在f中自动初始化为0了
    for (int i = n - 1; i >= 0; i--) { // 之所以i从n-1向0从大到小遍历,就是为了能先计算出i+1;
        for (int j = i + 1; j < n; j++) { // j从i向n从小到大遍历,就是为了能先计算出j - 1
            if (s[i] == s[j]) {
                f[i][j] = f[i + 1][j - 1];
            } else {
                // 第一次走到这里不会出错是因为f的初始化隐含了一步,i==j即字符串长度为1时自动初始化为0了
                f[i][j] = min(f[i][j - 1], f[i + 1][j]) + 1; // f[i][j - 1]和f[i + 1][j]肯定都提前算好了
            }
        }
    }
    return f[0][n - 1];
}

int main() {
    cin >> n >> s;
    cout << solve() << endl;
    return 0;
}

UVA10617 Again Palindrome

给出一个字符串 s,你可以删除一些字符,使得剩下的字符串是一个回文串。求有多少种不同的删字符的方案。其实就是求子串中回文串的个数

f [ i ] [ j ] f[i][j] f[i][j]表示 s [ i . . j ] s[i..j] s[i..j]之间的回文串个数

  • 阶段:区间[i,j],i和j的不同形成不同的区间,对应不同的子字符串s[i…j]
  • 状态:s[i]是否等于s[j]
  • 决策:s[i]等于或者不等于s[j]决定了不同的状态迁移
  • 策略:枚举所有可能的i和j, 0 ≤ i < n 0≤i<n 0in and i < j < n i<j<n ijn,在每一对i和j都找到 S [ i . . j ] S[i..j] S[i..j]之间的回文串个数,目标是cnt(计数),最终 f [ 0 ] [ n − 1 ] f[0][n-1] f[0][n1]就是最优策略对应的值(cnt:回文串个数)
  • 状态转移表达式:根据s[i]和s[j]是否相等来分情况讨论
    • 如果 s [ i ] = = s [ j ] s[i] == s[j] s[i]==s[j], 那么
      • ①统计 f [ i + 1 ] [ j ] f[i + 1][j] f[i+1][j] f [ i ] [ j − 1 ] f[i][j - 1] f[i][j1],会把 f [ i + 1 ] [ j − 1 ] f[i + 1][j - 1] f[i+1][j1]统计两次,需要扣除一次,表达式为: f [ i + 1 ] [ j ] + f [ i ] [ j − 1 ] − f [ i + 1 ] [ j − 1 ] f[i + 1][j] + f[i][j - 1] - f[i + 1][j - 1] f[i+1][j]+f[i][j1]f[i+1][j1]
      • f [ i + 1 ] [ j − 1 ] f[i + 1][j - 1] f[i+1][j1]中的回文串正好可以再加上S[i]和S[j]再形成 f [ i + 1 ] [ j − 1 ] f[i + 1][j - 1] f[i+1][j1]个与①完全不同的回文串,表达式为: f [ i + 1 ] [ j − 1 ] f[i + 1][j - 1] f[i+1][j1]
      • ③ s[i]和s[j]这两个本身就可以构成1个回文串:1
      • f [ i ] [ j ] = ① + ② + ③ f[i][j] = ① + ② + ③ f[i][j]=++ = ( f [ i + 1 ] [ j ] + f [ i ] [ j − 1 ] − f [ i + 1 ] [ j − 1 ] ) + ( f [ i + 1 ] [ j − 1 ] ) + 1 = (f[i + 1][j] + f[i][j - 1] - f[i + 1][j - 1]) + (f[i + 1][j - 1]) + 1 =(f[i+1][j]+f[i][j1]f[i+1][j1])+(f[i+1][j1])+1 = f [ i + 1 ] [ j ] + f [ i ] [ j − 1 ] + 1 = f[i + 1][j] + f[i][j - 1] + 1 =f[i+1][j]+f[i][j1]+1
    • 如果 S [ i ] ! = S [ j ] S[i] != S[j] S[i]!=S[j], 那么
      • 要统计 f [ i + 1 ] [ j ] f[i + 1][j] f[i+1][j] f [ i ] [ j − 1 ] f[i][j - 1] f[i][j1],会把 f [ i + 1 ] [ j − 1 ] f[i + 1][j - 1] f[i+1][j1]统计两次,此时 f [ i + 1 ] [ j − 1 ] f[i + 1][j - 1] f[i+1][j1]中的回文串就不能可以再加上s[i]和s[j]形成一个新的回文串了,所以需要减掉多算的部分, f [ i ] [ j ] = f [ i + 1 ] [ j ] + f [ i ] [ j − 1 ] − f [ i + 1 ] [ j − 1 ] f[i][j] = f[i + 1][j] + f[i][j - 1] - f[i + 1][j - 1] f[i][j]=f[i+1][j]+f[i][j1]f[i+1][j1]

        s[i]和s[j]两个字符自然也不能形成回文串了,因此+1也不需要了

区间DP实现如下:

#include <iostream>

using namespace std;
#define MAX_N 70
long long f[MAX_N][MAX_N];

/**
 * 注意下面这个用例,用int的话会溢出的
 * 1
 * AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
 * 结果:1152921504606846975
 */
long long solve(string &s) {
    int n = s.length();
    for (int i = 0; i < n; i++) {
        f[i][i] = 1; // 字符串只有一个字符时,回文串就是其本身
    }
    for (int len = 2; len <= n; len++) {
        int j;
        for (int i = 0; (j = i + len - 1) < n; i++) {
            if (s[i] == s[j]) {
                f[i][j] = f[i + 1][j] + f[i][j - 1] + 1;
            } else {
                f[i][j] = f[i + 1][j] + f[i][j - 1] - f[i + 1][j - 1];
            }
        }
    }
    return f[0][n - 1];
}

int main() {
    int N;
    cin >> N;
    while (N-- > 0) {
        string s;
        cin >> s;
        cout << solve(s) << endl;
    }
    return 0;
}

HDU2476 String Painter

给定两个字符串A和B,和一个刷子,刷子每次可以把一个区间都刷成同一个字符

区间DP + 线性DP

一开始,我们用f[i][j]表示字符串a[i…j]变成b[i…j]的最小步数,发现这样并不方便转移,因此我们引入一个空串

  • 状态表示与计算:
    • 区间DP:用g[i][j]表示从空串变成b[i…j]的最小步数
      • 按照边界特征刷新dp值
        • 如果 b [ i ] = = b [ j ] b[i] == b[j] b[i]==b[j],即区间两端点相等,只需要刷区间[i, j - 1]即可,表达式为: g [ i ] [ j ] = g [ i ] [ j − 1 ] ; g[i][j] = g[i][j - 1]; g[i][j]=g[i][j1];
        • 如果 b [ i ] ! = b [ j ] b[i] != b[j] b[i]!=b[j],即区间两端点不等,在刷完区间[i, j - 1]之后,还需要额外刷一次j位置,所以表达式为: g [ i ] [ j ] = g [ i ] [ j − 1 ] + 1 ; g[i][j] = g[i][j - 1] + 1; g[i][j]=g[i][j1]+1;
      • 按照区间dp的套路,枚举区间[i, j]的每个点k,不断更新dp值

    然后确定哪些部分保留原来的a、哪些部分需要重刷

    • 线性DP:然后来考虑哪些部分不刷、哪些部分保留,用f[i]表示从a[1…i]变成b[1…i]的最小步数
      • 如果 a [ i ] = = b [ i ] a[i]==b[i] a[i]==b[i],那么 f [ i ] = f [ i − 1 ] f[i] = f[i - 1] f[i]=f[i1]
      • 如果 a [ i ] ! = b [ i ] a[i]!=b[i] a[i]!=b[i],那么久需要枚举区间内的点k从哪里开始刷(1…k)步数最少,此时 f [ i ] = m i n ( f [ i ] , f [ k − 1 ] + g [ k ] [ i ] ) ∣ i ≤ k ≤ j f[i] = min(f[i], f[k - 1] + g[k][i]) | i≤k≤j f[i]=min(f[i],f[k1]+g[k][i])ikj

参考博客:https://blog.csdn.net/ldw201510803006/article/details/60469908

#include<cstdio>
#include<cstring>
#include<algorithm>

#define MAXN 105 // 字符串的最大长度

using namespace std;
int g[MAXN][MAXN];
char a[MAXN], b[MAXN];
int f[MAXN];

int main()
{
    while (scanf("%s%s", a + 1, b + 1) == 2) {
        memset(g, 0, sizeof(MAXN)); // 二维数组的初始化,非常值得学习
        int n = strlen(a + 1);

        /* 1.区间DP */
        for (int i = 1; i <= n; i++)g[i][i] = 1;
        for (int len = 2; len <= n; len++) {
            // 长度
            for (int i = 1; i <= n - len + 1; i++) {  // 起点i
                int j = i + len - 1;   // 终点j
                // 1.1 考虑区间[i, j - 1]
                if (b[i] == b[j]) { // 区间端点相等,只需要刷区间[i, j - 1]即可
                    g[i][j] = g[i][j - 1];
                } else { // 不相等,那么j位置需要额外多刷一次
                    g[i][j] = g[i][j - 1] + 1;
                }
                // 1.2 枚举k,k ∈ [i, j],因为下面有k + 1,所以不能是k≤j
                for (int k = i; k < j; k++) {    //找分割点
                    g[i][j] = min(g[i][j], g[i][k] + g[k + 1][j]);
                }
            }
        }

        /* 2.线性DP */
        for (int i = 1; i <= n; i++) { // 初始化为从空串到a[1...i]的需要的步数,下面刷新后只会更小
            f[i] = g[1][i];
        }

        for (int i = 1; i <= n; i++) {
            if (a[i] == b[i]) {
                f[i] = f[i - 1];
            } else {
                for (int k = 1; k < i; k++) {
                    f[i] = min(f[i], f[k] + g[k + 1][i]);
                }
            }
        }
        printf("%d\n", f[n]);
    }
    return 0;
}

HDU1421 搬寝室

先排序,然后用线性DP,参考:https://www.cnblogs.com/yym2013/p/3530301.html

因为是n个物品中选2k个,然后分成k组,直接DP不好找状态,需要先挖掘题目的性质,考虑选出了2k个行李后如何分组可以最小化疲惫度之和

如果有四个行李,重量从小到大分别是a、b、c、d,显然(a, b)和(c, d)的分组最优,因此2k个行李一定是先从小到大排序,然后依次取两个配对

这样一开始我就可以对n个物品的重量排序,这样就方便设计状态了

  • 状态表示:

    • 阶段:f[i][j],表示从前i个物品中选出了j对
    • 目标:min(从前i个物品中选出了j对的疲惫度最小值)
  • 状态计算:计算f[i][j]的时候,一定是考虑了第i个取不取

    • 如果取第i个,那么一定是和第i-1个配对,因此就要求前i-2个物品中选出了j-1对, f [ i ] [ j ] = m i n ( f [ i ] [ j ] , f [ i − 2 ] [ j − 1 ] + ( a [ i ] − a [ i − 1 ] ) ∗ ( a [ i ] − a [ i − 1 ] ) ) f[i][j] = min(f[i][j], f[i - 2][j - 1] + (a[i] - a[i - 1]) * (a[i] - a[i - 1])) f[i][j]=min(f[i][j],f[i2][j1]+(a[i]a[i1])(a[i]a[i1]))
    • 如果不取第i个,那么 f [ i ] [ j ] = f [ i − 1 ] [ j ] f[i][j] = f[i - 1][j] f[i][j]=f[i1][j]

循环所有阶段取上面的最小f[i][j]即可

下面是Java的实现,注意可能一次会有多个用例,所以需要sc.hasNext()进行检测

#include <iostream>
#include <algorithm>
#include <cstring>

#define MAXN 2010 // 2<=2*k<=n<2000

using namespace std;

int f[MAXN][MAXN / 2]; // f[i][j],表示从前i个物品中选出了j对

int main()
{
    int n, k;
    while (cin >> n >> k) { // 防止有多个样例一次性输入
        int a[MAXN];
        for (int i = 1; i <= n; i++) {
            cin >> a[i];
        }
        sort(a + 1, a + 1 + n); // 对数组排序
        memset(f, 0x3f, sizeof(f)); // memset用0x3f初始化最好
        for (int i = 0; i <= n; i++) {
            f[i][0] = 0; // i个物品中取0对,疲惫度肯定为0
        }
        for (int i = 2; i <= 2; i++) {
            int tire = (a[i] - a[i - 1]) * (a[i] - a[i - 1]); // 取第i个商品的时候的疲惫度
            for (int j = 1; j * 2 <= i; j++) {
                f[i][j] = min(f[i - 2][j - 1] + tire, f[i - 1][j]);
            }
        }
        cout << f[n][k] << endl;
    }
    return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

空無一悟

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值