取钱

import java.util.*;
public class Main{
    static int[] dp=new int[(int)1e5+1];
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        Arrays.fill(dp,(int)1e6);
        dp[0]=0;
        int[] arr = {1, 4, 5, 10, 20};
        for (int i = 0; i < dp.length; i++) {
            for (int j : arr) {
                if (j <= i) {
                    dp[i] = Math.min(dp[i - j] + 1, dp[i]);
                }
            }
        }
        while(sc.hasNext()){
            int t=sc.nextInt();
            System.out.println(dp[t]);
        }
    }
}

 

小蓝来到了一座高耸的楼梯前,楼梯共有 N 级台阶,从第 0 级台阶出发。小蓝每次可以迈上 1 级或 2 级台阶。但是,楼梯上的第 a1 级、第 a2 级、第 a3 级,以此类推,共 M 级台阶的台阶面已经坏了,不能踩上去。

现在,小蓝想要到达楼梯的顶端,也就是第 N 级台阶,但他不能踩到坏了的台阶上。请问他有多少种不踩坏了的台阶到达顶端的方案数?

由于方案数很大,请输出其对 109+7 取模的结果。

输入格式

第一行包含两个正整数 N1≤N≤105)和 M0≤M≤N),表示楼梯的总级数和坏了的台阶数。

接下来一行,包含 M 个正整数 a1,a2,…,aM1≤a1<a2<a3<aM≤N),表示坏掉的台阶的编号。

输出格式

输出一个整数,表示小蓝到达楼梯顶端的方案数,对 109+7 取模。

import java.util.*;
public class Main{
    static long[] dp=new long[(int)1e5+1];
    static int mod=(int)1e9+7;
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);

        dp[0]=1;

        int[] arr = {1, 2};
        int n=sc.nextInt();
        int t=sc.nextInt();
        while(t-->0){
            int m=sc.nextInt();
            dp[m]=-1;
        }
        for(int i=0;i<dp.length;i++){
            if(dp[i]==-1)continue;
            if(i>=1&&dp[i-1]!=-1) dp[i]+=dp[i-1]%mod;
            if(i>=2&&dp[i-2]!=-1) dp[i]+=dp[i-2]%mod;
            dp[i]%=mod;
        }
        if(dp[n]==-1) System.out.println(0);
        else
        System.out.println(dp[n]);

    }
}

传球游戏

题目描述

上体育课的时候,小蛮的老师经常带着同学们一起做游戏。这次,老师带着同学们一起做传球游戏。

游戏规则是这样的:n 个同学站成一个圆圈,其中的一个同学手里拿着一个球,当老师吹哨子时开始传球,每个同学可以把球传给自己左右的两个同学中的一个(左右任意),当老师再次吹哨子时,传球停止,此时,拿着球没传出去的那个同学就是败者,要给大家表演一个节目。

聪明的小蛮提出一个有趣的问题:有多少种不同的传球方法可以使得从小蛮手里开始传的球,传了 m 次以后,又回到小蛮手里。两种传球的方法被视作不同的方法,当且仅当这两种方法中,接到球的同学按接球顺序组成的序列是不同的。比如有 3 个同学 1 号、2 号、3 号,并假设小蛮为 1 号,球传了 3 次回到小蛮手里的方式有 1->2->3->1 和 1->3->2->1,共 2 种。

输入描述

输入一行,有两个用空格隔开的整数 n,m (3≤n≤30,1≤m≤30)

输出描述

输出一行,有一个整数,表示符合题意的方法数。

输入输出样例

示例 1

输入

3 3
import java.util.*;
public class Main{
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        int n=sc.nextInt();
        int m=sc.nextInt();
        int[][] dp=new int[m+1][n];
        dp[0][0]=1;
        for(int i=1;i<=m;i++){
            for(int j=0;j<n;j++){
                dp[i][j]+=(dp[i-1][(j+1)%n]+dp[i-1][(n+j-1)%n]);
                //传球来自左边和右边,j+1是右边,n+j-1是右边,注意使用时推导
            }
        }
        System.out.println(dp[m][0]);
    }
}

摆花

题目描述

