背包问题变形(蓝桥杯拿金币)与误区(网格贪吃蛇(求助))

469 阅读6分钟

「这是我参与2022首次更文挑战的第27天,活动详情查看:2022首次更文挑战

前言

首先背包问题是一个非常经典的dp题目思想解决的题目(当然,你也可以是贪心算法,或者直接暴力递归来解决问题)。之所以要说明这个其实主要是发现在蓝桥杯里面有一些关于路径(地图走格子的问题)问题在本质上其实就是属于背包问题的变形。这一点非常重要,(实际上我先前根本没反应过来)。

背包问题

在此事前我们先简单来阐述一下什么是背包问题,这样了解了本质我们就能够准确识别出来什么是可以使用这种方式来解决问题的。

首先背包问题就是,在一个有限的背包内,尽可能去装下更多的物品,每个物品都是有自己的质量和价值的,我们要让价值最大化!

那么在这里我们先来说说,为什么背包问题可以是一个动态规划的题目,对于动态规划有一个明显的特征,当前状态取决于上一个状态,例如斐波那契数列(当然关于斐波那契数列我们又可以使用矩阵幂乘法来加速解决问题)。那么在背包问题里面,显然,下一个物品要不要放置,取决于当前的背包容量和上一个物品有没有放置,之后放置之后的值就是 max(没有放置上一个物品,放置了上一个物品)而,上一个物品又是取决于上上个物品和上物品,这样我们发现这个就相当于变成了走楼梯,斐波那契数列的问题。这样一来我们就懂了,背包问题其实就是在原来简单的动态规划的前提下,加入了最大化(最小化)的约束,也就是多加了一个约束!

那么在理解了背包问题之后,我们来说说在蓝桥杯题目里面我见到的类似背包问题的题目,首先最明显的就是那个走格子问题,在格子上面有一定的value,然后让这个value最大。

例如:

image.png

这样就是一个标准的背包问题。

但是这篇文章的重点并不是说说明那个背包问题,而是说,聊聊那个变形。这里以蓝桥杯的两个例题来说明,我们的变形,关于背包问题的变形。

动态规划三部曲

在先前我们说过,那个关于动态规划的那个技巧,套路。 这里的话我在说一下

一:明确目标,确定动态数组含义

二:确定动态转移方程,确定清楚初始化操作

三:确定迭代过程,最终确定结果

这里的话大概其实就是三部曲,其实不难,主要是先想清楚第一步,后面再慢慢推出来,一定要耐心。

拿金币问题

这个是蓝桥杯上面的一道题目,仔细观察,你就会发现它其实是满足的背包的一些特点,当然并不是全部满足,但是对于待会的第二题你一定会发现直呼好家伙,但是那一题有个小问题(待会我会详细说明)。

image.png

这里我先给出解答的代码

import java.util.Scanner;
public class Main {
    public static void main(String[] args) {
        Scanner input = new Scanner(System.in);
        int n =input.nextInt();
        int a[][] = new int[n][n];
        for (int i = 0; i <n; i++) {
            for (int j = 0; j <n; j++) {
                a[i][j] = input.nextInt();
            }
        }
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < n; j++) {
                if(i==0&&j>0){  //判断上边界
                    a[i][j]=a[i][j]+a[i][j-1];
                }
                else if(j==0&&i>0){ //判断左边界
                    a[i][j]=a[i][j]+a[i-1][j];
                }
                else if(i>0){ //判断再中间,必选判断
                    a[i][j]=a[i][j]+Math.max(a[i-1][j],a[i][j-1]);
                }
            }
        }
        System.out.println(a[n-1][n-1]);//输出最后一个值
    }
}

解析

这个解析呢,其实很简单。

image.png

就是我一开始说的,那个只是加了一个约束。

那么接下来的一题就是基本上就是背包问题改编过来的。但是这里作为一个训练提高的问题,还是有点技巧的,这个我说明一下。

贪吃蛇网格

来先看题目:

image.png

方案一

这里其实还是说,我们有两个想法,第一个想法,当然就是,像先前那样。只是现在不是每一个格子都是有value了,那么我们就可以简单地处理为0。

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.StringTokenizer;

class Main {

