算法-动态规划

目录

动态规划:

最长子序列

0-1背包问题

石头相撞求最后剩余石头最小值(0-1背包)


动态规划:

概念:问题的最优解如果可以由子问题的最优解推导得到,则可以先求解子问题的最优解,在构造原问题的最优解;若子问题有较多的重复出现,则可以自底向上从最终子问题向原问题逐步求解

特点:

  • 把原始问题划分成一系列子问题;
  • 求解每个子问题仅一次,并将其结果保存在一个表中,以后用到时直接存取,不重复计算,节省计算时间
  • 自底向上地计算。
  • 整体问题最优解取决于子问题的最优解(状态转移方程)(将子问题称为状态,最终状态的求解归结为其他状态的求解)

最长子序列

从序列中选出若干个数组成一个新的序列,不改变他们的队伍的顺序,要求新的序列里xi≤xi+1≤xi+1.....举个例子{4,6,5,7,3},最长不下降子序列就是{4,6,7}/{4,5,7}

子问题表示:令dp[i]表示以第i个元素结尾的前i个元素构成的最长不下降子序列的长度

方程:dp[i] = max{dp[j] | 0<j<i , aj≥ai} + 1

package com.anran.example.test;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;

import java.util.*;

/**
 */
public class Main10 {

    public static void main(String[] args){
        int[] nums = getInput();
        // 用于存储当前位置到之前最大子序列的长度信息
        List<Map<String, Object>> dp = new ArrayList<>();
        int maxNum = 0;
        for (int i = 0; i < nums.length; i++) {
            // 先设置当前节点之前满足降序的长度为0
            dp.add(initDP());
            // 获取当前节点大于之前节点并且子序列最长的信息
            for (int j = 0; j <= i - 1; j++) {
                if (nums[j] <= nums[i] && (int)dp.get(j).get("count") >= (int)dp.get(i).get("count")) {
                    // 添加对应子序列信息
                    List<String> info = (List<String>) dp.get(i).get("info");
                    if ((int)dp.get(j).get("count") > (int)dp.get(i).get("count")) {
                        info.clear();
                    }
                    info.addAll((List<String>) dp.get(j).get("info"));

                    dp.get(i).put("count", dp.get(j).get("count"));
                }
            }
            // 添加当前节点长度和当前节点信息
            dealAfter(dp.get(i), nums[i]);
            maxNum = maxNum > (int)dp.get(i).get("count") ? maxNum : (int)dp.get(i).get("count");
        }
        // 找到早的的子序列并输出
        for (Map<String, Object> map : dp){
            if ((int)map.get("count") == maxNum) {
                List<String> info = (List<String>) map.get("info");
                for (String str : info) {
                    System.out.println(str);
                }
            }
        }
    }

    private static void dealAfter(Map<String, Object> dpMap, int num) {
        dpMap.put("count", (int)dpMap.get("count") + 1);
        List<String> info = (List<String>) dpMap.get("info");
        if (info.size() == 0) {
            info.add(String.valueOf(num));
        } else {
            for (int i = 0; i < info.size(); i++) {
                info.set(i, info.get(i) + "-" + num);
            }
        }
    }

    private static Map initDP(){
        Map<String, Object> map = new HashMap<>();
        map.put("count", 0);
        map.put("info", new ArrayList<String>());
        return map;
    }

    private static int[] getInput() {
        Scanner sc=new Scanner(System.in);
        String input = sc.next();
        String[] strs = input.split(",");
        int[] nums = new int[strs.length];
        for (int i = 0; i < strs.length; i++) {
            nums[i] = Integer.parseInt(strs[i]);
        }
        return nums;
    }
}

运行截图:

 

0-1背包问题

问题描述:

    现有n件物品和一个空间为c的背包。第i件物品的重量是重量为w[i],价值是v[i]。已知对于一件物品必须选择取(用1表示)或者不取(用0表示),且每件物品只能被取一次(这就是“0-1”的含义)。求放置哪些物品进背包,可使这些物品的空间总和不超过背包空间,且价值总和最大。

