动态规划篇——DP问题

动态规划篇——DP问题

本次我们介绍动态规划篇的DP问题,我们会从下面几个角度来介绍:

  • 区间DP
  • 计数DP
  • 树状DP
  • 记忆化搜索

区间DP

我们通过一个案例来讲解区间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)。

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

/*数据范围*/
    
1 ≤ N ≤ 300
    
/*输入样例*/
    
4
1 3 5 2

/*输出样例*/
    
22

我们对问题采用DP分析思路:

状态表示:f[i][j]
    
状态集合意义:表示将第i堆石子到第j堆石子堆在一起的合并方式
    
状态集合属性:保存其消耗的Min
    
状态计算方式:
    我们首先用s[i]来存储前i个石头的总值,为了方便我们计算部分石头范围的总值(前缀法)
    我们的每个f[i][j]都是最小值,我们希望采用k作为中间点,当使用k作为中间点时,f[i][j]为最小值
    因此我们f[i][j] = Math.min(f[i][j],f[i][k]+f[k+1][j]+s[j]-s[i-1])
    同时由于我们的比较大的数据都是由小数据产生的,所以我们根据石块范围大小来从头开始赋值

我们给出实际代码展示:

import java.util.*;

public class SectionDP {
    public static void main(String[] args){
        
        Scanner scan = new Scanner(System.in);
        
        // 数据准备,s是前i位石头和,f存放区间最优解
        int N = 310;
        int[] s = new int[N];
        int[][] f = new int[N][N];

        // 输入数据
        int n = scan.nextInt();
        for(int i = 1 ; i <= n ; i ++ ) s[i] = scan.nextInt();
        
        // 前缀和
        for(int i = 1 ; i <= n ; i ++ ) s[i] += s[i - 1]; 
        
        //这里是枚举的每种长度,比如n等于4,比如长度3,右边下标不超过n,求f[1-3]和f[2-4]里面的最小值
        for(int len =  2 ; len <= n ; len ++ ){
            // 然后我们从第1位开始枚举,枚举当前长度的所有情况
            for(int i = 1; i + len - 1 <= n ; i ++ ){
                // 每种长度的j
                int j = i + len -  1; 
                // 因为要枚举的是k里面的最小值,所以赋一个很大的数,
                // 如果没有赋最大的数,你的f[i][j] 初始值是0,所以最小是永远会被是0,最后输出也会是0  
                f[i][j] = (int)1e9; 
                // 关键步骤,当前大区间最优解是由小区间最优解产生的!!!
                for(int k = i ; k < j ; k ++ ){
                    f[i][j] = Math.min(f[i][j],f[i][k] + f[k + 1][j] + (s[j] - s[i - 1]));
                }
            }
        }
        
        // 最后输出1~n的石子和最优解即可
        System.out.println(f[1][n]);
    }
}

计数DP

我们通过一个案例来讲解计数DP:

/*题目展示*/

题目名:整数划分
    
一个正整数 n 可以表示成若干个正整数之和,形如:n=n1+n2+…+nk,其中 n1≥n2≥…≥nk,k≥1。
    
(上述的意思就是不需要计算重复情况,例如5=2+2+1=1+2+2是一种情况)

我们将这样的一种表示称为正整数 n 的一种划分。

现在给定一个正整数 n,请你求出 n 共有多少种不同的划分方法。

/*输入格式*/
    
共一行,包含一个整数 n。

/*输出格式*/
    
共一行,包含一个整数,表示总划分数量。
    
由于答案可能很大,输出结果请对 10e9+7 取模。

/*数据范围*/
    
1 ≤ n ≤ 1000
    
/*输入样例*/
    
5

/*输出样例*/
    
7

我们对问题采用DP分析思路:

状态表示:f[i][j]
    
状态集合含义:表示前i个数搭配总数相加为j
    
状态集合属性:表示数量
    
状态计算:
    原本形式为:
    f[i][j] = f[i-1][j] + f[i-1][j-i] + f[i-1][j-2i] + ... + f[i-1][j-si]
    但由于:
    f[i][j-i] = f[i-1][j-i] + f[i-1][j-2i] + ... + f[i-1][j-si]
    所以我们可以优化为:
    f[i][j] = f[i-1][j] + f[i][j-i]
    同时我们可以用移动数组来进行叠层优化:
    f[j] = f[j] + f[j-i];(注意:这里的f[j]是上一层,f[i-1]是这一层,我们需要注意遍历顺序)

最后我们给出解题代码:

import java.util.*;

