【Acwing 动态规划(二) 保姆级教程】Java代码 题号282、895、897、898

动态规划(二) 线性dp+区间dp

又来CSDN搬运笔记了,菜鸡一枚,生怕日后复习看不懂代码,所以进行了保姆级注释,希望能帮到在各个点卡住的小伙伴们~
这篇博客包含了Acwing题库282、895、897、898题,java代码实现,是 线性dp和区间dp的内容。

动态规划问题解决一致的思路:

898.数字三角形

题目描述

思路分析

i 表示行,j 表示列(如下图)。用 f [i] [j] 表示所有从起点走到 (i,j) 位置的路径的最大长度。则 f [i] [j] 可以分为两个子集:到(i,j) 的路径是来自左上方的点或右上方的点——如下图所示:


代码实现

static int N = 510;
static double INF = 1e9;
static int[][] a = new int[N][N];   //存储数字三角形
static double[][] f = new double[N][N];
public static void main(String[] args) {
    Scanner myscanner = new Scanner(System.in);
    int n = myscanner.nextInt();

    //        接收数字三角形
    for (int i = 1; i <= n; i++)
        for (int j = 1; j <= i ; j++)
            a[i][j] = myscanner.nextInt();

    //        将f初始化为无穷小
    for (int i = 1; i <= n; i++)
        //细节:j从0~i+1,因为边界位置可能存在没有左上或右上的点,如果不初始化成负无穷
        // 那之后比较的时候,如果碰到比如左上角的是负数,右上角没点的情况
        // 就会取右上角——因为f数组初始化的0更大
        for (int j = 0; j <= i+1 ; j++)
            f[i][j] = -INF;

    f[1][1] = a[1][1];
    for (int i = 2; i <= n; i++)
        for (int j = 1; j <= i ; j++)
            f[i][j] = Math.max(f[i-1][j-1]+a[i][j],f[i-1][j]+a[i][j]);

    double res = -INF;
    for (int i = 1; i <= n; i++)   //找最后一排的最大值,即我们要求的全局最大值
        if(res<f[n][i])
            res = f[n][i];
    System.out.println((int)res);
}

细节:当涉及到 f [i-1] [j-1] 时,i循环从1开始,避免越界问题。

895.最长上升子序列长度

给定一个长度为 N的数列,求数值严格单调递增的子序列的长度最长是多少。

输入格式:第一行包含整数 N。第二行包含 N个整数,表示完整序列。

输出格式:输出一个整数,表示最大长度。

思路分析

用 f [i] 表示以第 i 个数为结尾的最长上升子序列。则 f [i] 可划分为:该子序列的前一个数的下标为0、该子序列的前一个数的下标为1…该子序列的前一个数的下标为 i-1,如下图:

代码实现

static int N = 1010;
static int[] a = new int[N];
static int[] f = new int[N];
public static void main(String[] args) {
    Scanner myscanner = new Scanner(System.in);
    int n = myscanner.nextInt();
    for (int i = 1; i <= n; i++)
        a[i] = myscanner.nextInt();

    for (int i = 1; i <= n ; i++) {
        f[i] = 1;   //只有a[i]一个数
        for (int j = 1; j < i; j++)   //遍历到 i之前
            if (a[j]<a[i])     //如果前面位置为j的数更小,那就可以考虑更新
                f[i] = Math.max(f[i],f[j]+1);
    }

    int res = 1;
    for (int i = 1; i <= n; i++)
        res = Math.max(res,f[i]);

    System.out.println(res);
}

如果要输出这个最长上升子序列,则

static int[] g = new int[N];    // 存储最长上升子序列
for (int i = 1; i <= n ; i++) {
    f[i] = 1;
    g[i] = 0;
    for (int j = 1; j < i; j++) {
        if (a[j] < a[i]) {
            if (f[i] < f[j] + 1) {
                f[i] = f[j] + 1;
                g[i] = j;    //记录i的前缀
            }
        }
    }
}

