动态规划入门

简介: 动态规划入门

基础入门理论


动态规划是一种常见的算法设计方法,主要用于优化多阶段决策问题的求解过程,具有高效性和可靠性。其基本思想是将待求解问题分解成若干个子问题,逐个求解这些子问题,并保存每个子问题的结果,避免重复计算,以便快速地求出原问题的解。动态规划主要应用于最优化问题,如最长公共子序列、背包问题等。


动态规划算法主要有以下几个步骤:


定义状态:将原问题转化为子问题,定义状态表示子问题的解。


设计状态转移方程:根据状态之间的联系,设计状态转移方程,表示从子问题的解推导出原问题的解。


初始化:初始化子问题的解,即确定初始状态。


计算顺序:按照状态之间的依赖关系,按顺序计算子问题的解,直到计算出原问题的解。


输出解:输出原问题的解。


最大连续子串和


a8ace9a11d954ce8899bb75b08898b31.png

给出数组a,求出数组a中最大的连续子串的和。

暴力求解

两种方法,都是从起始点开始循环,但f2方法比f1优化了,没有去重复求出已经得到的结果。

import java.util.Arrays;
public class maxSubSum {
    public static void main(String[] args) {
        int[] a = {-2, 11, -4, 13, -5, 2};
        f1(a);
        f2(a);
    }
    public static void f1(int[] a) {
        int max = Integer.MIN_VALUE;//记录最大连续子串的和,先给定无穷小
        for (int start = 0; start < a.length; start++) {//穷举所有子串的起始点
            for (int len = 1; start + len <= a.length; len++) {
                //起始点固定,穷举长度分别为1-6的所有情况
                int sum = 0;
                for (int i = start; i < start + len; i++) {
                    sum += a[i];//sum为每次连续子串的和
                }
                max = Math.max(max, sum);//记录当前循环中最大的子串和
                System.out.printf("len=%d a[%d-%d] sum=%d\n",len, start, start + len, sum);
            }
        }
        System.out.println("最大子串和为:" + max);
    }
    public static void f2(int[] a) {
        int max = Integer.MIN_VALUE;//记录最大连续子串的和,先给定无穷小
        for (int start = 0; start < a.length; start++) {//穷举所有子串的起始点
            int sum = 0;
            for (int len = start; len < a.length; len++) {//从start开始一直循环到最后
                sum += a[len];//sum为每次连续子串的和
                max = Math.max(max, sum);//记录当前循环中最大的子串和
                System.out.printf("len=%d a[%d-%d] sum=%d\n",len, start, start + len, sum);
            }
        }
        System.out.println("最大子串和为:" + max);
    }
}


可以看出每次起始点和长度变化时sum的值。

70b6237023a043f4b71eafe6028c5c81.png


DP


1487c687b8e54e7dba5b788f37b7755a.png


判断dp[i  - 1]是否大于0,大于0时dp[i] = dp[i - 1] + a[i],如果小于等于0那么dp[i] = a[i]。也就是在dp[i - 1] + a[i]和a[i]中取最大值赋给dp[i]。也可以理解为每一个问题都会用到前一个子问题的最优解

import java.util.Arrays;
//最大连续子串和
public class maxSubSum {
    public static void main(String[] args) {
        int[] a = {-2, 11, -4, 13, -5, 2};
        f3(a);
    }
    public static void f3(int[] a)  {
        int[] dp = new int[a.length];
        int max = Integer.MIN_VALUE;//记录最大连续子串的和,先给定无穷小
        dp[0] = Math.max(0, a[0]);
        for (int i = 1 ; i < a.length; i++) {
            dp[i] = Math.max(dp[i - 1] + a[i], a[i]);
            if (max < dp[i]) {
                max = dp[i];
            }
        }
        System.out.println(Arrays.toString(dp));
        System.out.println(max);
    }
}


得到运行后的dp数组以及最大子串的和


9621d2077e8d4130be53ec7ce7721c0c.png

LCS最长公共子序列

       最长公共子序列(LCS)是一个在一个序列集合中(通常为两个序列)用来查找所有序列中最长子序列的问题。一个数列 ,如果分别是两个或多个已知数列的子序列,且是所有符合此条件序列中最长的,则称为已知序列的最长公共子序列。



给定两个字符串,找出这两个字符串中最大的公共子序列

s = BDCABA

t = ABCBDAB

a0b2eb7d47dd4a4f9e5f305c7db42e51.png


如果当前比较的两个字符相等,那么dp[i + 1][j + 1] = dp[i][j] + 1;如果不相等,那么dp[i + 1][j + 1] = Math.max(dp[i][j + 1], dp[i + 1][j]);