public class Main {
    public static void main(String[] args){
        
        Scanner scan = new Scanner(System.in);
        
        int N = 1010,mod = (int)(1e9 + 7);
        
        // f[i]为总和为i的方法总数
        int[] f = new int[N];
        
        int n = scan.nextInt();

        // 初始化:一个数都不选,总和是0,是一种方案f[i][0],前i个数中选,总和恰好等于0,只有一种都不选 
        f[0] = 1;

        // 进行n次遍历,这里是i的遍历,也就是前i个数选的遍历
        for(int i = 1 ; i <= n ; i ++ ){
            // 进行n次遍历,这里是j的遍历,也就是给f[j]赋值的遍历
            for(int j = i ; j <= n ; j ++ ){
                //状态表示:f[i][j] = f[i - 1][j] + f[i][j - i]
                f[j] = (f[j] + f[j - i]) % mod;
            }
        }
        System.out.println(f[n]);
    }
}

树状DP

我们通过一个案例来讲解树状DP:

/*题目展示*/

题目名:没有上司的舞会

Ural 大学有 N 名职员,编号为 1∼N。

他们的关系就像一棵以校长为根的树,父节点就是子节点的直接上司。

每个职员有一个快乐指数,用整数 Hi 给出,其中 1≤i≤N。

现在要召开一场周年庆宴会,不过,没有职员愿意和直接上司一起参会。

在满足这个条件的前提下,主办方希望邀请一部分职员参会,使得所有参会职员的快乐指数总和最大,求这个最大值。

/*输入格式*/
    
第一行一个整数 N。
    
接下来 N 行,第 i 行表示 i 号职员的快乐指数 Hi。

接下来 N−1 行,每行输入一对整数 L,K,表示 K 是 L 的直接上司。

/*输出格式*/
    
输出最大的快乐指数。

/*数据范围*/
    
1 ≤ N ≤ 6000,
−128 ≤ Hi ≤ 127
    
/*输入样例*/
    
7
1
1
1
1
1
1
1
1 3
2 3
6 4
7 4
4 5
3 5

/*输出样例*/
    
5

我们对问题采用DP分析思路:

状态表示:f[u][0],f[u][1]
    
状态集合含义:f[u][0]表示以u为根的子树中选择并且不选择u这个点
    		f[u][1]表示以u为根的子树中选择并且选择u这个点
    
状态集合属性:表示MAX
    
状态计算:
    f[u]表示根树,f[s]表示对应的子树
    首先我们根据自身是否选择,来设置初始值:f[u][0] = 0,f[u][1] = 1
    f[u][0]表示不选择该点,那么子树就可以选择:f[u][0] += Math.max(f[s][1],f[s][0])
    f[u][1]表示选择该点,那么子树就不可以选择:f[u][1] += f[s][0]

最后我们给出解题代码:

import java.util.*;

public class Main {
    
    static int N = 6010,n,idx;
    
    // 职员的快乐指数(输入值)
    static int[] happy = new int[N];
    
    // f[u][0]和f[u][1]的模型
    static int[][] f = new int[N][2];
    
    // 单链表模拟树
    static int[] h = new int[N],e = new int[N],ne = new int[N];
    
    // 判断是否有父类
    static boolean[] hasFather = new boolean[N];
    
    public static void main(String[] args){
        
        Scanner scan = new Scanner(System.in);
        
        // 输入
        n = scan.nextInt();
        for(int i = 1 ; i <= n ; i ++ ) happy[i] = scan.nextInt();

        // 邻接表初始设置
        Arrays.fill(h,-1);

        // 设置树
        for(int i = 0 ; i < n - 1 ; i ++ ){
            int a = scan.nextInt();
            int b = scan.nextInt();
            
            // 因为b是a的直系上司,所以需要b->a
            add(b,a);
            
            // 员工拥有父节点
            hasFather[a] = true;
        }

        // 设置根节点为初始值,从1开始判断
        int root = 1;
        
        // 寻找根节点(找那个没有父类的,就是根节点)
        while(hasFather[root]) root ++ ; 

        // 开始递归
        dfs(root); 
        
        // 最后输出的是选根节点跟不选根节点两种方案的最大值
        System.out.println(Math.max(f[root][0],f[root][1]));
    }
    
    // 递归
    public static void dfs(int u){
        
        // 不选时,该点初始快乐值为0
        f[u][0] = 0;
        
        // 选择该点时,该点初始快乐值为1
        f[u][1] = happy[u];

        // 从该点开始根据子节点更新数据
        for(int i = h[u]; i != -1 ; i = ne[i]){
            
            // 子节点
            int j = e[i];
            
            // 对子节点进行数据更新
            dfs(j);
            
            // 对该点进行数据更新
            
            // 如果这个根节点不选,就等于他的所有根节点选与不选的最大值之和
            f[u][0] += Math.max(f[j][0],f[j][1]);
            
            // 如果这个根节点选,就等于他的所有根节点不选的和
            f[u][1] += f[j][0];

        }
    }
    
