1 Java 集合框架
1.1 链表
LinkedList<Integer> linkedList = new LinkedList<>();
linkedList.add(234); // 链表末尾添加元素,返回是否成功,成功为 true,失败为 false
linkedList.add(1,456); // 向指定位置插入元素
linkedList.addFirst(1); // 元素添加到头部
linkedList.remove(1); // 删除指定位置的元素
linkedList.remove(); // 删除链表首元素
linkedList.addLast(2); // 元素添加到尾部
if (linkedList.contains(234)) {
// 判断是否含有某一元素
}
linkedList.get(1); // 返回指定位置的元素,还有 getFirst getLast
linkedList.indexOf(234); // 查找指定元素从前往后第一次出现的索引
linkedList.lastIndexOf(234); // 查找指定元素最后一次出现的索引
linkedList.set(1,234); // 设置指定位置的元素
Object[] a = linkedList.toArray(); // 返回一个由链表元素组成的数组
linkedList.clear(); // 清空链表
1.2 队列
1.3 栈
1.4 HashMap
HashMap 是一个散列表,它存储的内容是键值对(key-value)映射
HashMap<Integer,Character> hashMap = new HashMap<>();
hashMap.put(1,'A'); // 将键/值对添加到 hashMap 中
hashMap.putIfAbsent(1,'B'); // 如果 hashMap 中不存在指定的键,则将指定的键/值对插入到 hashMap 中
hashMap.remove(1); // 删除 hashMap 中指定键 key 的映射关系
if (hashMap.containsKey(1)) {
// 检查 hashMap 中是否存在指定的 key 对应的映射关系
if (hashMap.containsValue('B')) {
// 检查 hashMap 中是否存在指定的 value 对应的映射关系
}
}
hashMap.replace(1,'C'); // 替换 hashMap 中是指定的 key 对应的 value
hashMap.get(1); // 获取指定 key 对应对 value
hashMap.clear(); // 删除 hashMap 中的所有键/值对
1.5 HashSet
基于 HashMap 实现,是一个不允许有重复元素的集合
HashSet<Integer> hashSet = new HashSet<>();
hashSet.add(234); // 如果指定的元素尚不存在,则将其添加到此集合中
if (hashSet.contains(234)) {
// 如果包含指定的元素,则返回 true
}
hashSet.remove(234); // 如果存在,则从该集合中移除指定的元素
hashSet.clear(); // 从该集中删除所有元素
2 字符串处理
2.1 String类
String s = new String("hello");
// 注意 String 不可修改,所以以下方法都是有【返回】值
s.charAt(0); // 返回指定索引处的 char 值
s.compareTo("Hello"); // 按字典顺序比较,大于字符串参数,则返回正数
s.compareToIgnoreCase("Hello"); // (忽略大小写)按字典顺序比较,大于字符串参数,则返回正数
s.concat("world"); // 将指定字符串连接到此字符串的结尾
if (s.equals("hello")) {
// 如果给定对象与字符串相等,则返回 true;否则返回 false
}
s.indexOf("h"); // 返回指定子字符串在此字符串中第一次出现处的索引
s.lastIndexOf("h"); // 返回指定字符在此字符串中最后一次出现处的索引
s.split("regex"); // 根据给定正则表达式的匹配拆分此字符串
s.substring(1); // 返回一个新的字符串,它是此字符串的一个子字符串
s.substring(1,2); // 返回一个子字符串,包含起始索引,不包含末尾索引
s.toCharArray(); // 将此字符串转换为一个新的字符数组
s.toLowerCase(); // 转小写
if (s.contains("llo")) {
// 判断是否包含指定的字符系列
}
2.2 StringBuilder类
// 注意要初始化
StringBuilder stringBuilder = new StringBuilder("hello");
// String 的方法 StringBuilder 都可以用
stringBuilder.append("world"); // 将指定的字符串追加到此字符序列
stringBuilder.reverse(); // 字符串反转
stringBuilder.delete(1,2); // 移除此序列的子字符串中的字符
stringBuilder.insert(1,"abc"); // 插入
stringBuilder.replace(1,3,"zz"); // 使用给定 String 中的字符替换此序列的子字符串中的字符
3 大数
import java.math.BigInteger;
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
BigInteger a,b;
a = sc.nextBigInteger();
b = sc.nextBigInteger();
System.out.println(a.add(b)); // 加
System.out.println(a.subtract(b)); // 减
System.out.println(a.multiply(b)); // 乘
System.out.println(a.divide(b)); // 除
System.out.println(a.mod(b)); // 取余
}
}
4 算法
4.1 打表
打表是一种典型的用空间换时间的技巧,一般指将所有可能需要用到的结果事先计算出来,这样后面需要用到时就可以直接查表获得。打表常见的用法有如下几种:
1、在程序中一次性计算出所有需要用到的结果,之后的查询直接取这些结果
这个是最常用到的用法,例如在一个需要查询大量Fibonacci数F(n)的问题中,显然每次从头开始计算是非常耗时的,对Q次查询会产生O(nQ)的时间复杂度;而如果进行预处理,即把所有Fibonacci数预先计算并存在数组中,那么每次查询就只需O(1)的时间复杂度,对Q次查询就值需要O(n+Q)的时间复杂度(其中O(n)是预处理的时间)。
2、在程序B中分一次或多次计算出所有需要用到的结果,手工把结果写在程序A的数组中,然后在程序A中就可以直接使用这些结果
这种用法一般是当程序的一部分过程小号的时间过多,或是没有想到好的算法,因此在另一个程序中使用暴力算法算出结果,这样就能直接在源程序中使用这些结果。例如对n皇后问题来说,如果使用的算法不够好,就容易超时,而可以在本地用程序计算付出对所有n来说n皇后问题的方案数,然后把算出的结果直接卸载数组中,就可以根据题目输入的n来直接输出结果。
3、对一些感觉不会做的题目,先用暴力程序计算小范围数据的结果,然后找规律,或许就能发现一些“蛛丝马迹”
这种用法在数据范围非常大时候容易用到,因为这样的题目可能不是用直接能想到的算法来解决的,而需要寻找一些规律才能得到结果。
4.2 枚举
1,枚举算法的定义:
在进行归纳推理时,如果逐个考察了某类事件的所有可能情况,因而得出一般结论,那么该结论是可靠 的,这种归纳方法叫做枚举法。
2,枚举算法的思想是:
将问题的所有可能的答案一一列举,然后根据条件判断此答案是否合适,保留合适的,舍弃不合适的。
3,使用枚举算法解题的基本思路如下:
(1)确定枚举对象、范围和判定条件。
(2)逐一枚举可能的解并验证每个解是否是问题的解。
4,枚举算法步骤:
(1)确定解题的可能范围,不能遗漏任何一个真正解,同时避免重复。
(2)判定是否是真正解的方法。
(3)为了提高解决问题的效率,使可能解的范围将至最小,
5,枚举算法的流程图如下所示:
4.3 倍增
4.4 离散化
用 HashMap
4.5 前缀和
首先了解“前缀和”的概念。一个长度为n的数组a[1] ~ a[n],前缀和sum[i]等于a[1] ~ a[i]的和:
sum[i] = a[1] + a[2] + … + a[i]
利用递推,可以在O(n)时间内求得所有前缀和:
sum[i] = sum[i-1] + a[i]
如果预计算出前缀和,就能利用它快速计算出数组中任意一个区间a[i] ~ a[j]的和。即:
a[i] + a[i+1] + … + a[j-1] + a[j] = sum[j] - sum[i-1]
上式说明,复杂度为O(n)的区间求和计算,优化到了O(1)的前缀和计算。
(LeetCode)一维数组的动态和
class Solution {
public int[] runningSum(int[] nums) {
int[] a = new int[nums.length];
for (int i = 1; i <= nums.length; i++) {
a[i - 1] = recursionSum(nums, i);
}
return a;
}
public static int recursionSum(int[] nums, int k) {
if (k == 1) {
return nums[0];
}
return recursionSum(nums, k - 1) + nums[k - 1];
}
}
时间复杂度极高
(蓝桥杯)求和
import java.util.Scanner;
// 1:无需package
// 2: 类名必须Main, 不可修改
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
//在此输入您的代码...
int n = sc.nextInt();
int[] a = new int[n];
for (int i = 0; i < n; i++) {
a[i] = sc.nextInt();
}
// 暴力 -> 30%用例
// int sum = 0;
// for (int i = 0; i < n; i++) {
// for (int j = i + 1; j < n; j++) {
// sum += a[i] * a[j];
// }
// }
// 优化 -> 30%用例(实际上没有优化,sum要计算要遍历一遍数组,还是O(n^2))
// int aSum = 0;
// for (int i = 0; i < n; i++) {
// aSum += a[i];
// }
// int sum = 0;
// for (int i = 0; i < n; i++) {
// sum += (aSum-a[i])*a[i];
// aSum-=a[i];
// }
// 前缀和优化 -> 100%AC
long[] prefixSum = new long[n];
long sum = 0; //非常要注意【范围】!!!!!!!!!!!!!!!!!!!
prefixSum[0] = a[0];
for (int i = 1; i < n; i++) {
prefixSum[i] = prefixSum[i - 1] + a[i];
}
for (int i = 1; i < n; i++) {
sum += a[i] * prefixSum[i - 1];
}
System.out.println(sum);
sc.close();
}
}
4.6 差分
前缀和的主要应用是差分:差分是前缀和的逆运算
与一维数组a[]对应的差分数组d[]的定义:
d[k]=a[k]-a[k-1]
即原数组a[]的相邻元素的差。根据d[]的定义,可以推出:
a[k]=d[1]+d[2]+…+d[k]
把区间[L, R]内每个元素a[]加上v,只需要把对应的d[]做以下操作:
(1)把d[L]加上v: d[L] += v
(2)把d[R+1]减去v:d[R+1] -= v
还原的时候这样还原:
cnt[i] = cnt[i-1]+d[i];
【例题】:重新排序
import java.util.Arrays;
import java.util.Scanner;
public class 重新排序 {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
// 索引从1到n
int[] cnt = new int[n + 1];
int[] a = new int[n + 1];
for (int i = 1; i <= n; i++) {
a[i] = sc.nextInt();
}
int m = sc.nextInt();
int beforeSum = 0;
for (int i = 0; i < m; i++) {
int p = sc.nextInt();
int q = sc.nextInt();
for (int j = p; j <= q; j++) {
cnt[j]++;
beforeSum += a[j];
}
}
Arrays.sort(a);
Arrays.sort(cnt);
int afterSum = 0;
for (int i = n; i >= 1; i--) {
afterSum += a[i] * cnt[i];
}
System.out.println(afterSum - beforeSum);
}
}
(暴力法)引入一个cnt数组,存放每个数字的被访问次数,然后排序cnt和a,从后面做乘法
通过40%用例
【优化】:
使用差分数组
import java.util.Arrays;
import java.util.Scanner;
public class 重新排序 {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
// 索引从1到n
int[] cnt = new int[n + 2];
int[] a = new int[n + 2];
// 引入差分数组d[]
int[] d = new int[n + 2];
for (int i = 1; i <= n; i++) {
a[i] = sc.nextInt();
}
int m = sc.nextInt();
long beforeSum = 0;
for (int i = 0; i < m; i++) {
int p = sc.nextInt();
int q = sc.nextInt();
d[p]++;
d[q+1]--;
}
cnt[0] = d[0];
for (int i = 1; i <=n ; i++) {
cnt[i] = cnt[i-1]+d[i]; // 先拿到cnt
}
for (int i = 1; i <=n ; i++) {
beforeSum += (long)a[i]*cnt[i];
}
Arrays.sort(a,1,n+1); // 关键在于排序
Arrays.sort(cnt,1,n+1);
long afterSum = 0;
for (int i = 1; i<=n; i++) {
afterSum += (long)a[i] * cnt[i];
}
System.out.println(afterSum-beforeSum);
}
}
4.7 尺取
尺取法:顾名思义,像尺子一样取一段
尺取法通常是对数组保存一对下标,即所选取的区间的左右端点,然后根据实际情况不断地推进区间左右端点以得出答案。
尺取法比直接暴力枚举区间效率高很多,尤其是数据量大的时候,所以说尺取法是一种高效的枚举区间的方法,是一种技巧,一般用于求取有一定限制的区间个数或最短的区间等等。当然任何技巧都存在其不足的地方,有些情况下尺取法不可行,无法得出正确答案,所以要先判断是否可以使用尺取法再进行计算。
4.8 分治
【分而治之】把一个复杂的问题分成两个或更多的相同或相似的子问题,再把子问题分成更小的子问题……直到最后子问题可以简单的直接求解,原问题的解即子问题的解的合并
分治法在每一层递归上都有三个步骤:
step1 分解:将原问题分解为若干个规模较小,相互独立,与原问题形式相同的子问题;
step2 解决:若子问题规模较小而容易被解决则直接解,否则递归地解各个子问题
step3 合并:将各个子问题的解合并为原问题的解。
它的一般的算法设计模式如下:
Divide-and-Conquer(P)
1. if |P|≤n0
2. then return(ADHOC(P))
3. 将P分解为较小的子问题 P1 ,P2 ,...,Pk
4. for i←1 to k
5. do yi ← Divide-and-Conquer(Pi) △ 递归解决Pi
6. T ← MERGE(y1,y2,...,yk) △ 合并子问题
7. return(T)
4.9 贪心
【贪心】(Greedy):把整个问题分解成多个步骤,在每个步骤,都选取当前步骤的最优方案,直到所有步骤结束;在每一步,都不考虑对后续步骤的影响,在后续步骤中也不能回头改变前面的选择
总结活动安排问题的贪心策略:先按活动的结束时间(区间右端点)排序,然后每次选结束最早的活动,并保证选择的活动不重叠
P1803 凌乱的yyy / 线段覆盖
import java.util.Scanner;
public class P1803凌乱的yyy_线段覆盖 {
static Scanner sc = new Scanner(System.in);
static int n = sc.nextInt();
static int[][] a = new int[n][2];
static int res = 0;
public static void DFS(int time, int cnt) {
for (int i = 0; i < n; i++) {
if (a[i][0] >= time) {
DFS(a[i][1], cnt + 1);
}
}
res = Math.max(res, cnt);
}
public static void main(String[] args) {
for (int i = 0; i < n; i++) {
a[i][0] = sc.nextInt();
a[i][1] = sc.nextInt();
}
int time = 0;
DFS(0, 0);
System.out.println(res);
}
}
先尝试用dfs写,只能通过两个用例,其他全超时
【优化】注意这段代码的书写!!!!!
import java.util.*;
class Main {
static class Data { int L, R; } // 开始时间、结束时间
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
int n = scanner.nextInt();
Data[] a = new Data[n];
for (int i = 0; i < n; i++) {
a[i] = new Data();
a[i].L = scanner.nextInt();
a[i].R = scanner.nextInt();
}
Arrays.sort(a, new Comparator<Data>() {
public int compare(Data x, Data y) {
return x.R - y.R;
}
});
int ans = 0;
int lastend = -1;
for (int i = 0; i < n; i++)
if (a[i].L >= lastend) {
ans++;
lastend = a[i].R;
}
System.out.println(ans);
}
}
4.10 二分
二分法把长度为n的有序序列上O(n)的查找时间,优化到了O(logn)。
注意二分法的应用条件是:序列是单调有序的,从小到大,或从大到小。在无序的序列上无法二分,如果是乱序的,应该先排序再二分。
如果在乱序序列上只搜一次,不需要用二分法。如果用二分,需要先排序,排序复杂度O(nlogn),再二分是O(logn),排序加二分的总复杂度O(nlogn)。如果用暴力法直接在乱序的n个数里面找,复杂度是O(n)的,比排序加二分快。
但是如果不是搜一个数,而是搜m个数。那么先排序再做m次二分的计算复杂度是O(nlogn+mlogn),而暴力法是O(mn)的,当m很大时,二分法远好于暴力法。
做二分法题目时,需要建模出一个有序的序列,并且答案在这个序列中。编程时,根据题目要求确定区间[L, R]范围,并写一个check()函数来更新L和R。
模板
while (L < R){ //一直二分,直到区间[L,R]缩小到L=R
int mid = (L + R) / 2; //mid是L、R的中间值
if (check(mid)) R = mid; //答案在左半部分[L,mid],更新R=mid
else L = mid + 1; //答案在右半部分[mid+1, R],更新L=mid+1
}
public static int binarySearch(int[] a, int n) {
int left = 0;
int right = a.length;
while (left <= right) {
int mid = left + (right - left) / 2;
if (a[mid] == n) {
return mid;
} else if (a[mid] > n) {
right = mid - 1;
} else {
left = mid + 1;
}
}
return -1;
}
import java.util.Arrays;
import java.util.Scanner;
public class P1873_EKO砍树 {
static int[] a;
static int m;
public static boolean check(int high) {
int sum = 0;
for (int i = 0; i < a.length; i++) {
if (high < a[i]) {
sum += a[i] - high;
}
}
return sum >= m;
}
public static int bs(int l, int r) {
int res = -1;
while (l <= r) {
int mid = l + (r - l) / 2;
if (check(mid)) {
res = mid;
l = mid + 1;
} else {
r = mid - 1;
}
}
return res;
}
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
m = sc.nextInt();
a = new int[n];
for (int i = 0; i < n; i++) {
a[i] = sc.nextInt();
}
Arrays.sort(a);
System.out.println(bs(a[0], a[a.length - 1]));
}
}
4.11 快速幂
import java.util.Scanner;
public class P1226_模板快速幂_Long {
static long p;
public static long fastPow(long a, long n) { // a^n
// 注意【步步取模】!!!!
if (n == 0) {
return 1;
} else if (n % 2 == 1) { // 奇数,降一次幂
return fastPow(a, n - 1) * a % p;
} else { // 偶数,55分治
long t = fastPow(a, n / 2) % p;
return t * t % p;
}
}
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
long a = sc.nextLong();
long b = sc.nextLong();
p = sc.nextLong();
long res = fastPow(a, b);
res %= p;
System.out.println(a + "^" + b + " mod " + p + "=" + res);
}
}
6 DFS与DP
模板
ans; //答案,常常用全局变量表示
void dfs(层数,其他参数){
if (到达目的地、或者出局){ //到达最底层,或者满足条件退出
更新答案ans; //答案一般用全局变量表示,ans是最优解
return; //递归返回,即返回到上一层
}
(剪枝) //在进一步DFS之前剪枝
for (用i遍历下一层所有可能的情况) //对每一个情况继续DFS
if (used[i] == 0) { //如果状态i没有处理过,就可以进入下一层dfs
used[i] = 1; //标记状态i为已经使用,在后续dfs时不能再使用
dfs(层数+1,其他参数); //下一层,即搜小规模后继续dfs
used[i] = 0; //恢复状态i,回溯时,不影响上一层对这个状态的使用
}
return; //返回到上一层
}
dfs例题
P1048 [NOIP2005 普及组] 采药:
import java.util.*;
public class 采藥 {
static int[][] a = new int[10005][2];
static boolean[] vis = new boolean[10005];
static int t;
static int m;
static int res=0;
public static void dfs(int cnt,int time,int value) {
if (time>t) {
return;
}
if (cnt == m) {
res = Math.max(res, value);
return;
}
for (int i = 0; i < m; i++) {
if (a[i][0]>t) {
continue;
}
if (!vis[i]) {
vis[i] = true;
dfs(cnt+1, time, value); // 不采
dfs(cnt+1, time+a[i][0], value+a[i][1]); // 采
vis[i] = false;
}
}
}
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
t = sc.nextInt();
m = sc.nextInt();
for (int i = 0; i < m; i++) {
a[i][0] = sc.nextInt();
a[i][1] = sc.nextInt();
}
dfs(0, 0, 0);
System.out.print(res);
}
}
先用dfs,只能通过两个用例
考虑用dp:
import java.util.*;
public class 采藥 {
static int[][] a = new int[1050][2];
static int[][] dp = new int[1050][1050];
// 注意dp[i][j]的含義:前i個物品,容量限制為j,所取得的最大價值
static int t;
static int m;
static int res=0;
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
t = sc.nextInt();
m = sc.nextInt();
for (int i = 1; i <= m; i++) {
a[i][0] = sc.nextInt();
a[i][1] = sc.nextInt();
}
for (int i = 1; i <= m; i++) {
for (int j = 1; j <=t; j++) {
if (j>=a[i][0]) { // 放得下
dp[i][j] = Math.max(dp[i-1][j], dp[i-1][j-a[i][0]] + a[i][1]);
}else {
dp[i][j] = dp[i-1][j];
}
}
}
System.out.print(dp[m][t]);
}
}
另:完全背包的改动:
01背包:
dp[i][j] = Math.max(dp[i-1][j], dp[i-1][j-a[i][0]] + a[i][1]);
完全背包:
dp[i][j] = Math.max(dp[i-1][j], dp[i][j-a[i][0]] + a[i][1]);
7 数论
素数
static boolean isPrime(int s) {
if (s <= 1) return false;
for (int i = 2; i <= Math.sqrt(s); i++)
if (s % i == 0) return false;
return true;
}
最大公约数GCD
public static long gcd(long a, long b) {
if (b == 0) return a;
return gcd(b, a % b);
}
最小公倍数
public static long lcm(int a, int b) {
return (long) a / gcd(a, b) * b;
}
素数
public static boolean isPrime(int n) {
for (int i = 2; i <= Math.sqrt(n); i++) {
if (n % i == 0)
return false;
}
return true;
}
线性筛
public static List<Integer> generatePrimes(int n) {
boolean[] isPrime = new boolean[n + 1];
List<Integer> primes = new ArrayList<>();
Arrays.fill(isPrime, true); // 一开始认为所有的数都是质数
for (int i = 2; i <= n; i++) {
if (isPrime[i]) { // 从2到n,如果i是质数,就加入到质数表
primes.add(i);
}
for (int j = 0; j < primes.size() && i * primes.get(j) <= n; j++) {
isPrime[i * primes.get(j)] = false; // 标记非质数
if (i % primes.get(j) == 0) {
break;
}
}
}
return primes;
}
8 数据结构
import java.util.*;
public class P1125_NOIP2008提高组_笨小猴 {
public static boolean isPrime(int n) {
if (n == 1) {
return false;
}
for (int i = 2; i <= Math.sqrt(n); i++) {
if (n % i == 0)
return false;
}
return true;
}
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
HashMap<Character, Integer> hm = new HashMap<>(); // 哈希表的查找是 O(1),字母本身是不重复,设置为key
// 字母出现的次数不停加1,设为value
String s = sc.nextLine();
class WordList { // 不能在哈希表中排序,自定义单词表类,方便之后对 count 的排序
char word;
int count;
}
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
if (hm.containsKey(c)) {
hm.put(c, hm.get(c) + 1); // 哈希表重新设置value
} else {
hm.put(c, 1);
}
}
WordList[] w = new WordList[hm.size()];
Iterator<Map.Entry<Character, Integer>> entries = hm.entrySet().iterator(); // 迭代器放在循环外面
for (int i = 0; i < hm.size(); i++) {
w[i] = new WordList(); // 自定义类的每次使用都要初始化
if (entries.hasNext()) {
Map.Entry<Character, Integer> entry = entries.next(); // Map.Entry是一个键值对,entries是键值对的集合
w[i].word = entry.getKey();
w[i].count = entry.getValue();
}
}
Arrays.sort(w, new Comparator<WordList>() {
@Override
public int compare(WordList o1, WordList o2) {
return o2.count - o1.count; // 比较器 对 count 字段按降序排序
}
});
int div = w[0].count - w[w.length - 1].count;
if (div == 0) {
System.out.println("No Answer");
System.out.println(div);
} else {
if (isPrime(div)) {
System.out.println("Lucky Word");
System.out.println(div);
} else {
System.out.println("No Answer");
System.out.println(0);
}
}
}
}
9 日期
DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyyMMdd");
LocalDate start = LocalDate.parse(sStart,dateTimeFormatter);
2 记忆化搜索(DFS+DP)
2.1 滑行
import java.util.Scanner;
// 1:无需package
// 2: 类名必须Main, 不可修改
public class Main {
static Scanner scan = new Scanner(System.in);
static int n = scan.nextInt();
static int m = scan.nextInt();
static int[][] map = new int[n][m];
static int[][] dp = new int[n][m];
static int[][] dir = {
{1, 0},
{-1, 0},
{0, 1},
{0, -1}
};
public static void main(String[] args) {
int max = 0;
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
map[i][j] = scan.nextInt();
}
}
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
int temp = DFS(i, j);
max = Math.max(max, temp);
}
}
System.out.println(max);
scan.close();
}
public static int DFS(int x, int y) {
if (dp[x][y] > 0) {
return dp[x][y];
}
dp[x][y] = 1;
for (int i = 0; i < dir.length; i++) {
int dx = x + dir[i][0];
int dy = y + dir[i][1];
if (dx >= 0 && dx < n && dy >= 0 && dy < m) {
if (map[x][y] < map[dx][dy]) {
int temp = DFS(dx, dy);
dp[x][y] = Math.max(dp[x][y], temp + 1);
}
}
}
return dp[x][y];
}
}
DFS只能通过一些用例,需要进行剪枝,就使用dp来存储已经遍历过的节点的滑行距离
3 子矩阵遍历
4 DFS
4.1 yes or no
迷宫
迷宫dfs(T是终点)
public class Maze_DFS {
static String[] map = {
"S....",
".XXX.",
".X...",
"..XXX",
"....T",
};
static boolean[][] visited = new boolean[10][10];
static int[][] dir = {
{1, 0},
{-1, 0},
{0, 1},
{0, -1}
};
public static boolean DFS(int x, int y) {
visited[x][y] = true;
if (map[x].charAt(y) == 'T') {
System.out.println("successful");
return true;
}
for (int i = 0; i < dir.length; i++) {
int dx = x + dir[i][0];
int dy = y + dir[i][1];
if (dx >= 0 && dx < 5 && dy >= 0 && dy < 5 && map[dx].charAt(dy) != 'X' && !visited[dx][dy]) {
if (DFS(dx, dy)) {
System.out.println("( " + dx +", "+dy+")");
return true;
}
}
}
// 回溯
visited[x][y] = false;
return false;
}
public static void main(String[] args) {
DFS(0, 0);
}
}
【回溯】:把当前失败的节点标记为 false ,同时返回false,这样这个节点以后不会再去重复遍历,一步一步退回去
象棋
注意马的走法(八向)
import java.util.Scanner;
public class xiangqi_DFS {
static String[] map = new String[15];
static boolean[][] vis = new boolean[15][15];
static int[][] dir = {
{-2, -1},
{-2, 1},
{-1, -2},
{-1, 2},
{1, 2},
{2, 1},
{2, -1},
{1, -2},
};
public static boolean DFS(int x, int y) {
if (map[x].charAt(y) == 'T') {
return true;
}
vis[x][y] = true;
for (int i = 0; i < 8; i++) {
int dx = x + dir[i][0];
int dy = y + dir[i][1];
if (dx >= 0 && dx < 10 && dy >= 0 && dy < 10 && map[dx].charAt(dy) != '#' && !vis[dx][dy]) {
if (DFS(dx, dy)) {
return true;
}
}
}
vis[x][y] = false;
return false;
}
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
for (int i = 0; i < 10; i++) {
map[i] = sc.nextLine();
}
int x = 0, y = 0;
for (int i = 0; i < 9; i++) {
for (int j = 0; j < 9; j++) {
if (map[i].charAt(y) == 'S') {
x = i;
y = j;
break;
}
}
}
if (DFS(x, y)) {
System.out.println("Yes");
} else {
System.out.println("No");
}
}
}
和上一个问题【迷宫】很相似
4.2 how many steps
现在计算【象棋】共走了多少步
部分代码:
static int ans = 10000;
public static boolean DFS(int x, int y, int step) {
if (map[x].charAt(y) == 'T') {
if (step<ans){
ans = step;
}
return true;
}
vis[x][y] = true;
for (int i = 0; i < 8; i++) {
int dx = x + dir[i][0];
int dy = y + dir[i][1];
if (dx >= 0 && dx < 10 && dy >= 0 && dy < 10 && map[dx].charAt(dy) != '#' && !vis[dx][dy]) {
if (DFS(dx, dy,step+1)) {
//step++ 不要这么写!!!错误写法
return true;
}
}
}
vis[x][y] = false;
return false;
}
public static void main(String[] args) {
DFS(x,y,0);
System.out.println(ans + "步");
}
注意的是,不要在DFS递归中对step进行加减,要在递归调用DFS的时候,带上参数step
还有声明一个全局变量 ans,初始化一个大数,这样取全局最小步数,最后在主函数输出 ans
4.3 抽象DFS
4.3.1 选数
问题:给定n个数,要从中选择k个数,使得K个数的总和为sum
建立搜索树:S为当前选择数的总和,k为选择的数的个数
时间复杂度:O(2^k)
import java.util.Scanner;
public class ChooseNumsToSum_DFS {
/*
n个数,选k个,凑成sum
用例:
输入 5 3 9
1 2 3 4 5
输出 2
*/
static int n;
static int sum;
static int k;
static int ans = 0;
static int[] a = new int[100];
public static void DFS(int i, int cnt, int s) {
// i是当前的数 cnt是已经选择的数的个数 s是当前数的总和
if (i == n) {
if (cnt == k && s == sum) {
ans++;
}
return;
}
DFS(i + 1, cnt, s); // 一种是不选择这个数,i+1之间调用
DFS(i + 1, cnt + 1, s + a[i]); // 另一种是选择这个数,cnt+1,s加上这个数
}
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
n = sc.nextInt();
k = sc.nextInt();
sum = sc.nextInt();
for (int i = 0; i < n; i++) {
a[i] = sc.nextInt();
}
DFS(0, 0, 0);
System.out.println(ans);
}
}
4.4 剪枝
可行性剪枝:
当前数的和s大于目标sum,或者当前选的数个数大于目标k,都直接return而不接着往下
public static void DFS(int i, int cnt, int s) {
if (s > sum || cnt > k) {
return;
}
// i是当前的数 cnt是已经选择的数的个数 s是当前数的总和
if (i == n) {
if (cnt == k && s == sum) {
ans++;
}
return;
}
DFS(i + 1, cnt, s);
DFS(i + 1, cnt + 1, s + a[i]);
}
举个例子--->去重剪枝
需要去重,注意是 i+1
public static void chooseThreeNumsPermutationDFS(int cnt,int pos){
if (cnt == k) {
for (int i = 0; i < k; i++) {
System.out.print(path[i] + " ");
}
count++;
System.out.println("共计: " + count + " 次");
return;
}
for (int i = pos; i < n; i++) {
if (!vis[a[i]]) {
// if (a[i]<path[cnt]){
// continue;
// }
path[cnt] = a[i];
vis[a[i]] = true;
chooseThreeNumsPermutationDFS(cnt + 1,i+1);
vis[a[i]] = false;
}
}
}
【例】30个数,取8个凑sum,有多少方案?(答案70种)
加上剪枝明显减少运行时间
import java.util.Scanner;
public class ChooseNumsToSum_DFS_CutBranch {
static int n;
static int k;
static int sum;
static int[] a = new int[100];
static boolean[] vis = new boolean[100];
static int ans = 0;
public static void DFS(int s, int cnt, int pos) {
// 可行性剪枝
if (s > sum || cnt > k) {
return;
}
if (s == sum && cnt == k) {
ans++;
}
// 重复性剪枝 i从pos位置开始
for (int i = pos; i < n; i++) {
if (!vis[i]) {
vis[i] = true;
DFS(s + a[i], cnt + 1, i + 1);
vis[i] = false;// 回溯
}
}
}
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
n = 30;
k = 8;
sum = 200;
for (int i = 0; i < n; i++) {
a[i] = i + 1;
}
DFS(0, 0, 0);
System.out.println(ans);
}
}
4.5 全排列
import java.util.Scanner;
public class FullPermutation {
static int n;
static boolean[] vis = new boolean[100];
static int[] path = new int[100];
public static void DFS(int i) {
if (i == n) {
for (int j = 0; j < n; j++) {
System.out.print(path[j] + " ");
}
System.out.println();
return;
}
for (int j = 1; j <= n; j++) {
if (!vis[j]) {
path[i] = j;
vis[j] = true;
DFS(i + 1);
vis[j] = false;
}
}
}
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
n = sc.nextInt();
DFS(0);
}
}
与或异或
public class LanQiao与或异或 {
// time is 15:48
static int[][] a = new int[5][5];
/*
1 0 1 0 1
1 1 1 0
1 1 1
1 0
1
*/
static int res = 0;
static int[] path = new int[10]; // 1& 2| 3^
static int pathX =0;
static int pathY =0;
static int pIndex =0;
public static boolean check(){
pIndex=0;
a[0][0] = 1;
a[0][1] = 0;
a[0][2] = 1;
a[0][3] = 0;
a[0][4] = 1;
for (int i = 1; i <=4 ; i++) {
for (int j = 0; j <= 4-i; j++) {
if (path[pIndex]==1)
a[i][j] = a[i-1][j] & a[i-1][j+1];
if (path[pIndex]==2)
a[i][j] = a[i-1][j] | a[i-1][j+1];
if (path[pIndex]==3)
a[i][j] = a[i-1][j] ^ a[i-1][j+1];
pIndex++;
}
}
return a[4][0]==1;
}
public static void dfs(int cnt){
if (cnt==10){
if (check()){
res++;
}
return;
}
for(int i = 1; i <= 3; i++) { //第k个逻辑门有三种选择:与、或、异或
path[cnt] = i; //记录第k个逻辑门:与、或、异或
dfs(cnt + 1); //继续深搜第k+1个逻辑门
}
}
public static void main(String[] args) {
dfs(0);
System.out.println(res);
}
}
5 DP
5.1 路径
import java.util.Scanner;
public class CrossRiverSoldier_DP {
/*
用例
输入 5 5 2 4
n m hx hy
棋盘大小 马的位置
输出 14
*/
static int n, m;
static int[][] dir = {
{-2, -1},
{-2, 1},
{-1, -2},
{-1, 2},
{1, 2},
{2, 1},
{2, -1},
{1, -2},
};
static int[][] dp = new int[30][30];
static boolean[][] horseControl = new boolean[100][100];
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
n = sc.nextInt();
m = sc.nextInt();
int hx = sc.nextInt();
int hy = sc.nextInt();
horseControl[hx][hy] = true;
for (int i = 0; i < dir.length; i++) {
int dx = hx + dir[i][0];
int dy = hy + dir[i][1];
if (dx >= 0 && dx <= n && dy >= 0 && dy <= m) {
horseControl[dx][dy] = true;
}
}
dp[0][0] = 1;
for (int i = 0; i <= n; i++) {
for (int j = 0; j <= m; j++) {
if (!horseControl[i][j]) {
if (i != 0) {
dp[i][j] += dp[i - 1][j];
}
if (j != 0) {
dp[i][j] += dp[i][j - 1];
}
}
}
}
System.out.println(dp[n][m]);
}
}
【注意】边界条件 i <= n , j<=m
【为什么DP索引从1开始】为了不越界,方便判断
5.2 回家
要么往上走,要么往右走----->动态规划DP
import java.util.Scanner;
public class GoHome_DP {
/*
用例
输入
0 3 4
6 2 5
5 4 3
输出
12
*/
// 左下角走到右上角,数字代表代价
static int[][] map = new int[10][10];
static int n = 3;
static int[][] dp = new int[10][10];
// static int [][] dir = {
// {1,0},
// {0,1}
// }; // 注意这是DP而不是DFS,不需要方向
public static void main(String[] args) {
dp[1][1] = 0; // dp从1,1开始
Scanner sc = new Scanner(System.in);
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
map[i][j] = sc.nextInt();
}
}
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
if (i == 1 && j == 1) {
continue;
} else if (i == 1) {
dp[i][j] = dp[i][j - 1] + map[i][j];
} else if (j == 1) {
dp[i][j] = dp[i - 1][j] + map[i][j];
} else {
dp[i][j] = Math.min(dp[i - 1][j], dp[i][j - 1]) + map[i][j];
}
}
}
System.out.println(dp[n][n]);
}
}
【注意】处理边界条件
最后12为答案
5.3 捡水果
import java.util.Scanner;
public class PickFruit_DP {
/*
4
3
1 2
6 2 3
3 5 4 1
*/
static int[][] map = new int[100][100];
static int[][] dp = new int[100][100];
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int max = 0;
int n = sc.nextInt();
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= i; j++) {
map[i][j] = sc.nextInt();
}
}
dp[1][1] = map[1][1];
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= i; j++) {
if (i == 1 && j == 1) {
continue;
} else if (i == 1) {
dp[i][j] += map[i][j]; // 这行居然没有运行,原因是i等于1的时候j必等于1
// 所以可见初始化需要自己好好判断
} else if (j == 1) {
dp[i][j] += dp[i - 1][j] + map[i][j];
} else {
dp[i][j] += Math.max(dp[i - 1][j - 1], dp[i - 1][j]) + map[i][j];
}
// 判断最后一行的最大值
if (i == n) {
max = Math.max(max, dp[i][j]);
}
}
}
System.out.println(max);
}
}
5.4 爬楼梯
LeetCode 70
import java.util.Scanner;
public class UpFloor_DP {
/*
用例
输入4
输出5
输出答案需要mod 100007
*/
static int[] dp = new int[100]; // dp数组记录的是【方法数】
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int n= sc.nextInt();
dp[1]=1;
dp[2]=2;
for (int i = 3; i <= n; i++) {
dp[i] = dp[i-1]+dp[i-2];
}
System.out.println(dp[n] % 100007);
}
}