Acwing动态规划2——线性DP

本文详细介绍了动态规划在解决计算机科学问题中的应用,包括数字三角形的最大路径和、最长上升子序列的求解以及最短编辑距离的计算。对于每个问题,文章提供了原始代码并进行了优化,优化后的代码更简洁且效率更高。此外,还讨论了当数据规模增大时如何调整算法以避免超时,例如使用堆栈优化最长上升子序列的求解,将时间复杂度降低到O(nlogn)。

1、数字三角形

题目链接

1.题目描述
在这里插入图片描述
2.思路

分析方法:闫氏DP分析
在这里插入图片描述
时间复杂度:遍历二维O(n^2),求最大值O(1) => O(n^2)

3.代码

package chapter05.src;

import java.util.Scanner;

/**
 * @author mys
 * @date 2022/3/22 18:23
 */
public class p898 {
    static int N = 510;

    public static void main(String[] args) {
        int[][] a = new int[N][N], f = new int[N][N];//a:数字三角形 f:状态
        Scanner input = new Scanner(System.in);
        int n = input.nextInt();//三角形的层数

        //输入
        for (int i = 1; i <= n; i ++) {
            for (int j = 1; j <= i; j ++) {
                a[i][j] = input.nextInt();
            }
        }

        //从下往上开始遍历
        for (int i = n; i >= 1; i --) {
            for (int j = 1; j <= i; j ++) {
                //f[i + 1][j + 1]:从右边上去的值 f[i + 1][j]:从左边上去的值
                f[i][j] = Math.max(f[i + 1][j + 1] + a[i][j], f[i + 1][j] + a[i][j]);
            }
        }
        //输出最顶端元素和
        System.out.println(f[1][1]);
    }
}

代码优化:
1、二维数组a可以不要,直接用f表示即可
2、代码写法的优化 :f[i][j] = Math.max(f[i + 1][j + 1] + a[i][j], f[i + 1][j] + a[i][j]);
=> f[i][j] += Math.max(f[i + 1][j + 1], f[i + 1][j]); 逻辑不变,写法简单

最终代码

import java.util.Scanner;

/**
 * @author mys
 * @date 2022/3/22 18:23
 */
public class Main {
    static int N = 510;

    public static void main(String[] args) {
        int[][] f = new int[N][N];//a:数字三角形 f:状态
        Scanner input = new Scanner(System.in);
        int n = input.nextInt();//三角形的层数

        //输入
        for (int i = 1; i <= n; i ++) {
            for (int j = 1; j <= i; j ++) {
                f[i][j] = input.nextInt();
            }
        }

        //从下往上开始遍历
        for (int i = n; i >= 1; i --) {
            for (int j = 1; j <= i; j ++) {
                //f[i + 1][j + 1]:从右边上去的值 f[i + 1][j]:从左边上去的值
                f[i][j] += Math.max(f[i + 1][j + 1], f[i + 1][j]);
            }
        }
        //输出最顶端元素和
        System.out.println(f[1][1]);
    }
}

2、最长上升子序列

1.题目描述
在这里插入图片描述
2.思路
在这里插入图片描述
f[i] : 从第一个数字开始计算,以a[i]结尾的最长上升子序列
时间复杂度:状态数n 每个状态需要的时间 n -->O(n^2)

3.代码

package chapter05;

import java.util.Scanner;

/**
 * @author mys
 * @date 2022/3/24 11:24
 */
public class p895 {
    static int N = 1010;

    public static void main(String[] args) {
        int[] a = new int[N], f = new int[N];//a:序列 f:状态
        Scanner input = new Scanner(System.in);
        int n = input.nextInt();//序列长度

        //输入序列
        for (int i = 0; i < n; i ++) {
            a[i] = input.nextInt();
        }

        for (int i = 0; i < n; i ++) {//从前往后计算某个状态的上升序列长度
            f[i] = 1;//只有a[i]一个数时,上升序列最长为1
            for (int j = 0; j < i; j ++) {
                if (a[j] < a[i]) {//f[j]:i之前的小于自己值的最长子序列长度
                    f[i] = Math.max(f[i], f[j] + 1);//f[j] + 1:+1表示加上自己
                }
            }
        }

        //输出最长子序列
        int res = 0;
        for (int i = 0; i < n; i ++) {
            res = Math.max(res, f[i]);
        }
        System.out.println(res);
    }

}

4.优化

如果数据范围增大到如下,用上面的O(n^2)复杂度的就会超时
数据范围:1≤N≤100000,−109≤数列中的数≤109

