本文的网课内容学习自B站左程云老师的算法详解课程,旨在对其中的知识进行整理和分享~
一.问题规模和可用算法
一个基本事实
C/C++运行时间1s,java/python/go等其他语言运行时间1s~2s,
对应的常数指令操作量是 10^7 ~ 10^8,不管什么测试平台,不管什么cpu,都是这个数量级
所以可以根据这个基本事实,来猜测自己设计的算法最终有没有可能在规定时间内通过
运用根据数据量猜解法技巧的必要条件:
1,题目要给定各个入参的范围最大值,正式笔试、比赛的题目一定都会给,面试中要和面试官确认
2,对于自己设计的算法,时间复杂度要有准确的估计
这个技巧太重要了!既可以提前获知自己的方法能不能通过,也可以对题目的分析有引导作用!
问题规模和可用算法
logn | n | n*logn | n*根号n | n^2 | 2^n | n! | |
---|---|---|---|---|---|---|---|
n <= 11 | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
n <= 25 | Yes | Yes | Yes | Yes | Yes | Yes | No |
n <= 5000 | Yes | Yes | Yes | Yes | Yes | No | No |
n <= 10^5 | Yes | Yes | Yes | Yes | No | No | No |
n <= 10^6 | Yes | Yes | Yes | No | No | No | No |
n <= 10^7 | Yes | Yes | No | No | No | No | No |
n >= 10^8 | Yes | No | No | No | No | No | No |
这张表其实作用有限
因为时间复杂度的估计很多时候并不是一个入参决定,可能是多个入参共同决定。比如O(n*m), O(n+m)等
所以最关键的就是记住常数指令操作量是 10^7 ~ 10^8,然后方法是什么复杂度就可以估计能否通过了
二.消灭怪物
题目:消灭怪物
算法原理
-
整体原理
- 这是一个基于深度优先搜索(DFS)策略的算法,目的是在给定的技能组合和怪物血量的情况下,找到使用最少的技能数量来消灭怪物。通过对技能使用顺序的全排列搜索,计算每种顺序下消灭怪物所需的技能数量,最终找到最小值。
-
具体步骤
- 输入处理(
main
方法部分)- 读取测试用例数量:使用
BufferedReader
和StreamTokenizer
读取输入。首先读取一个整数t
,表示测试用例的数量。 - 处理每个测试用例:对于每个测试用例,先读取技能数量
n
和怪物血量m
。然后通过循环(for (int j = 0; j < n; j++)
)依次读取每个技能的伤害值kill[j]
和触发双倍伤害的血量最小值blood[j]
。 - 调用计算函数并输出结果:调用
f
函数计算消灭怪物所需的最少技能数量,将结果存储在ans
中。如果ans
等于Integer.MAX_VALUE
,说明没有找到可行的技能组合来消灭怪物,输出 - 1;否则输出ans
。最后,刷新PrintWriter
并关闭BufferedReader
和PrintWriter
以释放资源。
- 读取测试用例数量:使用
- 核心计算(
f
函数部分)- 递归基情况(终止条件)
- 成功消灭怪物:当怪物的剩余血量
r
小于等于0时,表示已经成功消灭怪物,直接返回当前已经使用的技能数量i
。 - 技能用尽仍未消灭怪物:当已经尝试了所有的技能(
i
等于技能总数n
),但怪物的血量r
仍然大于0,说明之前的技能使用顺序无法消灭怪物,返回Integer.MAX_VALUE
。
- 成功消灭怪物:当怪物的剩余血量
- 递归搜索过程
- 首先初始化
ans
为Integer.MAX_VALUE
,表示当前找到的最少技能数量(初始假设还未找到可行解)。 - 然后通过循环(
for (int j = i; j < n; j++)
)尝试不同的技能顺序。在每次循环中,先将第j
个技能和第i
个技能交换位置(通过swap
函数),这相当于尝试先使用第j
个技能的情况。 - 接着计算使用第
i
个技能后的怪物剩余血量。根据怪物当前血量r
与技能i
触发双倍伤害的血量最小值blood[i]
的关系,确定造成的伤害。如果r > blood[i]
,则造成的伤害为kill[i]
;否则为kill[i]*2
。然后递归调用f
函数(f(n, i + 1, r-(r > blood[i]? kill[i] : kill[i]*2))
),这个递归调用表示继续尝试下一个技能(i+1
),并根据当前技能造成的伤害更新怪物的剩余血量。 - 用
Math.min
函数不断更新ans
,使其始终保存当前找到的最少技能数量。最后,再将交换后的技能位置换回来(再次调用swap
函数),以便继续尝试其他可能的技能组合。
- 首先初始化
- 递归基情况(终止条件)
- 技能交换(
swap
函数部分)- 这个函数的作用是交换两个技能的相关信息,包括技能的伤害值
kill
和触发双倍伤害的血量最小值blood
。它接受两个索引i
和j
作为参数,通过临时变量tmp
,先交换kill[i]
和kill[j]
,再交换blood[i]
和blood[j]
,从而实现技能信息的交换。这一步操作是为了在递归搜索中模拟不同的技能使用顺序。
- 这个函数的作用是交换两个技能的相关信息,包括技能的伤害值
- 输入处理(
代码实现
// 现在有一个打怪类型的游戏,这个游戏是这样的,你有n个技能
// 每一个技能会有一个伤害,
// 同时若怪物小于等于一定的血量,则该技能可能造成双倍伤害
// 每一个技能最多只能释放一次,已知怪物有m点血量
// 现在想问你最少用几个技能能消灭掉他(血量小于等于0)
// 技能的数量是n,怪物的血量是m
// i号技能的伤害是x[i],i号技能触发双倍伤害的血量最小值是y[i]
// 1 <= n <= 10
// 1 <= m、x[i]、y[i] <= 10^6
// 测试链接 : https://www.nowcoder.com/practice/d88ef50f8dab4850be8cd4b95514bbbd
// 请同学们务必参考如下代码中关于输入、输出的处理
// 这是输入输出处理效率很高的写法
// 提交以下的所有代码,并把主类名改成"Main"
// 可以直接通过
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.StreamTokenizer;
public class Code01_KillMonsterEverySkillUseOnce {
public static int MAXN = 11;
public static int[] kill = new int[MAXN];
public static int[] blood = new int[MAXN];
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
StreamTokenizer in = new StreamTokenizer(br);
PrintWriter out = new PrintWriter(new OutputStreamWriter(System.out));
while (in.nextToken() != StreamTokenizer.TT_EOF) {
int t = (int) in.nval;
for (int i = 0; i < t; i++) {
in.nextToken();
int n = (int) in.nval;
in.nextToken();
int m = (int) in.nval;
for (int j = 0; j < n; j++) {
in.nextToken();
kill[j] = (int) in.nval;
in.nextToken();
blood[j] = (int) in.nval;
}
int ans = f(n, 0, m);
out.println(ans == Integer.MAX_VALUE ? -1 : ans);
}
}
out.flush();
br.close();
out.close();
}
// kill[i]、blood[i]
// n : 一共几个技能
// i : 当前来到了第几号技能
// r : 怪兽目前的剩余血量
public static int f(int n, int i, int r) {
if (r <= 0) {
// 之前的决策已经让怪兽挂了!返回使用了多少个节能
return i;
}
// r > 0
if (i == n) {
// 无效,之前的决策无效
return Integer.MAX_VALUE;
}
// 返回至少需要几个技能可以将怪兽杀死
int ans = Integer.MAX_VALUE;
for (int j = i; j < n; j++) {
swap(i, j);
ans = Math.min(ans, f(n, i + 1, r - (r > blood[i] ? kill[i] : kill[i] * 2)));
swap(i, j);
}
return ans;
}
// i号技能和j号技能,参数交换
// j号技能要来到i位置,试一下
public static void swap(int i, int j) {
int tmp = kill[i];
kill[i] = kill[j];
kill[j] = tmp;
tmp = blood[i];
blood[i] = blood[j];
blood[j] = tmp;
}
}
三.超级回文数
题目:超级回文数
算法原理
-
整体原理
- 这个算法主要是为了找出给定范围
[L, R]
中的超级回文数的数目。超级回文数是一个正整数自身是回文数,而且它也是一个回文数的平方。算法通过两种方式来实现:一种是通过枚举可能的回文数的根号值(num
),计算其平方并检查是否在范围内且是回文数;另一种是通过打表的方式(superpalindromesInRange2
方法),利用预定义的超级回文数数组record
来快速计算范围内的超级回文数数量。
- 这个算法主要是为了找出给定范围
-
具体步骤
- 第一步:数据初始化。
- 将输入的字符串表示的范围边界
left
和right
转换为long
类型,分别赋值给l
和r
。 - 计算范围上限
r
的平方根,得到limit
,这个值用于限制枚举的范围。 - 初始化枚举种子
seed = 1
,用于生成可能的回文数的根号值。初始化num = 0
和ans = 0
(ans
用于记录超级回文数的数量)。
- 将输入的字符串表示的范围边界
- 第二步:枚举可能的回文数的根号值。
- 进入
do - while
循环。 - 首先通过
evenEnlarge
方法根据种子seed
生成偶数长度的回文数字作为可能的num
(例如,seed = 123
,则生成123321
)。 - 然后调用
check
方法,检查num
的平方是否在[l, r]
范围内并且是回文数,如果是则ans
加1。 - 接着通过
oddEnlarge
方法根据种子seed
生成奇数长度的回文数字作为可能的num
(例如,seed = 123
,则生成12321
)。 - 再次调用
check
方法,检查num
的平方是否在[l, r]
范围内并且是回文数,如果是则ans
加1。 - 最后将
seed
加1,继续下一轮循环,直到num
大于等于limit
。
- 进入
- 第三步:生成偶数长度回文数(
evenEnlarge
方法)。- 接受一个
long
类型的种子seed
。 - 初始化
ans = seed
。 - 通过
while (seed!= 0)
循环,每次将ans
乘以10并加上seed
的最低位(seed % 10
),然后将seed
除以10。例如,seed = 123
,第一次循环ans = 123
,seed = 12
;第二次循环ans = 1233
,seed = 1
;第三次循环ans = 12332
,seed = 0
;最终ans = 123321
。
- 接受一个
- 第四步:生成奇数长度回文数(
oddEnlarge
方法)。- 接受一个
long
类型的种子seed
。 - 先将
seed
除以10(去掉最低位),初始化ans = seed
。 - 通过
while (seed!= 0)
循环,每次将ans
乘以10并加上seed
的最低位(seed % 10
),然后将seed
除以10。例如,seed = 123
,第一次seed = 12
,ans = 12
;第二次seed = 1
,ans = 123
;最终ans = 12321
。
- 接受一个
- 第五步:检查是否符合超级回文数条件(
check
方法)。- 接受一个数
ans
以及范围边界l
和r
。 - 首先检查
ans
是否在[l, r]
范围内(ans >= l && ans <= r
),然后调用isPalindrome
方法检查ans
是否为回文数,如果两个条件都满足则返回true
。
- 接受一个数
- 第六步:检查是否为回文数(
isPalindrome
方法,与之前的回文数检查类似)。- 接受一个
long
类型的数字num
。 - 初始化
offset = 1
,通过while (num / offset >= 10)
循环,不断将offset
乘以10,直到num
除以offset
小于10,以确定数字的最高位权重。 - 然后通过
while (num!= 0)
循环,比较num
的最高位(num / offset
)和最低位(num % 10
),如果不相等则返回false
,如果相等则对num
进行调整(num=(num%offset)/10
,同时offset /= 100
),直到num
变为0,此时返回true
。
- 接受一个
- 第七步:打表方法(
superpalindromesInRange2
方法)。- 将输入的字符串表示的范围边界
left
和right
转换为long
类型,分别赋值给l
和r
。 - 通过两个
for
循环,在预定义的超级回文数数组record
中找到第一个大于等于l
的数的下标i
和最后一个小于等于r
的数的下标j
。 - 返回
j - i + 1
,即范围内超级回文数的数量。
- 将输入的字符串表示的范围边界
- 第一步:数据初始化。
代码实现
import java.util.ArrayList;
import java.util.List;
// 超级回文数(java版)
// 如果一个正整数自身是回文数,而且它也是一个回文数的平方,那么我们称这个数为超级回文数。
// 现在,给定两个正整数 L 和 R (以字符串形式表示),
// 返回包含在范围 [L, R] 中的超级回文数的数目。
// 1 <= len(L) <= 18
// 1 <= len(R) <= 18
// L 和 R 是表示 [1, 10^18) 范围的整数的字符串
//测试链接 : https://leetcode.cn/problems/super-palindromes/
public class Code02_SuperPalindromes {
// [left, right]有多少超级回文数
// 返回数量
public static int superpalindromesInRange1(String left, String right) {
long l = Long.valueOf(left);
long r = Long.valueOf(right);
// l....r long
// x根号,范围limit
long limit = (long) Math.sqrt((double) r);
// seed : 枚举量很小,10^18 -> 10^9 -> 10^5
// seed : 奇数长度回文、偶数长度回文
long seed = 1;
// num : 根号x,num^2 -> x
long num = 0;
int ans = 0;
do {
// seed生成偶数长度回文数字
// 123 -> 123321
num = evenEnlarge(seed);
if (check(num * num, l, r)) {
ans++;
}
// seed生成奇数长度回文数字
// 123 -> 12321
num = oddEnlarge(seed);
if (check(num * num, l, r)) {
ans++;
}
// 123 -> 124 -> 125
seed++;
} while (num < limit);
return ans;
}
// 根据种子扩充到偶数长度的回文数字并返回
public static long evenEnlarge(long seed) {
long ans = seed;
while (seed != 0) {
ans = ans * 10 + seed % 10;
seed /= 10;
}
return ans;
}
// 根据种子扩充到奇数长度的回文数字并返回
public static long oddEnlarge(long seed) {
long ans = seed;
seed /= 10;
while (seed != 0) {
ans = ans * 10 + seed % 10;
seed /= 10;
}
return ans;
}
// 判断ans是不是属于[l,r]范围的回文数
public static boolean check(long ans, long l, long r) {
return ans >= l && ans <= r && isPalindrome(ans);
}
// 验证long类型的数字num,是不是回文数字
public static boolean isPalindrome(long num) {
long offset = 1;
// 注意这么写是为了防止溢出
while (num / offset >= 10) {
offset *= 10;
}
// num : 52725
// offset : 10000
// 首尾判断
while (num != 0) {
if (num / offset != num % 10) {
return false;
}
num = (num % offset) / 10;
offset /= 100;
}
return true;
}
// 打表的方法
// 必然最优解
// 连二分都懒得用
public static int superpalindromesInRange2(String left, String right) {
long l = Long.parseLong(left);
long r = Long.parseLong(right);
int i = 0;
for (; i < record.length; i++) {
if (record[i] >= l) {
break;
}
}
int j = record.length - 1;
for (; j >= 0; j--) {
if (record[j] <= r) {
break;
}
}
return j - i + 1;
}
public static long[] record = new long[] {
1L,
4L,
9L,
121L,
484L,
10201L,
12321L,
14641L,
40804L,
44944L,
1002001L,
1234321L,
4008004L,
100020001L,
102030201L,
104060401L,
121242121L,
123454321L,
125686521L,
400080004L,
404090404L,
10000200001L,
10221412201L,
12102420121L,
12345654321L,
40000800004L,
1000002000001L,
1002003002001L,
1004006004001L,
1020304030201L,
1022325232201L,
1024348434201L,
1210024200121L,
1212225222121L,
1214428244121L,
1232346432321L,
1234567654321L,
4000008000004L,
4004009004004L,
100000020000001L,
100220141022001L,
102012040210201L,
102234363432201L,
121000242000121L,
121242363242121L,
123212464212321L,
123456787654321L,
400000080000004L,
10000000200000001L,
10002000300020001L,
10004000600040001L,
10020210401202001L,
10022212521222001L,
10024214841242001L,
10201020402010201L,
10203040504030201L,
10205060806050201L,
10221432623412201L,
10223454745432201L,
12100002420000121L,
12102202520220121L,
12104402820440121L,
12122232623222121L,
12124434743442121L,
12321024642012321L,
12323244744232321L,
12343456865434321L,
12345678987654321L,
40000000800000004L,
40004000900040004L,
1000000002000000001L,
1000220014100220001L,
1002003004003002001L,
1002223236323222001L,
1020100204020010201L,
1020322416142230201L,
1022123226223212201L,
1022345658565432201L,
1210000024200000121L,
1210242036302420121L,
1212203226223022121L,
1212445458545442121L,
1232100246420012321L,
1232344458544432321L,
1234323468643234321L,
4000000008000000004L
};
public static List<Long> collect() {
long l = 1;
long r = Long.MAX_VALUE;
long limit = (long) Math.sqrt((double) r);
long seed = 1;
long enlarge = 0;
ArrayList<Long> ans = new ArrayList<>();
do {
enlarge = evenEnlarge(seed);
if (check(enlarge * enlarge, l, r)) {
ans.add(enlarge * enlarge);
}
enlarge = oddEnlarge(seed);
if (check(enlarge * enlarge, l, r)) {
ans.add(enlarge * enlarge);
}
seed++;
} while (enlarge < limit);
ans.sort((a, b) -> a.compareTo(b));
return ans;
}
public static void main(String[] args) {
List<Long> ans = collect();
for (long p : ans) {
System.out.println(p + "L,");
}
System.out.println("size : " + ans.size());
}
}
四.回文数
题目:回文数
算法原理
-
整体原理
- 这个算法主要是通过比较数字的首位和末位,然后逐步向内比较数字的对称部分来判断一个数是否为回文数。对于负数,直接判定不是回文数。对于非负数,通过巧妙地确定数字的最高位权重(通过
offset
),然后在循环中不断比较并去掉已经比较过的数字对,直到所有数字都比较完或者发现不对称的情况。
- 这个算法主要是通过比较数字的首位和末位,然后逐步向内比较数字的对称部分来判断一个数是否为回文数。对于负数,直接判定不是回文数。对于非负数,通过巧妙地确定数字的最高位权重(通过
-
具体步骤
- 第一步:处理负数情况。
- 如果
num < 0
,直接返回false
。因为回文数不能是负数。
- 如果
- 第二步:确定数字的位数(计算
offset
)。- 初始化
offset = 1
。 - 进入
while (num / offset >= 10)
循环,不断将offset
乘以10,直到num
除以offset
的结果小于10。例如,对于123,第一次num / offset
为123 / 1 = 123,满足条件,offset
变为10;第二次num / offset
为123 / 10 = 12.3,满足条件,offset
变为100;第三次num / offset
为123 / 100 = 1.23,不满足条件,此时offset = 100
。
- 初始化
- 第三步:首尾数字比较循环。
- 进入
while (num!= 0)
循环。 - 在循环中,首先比较
num
的最高位(num/offset
)和最低位(num%10
)。例如对于121,num/offset
为121/100 = 1,num%10
为121%10 = 1,两者相等。 - 如果不相等,直接返回
false
。 - 如果相等,对
num
进行调整,num=(num%offset)/10
,这一步去掉了已经比较过的首位和末位数字。例如对于121,num
变为(121%100)/10 = 2。同时,offset
除以100,因为已经去掉了两位数字,offset
从100变为1。 - 不断重复这个过程,直到
num
变为0,此时返回true
。
- 进入
- 第一步:处理负数情况。
代码实现
// 超级回文数中的一个小函数,本身也是一道题 : 判断一个数字是不是回文数
// 测试链接 : https://leetcode.cn/problems/palindrome-number/
public class Code03_IsPalindrome {
public static boolean isPalindrome(int num) {
if (num < 0) {
return false;
}
int offset = 1;
// 注意这么写是为了防止溢出
while (num / offset >= 10) {
offset *= 10;
}
// 首尾判断
while (num != 0) {
if (num / offset != num % 10) {
return false;
}
num = (num % offset) / 10;
offset /= 100;
}
return true;
}
}