小明的花店新开张,为了吸引顾客,他想在花店的门口摆上一排花,共 m 盆。通过调查顾客的喜好,小明列出了顾客最喜欢的 n 种花,从 1 到 n 标号。为了在门口展出更多种花,规定第 i 种花不能超过 ai 盆,摆花时同一种花放在一起,且不同种类的花需按标号的从小到大的顺序依次摆列。

试编程计算,一共有多少种不同的摆花方案。

输入描述

第一行包含两个正整数 n 和 m,中间用一个空格隔开。

第二行有 n 个整数,每两个整数之间用一个空格隔开,依次表示 a1、a2、......an

其中,0<n≤100,0<m≤100,0≤ai≤100

输出描述

输出只有一行,一个整数,表示有多少种方案。注意:因为方案数可能很多,请输出方案数对 106+7 取模的结果。

import java.util.Scanner;

public class Main {
public static void main(String[] args) {
    Scanner scan=new Scanner(System.in);
    int n=scan.nextInt();
    int m=scan.nextInt();
    int[][] dp=new int[n+1][m+1];//n为花的种类,m为总共盆数
    int[] a=new int[n+1];//用来表示每种花摆多少盆
    for(int i=1;i<=n;i++) {
        a[i]=scan.nextInt();//输入每种花有多少盆
    }
    for(int i=0;i<=n;i++) {//表示第i种花摆0盆为1种方案
        dp[i][0]=1;
    }
    for(int i=1;i<=n;i++) {//遍历花的种类
        for(int j=1;j<=m;j++) {//遍历花的总盆数
            for(int k=0;k<=j&&k<=a[i];k++) {//遍历每种花的盆数
                dp[i][j]+=dp[i-1][j-k];//表示第i种花摆j盆的方案数
                dp[i][j]%=(int)1e6+7;//取模
            }
        }
    }
    System.out.println(dp[n][m]);
}
}

DP 概述

DP(dynamic Progamming),动态规划算法,是一类常见、常考的算法。

在算法竞赛中,DP的考法多而杂,并且难度可以从简单到超难,主要难在状态的设计,以及思考如何转移;但是在蓝桥杯比赛中,涉及到的往往都是简单,基础的DP考点,十分考验基本功。

动态规划是一种通过把原问题分解为相对简单的子问题的方式求解复杂问题的方法。

由于动态规划并不是某种具体的算法,而是一种解决特定问题的方法,因此它会出现在各式各样的数据结构中,与之相关的题目种类也更为繁杂。

在算法类竞赛中,计数等非最优化问题的递推解法也常被不规范地称作 DP。

主要思想以及条件

DP 将原问题划分划分为若干个重叠的子问题,并且逐层递进,每个子问题在求解的过程中,都会被抽象为“阶段”,也叫做“状态”,在完成前一个阶段的计算后,才能进行下一阶段的计算。

最广泛运用的例子就是数字三角形问题:

给定一个三角形,第 i 行有 i 个元素,如下图:

        5 
      5   4 
    8   9   0 

你初始在第一行的第一个元素位置,每一次,可以选择左下,或者右下的位置进行移动,每次移动到一个位置上,可以获得相应的分数。问:你可以任意规划自己的路线,请问在走到最后一行时,能够获得的最大分数是多少?

同学们可以自行计算一下,当然,很多同学可以一眼看出来,答案是 5+5+9=19。从第一行开始,往左边,然后往右边。

如果利用动态规划的思想,应该如下考虑:

而对于每一个点,它的下一步决策只有两种:往左下角或者往右下角(如果存在)。因此只需要记录当前点的最大权值,用这个最大权值执行下一步决策,来更新后续点的最大权值。即,对于每一个点,当作一种状态,代表的意义是:到达当前点能累积的最大分数。

如果你理解了这一步,并且认为其设计的十分有道理,那么我们来剖析一下其中原理:

条件

能用动态规划解决的问题,需要满足三个条件:最优子结构,无后效性和子问题重叠。

  1. 无后效性

已经求解的子问题,不会再受到后续决策的影响,即后续状态不会影响前序状态,也说明了求解的有序性。

  1. 子问题重叠

子问题 A 和子问题 B 可能存在共同的子问题 C,那么我们可以将一些重叠的子问题存储下来,特别来说,重叠的越多,我们的空间利用率越高。

  1. 最优子结构

当前问题的最优解一定可以由子问题的最优解导出。

