BASIC Problemset
想不出什么漂亮话,
希望大家能精益求精把。
可能会 J a v a \mathrm{Java} Java、 C \mathrm{C} C++ 混写,
差别不大。
以蓝桥的总和难度而言,
基础练习入个门,然后历届真题一刷就能冲击国家一等奖,
题库后面的那些题个人综合来看,
建议是转其他平台刷,
虽然我在哪多少都刷了点就是的了。
BASIC 1 闰年判断
OJ:蓝桥
感觉应该分类为 B E G I N \mathrm{BEGIN} BEGIN,而非 B A S I C \mathrm{BASIC} BASIC。
因为,可以说你的程序就算执行效率再高,与自然语言描述的闰年判断差距也不大,
如:
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
int year = in.nextInt();
if ((year % 4 == 0 && year % 100 != 0) || year % 400 == 0)
System.out.println("yes");
else
System.out.println("no");
}
}
但其实还是有不小的优化空间,
如合并判断分支,使得至多判断两个布尔表达式的值就能得到结果。
boolean isLeapYear(int year) { return year % 100 == 0 ? year % 400 == 0 : year % 4 == 0; }
小压一个行。
还有就是,对于 n \mathrm{n} n 进制数 N \mathrm{N} N, n > 1 \mathrm{n} >1 n>1,
在其最低位后拼接 k \mathrm{k} k 个 0 0 0,
得到的数值为 N × n k \mathrm{N} × \mathrm{n^{k}} N×nk。
我们自然可以知道, 4 4 4 的倍数在二进制下,最低两位一定是 00 00 00。
将 y e a r \mathrm{year} year 与 0 b 11 0\mathrm{b}11 0b11 做与运算,结果为 0 0 0 则代表 y e a r \mathrm{year} year 为 4 4 4 的倍数。
运算速度稍微会快于取余。
boolean isLeapYear(int year) { return year % 100 == 0 ? year % 400 == 0 : (year & 0b11) == 0; }
还有就是
J
a
v
a
8
\mathrm{Java\ 8}
Java 8 提供了新的工具类 IsoChronology
,
直接调用 INSTANCE
成员的 isLeapYear
方法,就能达到判断闰年的效果。
IsoChronology.INSTANCE.isLeapYear(year);
BASIC 2 01字串
OJ:蓝桥
这个题很适合做复杂度下界一个分类的讨论,
一个程序从零个或多个输入经由有限个步骤到一个或多个输出,
通常我们在计算复杂度时,只去关系中间执行的有限步骤与那些输入有光,而忽略了输入输出的规模。
这个题就是让我们顺序输出所有长度为 n \mathrm{n} n 的 01 01 01,无论你中间经由几步,最后都要输出 O ( n log n ) O(n \log n) O(nlogn) 个字符。
也就是说,解决这个问题的程序最后都会首受限于输出的复杂程度,从而导致整个程序的复杂度不可能低于 O ( n log n ) O(n \log n) O(nlogn)。
因此没有什么花里胡哨,建议直接摆烂。
拼接式:
public class Main {
public static void main(String[] args) {
for (int i = 0; i < 2; i++)
for (int j = 0; j < 2; j++)
for (int l = 0; l < 2; l++)
for (int x = 0; x < 2; x++)
for (int y = 0; y < 2; y++) System.out.println(i + "" + j + "" + l + "" + x + "" + y);
}
}
十到二进制横跳:
public class Main {
public static void main(String[] args) {
for (int i = 0; i < 0x20; i++)
System.out.printf("%05d%n", Integer.parseInt(Integer.toBinaryString(i)));
}
}
手动进制转换:
public class Main {
public static void main(String[] args) {
for (int i = 0; i < 0x20; i++) {
for (int j = 4; j >= 0; j--)
System.out.print(i >> j & 1);
System.out.print('\n');
}
}
}
BASIC 3 字母图形
OJ:蓝桥
开始后悔整理基础练习了,都是摆烂题。
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
byte[] buff = {90, 89, 88, 87, 86, 85, 84, 83, 82, 81, 80, 79, 78, 77, 76, 75, 74, 73, 72, 71, 70, 69, 68, 67, 66, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90};
Scanner in = new Scanner(System.in);
int n = in.nextInt(), m = in.nextInt();
int offset = 26;
while(n-- > 0) {
System.out.write(buff, --offset, m);
System.out.println();
}
}
}
BASIC 4 数列特征
OJ:蓝桥
啊对对对,摆就完了。
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
int n = in.nextInt(), a, max = -10000, min = 10000, sum = 0;
while (n-- > 0) {
sum+= a = in.nextInt();
if (a > max) max = a;
if (a < min) min = a;
}
System.out.printf("%d\n%d\n%d", max, min, sum);
}
}
BASIC 5 查找整数
OJ:蓝桥
粗糙的讲,
算法竞赛中的题型大体分为传统(黑盒测试)形、结果提交形、 G r a d e r \mathrm{Grader} Grader 交互形。
也可以粗糙的理解成蓝桥编程、蓝桥填空、力扣刷的题。
有点跑题,
简单的描述一下传统形和交互形,忽略 I O \mathrm{IO} IO 细节就是做阅读理解先看问题还是后看问题的区别。
在这个问题上,如果我们预先知道整数 a a a,那么我们不需要额外的内存,直接对输入流或集合对象中的序列一一比对即可,空间复杂度在 O ( 0 ) O(0) O(0),
可惜并不是,因此对于序列上的这种询问,通常我们有两种处理方式。
预先处理出所有可能的询问的结果,空间复杂度在 O ( ∣ [ a n s ] ∣ ) O(\mid [ans]\mid) O(∣[ans]∣):
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
int n = in.nextInt(), a, b;
int[] idx = new int[10001];
for (int i = 1; i <= n; i++) {
b = in.nextInt();
if (idx[b] == 0) idx[b] = i;
}
a = in.nextInt();
System.out.println(idx[a] == 0 ? -1 : idx[a]);
}
}
保存序列,待取到 a a a 后进行第二遍的遍历,空间复杂度在 O ( n ) O(n) O(n):
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
int n = in.nextInt(), a, idx = -1;
int[] A = new int[n + 1];
for (int i = 1; i <= n; i++)
A[i] = in.nextInt();
a = in.nextInt();
for (int i = 1; i <= n; i++)
if (a == A[i]) {
idx = i;
break;
}
System.out.println(idx);
}
}
BASIC 6 杨辉三角形
OJ:蓝桥
用组合数的性质 C n + 1 m = C n m + C n m − 1 C_{n+1}^{m} = C_{n}^{m} + C_{n}^{m-1} Cn+1m=Cnm+Cnm−1,咔咔咔就出来了。
值得注意的是,给定的 n \mathrm{n} n 最大为 34 34 34,这意味着三角中最大的数字为 C 33 16 = 1166803110 C_{33}^{16} = 1166803110 C3316=1166803110。
使用 i n t \mathrm{int} int 表示、运算是完全可行的。
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
int n = new Scanner(System.in).nextInt();
int[][] pascal = new int[n + 1][n + 2];
pascal[0][0] = 1;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= i; j++) {
pascal[i][j] = pascal[i - 1][j] + pascal[i - 1][j - 1];
System.out.print(pascal[i][j]);
System.out.write(' ');
}
System.out.println();
}
}
}
直接在 n × n \mathrm{n × n} n×n 的二维数组中,递推这个过程,会接近一半的内存被浪费掉。
一种朴素的解决方式就是,动态开第二维的数组,
不过这里换一种做法,
显然可以发现,如果按横排的方式编号, C n + 1 m C_{n+1}^{m} Cn+1m 到 C n m − 1 C_{n}^{m-1} Cnm−1 中间隔着 n n n 个数字,而 C n m C_{n}^{m} Cnm 到 C n m − 1 C_{n}^{m-1} Cnm−1 中只隔着一个数字,我们可以根据这个性质在线性表中计算和表示一个杨辉三角。
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
int N = new Scanner(System.in).nextInt();
int[] pascal = new int[N * (N + 1) / 2];
for (int n = 1, i = 0; n <= N; n++) {
for (int m = 1; m <= n; m++, i++) {
if (m == 1 || n == m) pascal[i] = 1;
else pascal[i] = pascal[i - n] + pascal[i - n + 1];
System.out.print(pascal[i]);
System.out.write(' ');
}
System.out.println();
}
}
}
又或直接使用组合数计算公式
C n m = A n m A m = n ! m ! ( n − m ) ! C_{n}^{m}=\cfrac{A_{n}^{m}}{A_{m}} = \cfrac{n!}{m!(n - m)!} Cnm=AmAnm=m!(n−m)!n!
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
int N = new Scanner(System.in).nextInt();
for (int n = 0; n < N; n++) {
for (int m = 0; m <= n; m++) {
System.out.print(C(n, m));
System.out.write(' ');
}
System.out.println();
}
}
static int C(int n, int m) {
if (m == 0 || n == m) return 1;
long C = 1;
for (int i = 1; i <= m; i++)
C = C * (n - i + 1) / i;
return (int)C;
}
}
但这么做的复杂度几乎在 O ( n 4 ) O(n^{4}) O(n4),不过常数极小。
BASIC 7 特殊的数字
OJ:蓝桥
水仙花数,不谈,
硬说点什么的话,不建议在这类数字的性质上投入探索研究的时间,
因为大概率是无果的。
不过我们可以在这类自幂数上,看到为什么我们在设计通用计算机上的程序时要尽量避免取余运算。
以计算六合数为例,在 洛谷在线 IDE 提交的结果:
548834
13ms
548834
16ms
提交的程序:
public class Main {
public static void main(String[] args) {
long start, end;
int[] six = new int[10];
for (int i = 1; i < 10; i++)
six[i] = i * i * i * i * i * i;
start = System.currentTimeMillis();
for (int n = 100000, a = 1; a < 10; a++)
for (int b = 0; b < 10; b++)
for (int c = 0; c < 10; c++)
for (int d = 0; d < 10; d++)
for (int e = 0; e < 10; e++)
for (int f = 0; f < 10; f++, n++)
if (six[a] + six[b] + six[c] + six[d] + six[e] + six[f] == n)
System.out.println(n);
end = System.currentTimeMillis();
System.out.println(end - start + "ms");
start = System.currentTimeMillis();
for (int n = 100000; n < 1000000; n++)
if (six[n / 100000] + six[n / 10000 % 10] + six[n / 1000 % 10] + six[n / 100 % 10] + six[n / 10 % 10] + six[n % 10] == n)
System.out.println(n);
end = System.currentTimeMillis();
System.out.println(end - start + "ms");
}
}
可以看到,即使是这种地步的 f o r \mathrm{for} for 嵌套,性能也比得过频繁的取余运算。
多得就没能力讨论了,撤。
public class Main {
public static void main(String[] args) {
int[] cube = new int[10];
for (int i = 1; i < 10; i++) cube[i] = i * i * i;
for (int i = 1, j, l, num = 100; i < 10; i++)
for (j = 0; j < 10; j++)
for (l = 0; l < 10; l++, num++)
if (cube[i] + cube[j] + cube[l] == num) System.out.println(num);
}
}
BASIC 8 回文数
OJ:蓝桥
能讨论的同上,且使用按位组合的方式复杂度与值域中的回文数的个数相关。
public class Main {
public static void main(String[] args) {
char[] four = new char[4];
for (char i = '1'; i <= '9'; i++) {
four[0] = four[3] = i;
for (char j = '0'; j <= '9'; j++) {
four[1] = four[2] = j;
System.out.println(four);
}
}
}
}
BASIC 9 特殊回文数
同上同上。
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
int N = new Scanner(System.in).nextInt();
for (char i = '1', l; i <= '9'; i++)
for (char j = '0'; j <= '9'; j++) {
l = (char)(N - i * 2 - j * 2 + 0xF0);
if (l >= '0' && l <= '9')
System.out.printf("%c%c%c%c%c\n", i, j, l, j, i);
}
if (N % 2 == 0) {
N >>= 1;
for (char i = '1', l; i <= '9'; i++)
for (char j = '0'; j <= '9'; j++) {
l = (char)(N - i - j + 0x90);
if (l >= '0' && l <= '9')
System.out.printf("%c%c%c%c%c%c\n", i, j, l, l, j, i);
}
}
}
}
最好还是对编码有一定的了解,
一般的我们在算法竞赛中,提交的答案都是以 A S C I I \mathrm{ASCII} ASCII 码的形式组织。
其中0x0A
表示换行
、0x0D
表示回车
0x20
表示空格
0x30-0x39
表示0-9
0x61-0x7A
表示a-z
0x41-0x5A
表示A-Z
再加上0x28
、0x29
表示左右括号,
基本上了解这么多就行了。
除了使用它的编码来带入表达式中计算出某个字符的编码外,
还可以对像0x28
、0x29
做异或1
运算,来达到左右括号互换的作用。
就像 J 括号序列 这道题,
在熟悉的人眼里,我想是能达到简化代码,增加可读性的作用,
过。
BASIC 10 十进制转十六进制
BASIC 11 十六进制转十进制
程序就懒得挂了,直接来看
J
a
v
a
\mathrm{Java}
Java 提供的Scanner
在面对任意进制的输入以及输入,提供了那些操作。
J a v a 8 \mathrm{Java}\ 8 Java 8:
private int defaultRadix = 10;
public int nextInt() { return nextInt(defaultRadix); }
public int nextInt(int radix) {
if ((typeCache != null) && (typeCache instanceof Integer)
&& this.radix == radix) {
int val = ((Integer)typeCache).intValue();
useTypeCache();
return val;
}
setRadix(radix);
clearCaches();
try {
String s = next(integerPattern());
if (matcher.group(SIMPLE_GROUP_INDEX) == null)
s = processIntegerToken(s);
return Integer.parseInt(s, radix);
} catch (NumberFormatException nfe) {
position = matcher.start(); // don't skip bad token
throw new InputMismatchException(nfe.getMessage());
}
}
可以看到,在调用int nextInt()
后,Scanner
会继续调用int nextInt(int radix)
方法,
在nextInt(int radix)
方法体中,首先检查了缓存里是否有类型相同的数据(因为流是严格单向的,在调用如boolean hasNext()
的方法后,需要先将has的这一段取出,因此需要暂存起来),
随后就是一系列的判断检错,中间最核心的,也就是实在用到radix
这个参数的,只有
return Integer.parseInt(s, radix)
这句,可以粗糙的去理解,那就是对于任意输入,Scanner
总是使用对应类型的工具类中的转换方法,然后传入一个next()
进行转换。
好像应该单独开一篇博客,Integer.parseInt
先按住不表。
算了我摆烂了,转换可以用String toString(int i, int radix)
方法。
过过过过。
慢慢更
BEGIN 1 A+B问题
考察基本语法,
引出平台的输入输出规范。
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
int a = in.nextInt(), b = in.nextInt();
System.out.println(a + b);
}
}
BEGIN 2 序列求和
考察对输出范围的敏感度。
当 n = 1 E 9 \mathrm{n} = 1E9 n=1E9 时,结果显然超出 i n t \mathrm{int} int 表示范围。
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
long n = sc.nextInt();
System.out.println((n + 1) * n / 2);
}
}
BEGIN 3 圆的面积
考察是否为数盲。
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
int r = in.nextInt();
System.out.printf("%.7f", r * r * Math.PI);
}
}
BEGIN 4 Fibonacci数列
先挂二种最基本的写法,
迭代式:
import java.util.Scanner;
public class Main {
static int MOD = 10007;
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
int n = in.nextInt();
int[] fib = new int[n + 1];
fib[1] = 1;
for (int i = 2; i <= n; i++)
fib[i] = (fib[i - 1] + fib[i - 2]) % MOD;
System.out.println(fib[n]);
}
}
迭代式内存优化:
import java.util.Scanner;
public class Main {
static int MOD = 10007;
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
int n = in.nextInt();
int fib = 1, fib1 = 1, fib2 = 0;
while (n-- > 1) {
fib = (fib1 + fib2) % MOD;
fib2 = fib1;
fib1 = fib;
}
System.out.println(fib);
}
}
还可以压行:
fib1 = fib = (fib2 + (fib2 = fib1)) % MOD;
递归式:
import java.util.Scanner;
public class Main {
static int MOD = 10007;
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
int n = in.nextInt();
System.out.println(fib(n));
}
static int fib(int n) {
if (n == 0) return 0;
if (n == 1) return 1;
return (fib(n - 1) + fib(n - 2)) % MOD;
}
}
前面两种时间复杂度都在 O ( n ) O(n) O(n),
后面这个就厉害了,足足有 O ( 2 n ) O(2^{n}) O(2n),
不过线性复杂度在真正的大数据下,
也还是显得捉襟见肘,
而我们熟知的比内公式 F n = 1 5 [ ( 1 + 5 2 ) n − ( 1 − 5 2 ) n ] F_n = \cfrac{1}{\sqrt{5}}\begin{bmatrix}\left(\frac{1 + \sqrt{5}}{2}\right)^n - \left(\frac{1 - \sqrt{5}}{2}\right)^n\end{bmatrix} Fn=51[(21+5)n−(21−5)n] 不论是高频的浮点数计算,还是需要额外实现分步骤取余,
都是我们在设计计算机程序时,需要尽量避免的,
因此这里介绍两种信息学中常见的快速斐波那契数计算方法,
矩阵表示法:
斐波那契数的递推可以用矩阵的乘法来表示,如:
F n + 1 = F n + F n − 1 \:F_{n + 1} = F_{n} + F_{n - 1} Fn+1=Fn+Fn−1, n > 0 n > 0 n>0, F 0 = 0 F_{0} = 0 F0=0, F 1 = 1 F_{1} = 1 F1=1
[ F n − 1 F n ] = [ F n − 2 F n − 1 ] [ 0 1 1 1 ] \begin{bmatrix}F_{n - 1}&F_{n}\end{bmatrix} = \begin{bmatrix}F_{n - 2}&F_{n - 1}\end{bmatrix}\begin{bmatrix}0 &1\\1&1\end{bmatrix} [Fn−1Fn]=[Fn−2Fn−1][0111]
综上我们可以得到:
[ F n F n + 1 ] = [ 0 1 ] [ 0 1 1 1 ] n \begin{bmatrix}F_{n}&F_{n+1}\end{bmatrix} = \begin{bmatrix}0&1\end{bmatrix}\begin{bmatrix}0 &1\\1&1\end{bmatrix}^{_n} [FnFn+1]=[01][0111]n
进一步得知,
( a i , j ) = [ 0 1 1 1 ] n (a_{i,j}) = \begin{bmatrix}0 &1\\1&1\end{bmatrix}^{_n} (ai,j)=[0111]n, F n = a 2 , 1 F_{n} = a_{2,1} Fn=a2,1
利用快速幂的技巧,可以实现 O ( log n ) O(\log n) O(logn) 意义下的斐波那契数计算。
import java.util.Scanner;
public class Main {
public static void main(String[] args) { new Main().run(); }
int MOD = 10007;
void run() {
Scanner in = new Scanner(System.in);
int n = in.nextInt();
int[][] P = Apow(n);
System.out.println(P[1][0]);
}
int[][] A = {{0, 1}, {1, 1}};
int[][] E = {{1, 0}, {0, 1}};
int[][] Apow(int n) {
if (n == 0) return E;
if (n == 1) return A;
int[][] AP = Apow(n >> 1);
int[][] res = mul(AP, AP);
if ((n & 1) == 1)
res = mul(res, A);
return res;
}
int[][] mul(int[][] A, int[][] B) {
int[][] C = new int[2][2];
for (int i = 0; i < 2; i++)
for (int j = 0; j < 2; j++)
for (int k = 0; k < 2; k++)
C[i][j] = (C[i][j] + A[i][k] * B[k][j]) % MOD;
return C;
}
}
快速倍增法:
引用一下斐波那契数列的 附加性质 (費氏數列的性質整理)
三、(性質 2)費氏數列中,第(m+n)項等於第 m 項乘上第(n-1)項後再加上第(m+1)項乘上第 n 項之和,即 f m + n = f m f n − 1 + f m + 1 f n , m , n ≥ 1 f_{m+n}=f_{m}f_{n-1} + f_{m+1}f_{n},m,n\ge1 fm+n=fmfn−1+fm+1fn,m,n≥1 湾湾人高一捣鼓的东西,🐮🍺。
不像我,连个高中都没考上。
这里简单归纳一下,
f N = f 1 f N − 2 + f 2 f N − 1 = f 1 f N − 2 + f 2 f N − 2 + f 2 f N − 3 = f 3 f N − 2 + f 2 f N − 3 = f 4 f N − 3 + f 3 f N − 4 ⋯ ⋯ = f m + 1 f n + f m f n − 1 \begin{array}{r l} f_{N}& = f_{1}f_{N-2} + f_{2}f_{N-1}\\ & = f_{1}f_{N-2} + f_{2}f_{N-2} + f_{2}f_{N-3}\\ & = f_{3}f_{N-2} + f_{2}f_{N-3}\\ & = f_{4}f_{N-3} + f_{3}f_{N-4}\\ &\cdots\cdots\\ & = f_{m+1}f_{n}+f_{m}f_{n-1}\\ \end{array} fN=f1fN−2+f2fN−1=f1fN−2+f2fN−2+f2fN−3=f3fN−2+f2fN−3=f4fN−3+f3fN−4⋯⋯=fm+1fn+fmfn−1
设 k = n k=n k=n、 m = k ∣ k + 1 m = k \mid k+1 m=k∣k+1 可以得到两个特殊的等式。
F 2 k = F k ( 2 F k + 1 − F k ) F_{2k} = F_{k}(2F_{k+1} - F_{k}) F2k=Fk(2Fk+1−Fk)
F 2 k + 1 = F k + 1 2 + F k 2 F_{2k+1} = F_{k+1}^{2} + F_{k}^{2} F2k+1=Fk+12+Fk2
通过这个公式,我们可以快速确定两个相邻的斐波那契数。
import java.util.Scanner;
public class Main {
public static void main(String[] args) { new Main().run(); }
int MOD = 10007;
void run() {
Scanner in = new Scanner(System.in);
int n = in.nextInt();
System.out.println(fib(n)[0]);
}
int[] fib(int n) {
if (n == 0) return new int[]{0, 1};
int[] fd2 = fib(n >> 1);
int fib1 = fd2[0] * (2 * fd2[1] - fd2[0] + MOD);
int fib2 = fd2[0] * fd2[0] + fd2[1] * fd2[1];
if ((n & 1) == 1) return new int[]{fib2 % MOD, (fib1 + fib2) % MOD};
else return new int[]{fib1 % MOD, fib2 % MOD};
}
}