例如如上表格中,dp[3][5]就表示字符串 BDC 和 ABCBD,公共子序列包含BD和BC,长度为2,所以dp[3][5] = 2。在其基础上都增加一个字符,也就是在dp[4][6]位置,表示字符串 BDCA 和 ABCBDA,增加的都是A字符,所以dp[4][6] = dp[3][5] + 1 = 3。

import java.util.Arrays;
import java.util.Scanner;
public class LCS {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        String s, t;
        while (scanner.hasNext()) {
            s = scanner.next();
            t = scanner.next();
            //dp[i][j]表示s1带si yu t1到tj的最长公共子序列
            int[][] dp = new int[s.length() + 1][t.length() +1];
            //因为s和t是从1开始的所以长度要+1
            for (int i = 0; i < s.length(); i++) {
                for (int j = 0; j < t.length(); j++) {
                    if (s.charAt(i) == t.charAt(j)) {
                        dp[i + 1][j + 1] = dp[i][j] + 1;
                    } else {
                        dp[i + 1][j + 1] = Math.max(dp[i][j + 1], dp[i + 1][j]);
                    }
                }
            }
            System.out.println(dp[s.length()][t.length()]);
            for (int[] i:dp) {
                System.out.println(Arrays.toString(i));
            }
        }
    }
}


运行后求出最大公共子序列和表格

9cecd53b497147948d07f0d8997f4587.png

LIS最长上升子序列


最长上升子序列(Longest Increasing Subsequence,LIS),在计算机科学上是指一个序列中最长的单调递增的子序列,也有些情况求的是最长非降序子序列,二者区别就是序列中是否可以有相等的数。对于固定的数组,虽然LIS序列不一定唯一,但LIS的长度是唯一的。例如给出如下序列 ( 2, 7, 1, 5, 6, 4, 3, 8, 9),就饿可以得到最长上升子序列长度为5,但序列可以为 (2, 7, 6, 8, 9),也可以为 (2, 5, 6, 8, 9)。

eb6f05a80feb49a699080addee1005a3.png

import java.util.Arrays;
import java.util.Scanner;
public class LIS {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        while (scanner.hasNext()) {
            int n = scanner.nextInt();//序列的长度
            int[] a = new int[n];
            for (int i = 0; i < n; i++) {
                a[i] = scanner.nextInt();
            }
            int[] dp = new int[n];
            Arrays.fill(dp, 1);//最小的上升子序列就是当前a[i]本身,例如 5 4 3 2 1
            int l = 0;//记录全局最长的上升子序列
            for (int i = 0; i < n; i++) {
                //dp[0],dp[1] ... dp[n - 1]
                for (int j = 0; j < i; j++) {
                    //求dp[i],需要穷举前[0, 1, .... i - 1]的子状态
                    if (a[i] > a[j]) {//硬性条件,因为只有这样才能和a[i]构成升序
                        dp[i] = Math.max(dp[i], dp[j] + 1);//因为已经默认了所有的最小子序列为1,所以这里dp[j]要+1
                    }
                }
                l = Math.max(dp[i], l);//将dp[i]或者当前的l最大的赋给l
            }
            System.out.println(Arrays.toString(dp));
            System.out.println(l);
        }
    }
}


5bc59a97362d4e67be2b5845fd758192.png


数塔



b7d8babf79874417965fa84aef4ee4c9.png


思路:可以从数塔的下方向上进行循环相加然后比较,如果从上向下无法选择哪一条路得到的结果是最大的。


找到状态转移方程为  a[i][j] += Math.max(a[i + 1][j], a[i + 1][j + 1]),  表示当前的这个数的值等于其下方或右下方两个值其中最大的一个,所以我们也可以判断出需要从倒数第二行进行增加,因为最后一行下方没有值。

import java.util.Arrays;
import java.util.Scanner;
public class numberTower {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        int n = scanner.nextInt();//输入数据行数
        int[][] a = new int[n][n];//数组底和高长度都为n,三角形
        for (int i = 0; i < n; i++) {
            for (int j = 0; j <= i; j++) {
                a[i][j] = scanner.nextInt();//输入数据
            }
        }
        for (int[] b : a) System.out.println(Arrays.toString(b));
        //a[i][j] += Math.max(a[i + 1][j], a[i + 1][j + 1])
        for (int i = n - 2; i >= 0; i--) {//从倒数第二行开始dp,因为从倒数第一行开始的话无法获得最大值
            for (int j = 0; j <= i; j++) {
                a[i][j] += Math.max(a[i + 1][j], a[i + 1][j + 1]);
                //表示当前的这个数的值等于其下方或右下方两个值其中最大的一个
            }
        }
        System.out.println();
        for (int[] b : a) System.out.println(Arrays.toString(b));
        System.out.println(a[0][0]);
    }
}