再抽象一点,其实大部分的 DP 问题,都可以抽象为数字三角形问题,结构图如下:

图片描述

箭头源代表子问题,箭头指向,代表后续问题。

例如:{1} 同时是 {2,3} 的子问题,并且 2 问题的解可以由 1 导出。

总结:动态规划对状态的遍历,构成了一张有向无环图,遍历顺序(或者求解顺序)应该是该图的一个拓扑序

状态设计表示法

DP 的难点在于状态的设计和子结构的发掘,即使学界讨论了诸多DP转移的状态设计和优化手段,但是如何把问题形式化为状态空间,往往是一件考察智力而非套路的事情。

但是在蓝桥杯的赛题中,考察的是选手的基本功,与一点点的拔高,所以不会太难,可以遵循某些套路。

笔者按照经验,先讲授一些自己常用的状态设计思路,然后结合例题讲解。

设计一个状态分为如下几步:

  1. 尝试找到题目中的需要优化的值,例如最小值,最大值,次数等,做为目标,也是就是状态的最优值。
  2. 尝试找到题目中的条件,例如长度,区间数量等,做为状态设计。
  3. 尝试模拟题目中的求解步骤,这往往是题目中的条件,例如对某个数加一,或者在占领某些土地。作为转移的部分。
  4. 尝试结合将 1,2,3 结合起来,看能否找到一个合理的最优结构,并且无后向性。
  5. 如果不是最优,尝试这加大状态的条件,例如在补上一个必要的量,在进行3步骤。

笔者还有一些习惯,例如在看到某个题目时,如果准备尝试用DP解题,就是按照题目中的量进行一个简单尝试。例如,题目中有三个量,就会尝试写出 dpi,j,k ,然后尝试读出其代表的实际意义。如果可行,就进行优化或者细化。

一般而言,常见的线性DP,都能用此种方法解决,对于较难的问题,往往很难一下子设计出一个较好的状态,这就需要大量的经验以及一些天赋。

例如,你可以尝试设计这两个问题的状态:

  1. 蓝桥云课-青蛙吃虫

  2. 蓝桥算法赛-食堂

可以很明显的感觉出设计的难点。

线性DP

本部分将结合一些题,来不断的重复上述建立 DP 状态的方法,意图帮助读者强化一些简单的DP状态设计。

蓝桥云课-青蛙吃虫

图片描述

  1. 找到题目中的需要优化的值,“最多吃多少昆虫”,完美符合要求,我们将这个最大值作为状态的最优值,也就是说,如果我们设计了一个状态 s,那么我们的 dps 所代表的意义大概率在 s 情况下的吃虫的最大虫数。
  2. 尝试找到题目中的条件,路径长度为 N,最多跳 K 次。
  3. 尝试模拟题目中的求解步骤,这往往是题目中的条件。每次跳 T 格,但是满足 A≤T≤B,可以看作跳一次,就是一次转移。

尝试结合将 1,2,3 结合起来,看能否找到一个合理的最优结构,并且无后向性。

得到的状态以及转移如下:

定义 dpi,j 为跳跃了 i 次后,当前处在 j 位置能吃到的最大昆虫数量。

那么由定义得到 dp0,0=0,这是初始状态,代表的意义是在未跳跃前的状态,很明显符合定义(初始状态的定义,往往是需要对应于真实情况)。

我们思考如何转移,由于转移的过程为题目中的求解条件:每次跳跃一些格子,那么跳跃就是转移的过程。

我们思路如何得到 dpi,j ,复习定义:跳跃了 i 次后,当前处在 j 位置能吃到的最大昆虫数量。

那么这个状态的前一个状态是什么,也是就说,他的子问题是什么,根据实际情况,我们可以得到上一个状态一定是 dpi−1,j′,定义为 跳跃了 i−1 次后,到达位置为 j′ 的位置,如果满足跳一次可以到达 j,那么必须满足 A≤j−j′≤B

有了这个过程,那么我们实际上就得到了转移的逻辑,你完全可以根据这个思路写出代码核心:

for (int[] row : dp) {
    for (int j = 0; j < row.length; j++) {
        row[j] = -0x3f3f3f3f;
    }
}
dp[0][0] = 0;
for (int i = 1; i <= K; ++i) {
    for (int j = 1; j <= n; ++j) {
        for (int k = A; k <= B; ++k) {
            if (j - k < 0) continue;
            dp[i][j] = Math.max(dp[i][j], dp[i - 1][j - k] + a[j]);
        }
        ans = Math.max(ans, dp[i][j]);
    }
}
System.out.println(ans);