思路:模拟一个堆栈(用数组表示栈比直接使用库函数更快),遍历元素,如果该元素大于栈顶元素,入栈;否则替换掉第一个大于或等于该元素的数字

例 n: 7
arr : 3 1 2 1 8 5 6

stk : 3

13 小
stk : 1

21 大
stk : 1 2

12 小
stk : 1 2

82 大
stk : 1 2 8

58 小
stk : 1 2 5

65 大
stk : 1 2 5 6

stk 的长度就是最长递增子序列的长度

最终代码实现:

import java.util.Scanner;

/**
 * @author mys
 * @date 2022/3/24 12:26
 * 数据范围增大,O(n^2)会超时,使用优化方法,时间复杂度O(nlogn)
 */
public class p896 {

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

        //输入序列
        int[] a = new int[n];
        for (int i = 0; i < n; i ++) {
            a[i] = input.nextInt();
        }

        int[] stk = new int[n];//模拟堆栈
        stk[0] = a[0];//现将第一个元素入栈
        int tt = 0;

        for (int i = 1; i < n; i ++) {
            if (a[i] > stk[tt]) {//如果当前元素大于栈顶元素,当前元素入栈
                stk[++ tt] = a[i];
            } else {
                //否则找到栈中第一个大于or等于该元素的数字,并用该元素替换
                //因为栈中元素是有序递增的,可以使用二分查找
                int l = 0, r = tt;
                while (l < r) {
                    int mid = (l + r) >> 1;
                    if (stk[mid] >= a[i]) {//数字在左半部分
                        r = mid;
                    } else {//数字在右半部分
                        l = mid + 1;
                    }
                }
                stk[l] = a[i];//l就是查找到的位置,替换
            }
        }
        System.out.println(tt + 1);
    }
}

3、最长公共子序列

1.题目描述
在这里插入图片描述

2.思路
在这里插入图片描述
3.代码

/**
 * @author mys
 * @date 2022/3/24 13:28
 */
public class poffer95_longestCommonSubsequence {
    public int longestCommonSubsequence(String text1, String text2) {
        int n = text1.length(), m = text2.length();
        int[][] f = new int[n + 1][m + 1];//状态
        //遍历所有状态
        for (int i = 1; i <= n; i ++) {
            for (int j = 1; j <= m; j ++) {
                //分为a[i] == a[j]和不等于两种情况来看
                if (text1.charAt(i - 1) == text2.charAt(j - 1)) {
                    f[i][j] = f[i - 1][j - 1] + 1;//等于
                } else {
                    f[i][j] = Math.max(f[i - 1][j], f[i][j - 1]);//不等于,再看是否包含a[i]或者b[j]
                }
            }
        }
        return f[n][m];
    }
}

对应leetcode题目:剑指 Offer II 095. 最长公共子序列

4、最短编辑距离

1.题目描述
在这里插入图片描述

2.思路
在这里插入图片描述
集合划分的思路:通常看最后一个操作之前的状态,然后加入最后一个操作的状态
时间复杂度:遍历所有状态:n^2,每个状态操作次数:3,==> O(n^2)

3.代码

package chapter05;

import java.util.Scanner;

/**
 * @author mys
 * @date 2022/3/25 13:38
 */
public class p902 {
    static int N = 1010;

    public static void main(String[] args) {
    	//输入
        Scanner input = new Scanner(System.in);
        int n = input.nextInt();
        String a = input.next();
        int m = input.nextInt();
        String b = input.next();

        int[][] f = new int[N][N];

        //边界初始化
        //1.从a的前0个字符匹配b的前i个字符,进行的是插入操作,操作次数就是i
        for (int i = 0; i <= m; i ++) {
            f[0][i] = i;
        }
        //2.从a的前i个字符匹配b的前0个字符,进行的是删除操作,操作次数就是i
        for (int i = 0; i <= n; i ++) {
            f[i][0] = i;
        }
        //遍历所有状态
        for (int i = 1; i <= n; i ++) {
            for (int j = 1; j <= m; j ++) {
                //(1)f[i - 1][j] + 1:删除  (2)f[i][j - 1] + 1:插入
                f[i][j] = Math.min(f[i - 1][j] + 1, f[i][j - 1] + 1);
                //(3)替换,分为两种情况 charAt(i - 1):从1开始,所以需要-1
                if (a.charAt(i - 1) == b.charAt(j - 1)) {
                    f[i][j] = Math.min(f[i][j], f[i - 1][j - 1]);
                } else {
                    f[i][j] = Math.min(f[i][j], f[i - 1][j - 1] + 1);
                }
            }
        }
        //求得把a的前n个字符与b的前m个字符进行匹配
        System.out.println(f[n][m]);
    }
}