得到经过节点之和最大为59


526e89a1bd704d8ebed78ce2a940ba7c.png

最大子矩阵和

求矩阵中最大的子矩阵的和


4c6ef0d2110e40848c89bdb6ef262cf6.png

利用前缀和的思想,求出所有可能性子矩阵的前缀和,也就是将一个矩阵块利用前缀和快速的压缩成单行,然后找出这一行中最大的字段和

acd9e60c159645ffa2c3e57d011b3805.png



6b5990d01cda4e3eb95313ce7f83d010.png

import java.util.Arrays;
import java.util.Scanner;
public class maxSubmatrixArray {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        int n = scanner.nextInt();//表示矩阵的长和宽的长度
        int[][] g = new int[n + 1][n + 1];
        for (int i = 1; i <= n; i++) {
            for (int j = 1; j <= n; j++) {
                g[i][j] = scanner.nextInt();
                g[i][j] += g[i - 1][j];//按行同步计算二维数组的前缀和
            }
        }
        int ans = Integer.MIN_VALUE;//开始时先给最小值,用来存放子矩阵的最大的和
        for (int start = 1; start <= n; start++) {
            for (int end = 1; end <= n; end++) {
                //枚举从start到end行的矩阵块
                //从start到end行的矩阵块,我们可以用前缀和快速地压缩成单行的和
                int dpi = 0;
                for (int col = 1; col <= n; col++) {
                    int ai = g[end][col] - g[start - 1][col];//根据前缀和快速计算start行到end列的累加和
                    dpi = Math.max(dpi + ai, ai);
                    ans = Math.max(ans, dpi);
                }
            }
        }
        for (int[] x : g) System.out.println(Arrays.toString(x));
        System.out.println(ans);
    }
}

5ce96fdea1cd461997c29c6ae03b68ac.png


背包问题

01背包

       01背包问题是一个经典的背包问题,是指一个固定容量的背包,以及一些物品,每个物品有自己的体积和价值,在不超过背包容量的情况下,将一些物品装入背包中,使得背包中物品的总价值最大。


给定5个物品,以及一个容量为10的背包,根据所有物品的价值,找出背包中物品总价值最大的值。


物品体积为:{2, 5, 4, 2, 3} 所对应的价值为:{6, 3, 5, 4, 6}


求解思路:


使用动态规划算法。先定义一个二维数组 dp,其中 dp[i][j] 表示前 i 个物品放入容量为 j 的背包可以获得的最大价值。对于第 i 个物品,它可以选择放入背包中或不放入背包中,所以对于每一个物品,我们需要遍历所有的背包容量 j,如果选择放入背包,那么它对 dp[i][j] 的贡献就是当前物品的价值,同时还需要考虑容量剩余 j - v[i] 的最大价值,即 dp[i - 1][j - v[i]];如果选择不放入背包,那么它对 dp[i][j] 的贡献就是 0,容量也不需要修改。


所以,动态转移方程为:dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - v[i]] + w[i])

import java.util.Arrays;
import java.util.Scanner;
public class beibao01 {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        int n = scanner.nextInt();//n个物品的体积
        int m = scanner.nextInt();//背包的容量
        int[] v = new int[n + 1];//存放n个物品的体积,n+1是因为i从1开始
        int[] w = new int[n + 1];//存放n个物品的价值
        int[][] dp = new int[n + 1][m + 1];
        //dp[i][j]只考虑前i种物品,书包容量为j时为最后装载的价值
        for (int i = 1; i <= n; i++) {
            //输入每个物品的体积和价值
            v[i] = scanner.nextInt();
            w[i] = scanner.nextInt();
        }
        for (int i = 1; i <= n; i++) {//穷举前i个物品的体积
            for (int j = 1; j <= m; j++) {//前i个物品的体积确定后穷举背包的容量
                if (j < v[i]) {
                    //如果当前物品的体积大于背包的容量,就相当于用第i - 1个物品去装j容量的背包
                    dp[i][j] = dp[i - 1][j];
                } else {
                    //如果当前物品可以被背包装下,就要考虑价值的问题,可以选择装或者不装,取价值最大值
                    dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - v[i]] + w[i]);
                }
            }
        }
        for (int[] x : dp) System.out.println(Arrays.toString(x));
        System.out.println(dp[n][m]);
    }
}

得到规划后的数组以及最大的价值为17

c6d7b0bf030b4498843d96c1caa7fc01.png


完全背包