我们可以写出更具规整的转移:dp0,0=0dpi,j=max⁡A≤k≤B(dpi−1,k)我们要求的答案为 max⁡0≤i≤k,0≤j≤n(dpi,j)

蓝桥算法赛-奇怪的段

图片描述

我们继续重复上述解题方法:

  1. 找到题目中的需要优化的值,“加权和值最大”,完美符合要求,我们将这个最大值作为状态的最优值,也就是说,如果我们设计了一个状态 s,那么我们的 dps 所代表的意义大概率在 s 情况下的最大加权和值。

  2. 尝试找到题目中的条件,序列长度为 n,划分出 k 个区间。

  3. 尝试模拟题目中的求解步骤,这往往是题目中的条件。在本题中,划分区间就是一次转移。

  4. 尝试结合将1,2,3结合起来,看能否找到一个合理的最优结构,并且无后向性。

我们可以尝试建立如下转移:

定义状态 dpi,j 表示处理到第 i 个数字,分出 j 个区间的最大值。

初始状态为 dp0,0=0。这个代表初始情况下,未划分区间的最大值,是符合实际情况,并且是符合定义的。

由于一次划分就是一次转移,我们考虑 dpi,j,他的上一次划分一定是 dpi′,j−1,所代表的意义是,划分 j−1 段时,最后一个元素是 i 的情况。

那么我们的转移就是 dpi,j=dpi′,j−1+pj×∑y=i′+1iay

当然,我们要求的是最大值,所以我们需要加一个条件:dpi,j=max⁡j≤i′≤i(dpi′,j−1+pj×∑y=i′+1iay)状态是 n×k 个,每次转移的代价是 n 次,那么这个转移的复杂度为 O(n2×k)

当然,对于这个题来说,复杂度太高了,无法通过本题,由于是算法赛,所以无法通过,如果是蓝桥大赛的题,那么大概率能得到 50 % 的分数。 当然需要一些优化策略,不然求和的这一部分也会增加复杂度,同学们自行思考(提示:前缀和,或者边循环边算)。

如果要解决本题,我们考虑优化

将式子拆开

图片描述

上述的理解为,将 ai 新开一个区间,还是并入旧区间。

转移的复杂度为:O(n×k)

实现过程中用了滚动数组,用来节约空间,当然也可以不用。

滚动数组: 由于每次转移 dpi,j 只与 dpi−1,j′ 有关,对于第一维来说, dpi 只与 dpi−1 有关,所以,我们用两个量来表示当前的 dpi 和 dpi−1 即可。在代码中用 dpnow 和 dppre 表示。

代码如下:

import java.util.Scanner;
public class std {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        int N = 100500;
        long[][] dp = new long[2][205];
        int n = scanner.nextInt();
        int k = scanner.nextInt();
        int[] a = new int[N];
        int[] b = new int[205];

        for (int i = 1; i <= n; ++i) {
            a[i] = scanner.nextInt();
        }
        for (int i = 1; i <= k; ++i) {
            b[i] = scanner.nextInt();
        }
        for (int i = 0; i < 205; ++i) dp[0][i] = dp[1][i] = -9000000000000000000L;
        dp[0][0] = 0;
        int now = 0, pre = 1;
        for (int i = 1; i <= n; ++i) {
            now ^= 1;
            pre ^= 1;
            for (int j = 0; j < 205; ++j) dp[now][j] = -9000000000000000000L;
            for (int j = 1; j <= k; ++j) {
                dp[now][j] = Math.max(dp[pre][j], dp[pre][j - 1]) + 1L * a[i] * b[j];
            }
        }
        System.out.println(dp[now][k]);
    }
}

记忆化搜索

记忆化搜索是一种方法,常常用来递推或者动态规划中,能够让选手更加专注于转移/递推过程,而不用分析其转移结构与拓扑关系。

我们从斐波那契数列开始讲:

递归公式:F(n)=F(n−1)+F(n−2) 边界条件:F(0)=0,F(1)=1