int k = 1;
for (int i = 1; i <= n; i++) //找到最长上升子序列的末位下标
    if(f[k]<f[i])
        k=i;  

System.out.println(f[k]);
int len = f[k];
for (int i = 1; i <= len; i++) {
    System.out.println(a[k]);
    k=g[k];  //循环往前找前缀
}

记录一个低级错误:

for (int i = 1; i <= f[k]; i++) {
    System.out.println(a[k]);
    k=g[k];
}

如果这样写,因为k的改变导致f[k]也在改变,所以循环退出的条件是错的,应该先定义一个len来存储f[k]。

897.最长公共子序列

给定两个长度分别为 N和 M的字符串 A和 B,求既是 A 的子序列又是 B 的子序列的字符串长度最长是多少。

输入格式:第一行包含两个整数 N、M。第二行为一个长度为 N 的字符串 A。第三行为一个长度为 M的字符串B。

输出格式:输出一个整数,表示最大长度。

思路分析

用 f [i] [j] 表示所有在 a 序列的前 i 个字母中出现,且在 b 序列的前 j 个字母中出现的子序列的最大长度。此处的集合划分是一个难点:对于每一个 f [i] [j] ,包不包含a[i]、b[j]可以划分成四种情况:

  • 情况1:a[i],b[j] 均不在
  • 情况2:a[i]不在,b[j]在
  • 情况3: a[i] 在,b[j] 不在
  • 情况4: a[i],b[j] 均存在于 最长公共子序列中 (前提a[i]==b[j])

其中00和11的情况好表示,重点是中间两种情况。实际上,中间两种情况并不等价于图中所示的式子。但好处在于, f [ i-1 ] [ j ]虽然自己本身就有4种子集,但一定会包括这里我们要的01的情况;10同理。而我们将来要求的是最大值,因此就算不完全等价也无碍。又因为f [ i-1 ] [ j ]和f [ i ] [ j-1 ]的情况包括了f [ i-1 ] [ j-1 ]的情况,则可以将00的情况删去。

另一个思路理解集合划分:

f[i] [j-1]的含义是A前i个字符,B前j-1个字符的最长公共子序列长度 -->②
a[i] 可能在也可能不在,b[j]一定不在。
而情况3 的限制是:a[i] 一定在,b[j]一定不在 -->①
很显然①是②的子集,即②包含了①,并不一定为①

则我们可以用f[i] [j-1]它来表示 情况1和情况3
因为 f[i] [j-1]其实是a[i]在时 的最长公共子序列的长度 和 a[i]不在时的长度 的最大值

同理:f[i-1] [j] 不能表示情况2,但可以用来表示 情况1和情况2

我们需要 求得的是 max(情况1,情况2,情况3,情况4)
而:f[i-1] [j-1]+1 可以表示情况4 --> a
f[i] [j-1]=max(情况1,情况3) --> b
f[i-1] [j]=max(情况1,情况2) --> c
所以我们最终只需要 求 max(a,b,c) 即可

代码实现

static int N = 1010;
static int[][] f = new int[N][N];
public static void main(String[] args) {
    Scanner myscanner = new Scanner(System.in);
    int n = myscanner.nextInt();
    int m = myscanner.nextInt();
    char[] a = new char[N];
    char[] b = new char[N];
    String aa = myscanner.next();
    String bb = myscanner.next();

    for(int i=1; i<=n; i++){
        a[i] = aa.charAt(i-1);
    }
    for(int i=1; i<=m; i++){
        b[i] = bb.charAt(i-1);
    }

    for (int i = 1; i <= n; i++) {
        for (int j = 1; j <= m; j++) {
            f[i][j] = Math.max(f[i-1][j],f[i][j-1]);
            if (a[i]==b[j])
                f[i][j]=Math.max(f[i][j],f[i-1][j-1]+1);
        }
    }
    System.out.println(f[n][m]);
}

282.石子合并(区间dp)