完全背包问题是一个经典的动态规划问题,它是背包问题的一个变种。在这个问题中,有一个固定大小的背包,和一些可放入背包中的物品。每种物品都有一个对应的价值和重量,无限个可用。需要确定如何选择物品放入背包,使得背包中物品的总价值最大。和0-1背包问题不同的是,在完全背包问题中,每个物品是无限可用的,可以选择多次放入。

import java.util.*;
public class Main {
    public static void main(String[] args) {
        Scanner input = new Scanner(System.in);
        // 输入物品数量和背包容量
        int n = input.nextInt(); // 物品数量
        int m = input.nextInt(); // 背包容量
        // 初始化两个数组
        int[] w = new int[n + 1]; // 物品的重量
        int[] v = new int[n + 1]; // 物品的价值
        // 输入每个物品的重量和价值
        for (int i = 1; i <= n; i++) {
            w[i] = input.nextInt();
            v[i] = input.nextInt();
        }
        // 初始化dp数组,dp[i][j]表示前i个物品,容量为j的背包的最大价值
        int[][] dp = new int[n + 1][m + 1];
        // 递推求解dp数组
        for (int i = 1; i <= n; i++) {
            for (int j = 1; j <= m; j++) {
                // 当前物品可以重复选择
                for (int k = 0; k <= j / w[i]; k++) {
                    dp[i][j] = Math.max(dp[i][j], dp[i - 1][j - k * w[i]] + k * v[i]);
                }
            }
        }
        // 输出最大价值
        System.out.println(dp[n][m]);
    }
}

多重背包


多重背包问题是背包问题的一种扩展,指的是有一批物品,每种物品有数量限制,每种物品的数量可以有多个,而背包的容量是固定的,目标是选取一些物品放入背包中,使得在物品数量不超过限制的前提下,背包中物品的总价值最大。


       和01背包问题相比,多重背包问题的每种物品可以选择多个,而01背包问题的每种物品只能选择一个。而和完全背包问题相比,多重背包问题的每种物品有数量限制,而完全背包问题的每种物品可以选择无限个。因此,多重背包问题是背包问题的中间难度,介于01背包和完全背包之间。

import java.util.Scanner;
public class MultipleKnapsack {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        // 物品数量
        int N = sc.nextInt();
        // 背包容量
        int V = sc.nextInt();
        // 物品价值数组
        int[] values = new int[N + 1];
        // 物品重量数组
        int[] weights = new int[N + 1];
        // 物品数量数组
        int[] nums = new int[N + 1];
        // 输入物品信息
        for (int i = 1; i <= N; i++) {
            values[i] = sc.nextInt();
            weights[i] = sc.nextInt();
            nums[i] = sc.nextInt();
        }
        // dp 数组,dp[i][j] 表示前 i 件物品,容量为 j 时的最大价值
        int[][] dp = new int[N + 1][V + 1];
        // 逐个物品考虑
        for (int i = 1; i <= N; i++) {
            // 逐个容量考虑
            for (int j = 0; j <= V; j++) {
                // 初始化 dp[i][j],即前 i 件物品容量为 j 时的最大价值
                dp[i][j] = dp[i - 1][j];
                // 遍历物品数量,尝试更新 dp[i][j]
                for (int k = 1; k <= nums[i]; k++) {
                    // 如果当前容量 j 可以放下当前物品
                    if (j >= k * weights[i]) {
                        // 更新 dp[i][j]
                        dp[i][j] = Math.max(dp[i][j], dp[i - 1][j - k * weights[i]] + k * values[i]);
                    }
                }
            }
        }
        System.out.println(dp[N][V]);
    }
}


相关文章
|
6月前
|
算法
数据结构与算法之动态规划
数据结构与算法之动态规划
66 2
|
存储 机器学习/深度学习 算法
第 12 天_动态规划【算法入门】
第 12 天_动态规划【算法入门】
124 0
|
算法
数据结构与算法(五) 动态规划 下
数据结构与算法(五) 动态规划
66 0
|
算法
数据结构与算法(五) 动态规划 上
数据结构与算法(五) 动态规划
70 0
|
算法
动态规划从入门到LeetCode
动态规划从入门到LeetCode
92 0
|
算法
数据结构与算法(九)动态规划
数据结构与算法(九)动态规划
87 0
|
存储 缓存 算法
一文搞懂动态规划
很久前就有小伙伴被动态规划所折磨,确实,很多题动态规划确实太难看出了了,甚至有的题看了题解理解起来都费劲半天。 动态规划的范围虽然确实是很广很难,但是从整个动态规划出现的频率来看,这几种基础的动态规划理解容易,学习起来压力不大,并且出现频率非常高。
180 0
一文搞懂动态规划
|
算法 机器人
动态规划合集一
动态规划合集一
132 0
|
算法 索引
动态规划合集二
动态规划合集二
86 0