求解思路:

    假设有5件物品,其空间分别是w={2,2,6,5,4},价值分别是v={6,3,5,4,6},背包空间为10。在数学问题中这是典型的线性规划问题,我们可以在线性约束范围内求解目标表达式。但是怎么用计算机语言实现呢?我们可以先这样考虑,当背包空间为1时,如何放置物品才能使背包中价值最大;同样当背包空间为2时,如何放置能使背包中价值最大,以此类推,直到背包空间为10。此时我们需要维护一张二维表m[i][j],其中横坐标i表示物品,纵坐标表示背包空间(1<=j<=10)。

 m[i][j]表示当可以放入前i件物品且背包空间为j时的最大价值。当只能放入第一件物品即i=0时:若背包空间j<w[0],物品不能够被放入背包;若j>=w[0]时,物品可以放入背包,此时m[0][j]=v[0]。当可以放入前2件物品即i=1时,我们需要进行这样的处理:若j<w[1]时,说明第2件物品不能被放入背包内,此时背包的最大价值为背包中只放入第一件物品的最大价值,即m[1][j]=m[0][j];若j>=w[1]时,假设此时背包空间j=8,第二件物品可以被放入背包内,那么便会出现两种情况:

    (1)将第二件物品放入背包,那么背包中物品的最大价值是多少呢?因为第二件物品空间为w[1]=2,在将第二件物品放入背包之前,背包的空间应为j-w[1]=8-2=6,此时背包的最大价值是m[0][6],因此若将第二件物品放入背包,其背包的最大价值m[1][j]=m[0][j-w[1]]+v[1];

    (2)不将第二件物品放入背包,那么此时背包中物品的最大价值依然为只放入第一件物品时背包的最大价值,即m[1][j]=m[0][j];

    我们选取(1)(2)中价值的较大者作为i=1,j=8时背包中的最大价值。

    i=2,3,4时的分析同上,直到背包的空间为10,此时m[4][10]即为背包中物品的最大价值。

    有了上面的分析,我们很容易写出下面的递归关系:

    (1)i=0  当j<w[0]时,m[0][j]=0;当j>=w[0]时,m[0][j]=v[0]。

    (2)i>0  当j<w[i],m[i][j]=m[i-1][j];当j>=w[i],m[i][j]=max{m[i-1][j-w[i]]+v[i],m[i-1][j]}。

    得到了满足约束条件的背包中物品的最大价值后,需要知道是哪些物品被放入了背包。观察二维表m[i][j],我们注意到m[i][c]表示当背包空间为题目中要求的c时背包的最大价值,那么在得到m[i][c]之前,我们必然是比较了m[i-1][j-w[i]]+v[i]与m[i-1][j]的大小,从而决定是否将物品放入背包。所以我们可以利用回溯的方法,若m[i][j]=m[i-1][j],那么物品没有放入背包;否则物品一定被放入背包。因此我们可以从最后一件物品开始,一步一步回退到第一件物品,直到找到所有的物品放入背包的情况。本题中物品的装入情况如表中红色和蓝色部分所示,其中红色表示当前物品被装入背包,蓝色表示没有装入背包。

package com.anran.example.test;

import java.util.*;

/**
 */
public class Main11 {

    public static void main(String[] args){
        // 背包空间
        int packageNum = 10;
        // 商品所有空间
        int[] w = {2, 2, 6, 5, 4};
        // 商品价值
        int[] v = {6, 3, 5 ,4, 6};
        // 用于存放背包容量和存放物品时的最大价值
        int[][] maxValues = new int[w.length][packageNum + 1];
        // 单个善意一次加入背包时,计算背包的自大价值
        for (int i = 0; i < w.length; i++) {
            for (int j = 0; j <= packageNum; j++) {
                // 判断商品空间和背包空间大小
                if (w[i] > j) {
                    // 当前商品空间大于背包空间,商品不能够放入背包,如果是第一次放入价值是0,如果不是第一次放入,最大价值是上一次放入相同空间的值
                    maxValues[i][j] = i == 0 ? 0 : maxValues[i - 1][j];
                } else {
                    // 当前商品可以放入背包
                    if (i == 0) {
                        // 如果是第一次放入,对应最大价值就是当前商品的价值
                        maxValues[i][j] = v[i];
                    } else {
                        // 当前商品放入:最大值等于当前商品的价值加上剩余空间的上一个商品的最大价值
                        int addMaxValue = v[i] + maxValues[i - 1][j - w[i]];
                        // 当前商品不放入:最大价值等于上一个商品相同容量的最大值
                        int noAddMaxVaule = maxValues[i - 1][j];
                        maxValues[i][j] = addMaxValue > noAddMaxVaule ? addMaxValue : noAddMaxVaule;
                    }
                }
            }
        }
        System.out.println("可以装的最大价值 : " + maxValues[w.length - 1][packageNum]);
        // 通过回溯的方式计算那些商品加入到背包
        int[] x = new int[w.length];
        int nextPackageNum = packageNum;
        for (int i = w.length - 1; i > 0; i--) {
            // 如果当产品的值和上一个产品的值相等
            if (maxValues[i][nextPackageNum] > maxValues[i - 1][nextPackageNum]) {
                // 大于表示当前产品装入了背包
                x[i] = 1;
                nextPackageNum = nextPackageNum - w[i];
            } else {
                // 没有装入
                x[i] = 0;
            }
        }
        // 处理第一个产品
        if (nextPackageNum >= w[0]) {
            x[0] = 1;
        }
        // 输出装入的产品
        System.out.print("装入产品的下标是:");
        for (int i = 0; i < x.length; i++) {
            if (x[i] == 1) {
                System.out.print(i + " ");
            }
        }
    }
}