普通递归实现时间复杂度为 O(2n),存在大量重复计算。

int f(int n) {
  if (n <= 1) return n;
  return f(n - 1) + f(n - 2);
}
 

由于我们发现很多的中间过程是重复计算的,我们可以使用一个数组,在计算的同时保存中间过程结果,当发现该过程重复计算后,立刻返回其结果,而不用再次重复计算。

int[][][] dp = new int[N][M][L]; // 全部初始化为-1,或者初始化为一个不可能的数。
int dfs(int i, int j, int k) {// 函数参数即为状态
  if (dp[i][j][k] != -1) return dp[i][j][k];
  if (到达了初始状态) {
    return dp[i][j][k] = <初始值>;
  }
  // 进行转移
  /*
  dp[i][j][k] = .....
  code .....
  */
  return dp[i][j][k];
}

区间DP

区间类动态规划是线性动态规划的扩展,它在分阶段地划分问题时,与阶段中元素出现的顺序和由前一阶段的哪些元素合并而来有很大的关系。

蓝桥杯比赛中,区间动态规格的特点有:

  1. 强调区间操作,题目中的操作往往涉及到区间。
  2. 一个区间的结果可以由内部的子区间进行合并。
  3. 状态常常定义为 dpl,r ,即代表区间 [l,r] 操作后的最优值。

更一般化来说,区间DP的特点如下:

合并:即将两个或多个部分进行整合,也可以反过来;

特征:能将问题分解为能两两合并的形式;

求解:对整个问题设最优值,枚举合并点,将问题分解为左右两个部分,最后合并两个部分的最优值得到原问题的最优值。

真题-更小的数

图片描述

首先需要分析题目,由于题目中,要求子串进行反转后,需要小于原串,实际上就是需要比较所反转子串后的大小,例如 {3,2,1} 反转后 {1,2,3}。反转后的值就比原值小。

由于题目中强调区间,我们定义 dpl,r 为反转子串 slsl+1...sr 后的串是否比原串小。

我们定义三个值:

  1. dpl,r=0 代表反转后,比原串大。
  2. dpl,r=1 代表反转后,和原串相等。
  3. dpl,r=2 代表反转后,比原串小。

区间DP也分为三部分,初始值,转移过程,答案值。

  1. 初始化:区间DP的初始状态一般定义为 dpi,i 即区间是一个长度为1的区间,在此题中,dpi,i=1
  2. 转移过程:在对区间 [l,r] 进行反转时,r 位置会被置换到 ll 会被置换到 r 位置,中间的区间会进行一个 [l+1,r−1] 的反转。
  3. 答案,我们需要统计所有区间的情况,判断多少个区间的DP值为2。

区间DP非常适合采用记忆化搜索进行编写,我们给出代码:

import java.util.Scanner;

public class Main {
    static char[] s;        // 字符数组,下标从1开始
    static int[][] dp;      // 记忆化数组

    /**
     * 递归计算子串s[l..r]的反转比较结果
     * @param l 子串左端点(包含)
     * @param r 子串右端点(包含)
     * @return 2-反转后更小,0-反转后更大,1-反转后相等
     */
    static int DP(int l, int r) {
        if (l > r) return 1; // 空区间视为相等
        if (l == r) return 1; // 单字符相等
        if (dp[l][r] != -1) return dp[l][r];
        
        // 两端字符直接比较
        if (s[r] < s[l]) return dp[l][r] = 2;
        if (s[r] > s[l]) return dp[l][r] = 0;
        return dp[l][r] = DP(l+1, r-1); // 递归处理内部子串
    }

    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        String input = " " + sc.next(); // 输入字符串,下标从1开始
        s = input.toCharArray();
        int n = s.length - 1;
        dp = new int[n+2][n+2];
        
        // 初始化记忆化数组
        for (int i = 1; i <= n; ++i) {
            for (int j = i; j <= n; ++j) {
                dp[i][j] = -1;
            }
        }

        int ans = 0;
        // 遍历所有子串
        for (int i = 1; i <= n; ++i) {
            for (int j = i; j <= n; ++j) {
                if (DP(i, j) == 2) {
                    ans++;
                }
            }
        }
        System.out.println(ans);
    }
}

 

posted on 2025-03-14 21:01  fafrkvit  阅读(20)  评论(1)    收藏  举报