4.改变条件
在这里插入图片描述
代码:

package chapter05.src;

import java.util.Scanner;

/**
 * @author mys
 * @date 2022/3/25 14:15
 */
public class p899 {
    static int N = 1010, M = 15;

    public static void main(String[] args) {
        Scanner input = new Scanner(System.in);
        int n = input.nextInt();//字符串个数
        int m = input.nextInt();//询问次数
        String[] str = new String[N];
        for (int i = 0; i < n; i ++) {
            str[i] = input.next();
        }

        for (int i = 0; i < m; i ++) {
            int count = 0;
            String s = input.next();
            int limit = input.nextInt();//限制的操作次数
            for (int j = 0; j < n; j ++) {
                if (minDistance(str[j], s) <= limit) {
                    count++;
                }
            }
            System.out.println(count);
        }
    }

    public static int minDistance(String s1, String s2) {
        int n = s1.length(), m = s2.length();
        int[][] f = new int[M][M];

        //边界条件初始化
        //1.从a的前0个字符匹配b的前i个字符,进行的是插入操作,操作次数就是i
        for (int i = 0; i <= m; i ++) {
            f[0][i] = i;
        }
        //2.从a的前i个字符匹配b的前0个字符,进行的是删除操作,操作次数就是i
        for (int i = 0; i <= n; i ++) {
            f[i][0] = i;
        }

        //遍历所有状态
        for (int i = 1; i <= n; i ++) {
            for (int j = 1; j <= m; j ++) {
                //f[i - 1][j] + 1:删除  f[i][j - 1] + 1:插入
                f[i][j] = Math.min(f[i - 1][j] + 1, f[i][j - 1] + 1);
                //替换,分为两种情况
                if (s1.charAt(i - 1) == s2.charAt(j - 1)) {
                    f[i][j] = Math.min(f[i][j], f[i - 1][j - 1]);
                } else {
                    f[i][j] = Math.min(f[i][j], f[i - 1][j - 1] + 1);
                }
            }
        }
        //求得把a的前n个字符与b的前m个字符进行匹配的操作次数
        return f[n][m];
    }
}
<think>好的,用户想学习或解决AcWing平台上动态规划相关的算法问题,我需要提供相应的题解和学习资源。首先,我得回忆一下AcWing动态规划题目有哪些典型题,以及用户可能需要的学习路径。 用户提到的引用中有几个题解,比如341题最优贸易的分层图解法,这可能属于动态规划的变种。还有购物单问题,可能涉及背包DP。另外,引用4提到的离散化优化空间,可能和状态压缩有关。引用5的排队打水,可能需要贪心,但动态规划有时候也会结合贪心策略。 首先,动态规划的基础知识很重要,比如状态定义、转移方程、初始化等。用户可能需要从简单题开始,比如01背包问题,这是动态规划的经典入门题。然后逐步过渡到更复杂的问题,如完全背包、多重背包,以及像最长公共子序列、最长上升子序列这样的线性DP问题。 接下来,我需要整理AcWing上的相关题目。例如,AcWing 2. 01背包问题,这是最基础的。然后像AcWing 3. 完全背包问题,AcWing 4. 多重背包问题,这些都是背包问题的变种。还有线性DP中的AcWing 895. 最长上升子序列,AcWing 896. 最长上升子序列 II(优化版),以及区间DP问题如AcWing 282. 石子合并。 用户提到的AcWing 341最优贸易题解,使用分层图方法,这可能涉及到状态机模型,属于动态规划的一种高级应用。状态机模型将问题分解成多个状态,每个状态之间的转移对应不同的操作,比如买入和卖出。这时候可能需要分层的状态设计,每一层代表不同的状态,比如未买入、已买入未卖出、已卖出。这种方法在动态规划中用于处理有多个阶段或操作的问题。 另外,购物单问题可能属于有依赖的背包问题,比如金明的预算方案,每个主件可以有多个附件,选择主件后才能选附件。这类问题需要处理依赖关系,通常使用分组背包或树形DP来解决。 对于学习资源,用户可能需要系统的课程或题解集合。AcWing的算法基础课和提高课都包含动态规划章节,适合系统学习。此外,题解区有很多高质量的解答,用户可以参考。比如,y总的讲解视频,或者其他用户的代码实现。 用户可能的疑问点包括如何定义状态,如何写出转移方程,以及如何进行空间优化。例如,在背包问题中,一维数组优化是通过逆序更新来避免重复计算。而在状态机模型中,需要明确每个状态转移的条件和计算方式。 另外,动态规划的优化技巧如单调队列、斜率优化、离散化等,可能在高级题目中出现。例如,引用4提到的离散化优化,用于减少空间复杂度,这在处理大范围数据时非常有用,比如当状态中的某些维度范围很大但实际使用较少时,可以通过离散化映射到更小的空间。 在推荐学习路径时,建议用户先掌握基本模型,再逐步挑战复杂问题。比如从背包问题开始,然后线性DP,区间DP,状态压缩DP,最后是状态机模型、树形DP等。同时,多练习相关题目,理解不同问题的状态设计和转移方式。 最后,用户可能需要具体的题解示例,比如针对某个题目的代码和思路分析。例如,对于AcWing 2. 01背包问题,可以给出二维和一维的解法,并解释状态转移的过程。对于状态机模型的问题,如AcWing 1052. 设计密码,需要详细说明状态的定义和转移条件。 总结来说,我需要提供动态规划的学习路径、典型例题、解题思路以及AcWing上的相关资源,帮助用户系统地学习和解决动态规划问题。</think>### AcWing动态规划问题学习指南 #### 一、动态规划基础知识 动态规划DP)核心在于**状态定义**和**状态转移方程**。基本步骤包括: 1. 定义状态:$dp[i][j]$ 表示某种条件下的最优解 2. 初始化边界条件 3. 建立状态转移关系:$dp[i][j] = f(dp[i-1][k], ...)$ 4. 确定最终解的位置 #### 二、经典题型分类 ##### 1. 背包问题 - **01背包**:每个物品选一次 例题:AcWing 2. 01背包问题 状态转移:$dp[j] = \max(dp[j], dp[j - v_i] + w_i)$(一维数组逆序更新)[^1] - **完全背包**:物品无限次选取 例题:AcWing 3. 完全背包问题 状态转移:$dp[j] = \max(dp[j], dp[j - v_i] + w_i)$(一维数组正序更新) - **多重背包**:物品有数量限制 例题:AcWing 4. 多重背包问题 优化方法:二进制拆分、单调队列优化 ##### 2. 线性DP - **最长上升子序列(LIS)** 例题:AcWing 895. 最长上升子序列 状态定义:$dp[i]$ 表示以第$i$个元素结尾的LIS长度 优化后时间复杂度:$O(n \log n)$[^4] - **编辑距离** 例题:AcWing 899. 编辑距离 状态转移: $$dp[i][j] = \min\left\{\begin{array}{l} dp[i-1][j] + 1, \\ dp[i][j-1] + 1, \\ dp[i-1][j-1] + (a_i \neq b_j) \end{array}\right.$$ ##### 3. 区间DP - **石子合并** 例题:AcWing 282. 石子合并 状态定义:$dp[l][r]$ 表示合并区间$[l,r]$的最小代价 转移方程: $$dp[l][r] = \min_{k=l}^{r-1}(dp[l][k] + dp[k+1][r]) + \sum_{i=l}^r a_i$$ ##### 4. 状态机模型 - **最优贸易问题** 例题:AcWing 341. 最优贸易[^2] 解法:建立三层状态表示不同交易阶段 - 第一层:未买入 - 第二层:已买入未卖出 - 第三层:已卖出 #### 三、学习资源推荐 1. **AcWing官方课程** - 算法基础课:涵盖DP基础模型 - 算法提高课:包含树形DP、状态压缩等高级内容 2. **题解资源** - AcWing题解区:搜索题目编号查看高质量题解 - 经典教材参考:《算法竞赛进阶指南》动态规划章节 3. **代码模板** ```python # 01背包一维优化模板(Python示例) n, m = map(int, input().split()) dp = [0] * (m+1) for _ in range(n): v, w = map(int, input().split()) for j in range(m, v-1, -1): dp[j] = max(dp[j], dp[j - v] + w) print(dp[m]) ``` #### 四、常见优化技巧 1. **滚动数组**:将二维状态压缩为一维 2. **离散化**:处理大范围稀疏数据(如坐标范围大但实际使用点少)[^4] 3. **单调队列优化**:适用于决策单调性问题 4. **斜率优化**:将转移方程转化为凸包问题 #### 五、练习建议 1. 从简单模板题开始(如AcWing 2, 3, 895) 2. 逐步过渡到应用题(如AcWing 341, 282) 3. 每道题尝试写出: - 状态定义 - 转移方程 - 边界条件 - 空间优化方案
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值