    public static void main(String[] args) throws IOException {

        //获取初始化数据
        Reader.init(System.in);
        int M = Reader.nextInt();
        int N = Reader.nextInt();
        int BeansNumber = Reader.nextInt();

        int[][] dp = new int[M][N];
        for(int i=0;i<BeansNumber;i++){
            int X = Reader.nextInt();
            int Y = Reader.nextInt();
            dp[X][Y] = 1;//初始化网格
        }
        //这里的dp表示在i,j格子内能够得到的最大的豆子得分

        for(int i=0;i<M;i++){
            for(int j=0;j<N;j++){
                if(i==0&&j>0){  //判断上边界
                    dp[i][j]=dp[i][j]+dp[i][j-1];
                }
                else if(j==0&&i>0){ //判断左边界
                    dp[i][j]=dp[i][j]+dp[i-1][j];
                }
                else if(i>0){ //判断再中间,必选判断
                    dp[i][j]=dp[i][j]+Math.max(dp[i-1][j],dp[i][j-1]);
                }
            }
            }
        System.out.println(dp[M-1][N-1]);
        }
        

    }


class Reader {
    static BufferedReader reader;
    static StringTokenizer tokenizer;

    /**
     * call this method to initialize reader for InputStream
     */
    static void init(InputStream input) {
        reader = new BufferedReader(
                new InputStreamReader(input));
        tokenizer = new StringTokenizer("");
    }

    /**
     * get next word
     */
    static String next() throws IOException {
        while (!tokenizer.hasMoreTokens()) {
            //TODO add check for eof if necessary
            tokenizer = new StringTokenizer(
                    reader.readLine());
        }
        return tokenizer.nextToken();
    }

    static int nextInt() throws IOException {
        return Integer.parseInt(next());
    }

    static double nextDouble() throws IOException {
        return Double.parseDouble(next());
    }
}

这里注意我把坐标做了一个转换,这个其实无伤大雅。

方案二(错误思想)

这个就是把它当做背包问题来做,这个其实一样,只是我们的动态方程会发生一点改变。 来我们先简单地把它看做背包模型来做,首先我们明确地知道它对应的value就是一,此时我们把X轴看做物品编号,有M个物品从1到M,Y看做对应的质量。

例如有一个点 2,4 那么我就看做编号2的物品质量为4,对应的值为1。那么原点那我就看成物品编号0 质量0 对应的值也为0。这样看来不就变成了一个背包问题了嘛,为什么要看做背包问题,这里有两个原因,第一个就是我要说明的这种类型的题目是背包问题的变形,还有一个原因,待会你就知道了。那么现在我们改造一下这个问题的代码,写把它变成普通的0 1 背包问题的代码。至于那个背包容量不就是N嘛。但是这里有一个误区,那就是,有可能出现5,2 5,3 这种点,那这样的话,这样同一个物体就出现了两个质量这个显然不ok,当然这个是我后面才想到的。

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.StringTokenizer;

class Main {

    public static void main(String[] args) throws IOException {

        Reader.init(System.in);
        int M = Reader.nextInt();
        int N = Reader.nextInt();
        int BeansNumber = Reader.nextInt();
        int[] Y_weight = new int[N];
        int[] Y_V = new int[N];
        for (int i = 0; i < BeansNumber; i++) {
            int X = Reader.nextInt();
            int Y = Reader.nextInt();
            Y_weight[X] = Y;
            Y_V[X] = 1;
        }
        int[] dp =new int[N];
        for(int i=0;i<Y_weight.length;i++){
            //逆序
            for(int j=dp.length-1;j>Y_weight[i];j--){
                dp[j] = Math.max(dp[j],dp[j-Y_weight[i]]+Y_V[i]);
            }
        }
        System.out.println(dp[N-1]);
    }
}


class Reader {
    static BufferedReader reader;
    static StringTokenizer tokenizer;

    /**
     * call this method to initialize reader for InputStream
     */
    static void init(InputStream input) {
        reader = new BufferedReader(
                new InputStreamReader(input));
        tokenizer = new StringTokenizer("");
    }

    /**
     * get next word
     */
    static String next() throws IOException {
        while (!tokenizer.hasMoreTokens()) {
            //TODO add check for eof if necessary
            tokenizer = new StringTokenizer(
                    reader.readLine());
        }
        return tokenizer.nextToken();
    }

    static int nextInt() throws IOException {
        return Integer.parseInt(next());
    }

    static double nextDouble() throws IOException {
        return Double.parseDouble(next());
    }
}

这里我之所以会想到这个其实是因为上面那个解法,超内存了,所以想到背包问题可以将维(把dp数组)但是结果完全不对,就是我刚刚说的。 那么现在如何解决,这个说实话,我没有想到一个好的解决方案...... 求大佬踹我!