运行截图:

传送门:https://www.nowcoder.com/discuss/3574

 

石头相撞求最后剩余石头最小值(0-1背包)

题目:两个石头相撞,生成一个新的石头,新石头的质量是两者差的绝对值,求一堆石头相撞剩余最后一个的最小质量?

输入描述: 第一行输入石头个数(<=100)   第二行输入石头质量,以空格分割,石头质量总和<=10000 输出描述: 最终的石头质量 输入例子1: 6 2 7 4 1 8 1 输出例子1: 1

变相理解:将一堆石头分成两堆,然后这两堆相撞,最终生成一个石头,要想最后一个石头质量最小,则这两堆的质量应该相近,也就是说从一堆石头怎样组合质量能够保证最接所有石头的中间质量

实现逻辑:
1:计算石头的总重量以及中间重量的数值,将中间数值作为背包的中最大量容量,计算那种场景背包可以放入的重量最大
2:因为背包要放入最大的质量,因此当前背包的最大值应该从(上一次当前容量的最大值)和(当前放入质量和剩余容量在上一次背包的最大值)的最大值
3:在计算完所有物品放入场景之后,当前背包中最大容量对应的实际数量就是一个堆里面放入石头的重量
4:此时用总重量减去当前计算出堆中最合理存放的最大重量就是最后剩余石头最小质量

图表展示如下:

编号重量01234567891011
12  2222222222
27       77999
34    446779911
41 1234567891011
58        891011
61 1234567891011

代码实现如下:

package com.anran.example.kuaishou0511;

import java.util.Scanner;

public class Main2 {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        int count  = sc.nextInt();
        int sum = 0;
        int[] infos = new int[count];
        for (int i = 0; i < count; i++) {
            infos[i] = sc.nextInt();
            sum += infos[i];
        }
        // 获取背包的最大容量
        int packageMaxNum = (sum / 2);
        // 使用一维数组保存当前背包容量下最多可以放入多少石头,数组+1是因为存在都不放入的数据
        // (数组下标表示容量,内容表示最大放入的质量)
        int[] dp = new int[packageMaxNum + 1];

        // 依次将石头放入背包
        for (int info : infos) {
            // 从背包最大容量到背包容量和相等,依次放入同一个石头
            for (int i = packageMaxNum; i >= info;i--) {
                // 上一次比对当前容量的最大值
                // 当前石头的重量+背包剩余容量在上一次的最大值
                // 两者取其重量大的作为当前背包重量的最优选
                dp[i] = Math.max(dp[i], dp[i - info] + info);
            }
        }
        // 所有石头都尝试放入之后,背包中最后一位存放的是背包容量最多的值
        System.out.println(sum - dp[packageMaxNum] * 2);
    }
}

运行截图:

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
五大常用算法动态规划、分治、递归、贪心和回溯。 动态规划是一种将问题分解成子问题并保存子问题解的方法。通过求解子问题,可以逐步推导出原始问题的解。动态规划通常用于求解最优化问题,例如最长公共子序列、最短路径等。 分治是将原问题划分成多个相互独立的子问题,然后通过递归的方式求解子问题,并将子问题的解合并成原问题的解。分治算法常用于排序、快速幂等问题。 递归是通过函数调用自身来解决问题的方法。递归算法在问题定义可以被分解为较小规模或更简单情况的时候很有用。例如,计一个数的阶乘,就可以使用递归实现。 贪心算法是一种选择当前最优策略的方法,即在每一步选取最优解,最终得到全局最优解的算法。贪心算法通常用于解决无后效性的问题,例如最小生成树、哈夫曼编码等。 回溯是一种通过穷举搜索所有可能的解空间,找到满足条件的解的方法。回溯算法在解决组合问题、排序问题、子集和问题等方面很有效。回溯算法通过递归的方式逐步构建解,当发现当前解不满足条件时,会回退到上一步继续搜索其他可能的解。 这五种常用算法在不同的问题领域中都有广泛应用,每种算法都有自己的特点和适用范围。在解决具体问题时,可以根据问题的性质和要求选择最适合的算法进行求解。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值