    // 邻接表衔接
    public static void add(int a,int b){
        e[idx] = b;
        ne[idx] = h[a];
        h[a] = idx ++;
    }
}

记忆化搜索

我们通过一个案例来讲解记忆化搜索:

/*题目展示*/

给定一个 R 行 C 列的矩阵,表示一个矩形网格滑雪场。

矩阵中第 i 行第 j 列的点表示滑雪场的第 i 行第 j 列区域的高度。

一个人从滑雪场中的某个区域内出发,每次可以向上下左右任意一个方向滑动一个单位距离。

当然,一个人能够滑动到某相邻区域的前提是该区域的高度低于自己目前所在区域的高度。

下面给出一个矩阵作为例子:

1  2  3  4 5

16 17 18 19 6

15 24 25 20 7

14 23 22 21 8

13 12 11 10 9
    
在给定矩阵中,一条可行的滑行轨迹为 24−17−2−1。

在给定矩阵中,最长的滑行轨迹为 25−24−23−…−3−2−1,沿途共经过 25 个区域。

现在给定你一个二维矩阵表示滑雪场各区域的高度,请你找出在该滑雪场中能够完成的最长滑雪轨迹,并输出其长度(可经过最大区域数)。

/*输入格式*/
    
第一行包含两个整数 R 和 C。

接下来 R 行,每行包含 C 个整数,表示完整的二维矩阵。

/*输出格式*/
    
输出一个整数,表示可完成的最长滑雪长度。

/*数据范围*/
    
1 ≤ R, C ≤ 300,
0 ≤ 矩阵中整数 ≤ 10000
    
/*输入样例*/
    
5 5
1 2 3 4 5
16 17 18 19 6
15 24 25 20 7
14 23 22 21 8
13 12 11 10 9

/*输出样例*/
    
25

我们对问题采用DP分析思路:

状态表示:f[i][j]
    
状态集合含义:f[i][j]表示从i,j位置开始移动所经过的路径
    
状态集合属性:表示MAX
    
状态计算:
    f[i][j]有四个移动方向,分别是[0][1],[0][-1],[1][0],[-1][0]
    我们首先需要判定四个方向是否可以移动:是否达到边界?是否高度不对等?
    我们用f[i][j]表示当前位置,f[a][b]表示已经移动后的位置
    f[i][j] = Math.max(f[i][j],f[a][b] + 1) 

最后我们给出解题代码:

import java.util.*;

public class Main {
    
    static int N = 310,n,m;
    
    // h表示高度,f表示当前位置开始移动所移动的最大距离
    static int[][] h = new int[N][N];
    static int[][] f = new int[N][N];
    
    // 用数组模拟移动上下左右
    static int[] dx = {-1,0,1,0},dy = {0,1,0,-1};
  
    public static void main(String[] args){
        
        Scanner scan = new Scanner(System.in);
        
        // 赋值
        n = scan.nextInt();
        m = scan.nextInt();
        
        // 高度赋值
        for(int i = 1 ; i <= n ; i ++ )
            for(int j = 1 ; j <= m ; j ++ )
                h[i][j] = scan.nextInt();

        // 将所有的方案初始化成-1,检测该路径是否已经dp过
        for(int i = 0 ; i < N ; i ++ ) Arrays.fill(f[i],-1);

        // 返回值
        int res = 0; 
        
        // 对每个点都进行递归
        for(int i = 1 ; i <= n ; i ++ )
            for(int j = 1 ; j <= m ; j ++ )
                // 取从哪个点开始能滑长度最长
                res = Math.max(res,dp(i,j)); 

        System.out.println(res);

    }
    
    public static int dp(int x,int y){
        
        // 如果已经计算过了,就直接返回结果
        if(f[x][y] != -1) return f[x][y];  

        // 最开始只包括自己一个点,f[x][y] = 1
        f[x][y] = 1; 
        
        // 向四个方向遍历
        for(int i = 0 ; i < 4 ; i ++ ){
            
            // 移动后的点位
            int a = x + dx[i];
            int b = y + dy[i];
            
            // 判定条件(边界判定+高度判定)
            if(a >= 1 && a <= n && b >= 1 && b <= m && h[a][b] < h[x][y]){
                // 进行比较,这里需要对ab点进行dp,若未dp进行dp,若已dp返回其移动距离,加上当前点,然后与最大距离比较
                f[x][y] = Math.max(f[x][y],dp(a,b) + 1);
            }
        }
        
        // 返回结果
        return f[x][y];
    }
}

结束语

好的,关于动态规划篇的DP问题就介绍到这里,希望能为你带来帮助~

posted @ 2022-11-25 08:11  秋落雨微凉  阅读(91)  评论(0编辑  收藏  举报