1、最优路线
通用思路
1、递归
#案例1-最优路测路线
题目描述
评估一个网络的信号质量,其中一个做法是将网络划分为栅格,然后对每个栅格的信号质量计算。
路测的时候,希望选择一条信号最好的路线(彼此相连的栅格集合)进行演示。
现给出 R 行 C 列的整数数组 Cov,每个单元格的数值 S 即为该栅格的信号质量(已归一化,无单位,值越大信号越好)。
要求从 [0, 0] 到 [R-1, C-1]设计一条最优路测路线。返回该路线得分。
规则:
- 路测路线可以上下左右四个方向,不能对角
- 路线的评分是以路线上信号最差的栅格为准的,例如路径 8→4→5→9 的值为4,该线路评分为4。线路最优表示该条线路的评分最高。
输入描述
一行表示栅格的行数 R
第二行表示栅格的列数 C
第三行开始,每一行表示栅格地图一行的信号值,如5 4 5
输出描述
最优路线的得分
备注
- 1 ≤ R,C ≤ 20
- 0 ≤ S ≤ 65535
用例1
输入
3
3
5 4 5
1 2 6
7 4 6
输出
4
说明
路线为:5→4→5→6→6
用例2
输入
6
5
3 4 6 3 4
0 2 1 1 7
8 8 3 2 7
3 2 4 9 8
4 1 2 0 0
4 6 5 4 3
输出
3
说明
路线为:3→4→6→3→4→7→7→8→9→4→3→8→8→3→4→4→6→5→4→3
代码:
package algorithm.huawei;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Scanner;
public class Test61ZuiyouLuxian {
static int min = Integer.MAX_VALUE;
static List<List<Path>> ll = new ArrayList<List<Path>>();
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
while (in.hasNextInt()) {
int m = in.nextInt();in.nextLine();
int n = in.nextInt();
int[][] mn = new int[m][n];
for(int i=0;i<m;i++){
for(int j=0;j<n;j++){
mn[i][j] = in.nextInt();
}
in.nextLine();
}
int min=mn[0][0];
List<Integer> li = new LinkedList<Integer>();
// 数据准备完毕
dfs(mn,m,n,0,0,min,li);
System.out.println(li.get(0));
}
}
private static void dfs(int[][] mn, int m, int n, int i, int j, int min, List<Integer> li) {
if(i<0 || j<0 || i>m-1 || j>n-1 || mn[i][j]==-1){
return;
}
if(li.size()>0 && min<=li.get(0)){
return;
}
min = Math.min(min, mn[i][j]);
if(i==m-1 && j==n-1){
li.clear();
li.add(min);
}
int temp=mn[i][j];
mn[i][j] = -1;//禁止走回头路
dfs(mn,m,n,i-1,j,min,li);//向上
dfs(mn,m,n,i+1,j,min,li);//向下
dfs(mn,m,n,i,j-1,min,li);//向左
dfs(mn,m,n,i,j+1,min,li);//向右
mn[i][j] = temp;//回溯
}
}
2、最长子串
滑动窗口、状态打标
#案例1-出现偶数次最长子字符串的长度
题目描述
给你一个字符串 s,字符串 s 首尾相连成一个环形,请你在环中找出 'l'、'o'、'x' 字符都恰好出现了偶数次最长子字符串的长度。
输入描述
输入是一串小写的字母组成的字符串
输出描述
输出是一个整数
1 ≤ s.length ≤ 5 * 10^5
s只包含小写英文字母
代码:
package algorithm.huawei;
import java.util.Scanner;
//思路
//经典的状态压缩动态规划, 一般遇到要保存的状态不多的问题时都可以通过状态压缩来解决
//注意到元音字母只有 5 个, 所以根据不同字母的奇偶性总共也就 32 个状态
//状态为 0 时代表[0,0,0,0,0], 表示所有元音字母都有偶数个
//状态为 1 时代表[0,0,0,0,1], 表示只有 a 是奇数, 其余字母有偶数个
//状态为 2 时代表[0,0,0,1,0], 表示只有 e 是奇数, 其余字母有偶数个
//...
//状态为 31 时代表[1,1,1,1,1], 表示所有元音字母都有奇数个
//可以利用这一点, 存不同状态下的最左边下标, 并保存当前的状态, 初始化状态为 0, 因为此时各个元音字母都是 0 个, 即偶数个
//如果当前下标 i 对应的状态已经有最左下标 j 了, 那么[j+1,i]这段区间的各个元音字母的数目一定是偶数个, 这样才能保证 i 和 j 处的状态相同, 如果其长度大于结果值就更新结果即可
//特别注意当状态为 0 时本身就是合法的, 长度就是当前下标+1, 所以可以初始化该状态的下标为-1, 这样就可以和其他状态下的逻辑一致了
//
public class Test12ZichuanChangdu {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
String input = sc.nextLine();
System.out.println(getResult(input));
}
public static int getResult(String s) {
int index = 0b000;
// 每个下标代表一个状态,值代表符合该状态的第一个元素的下标
int[] status = new int[8];
status[0]=-1;
int maxLen = 0;
for (int i = 0; i < s.length() * 2; i++) {
char c = s.charAt(i % s.length());
switch (c) {
case 'l':
index ^= 0b100;
break;
case 'o':
index ^= 0b010;
break;
case 'x':
index ^= 0b001;
break;
}
if (status[index] == 0) {
status[index] = i;
}else{
maxLen = Math.max(maxLen, i-status[index]<=s.length()?i-status[index]:maxLen);
}
}
return maxLen;
}
}
3、最长公共子串
动态规划
4、最长序列
线段树
5、四则运算
栈
6、进出问题
通用思路
队列、栈、双端队列
#案例1-篮球游戏
幼儿园里有一个放倒的圆桶,它是一个线性结构,允许在桶的右边将篮球放入,可以在桶的左边和右边将篮球取出。
每个篮球有单独的编号,老师可以连续放入 一个或多个篮球,小朋友可以在桶左边或右边将篮球取出,当桶里只有一个篮球的情况下,必须从左边取出。
如老师按顺序放入1、2、3、4、5 共5个编号的篮球,那么小朋友可以依次取出的编号为“1,2,3,4,5”或者“3,1,2,4,5”编号的篮球,无法取出 “5,1,3,2,4” 编号的篮球。
其中“3,1,2,4,5”的取出场景为:
-
连续放入1,2,3号
-
从右边取出3号
-
从左边取出1号
-
从左边取出2号
-
放入4号
-
从左边取出4号
-
放入5号
-
从左边取出5号
简单起见,我们以L表示左,R表示右,此时的篮球的依次取出序列为“ RLLLL ”
输入描述
1、第一行的数字作为老师依次放入的篮球编号;
2、第二行的数字作为要检查是否能够按照放入顺序取出的篮球编号;
其中篮球编号用逗号进行分隔。
输出描述
对于每个篮球的取出序列,如果确实可以获取,请打印出其按照左右方向的操作的取出顺序,如果无法获取则打印"NO" 。
补充说明:
-
1<=篮球的编号,篮球个数<=200;
-
篮球上的数字不重复;
-
输出的结果中LR的必须为大写;
示例1
输入:
4,5,6,7,0,1,2
6,4,0,1,2,5,7
输出:
RLRRRLL
说明:
篮球的取出顺序依次为 “右,左,右,右,右,左,左”
示例2
输入:
4,5,6,7,0,1,2
6,0,5,1,2,4,7
输出:
NO
说明:
无法取出对应序列的篮球
示例3
输入:
1,2,3,4
1,2,3,5
输出:
NO
说明:
不存在编号为5的篮球,所以无法取出对应的编号数据
代码:
package algorithm.huawei;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.LinkedList;
import java.util.List;
import java.util.Scanner;
public class Test126 {
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
while (in.hasNextLine()) {
String[] sa = in.nextLine().split(",");
Deque<Integer> que = new ArrayDeque<Integer>();
for(int i=0;i<sa.length;i++){
que.offer(Integer.parseInt(sa[i]));
}
String[] sa2 = in.nextLine().split(",");
List<Integer> take = new ArrayList<Integer>();
for(int i=0;i<sa2.length;i++){
take.add(Integer.parseInt(sa2[i]));
}
LinkedList<Integer> ll = new LinkedList<Integer>();
int takeIndex = 0;
StringBuilder sb = new StringBuilder();
while(!que.isEmpty() || ll.size()>0){
while(que.size()>0 && (ll==null || !ll.contains(take.get(takeIndex)))){
ll.add(que.poll());
}
if(ll.peekFirst()==take.get(takeIndex)){
sb.append("L");
ll.pollFirst();
}else if(ll.peekLast()==take.get(takeIndex)){
sb.append("R");
ll.pollLast();
}else{
sb=new StringBuilder("NO");
break;
}
takeIndex++;
}
System.out.println(sb.toString());
}
}
}
#案例2-火车进出站
给定一个正整数N代表火车数量,0<N<10,接下来输入火车入站的序列,一共N辆火车,每辆火车以数字1-9编号,火车站只有一个方向进出,同时停靠在火车站的列车中,只有后进站的出站了,先进站的才能出站。
要求输出所有火车出站的方案,以字典序排序输出。
数据范围:1≤n≤10
进阶:时间复杂度:O(n!) ,空间复杂度:O(n)
输入描述:
第一行输入一个正整数N(0 < N <= 10),第二行包括N个正整数,范围为1到10。
输出描述:
输出以字典序从小到大排序的火车出站序列号,每个编号以空格隔开,每个输出序列换行,具体见sample。
示例1
输入:
3 1 2 3
复制输出:
1 2 3 1 3 2 2 1 3 2 3 1 3 2 1
复制说明:
第一种方案:1进、1出、2进、2出、3进、3出 第二种方案:1进、1出、2进、3进、3出、2出 第三种方案:1进、2进、2出、1出、3进、3出 第四种方案:1进、2进、2出、3进、3出、1出 第五种方案:1进、2进、3进、3出、2出、1出 请注意,[3,1,2]这个序列是不可能实现的。
代码:
import java.util.*;
// 队列表示未进站的火车
// 栈表示已进站的火车
// 每次火车进站后有两种选择:1.直接出站 2.等待下列火车进站 使用递归考虑
// 记录和打印的递归函数往往是void,如果递归完成后还存在后续逻辑,一般情况下当前输入变量不应受到递归的影响(所以当函数参数为引用类型时,递归的参数应当重新new出来或者深拷贝出来)
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
while (sc.hasNext()) {
int n = sc.nextInt();
// 未进站的火车
Queue<Integer> queue = new LinkedList<>();
// 已进站的火车
Stack<Integer> stack = new Stack<>();
for (int i = 0; i < n; i++) {
queue.offer(sc.nextInt());
}
List<String> outQueueList = new ArrayList<>();
// 获取所有出站队列保存到outQueueList
processOutQueue(queue, stack, "", outQueueList);
// 排序
Collections.sort(outQueueList);
for (String str : outQueueList) {
System.out.println(str);
}
}
}
private static void processOutQueue(Queue<Integer> queue, Stack<Integer> stack,
String outQueue, List<String> outQueueList) {
// 如果队列和栈都为空,则保存出站信息
if (queue.isEmpty() && stack.isEmpty()) {
outQueueList.add(outQueue.trim());
return;
}
// 二:进栈
if (!queue.isEmpty()) {
// 先克隆,这里全部克隆的原因是因为递归完回来后,不能影响我的第二种选择
// processOutQueue前面的逻辑,递归越深的越晚执行;processOutQueue后面的代码(下方的出栈代码)递归越深的越先执行
Queue<Integer> tempQueue = new LinkedList<>(queue);
Stack<Integer> tempStack = (Stack<Integer>) stack.clone();
tempStack.push(tempQueue.poll());
processOutQueue(tempQueue, tempStack, outQueue, outQueueList);
}
// 队列和栈有两种情况:出栈或进栈
// 一:出栈
if (!stack.isEmpty()) {
// 先克隆
Queue<Integer> tempQueue = new LinkedList<>(queue);
Stack<Integer> tempStack = (Stack<Integer>) stack.clone();
String tempOutQueue = outQueue + (tempStack.pop() + " ");
processOutQueue(tempQueue, tempStack, tempOutQueue, outQueueList);
}
}
}
7、最大匹配数问题
通用思路
匈牙利匹配算法(先到先得,能让则让)
#案例1-素数伴侣
题目描述
若两个正整数的和为素数,则这两个正整数称之为“素数伴侣”,如2和5、6和13,它们能应用于通信加密。现在密码学会请你设计一个程序,从已有的 N ( N 为偶数)个正整数中挑选出若干对组成“素数伴侣”,挑选方案多种多样,例如有4个正整数:2,5,6,13,如果将5和6分为一组中只能得到一组“素数伴侣”,而将2和5、6和13编组将得到两组“素数伴侣”,能组成“素数伴侣”最多的方案称为“最佳方案”,当然密码学会希望你寻找出“最佳方案”。
输入:
有一个正偶数 n ,表示待挑选的自然数的个数。后面给出 n 个具体的数字。
输出:
输出一个整数 K ,表示你求得的“最佳方案”组成“素数伴侣”的对数。
数据范围: 1≤n≤100 ,输入的数据大小满足 2≤val≤30000
输入描述:
输入说明
1 输入一个正偶数 n
2 输入 n 个整数
输出描述:
求得的“最佳方案”组成“素数伴侣”的对数。
示例1
输入:
4 2 5 6 13
复制输出:
2
复制
示例2
输入:
2 3 6
复制输出:
0
代码:
import java.util.*;
// 注意类名必须为 Main, 不要有任何 package xxx 信息
// 遇到递归出现在循环里面的,终止条件则是想办法让越深层的递归循环次数越少,直到一次循环条件都不满足则返回默认的值
public class Main {
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
// 注意 hasNext 和 hasNextLine 的区别
while (in.hasNextInt()) { // 注意 while 处理多个 case
int n = in.nextInt();
in.nextLine();
// 奇数数组
List<Integer> odds = new ArrayList<Integer>();
// 偶数数组
List<Integer> events = new ArrayList<Integer>();
for(int i=0;i<n;i++){
int var = in.nextInt();
if(var%2 == 0){
events.add(var);
}else{
odds.add(var);
}
}
int count = 0;
int[] ifMatch = new int[events.size()];
for(int i=0;i<odds.size();i++){
boolean[] canuse = new boolean[events.size()];
Arrays.fill(canuse,true);
if(dfs(odds.get(i),events,ifMatch,canuse)){
count++;
};
}
System.out.println(count);
}
}
public static boolean dfs(int x,List<Integer> events,int[] ifMatch,boolean[] canuse) {
for(int i=0;i<events.size();i++){
// 先到先得:如果该位置能用且符合素数且该位置没被占用则可以占用
if(canuse[i] && isPrime(x+events.get(i))){
canuse[i] = false;//把位置改为不可用
if(ifMatch[i]==0 || dfs(ifMatch[i],events,ifMatch,canuse)){//如果该位置没被占用,或者被占用了但是占用的odd能重新找到其他event进行匹配,则可以使用
ifMatch[i] = x;
return true;
}
}
}
return false;
}
public static boolean isPrime(int x) {
for(int i=2;i<=Math.sqrt(x);i++){
if(x%i==0){
return false;
}
}
return true;
}
}
#案例2-矩阵匹配
题目描述
从一个 N * M(N ≤ M)的矩阵中选出 N 个数,任意两个数字不能在同一行或同一列,求选出来的 N 个数中第 K 大的数字的最小值是多少。
输入描述
输入矩阵要求:1 ≤ K ≤ N ≤ M ≤ 150
输入格式:
N M K
N*M矩阵
输出描述
N*M 的矩阵中可以选出 M! / N! 种组合数组,每个组合数组种第 K 大的数中的最小值。无需考虑重复数字,直接取字典排序结果即可。
备注
注意:结果是第 K 大的数字的最小值
代码:
思路:每一个行号只能跟一个列号匹配,我们可以利用二分法枚举第k大的值,然后通过匈牙利算法检查符合条件的且小于这个值的匹配数量是否满足要求,如果满足则说明枚举值kth给大了,如果不满足说明枚举值kth给小了。
import java.util.Arrays;
import java.util.Scanner;
public class Main {
static int n;
static int m;
static int k;
static int[][] matrix;
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
n = sc.nextInt();
m = sc.nextInt();
k = sc.nextInt();
int min = 1;
int max = Integer.MIN_VALUE;
matrix = new int[n][m];
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
matrix[i][j] = sc.nextInt();
max = Math.max(max, matrix[i][j]);
}
}
// 二分枚举第K大值
while (min <= max) {
// mid就是被枚举出来的N个数中的第K大值
int mid = (min + max) >> 1;
// 检查mid作为N个数中第K大值时,是否存在N-K+1个不大于它的值
if (check(mid)) {
max = mid - 1;
} else {
min = mid + 1;
}
}
System.out.println(min);
}
public static boolean check(int kth) {
// 利用二分图最大匹配来求解,小于等于kth(第K大值)的元素个数(即二分图最大匹配)
int smallerCount = 0;
// 记录每个列号的匹配成功的行号
int[] match = new int[m];
// 初始时每个列号都处于未配对状态,此时将列号配对的行号赋值为-1
Arrays.fill(match, -1);
// 遍历行号,每个行号对互相心仪的列号发起配对请求
for (int i = 0; i < n; i++) {
// 记录增广路访问过的列号
boolean[] vis = new boolean[m];
if (dfs(i, kth, match, vis)) smallerCount++;
}
return smallerCount >= n - k + 1;
}
public static boolean dfs(int i, int kth, int[] match, boolean[] vis) {
// 行号 i 发起了配对请求
// 遍历每一个列号j
for (int j = 0; j < m; j++) {
// 如果当前列号j未被增广路探索过 && 当前列j行i可以配对(如果行列号位置(i,j)对应矩阵元素值小于等于kth(第K大值),则可以配对)
if (!vis[j] && matrix[i][j] <= kth) {
vis[j] = true;
// 如果对应列号j未配对,或者,已配对但是配对的行号match[j]可以找到其他列号重新配对
if (match[j] == -1 || dfs(match[j], kth, match, vis)) {
// 则当前行号i 和 列号j 可以配对
match[j] = i;
return true;
}
}
}
return false;
}
}
本案例参考&鸣谢:
华为OD机试 - 矩阵匹配(Java & JS & Python & C & C++)_od 矩阵匹配-CSDN博客