设有 N 堆石子排成一排,其编号为 1,2,3,…,N。每堆石子有一定的质量,现在要将这 N堆石子合并成为一堆。每次只能合并相邻的两堆,合并的代价为这两堆石子的质量之和,合并后与这两堆石子相邻的石子将和新堆相邻,合并时由于选择的顺序不同,合并的总代价也不相同。

例如有 4堆石子分别为 1 3 5 2, 我们可以先合并 1、2 堆,代价为 4,得到 4 5 2, 又合并 1、2 堆,代价为 9,得到 9 2 ,再合并得到 11,总代价为 4+9+11=24;如果第二步是先合并 2、3 堆,则代价为 7,得到 4 7,最后一次合并代价为 11,总代价为 4+7+11=22。

问题是:找出一种合理的方法,使总的代价最小,输出最小代价。

输入格式:第一行一个数 N 表示石子的堆数 N。第二行 N个数,表示每堆石子的质量(均不超过 1000)。

输出格式:输出一个整数,表示最小代价。

思路分析

用 f [i] [j] 表示所有将第i堆石子到第j堆石子合并成一堆石子的合并方式的最小代价

集合划分:只考虑最后一步的所有情况:对于最后一步,所有的情况为:左边区间有1个石子、右边区间有k-1个石子;左边区间有2个石子、右边区间有k-2个石子…左边区间有k-1个石子、右边区间有1个石子。


则区间 i~j 合并的代价就 = 左边区间合并的最小代价f [i] [k] + 左边区间合并的最小代价f [ k+1 ] [j] + 两个区间合并的代价(左区间的总重量+右区间的总重量,即 i~j 堆石子的总重量,即 j的前缀和 - i 的前缀和,即s[j] - s[i-1]。(s[i]为第 i 堆石子的前缀和)(前缀和:到第 i 个位置的累计值)。遍历所有有可能划分的 k 值,取最小值就是我们要求的最小代价。

代码实现

static int N =310;
static int[] a =new int[N];
static double[][] f =new double[N][N];

public static void main(String[] args) {
    Scanner myscanner = new Scanner(System.in);
    int n = myscanner.nextInt();

    for (int i = 1; i <= n; i++)
        a[i] = myscanner.nextInt();

    for (int i = 1; i <= n; i++)   //求前缀和
        a[i] += a[i-1];

    for (int len = 1; len <n ; len++) {    //遍历所有可能的区间长度
        for (int i = 1; i+len <= n ; i++) {  //遍历所有可能的区间起点
            int l = i;
            int r = i+len;
            f[l][r] = 1e8;
            for (int k = l; k < r; k++)  //遍历所有可能的左右区间分割点
                f[l][r] = Math.min(f[l][r],f[l][k] +f[k+1][r]+a[r]-a[l-1] );
        }
    }
    System.out.println((int)f[1][n]);
}

细节 :所有的区间dp问题枚举时,第一维通常是枚举区间长度(从小到大);第二维枚举起点 i (从小到大)(右端点 j 自动获得,j = i + len )。

区间长度从小到大遍历的原因:f[l] [r] = min(f [l] [r] , f [l] [k] +f [k+1] [r]+a[r]-a[l-1] )。k从小到大递增,意味着计算 f[l] [r] 时要用到的f [l] [k] 应该已知,即区间长度更小的情况应该先被计算。

区间第一维,起点第二维的原因:按这个顺序遍历的话,f [l] [k] 满足已知(因为大方向是len,k<len,肯定被计算过了);同上,f [k+1] [r] 也应该先于f[l] [r]已知,只有在大方向是len的情况,才有可能满足(因为 [k+1,r] 区间长度一定<len)。若先从起点遍历,此时的f [k+1] [r]就还是未知的(因为大方向是起点,起点还没到k+1)。

当然了,如果能倒着遍历,满足条件也可以。

总结:遇到不确定循环的维度时,多观察一下待定式子,等号右边一定是确定已知的东西,才能更新等号左边的东西,那么等号右边的东西就一定要保证先于等号左边确定值。可以根据这个点来确定大方向应该先遍历谁。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值