蓝桥杯(Java B组)的总结
Scanner类
使用Scanner类的三步:
- 导包
- 创建一个Scanner的对象
- 用Scanner对象调用相应的方法
java中的从键盘输入主要是依赖于Scanner类,下面将介绍Scanner类的一些比较常见并且重要的方法。
1.nextInt()/nextFloat()/nextDouble()…
这种只会读取一个数值,并且不会读取最后的换行符’\n’
2.String nextLine()
扫描的是一行数据,并且当作字符串来处理,不会读取最后的换行符’\n’
3.String next()
遇见第一个有效字符(非空格,非换行符)时,开始扫描,当遇见第一个分隔符或结束符(空格或换行符)时,结束扫描,获取扫描到的内容,即获得第一个扫描到的不含空格、换行符的单个字符串。
测试:
public static void main(String []args){
Scanner scanner=new Scanner(System.in); //从键盘获取标准输入
String pro=scanner.nextLine();
String name=scanner.next();
int age=scanner.nextInt();
System.out.println("座右铭:"+pro);
System.out.println("名字:"+name);
System.out.println("年纪:"+age);
}
接下来我们演示一个最简单的数据输入,并通过 Scanner 类的 next() 与 nextLine() 方法获取输入的字符串,在读取前我们一般需要 使用 hasNext 与 hasNextLine 判断是否还有输入的数据:
使用 next 方法:
ScannerDemo.java 文件代码:
import java.util.Scanner;
public class ScannerDemo {
public static void main(String[] args) {
Scanner scan = new Scanner(System.in); // 从键盘接收数据
// 判断是否还有输入
if (scan.hasNext()) {
String str1 = scan.next(); //runoob com
System.out.println("输入的数据为:" + str1);
}
scan.close();
}
}
执行以上程序输出结果为:
输入的数据为:runoob
可以看到 com 字符串并未输出,接下来我们看 nextLine。
使用 nextLine 方法:
ScannerDemo.java 文件代码:
import java.util.Scanner;
public class ScannerDemo {
public static void main(String[] args) {
Scanner scan = new Scanner(System.in);
// 判断是否还有输入
if (scan.hasNextLine()) {
String str2 = scan.nextLine(); //runoob com
System.out.println("输入的数据为:" + str2);
}
scan.close();
}
}
执行以上程序输出结果为:
输入的数据为:runoob com
可以看到 com 字符串输出。
next() 与 nextLine() 区别
next():
- 1、一定要读取到有效字符后才可以结束输入。
- 2、对输入有效字符之前遇到的空白,next() 方法会自动将其去掉。
- 3、只有输入有效字符后才将其后面输入的空白作为分隔符或者结束符。
- next() 不能得到带有空格的字符串。
nextLine():
- 1、以Enter为结束符,也就是说 nextLine()方法返回的是输入回车之前的所有字符。
- 2、可以获得空白。
Tips: 存在BUG的地方:
实验发现,如果在nextLine(),之前有其他的输入的话(不包含nextLine(),也就是说2个nextLine()不会出现这个问题),nextLine()会无法输入,原因是:nextLine()会读取上一个输入的回车,解决方法是:加多一个nextLine()来读取上一次的回车即可;有点类似c++的getchar()来读取上一个的回车。
全排列(本质为DFS)
DFS算法模板图
应用这个模板我们可以解决很多全排列问题!
实例一:输入一个数N,输出从1~N的全排列
import java.util.Scanner;
public class Main{
static int n;
static int[] arr;//用来记录已经在排列中的数字
static int[] brr;//标记
static void dfs(int step) {
if(step==n) {//边界输出
for(int i=0;i<n;i++){
System.out.print(arr[i]+" ");
}
System.out.println();
}else if(step<n) {
for(int i=1;i<=n;i++) {//每一次循环都将i作为第一个数进行排列
if(brr[i]==0) {//判断是否重复,相当于check方法是否满足条件
brr[i] = 1;//标记已经使用
arr[step] = i;
dfs(step+1);//进行下一步
brr[i] = 0;//清空标记
}
}
}else{
return;
}
}
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
n = sc.nextInt();
arr = new int[n+1];
brr = new int[n+1];
dfs(0);
}
}
实例二:纸牌三角形(2017 Java B组第二题)
方法一:数组标记法
public class _02纸牌三角形 {
// 定义全局的变量,数组
// 标记数组:表示该位置已有元素
static int []flag=new int[9];
// 存储9个数字
static int []num=new int[9];
// 计算各边之和
static int sum;
/* 采用递归的方法:
* ①向数组中放元素
* ②当9个位置都放好后,判断各边之和是否相等,递归结束条件
*/
public static void f(int code) {
// 递归结束的条件
if(code==9) {
// 判断各边之和是否相等
if(num[0]+num[1]+num[3]+num[5]==num[0]+num[2]+num[4]+num[8]
&&num[0]+num[2]+num[4]+num[8]==num[5]+num[6]+num[7]+num[8]) {
sum++;
// return;
}
}else {
// 方法一:数组标记法
for (int i = 0; i < flag.length; i++) {
// 判断该位置是否放入了数字
if(flag[i]==0) {
// 如果未放入,则在该位置放入一个数字
flag[i]=1;
num[code]=i+1;
// System.out.print(num[code]+" ");
// 递归调用,判断下一个位置的情况
f(code+1);
// 考虑下一种情况,将标记数组重置
flag[i]=0;
}
}
}
}
public static void main(String[] args) {
f(0);
// 考虑旋转(3种)、镜像(3种)后相同的算同一种
System.out.println(sum/6);//144
}
}
方法二:交换法
public class _02纸牌三角形_2 {
// 定义全局的变量,数组
static int []num={1,2,3,4,5,6,7,8,9};
// 计算各边之和
static int sum;
public static void f(int code) {
// 递归结束的条件
if(code==9) {
// 判断各边之和是否相等
if(num[0]+num[1]+num[3]+num[5]==num[0]+num[2]+num[4]+num[8]
&&num[0]+num[2]+num[4]+num[8]==num[5]+num[6]+num[7]+num[8]) {
sum++;
}
}else {
// 方法二: 交换法
for (int i = code; i < 9; i++) {
int y=num[code];
num[code]=num[i];
num[i]=y;
f(code+1);
y=num[code];
num[code]=num[i];
num[i]=y;
}
}
}
public static void main(String[] args) {
f(0);
// 考虑旋转(3种)、镜像(3种)后相同的算同一种
System.out.println(sum/6);//144
}
}
本题是最典型的最入门的DFS问题,可采用回溯法。
首先要知道DFS的原理,如果不理解的话一定要多画图,每画一笔都要想想为什么这么画,下一步怎么得出来的。不多说,直接上代码后面分析。
import java.util.Scanner;
public class 全排列_标记法 {
static Scanner sc = new Scanner(System.in);
static int n = sc.nextInt();
static int result[] = new int[n+1];
static int vis[] = new int[n+1];
public static void main(String[] args) {
DFS(1);//对应下文问题1
}
private static void DFS(int step) {
if(step >= n+1) {//对应问题3
print();
return;
}
for(int j = 1 ; j <= n ; j++) {//对应问题3
if(vis[j] == 0) {
vis[j] = 1;//标记数组,见问题2
result[step] = j;
DFS(step+1);
vis[j] = 0;//回溯,见问题2
}
}
}
private static void print() {
for(int i = 1; i <= n; i++) {//对应问题3
System.out.printf("%5d",result[i]);
}
System.out.println();
}
}
对于代码中的一些问题这里说一下。
1.为什么dfs参数从1开始,1是什么意思?
答:我们将题目当成装箱子问题,第一步(step)是选择第一个箱子,然后是第二个箱子,直到把所有的箱子选完并装完数字。要注意的是,每选完一个箱子,就立即选择一个数字装入,再进入dfs循环,选择下一个箱子。
2.这的vis[]数组怎么理解?
答:vis[]数组的vis -> visit访问,指的是判断访问数组,其数组默认值为0,所以当vis[i]==0时,当作此元素未被访问过(也就是未被标记过),而vis[i]==1时,指此元素已被访问标记过。
3.回溯不知如何层次在哪里,vis[i] = 0有何作用?
答:这个问题,你应该自己去仔细画一遍dfs的图,看看其原理,是什么时候往回退的,一步步往回退的过程就是回溯,过程中将已经访问过的元素重新赋值为0。因为你赋值为0的一个重要作用,就是在你触底,不能在继续访问新元素的时候,要往回退,往回退的过程中还要有另一条路会用到此元素,有种重点理解的地方,这个数字只存在与一个数组中,你访问数字,即为访问数组的元素,而且是只有一个数组,所以要一步步的回溯。例如:从1-4的过程中,当你选择step1:1 -> step2 : 2 -> step3 : 3 -> step4 : 4 。此时1234这4个数字都被访问过,vis[i]都为1,如果你要换一种排列,他们都不在访问范围内,所以第一步回溯,将step4中的4 vis[i]重新置为0,然后发现除了4没有其他数字选择,那么再次回溯,将step3的数字的也置为0(不是数字置为0,而是vis[]置为0),此时就有了选择,除了3以外,4也没被访问过,所以第二种方案,为step3为4,然后继续dfs,step4为3.(最后的结果 为 :1243)。
4.step == n+1为什么不能是step==n,循环为何要从int i = 1开始?
答:前者,step == n是可行的,他与dfs函数一开始的位置有关,但是输出可能不尽人意,dfs从1开始,意味着选择了第一个箱子,所以对应的触底反弹的底就是step==n+1。如果你愿意写从第0个箱子开始,也是可以的。
后者问题的答案,在DFS函数中 int i = 1,指的是数字i从1开始,就是题目要求的1-n之间的数字,不能从0开始,这样违反题目规则。在print函数中,其实与前者问题挂钩,我们舍弃了result[0]这个位置,因为主观上我们都是从第一个箱子开始,当然你改成int i = 0也可以,但是的dfs参数和step==n+1也需要改。这三者是互相联系的。
整体理解:切记dfs的顺序,step到底时不代表全部结束,因为此程序跑完会有很多次step == n或者step==n+1的情况。
这是数组标记法,还有一种交换法dfs的方法。
import java.util.Scanner;
public class 生成排列交换法 {
static Scanner sc = new Scanner(System.in);
static int ans = 0;
static int n = sc.nextInt();
static int pre[] = new int[n];
public static void main(String[] args) {
for(int i = 0 ; i < pre.length ; i++) {
pre[i] = sc.nextInt();
}
dfs(0);//从第0层,指最上层为0
/*
*此处的dfs的参数为0,是指将最上层当作第0层,也可以改为1,但对应dfs函数中的
*if(step == n)就要相应的改动
*/
System.out.println(ans);
}
public static void dfs(int step) {
if(step == n) {
ans++;
print();
}
for(int i = step ; i < n; i++) {
//此循环体一定要重点理解,弄清原理主要就靠这循环体
//交换
swap(i,step);//对应图中的元素交换
dfs(step+1);
//恢复原来的状态
swap(step,i);
}
}
public static void print() {
for(int i = 0 ; i < n ; i++) {
System.out.printf("%5d",pre[i]);
}
System.out.println();
}
public static void swap(int i, int j) {
int temp;
temp = pre[i];
pre[i] = pre[j];
pre[j] = temp;
}
}
注意在顺序问题上,是属于dfs情形,一直探寻到底,直到最后框里只剩一个元素的时候,再折返,这就是循环体中swap(step,i)的意义,采用回溯。
三.关于dfs的方法模板如下,仅限于简单初始的dfs
具体的模板完全可以参照上面两个代码块,其实算比较标准的模板代码了
public static void dfs()//参数用来表示状态
{
if(到达终点状态,也称出口)
{
...//主要为打印或者结束处理
return;
}
for(扩展方式) //循环一层的所有情况
{
if(扩展方式所达到状态合法)
{
具体操作;//根据题意来添加
标记;
dfs();
(还原标记); //是否还原标记根据题意,如果加上(还原标记)就是 回溯法
}
}
}
public static void dfs()//参数用来表示状态
{
if(到达终点状态,也称出口)
{
...//主要为打印或者结束处理
return;
}
for(扩展方式) //循环一层的所有情况
{
if(扩展方式所达到状态合法)
{
交换;
dfs();
(还原标记,一般为交换回来)//是否还原标记根据题意,如果加上(还原标记)就是 回溯法
}
}
}
动态规划(转载整理)
导语
要理解动态规划的概念,我们需要熟悉一些主题:
1. 什么是动态规划?
2. 贪心算法
3. 简化的背包问题
4. 传统的背包问题
5. LCS-最长的共同子序列
6. 利用动态规划的其他问题
7. 结论
什么是动态规划?
动态规划是一种编程原理,可以通过将非常复杂的问题划分为更小的子问题来解决。这个原则与递归很类似,但是与递归有一个关键点的不同,就是每个不同的子问题只能被解决一次。
为了理解动态规划,我们首先需要理解递归关系的问题。
递归,就是在运行的过程中调用自己。
构成递归需具备的条件:
- 子问题须与原始问题为同样的事,且更为简单;
- 不能无限制地调用本身,须有个出口,化简为非递归状况处理。
每个单独的复杂问题可以被划分为很小的子问题,这表示我们可以在这些问题之间构造一个递归关系。
让我们来看一个我们所熟悉的例子:斐波拉契数列,斐波拉契数列的定义具有以下的递归关系:
注意:递归关系是递归地定义下一项是先前项的函数的序列的等式。
Fibonacci
序列就是一个很好的例子。
所以,如果我们想要找到斐波拉契数列序列中的第n个数,我们必须知道序列中第n个前面的两个数字。
以此类推,直到初始值已知,即找到了递归的出口。
为什么要用动态规划?
动态规划典型的被用于优化递归算法,因为它们倾向于以指数的方式进行扩展。
动态规划主要思想是将复杂问题(带有许多递归调用)分解为更小的子问题,然后将它们保存到内存中,这样我们就不必在每次使用它们
时重新计算它们。
接着上面的例子:我们虽然用递归算法就可以解决Fibonacci
序列问题,但是,每次我们想要计算Fibonacci
序列的不同元素时,我们在递归调用中都有一些重复调用,如下图所示,我们计算Fibonacci(5)
:
例如:如果我们想计算F(5)
,明显的我们需要计算F(3)
和F(4)
作为计算F(5)
的先决条件。然而,为了计算F(4)
,我们需要计算F(3)
和F(2)
,因此我们又需要计算F(2)
和F(1)
来得到F(3)
,其他的求解诸如此类。
这样的话就会导致很多重复的计算,这些重复计算本质上是冗余的,并且明显的减慢了算法的效率。为了解决这种问题,我们介绍动态规划。
在这种方法中,我们对解决方案进行建模,就像我们要递归地解决它一样,但我们从头开始解决它,将解决方案记忆到我们到达顶部的子问题(步骤)。
因此,对于Fibonacci
序列,我们首先求解并记忆F(1)
和F(2)
,然后使用两个记忆步骤计算F(3)
,依此类推。这意味着序列中每个单独元素的计算都是O(1)
,因为我们已经知道前两个元素。
动规解题的一般思路
1. 将原问题分解为子问题
- 把原问题分解为若干个子问题,子问题和原问题形式相同或类似,只不过规模变小了。子问题都解决,原问题即解决(数字三角形例)。
- 子问题的解一旦求出就会被保存,所以每个子问题只需求解一次。
2.确定状态
- 在用动态规划解题时,我们往往将和子问题相关的各个变量的一组取值,称之为一个“状 态”。一个“状态”对应于一个或多个子问题, 所谓某个“状态”下的“值”,就是这个“状 态”所对应的子问题的解。
- 所有“状态”的集合,构成问题的“状态空间”。“状态空间”的大小,与用动态规划解决问题的时间复杂度直接相关。 在数字三角形的例子里,一共有N×(N+1)/2个数字,所以这个问题的状态空间里一共就有N×(N+1)/2个状态。
整个问题的时间复杂度是状态数目乘以计算每个状态所需时间。在数字三角形里每个“状态”只需要经过一次,且在每个状态上作计算所花的时间都是和N无关的常数。
3.确定一些初始状态(边界状态)的值
以“数字三角形”(见下面典例)为例,初始状态就是底边数字,值就是底边数字值。
4. 确定状态转移方程
定义出什么是“状态”,以及在该“状态”下的“值”后,就要找出不同的状态之间如何迁移――即如何从一个或多个“值”已知的 “状态”,求出另一个“状态”的“值”(递推型)。状态的迁移可以用递推公式表示,此递推公式也可被称作“状态转移方程”。
*能用动规解决的问题的特点*
- 问题具有最优子结构性质。如果问题的最优解所包含的子问题的解也是最优的,我们就称该问题具有最优子结 构性质。
- 无后效性。当前的若干个状态值一旦确定,则此后过程的演变就只和这若干个状态的值有关,和之前是采取哪种手段或经过哪条路径演变到当前的这若干个状态,没有关系。
应用动规的实例
数字三角形问题
运用以上的思路,我们来看一道典型例题
数字三角形(POJ1163):从上面的数字三角形中寻找一条从顶部到底边的路径,使得路径上所经过的数字之和最大。路径上的每一步都只能往左下或 右下走。只需要求出这个最大和即可,不必给出具体路径。 三角形的行数大于1小于等于100,数字为 0 - 99
输入格式:
5 //表示三角形的行数 接下来输入三角形
7
3 8
8 1 0
2 7 4 4
4 5 2 6 5
输出: 要求输出最大和
接下来,我们来分析一下解题思路:
首先,肯定得用二维数组来存放数字三角形
然后我们用D( r, j) 来表示第r行第 j 个数字(r,j从1开始算)
我们用MaxSum(r, j)表示从D(r,j)到底边的各条路径中,最佳路径的数字之和。
因此,此题的最终问题就变成了求 MaxSum(1,1)
当我们看到这个题目的时候,首先想到的就是可以用简单的递归来解题:
D(r, j)出发,下一步只能走D(r+1,j)或者D(r+1, j+1)。故对于N行的三角形,我们可以写出如下的递归式:
if ( r == N)
MaxSum(r,j) = D(r,j)
else
MaxSum( r, j) = Max{ MaxSum(r+1,j), MaxSum(r+1,j+1) } + D(r,j)
根据上面这个简单的递归式,我们就可以很轻松地写出完整的递归代码:
#include <iostream>
#include <algorithm>
#define MAX 101
using namespace std;
int D[MAX][MAX];
int n;
int MaxSum(int i, int j){
if(i==n)
return D[i][j];
int x = MaxSum(i+1,j);
int y = MaxSum(i+1,j+1);
return max(x,y)+D[i][j];
}
int main(){
int i,j;
cin >> n;
for(i=1;i<=n;i++)
for(j=1;j<=i;j++)
cin >> D[i][j];
cout << MaxSum(1,1) << endl;
}
对于如上这段递归的代码,当我提交到POJ时,会显示如下结果:
对的,代码运行超时了,为什么会超时呢?
答案很简单,因为我们重复计算了,当我们在进行递归时,计算机帮我们计算的过程如下图:
就拿第三行数字1来说,当我们计算从第2行的数字3开始的MaxSum时会计算出从1开始的MaxSum,当我们计算从第二行的数字8开始的MaxSum的时候又会计算一次从1开始的MaxSum,也就是说有重复计算。这样就浪费了大量的时间。也就是说如果采用递规的方法,深度遍历每条路径,存在大量重复计算。则时间复杂度为 2的n次方,对于 n = 100 行,肯定超时。
接下来,我们就要考虑如何进行改进,我们自然而然就可以想到如果每算出一个MaxSum(r,j)就保存起来,下次用到其值的时候直接取用,则可免去重复计算。那么可以用n方的时间复杂度完成计算。因为三角形的数字总数是 n(n+1)/2
根据这个思路,我们就可以将上面的代码进行改进,使之成为记忆递归型的动态规划程序:
#include <iostream>
#include <algorithm>
using namespace std;
#define MAX 101
int D[MAX][MAX];
int n;
int maxSum[MAX][MAX];
int MaxSum(int i, int j){
if( maxSum[i][j] != -1 )
return maxSum[i][j];
if(i==n)
maxSum[i][j] = D[i][j];
else{
int x = MaxSum(i+1,j);
int y = MaxSum(i+1,j+1);
maxSum[i][j] = max(x,y)+ D[i][j];
}
return maxSum[i][j];
}
int main(){
int i,j;
cin >> n;
for(i=1;i<=n;i++)
for(j=1;j<=i;j++) {
cin >> D[i][j];
maxSum[i][j] = -1;
}
cout << MaxSum(1,1) << endl;
}
当我们提交如上代码时,结果就是一次AC
虽然在短时间内就AC了。但是,我们并不能满足于这样的代码,因为递归总是需要使用大量堆栈上的空间,很容易造成栈溢出,我们现在就要考虑如何把递归转换为递推,让我们一步一步来完成这个过程。
我们首先需要计算的是最后一行,因此可以把最后一行直接写出,如下图:
现在开始分析倒数第二行的每一个数,现分析数字2,2可以和最后一行4相加,也可以和最后一行的5相加,但是很显然和5相加要更大一点,结果为7,我们此时就可以将7保存起来,然后分析数字7,7可以和最后一行的5相加,也可以和最后一行的2相加,很显然和5相加更大,结果为12,因此我们将12保存起来。以此类推。。我们可以得到下面这张图:
然后按同样的道理分析倒数第三行和倒数第四行,最后分析第一行,我们可以依次得到如下结果:
上面的推导过程相信大家不难理解,理解之后我们就可以写出如下的递推型动态规划程序:
#include <iostream>
#include <algorithm>
using namespace std;
#define MAX 101
int D[MAX][MAX];
int n;
int maxSum[MAX][MAX];
int main(){
int i,j;
cin >> n;
for(i=1;i<=n;i++)
for(j=1;j<=i;j++)
cin >> D[i][j];
for( int i = 1;i <= n; ++ i )
maxSum[n][i] = D[n][i];
for( int i = n-1; i>= 1; --i )
for( int j = 1; j <= i; ++j )
maxSum[i][j] = max(maxSum[i+1][j],maxSum[i+1][j+1]) + D[i][j];
cout << maxSum[1][1] << endl;
}
我们的代码仅仅是这样就够了吗?当然不是,我们仍然可以继续优化,而这个优化当然是对于空间进行优化,其实完全没必要用二维maxSum数组存储每一个MaxSum(r,j),只要从底层一行行向上递推,那么只要一维数组maxSum[100]即可,即只要存储一行的MaxSum值就可以。
对于空间优化后的具体递推过程如下:
接下里的步骤就按上图的过程一步一步推导就可以了。进一步考虑,我们甚至可以连maxSum数组都可以不要,直接用D的第n行直接替代maxSum即可。但是这里需要强调的是:虽然节省空间,但是时间复杂度还是不变的。
依照上面的方式,我们可以写出如下代码:
#include <iostream>
#include <algorithm>
using namespace std;
#define MAX 101
int D[MAX][MAX];
int n;
int * maxSum;
int main(){
int i,j;
cin >> n;
for(i=1;i<=n;i++)
for(j=1;j<=i;j++)
cin >> D[i][j];
maxSum = D[n]; //maxSum指向第n行
for( int i = n-1; i>= 1; --i )
for( int j = 1; j <= i; ++j )
maxSum[j] = max(maxSum[j],maxSum[j+1]) + D[i][j];
cout << maxSum[1] << endl;
}
贪心算法
下面来以这个为例子:
Given a rod of length n and an array that contains prices of all pieces of size smaller than n.
Determine the maximum value obtainable by cutting up the rod and selling the pieces.
对于没有经验的开发者可能会采取下面这种做法:
public class naiveSolution {
static int getValue(int[] values, int length) {
if (length <= 0)
return 0;
int tmpMax = -1;
for (int i = 0; i < length; i++) {
tmpMax = Math.max(tmpMax, values[i] + getValue(values, length - i - 1));
}
return tmpMax;
}
public static void main(String[] args) {
int[] values = new int[]{3, 7, 1, 3, 9};
int rodLength = values.length;
System.out.println("Max rod value: " + getValue(values, rodLength));
}
}
123456789101112131415161718
输出结果:
Max rod value: 17
虽然这段代码得到的结果是正确的,按时代码的效率和很低。因为递归调用的结果不可以被保存因此代码不得不每次都去解决相同的子问题,这里就会存在一个重叠的答案。
动态规划思想解题:
利用上面相同的基本原理,但添加memoization并排除递归调用,我们得到以下实现:
public class dpSolution {
static int getValue(int[] values, int rodLength) {
int[] subSolutions = new int[rodLength + 1];
for (int i = 1; i <= rodLength; i++) {
int tmpMax = -1;
for (int j = 0; j < i; j++)
tmpMax = Math.max(tmpMax, values[j] + subSolutions[i - j - 1]);
subSolutions[i] = tmpMax;
}
return subSolutions[rodLength];
}
public static void main(String[] args) {
int[] values = new int[]{3, 7, 1, 3, 9};
int rodLength = values.length;
System.out.println("Max rod value: " + getValue(values, rodLength));
}
}
1234567891011121314151617181920
输出结果:
Max rod value: 17
正如我们所看到的的,输出结果是一样的,所不同的是时间和空间复杂度。
通过从头开始解决子问题,我们消除了递归调用的需要,利用已解决给定问题的所有先前子问题的事实。
为了给出动态方法效率更高的观点的证据,让我们尝试使用30个值来运行该算法。 一种算法需要大约5.2秒来执行,而动态解决方法需要大约0.000095秒来执行。
0-1背包问题
0-1背包问题指的是每个物品只能使用一次
递归方法
编程实现如下:
public class KnapSack01 {
/**
* 解决背包问题的递归函数
*
* @param w 物品的重量数组
* @param v 物品的价值数组
* @param index 当前待选择的物品索引
* @param capacity 当前背包有效容量
* @return 最大价值
*/
private static int solveKS(int[] w, int[] v, int index, int capacity) {
//基准条件:如果索引无效或者容量不足,直接返回当前价值0
if (index < 0 || capacity <= 0)
return 0;
//不放第index个物品所得价值
int res = solveKS(w, v, index - 1, capacity);
//放第index个物品所得价值(前提是:第index个物品可以放得下)
if (w[index] <= capacity) {
res = Math.max(res, v[index] + solveKS(w, v, index - 1, capacity - w[index]));
}
return res;
}
public static int knapSack(int[] w, int[] v, int C) {
int size = w.length;
return solveKS(w, v, size - 1, C);
}
public static void main(String[] args){
int[] w = {2,1,3,2};
int[] v = {12,10,20,15};
System.out.println(knapSack(w,v,5));
}
}
123456789101112131415161718192021222324252627282930313233343536
记忆化搜索
我们用递归方法可以很简单的实现以上代码,但是有个严重的问题就是,直接采用自顶向下的递归算法会导致要不止一次的解决公共子问题,因此效率是相当低下的。
我们可以将已经求得的子问题的结果保存下来,这样对子问题只会求解一次,这便是记忆化搜索。
下面在上述代码的基础上加上记忆化搜索
public class KnapSack01 {
private static int[][] memo;
/**
* 解决背包问题的递归函数
*
* @param w 物品的重量数组
* @param v 物品的价值数组
* @param index 当前待选择的物品索引
* @param capacity 当前背包有效容量
* @return 最大价值
*/
private static int solveKS(int[] w, int[] v, int index, int capacity) {
//基准条件:如果索引无效或者容量不足,直接返回当前价值0
if (index < 0 || capacity <= 0)
return 0;
//如果此子问题已经求解过,则直接返回上次求解的结果
if (memo[index][capacity] != 0) {
return memo[index][capacity];
}
//不放第index个物品所得价值
int res = solveKS(w, v, index - 1, capacity);
//放第index个物品所得价值(前提是:第index个物品可以放得下)
if (w[index] <= capacity) {
res = Math.max(res, v[index] + solveKS(w, v, index - 1, capacity - w[index]));
}
//添加子问题的解,便于下次直接使用
memo[index][capacity] = res;
return res;
}
public static int knapSack(int[] w, int[] v, int C) {
int size = w.length;
memo = new int[size][C + 1];
return solveKS(w, v, size - 1, C);
}
public static void main(String[] args) {
int[] w = {2, 1, 3, 2};
int[] v = {12, 10, 20, 15};
System.out.println(knapSack(w, v, 5));
}
}
12345678910111213141516171819202122232425262728293031323334353637383940414243444546
动态规划算法
public class KnapSack01 {
public static int knapSack(int[] w, int[] v, int C) {
int size = w.length;
if (size == 0) {
return 0;
}
int[][] dp = new int[size][C + 1];
//初始化第一行
//仅考虑容量为C的背包放第0个物品的情况
for (int i = 0; i <= C; i++) {
dp[0][i] = w[0] <= i ? v[0] : 0;
}
//填充其他行和列
for (int i = 1; i < size; i++) {
for (int j = 0; j <= C; j++) {
dp[i][j] = dp[i - 1][j];
if (w[i] <= j) {
dp[i][j] = Math.max(dp[i][j], v[i] + dp[i - 1][j - w[i]]);
}
}
}
return dp[size - 1][C];
}
public static void main(String[] args) {
int[] w = {2, 1, 3, 2};
int[] v = {12, 10, 20, 15};
System.out.println(knapSack(w, v, 5));
}
}
1234567891011121314151617181920212223242526272829303132
空间复杂度的极致优化
上面的动态规划算法使用了O(n*C)的空间复杂度(因为我们使用了二维数组来记录子问题的解),其实我们完全可以只使用一维数组来存放结果,但同时我们需要注意的是,为了防止计算结果被覆盖,我们必须从后向前分别进行计算
最终的动态规划代码如下
public class KnapSack01 {
public static int knapSack(int[] w, int[] v, int C) {
int size = w.length;
if (size == 0) {
return 0;
}
int[] dp = new int[C + 1];
//初始化第一行
//仅考虑容量为C的背包放第0个物品的情况
for (int i = 0; i <= C; i++) {
dp[i] = w[0] <= i ? v[0] : 0;
}
for (int i = 1; i < size; i++) {
for (int j = C; j >= w[i]; j--) {
dp[j] = Math.max(dp[j], v[i] + dp[j - w[i]]);
}
}
return dp[C];
}
public static void main(String[] args) {
int[] w = {2, 1, 3, 2};
int[] v = {12, 10, 20, 15};
System.out.println(knapSack(w, v, 5));
}
}
大数类(转载整理)
java中的基础数据类型能存储的最大的二进制数是 2 ^ 63 - 1,
对应的十进制数是9223372036854775807,也就是说只要运算过程中会超过这个数,就会造成数据溢出,从而造成错误.
- 而java.math.*包中提供了大数类,其理论上可以存储无限位的大数,只要内存足够的话。
- 大数类又分为整数和浮点数.即BigInteger and BigDecimal
- 大数类的对象不能直接进行运算,需要调用类中相应的方法,并且方法的参数必须和调用的类相同,BigInteger不能调用BigDecimal, 不能作为其方法参数, 即整数和浮点数不能混合运算.
- 本文举例了一些常用的方法,不需要背会,需要用的时候查java API就行了。
BigInteger 和 BigDecimal
创建
//1.直接声明
BigInteger a;
BigDecimal b;
//2.使用构造函数初始化
BigInteger a = new BigInteger("123456789101112131415");
BigDecimal b = new BigDecimal("123456.123456");
123456
赋值
BigInteger.valueOf(long val);
BigDecimal.valueOf(double val);
BigInteger a;
BigDecimal b;
//注意 val 不能超过 long 类型的最大取值9223372036854775807, 超过int时要在数后面加L如:
a = BigInteger.valueOf(123456789101112L); //大于int范围的要加L
b = BigDecimal.valueOf(123456.12341235); // 超出的小数位数会自动舍弃
12345
使用 = 将同类型变量的值赋给另一个变量
BigInteger a;
BigInteger b = new BigInteger("123456");
a = b;
System.out.print(a);
输出:
123456
1234567
加法
BigInteger.add(BigInteger);
BigDecimal.add(BigDecimal);
BigInteger a, b, c;
a = BigInteger.valueOf(123456789); // 赋值为 123456789
b = BigInteger.valueOf(987654321); // 赋值为 987654321
c = a.add(b);
System.out.print(c);
输出:
1111111110
1111111110
12345678
减法
BigInteger.subtract(BigInteger);
BigDecimal.sbutract(BigDecimal);
BigInteger a, b, c;
a = BigInteger.valueOf(123456789); // 赋值为 123456789
b = BigInteger.valueOf(987654321); // 赋值为 987654321
c = a.subtract(b);
System.out.print(c);
输出:
-864197532
12345678
乘法
BigInteger.multiply(BigInteger);
BigDecimal.multiply(BigDecimal);
BigInteger a, b, c;
a = BigInteger.valueOf(123456789); // 赋值为 123456789
b = BigInteger.valueOf(987654321); // 赋值为 987654321
c = a.multiply(b);
System.out.print(c);
输出:
121932631112635269
12345678
除法
BigInteger.divide(BigInteger);
BigDecimal.divide(BigDecimal);
BigInteger a, b, c;
a = BigInteger.valueOf(987654321); // 赋值为 987654321
b = BigInteger.valueOf(123456789); // 赋值为 123456789
c = a.divide(b); // 整数相除仍为整数
System.out.print(c);
输出:
8
12345678
取余
BigInteger.mod(BigInteger);
BigInteger a, b, c;
a = BigInteger.valueOf(987654321); // 赋值为 987654321
b = BigInteger.valueOf(123456789); // 赋值为 123456789
c = a.mod(b);
System.out.print(c);
输出:
9
12345678
求最大公因数
BigInteger.gcd(BigInteger);
BigInteger a, b, c;
a = BigInteger.valueOf(987654321); // 赋值为 987654321
b = BigInteger.valueOf(123456789); // 赋值为 123456789
c = a.gcd(b);
System.out.print(c);
输出:
9
12345678
求最值
BigInteger.max(BigInteger) , BigDecimal.max(BigDecimal) 最大值
BigInteger.min(BigInteger) , BigDecimal.min(BigDecimal) 最小值
BigInteger a, b, c, d;
a = BigInteger.valueOf(987654321); // 赋值为 987654321
b = BigInteger.valueOf(123456789); // 赋值为 123456789
c = a.max(b); //a,b中的最大值
d = a.min(b); //a,b中的最小值
System.out.println(c);
System.out.println(d);
输出:
987654321
123456789
1234567891011
(a^b)%mod
BigInteger.modPow(BigInteger, BigInteger);
BigInteger a, b, c, mod;
a = BigInteger.valueOf(987654321); // 赋值为 987654321
b = BigInteger.valueOf(123456789); // 赋值为 123456789
mod = BigInteger.valueOf(10007);
c = a.modPow(b, mod); //(a^b)%mod
System.out.println(c);
输出:
718
12345678
比较大小
BigInteger.compareTo(BigInteger);
BigDecimal.compareTo(BigDecimal);
BigInteger a, b;
int c;
a = BigInteger.valueOf(987654321); // 赋值为 987654321
b = BigInteger.valueOf(123456789); // 赋值为 123456789
c = a.compareTo(b); // a 和 b
System.out.println(c);
c = b.compareTo(b); // b 和 b
System.out.println(c);
c = b.compareTo(a); // c 和 c
System.out.println(c);
输出:
1
0
-1
1234567891011121314
可见, 对于a.compareTo(b), a和b进行比较如果:
a > b 返回 1
a == b 返回 0
a < b 返回-1
1234
进制转化
使用构造函数BigInteger(String, int index);可以把一个index进制的字符串,转化为10进制的BigInteger;
BigInteger a = new BigInteger("111110", 2);把111110变为10进制赋值给a
System.out.println(a.toString(16));把a转化为16进制的字符串输出
12
类型转化
BigInteger.toBigDecimal() //把BigInteger 转化为 BigDecimal
BigDecimal.toBigInteger() //把BigDecimal 转化为 BigInteger
BigInteger a = new BigInteger(1);
BigDecimal b = new BigDecimal(2);
b.toBigInteger(); // 把BigDecimal转为BigInteger
a.toBigDecimal(); // 把BigInteger转为BigDecimal
1234
-BigDecimal的精度问题
BigDecimal的舍入模式
想象一个数轴,从负无穷到正无穷,向哪舍入,就是趋向于哪, 向0就是舍入后要更趋近于0.
ROUND_DOWN 向零舍入。 即1.55 变为 1.5 , -1.55 变为-1.5
ROUND_CEILING 向正无穷舍入. 即 1.55 变为 1.6 , -1.55 变为 -1.5
ROUND_FLOOR 向负无穷舍入. 即 1.55 变为 1.5 , -1.55 变为 -1.6
ROUND_HALF_UP 四舍五入 即1.55 变为1.6, -1.55变为-1.6
ROUND_HALF_DOWN 五舍六入 即 1.55 变为 1.5, -1.5变为-1.5
ROUND_HALF_EVEN 如果舍入前一位的数字为偶数,则采用HALF_DOWN奇数则采用HALF_UP 如1.55 采用HALF_UP 1.45采用HALF_DOWN
ROUND_UP 向远离0的方向舍入 即 1.55 变为 1.6 , -1.55 变为-1.6
ROUND_UNNECESSARY 有精确的位数时,不需要舍入
在需要精确舍入的方式时可以使用以上的舍入模式。
(另:Math 类的 ceil()和 floor方法对应普通浮点型的上取整和下取整.)
BigDecimal进行加减乘除时可以进行舍入
如 除法
divide(BigDecimal divisor, int scale, RoundingMode roundingMode) 返回一个
BigDecimal ,其值为 (this / divisor) ,其小数位数为scale。
import java.math.*;
import java.util.*;
import java.io.*;
public class Main{
public static void main(String[] args){
BigDecimal a, b, c;
a = BigDecimal.valueOf(1.51);
b = BigDecimal.valueOf(1.37);
c = a.divide(b,100,BigDecimal.ROUND_DOWN);//采用向0舍入并并保留100位小数
System.out.println(c);
}
}
输出:
1.1021897810218978102189781021897810218978102189781021897810218978102189781021897810218978102189781021
1234567891011121314
保留n位小数
setScale(int newScale, RoundingMode roundingMode);
import java.math.*;
import java.util.*;
import java.io.*;
public class Main{
public static void main(String[] args){
BigDecimal a = new BigDecimal("1.10218");
a = a.setScale(4,BigDecimal.ROUND_HALF_UP);//四舍五入保留四位小数
System.out.println(a);
}
}
本文的篇幅已经不短了,只整理了用的比较多的一些知识,关于蓝桥杯,涉及到的关键字基本就是:递归,二分查找,dfs,bfs,dp,图,树,排序算法,堆
每道题即使简单,也基本都有坑,常规解法易理解但复杂度高,灵活套用相应算法则可简化运算。