第一题
地产大亨Q先生临终的遗愿是:拿出100万元给X社区的居民抽奖,以稍慰藉心中愧疚。
麻烦的是,他有个很奇怪的要求:1.100万元必须被正好分成若干份(不能剩余)。
每份必须是7的若干次方元。
比如:1元, 7元,49元,343元,…
2.相同金额的份数不能超过5份。
3.在满足上述要求的情况下,分成的份数越多越好!请你帮忙计算一下,最多可以分为多少份?
解法:
先上暴力法,观察发现题目中给出的情况皆是7的若干次方,每位不超过5,设置一个进制计数数组picks做7进制记录,直接枚举出100万以内的所有可能的数
public class Main {
public static void main(String[] args) {
int[] sevens = {1, 7, 49, 343, 2401, 16807, 117649, 823543};
int[] picks = new int[sevens.length];
Arrays.fill(picks, 0);
pick(sevens, picks, 0);
}
/**
* 暴力法,组合判断
* @param sevens
* @param picks
* @param k
*/
public static void pick(int[] sevens, int[] picks, int k) {
if (k == picks.length) {
if (judge(sevens, picks)) {
System.out.println(getParts(picks));
}
return;
}
if (picks[k] == 5) {
pick(sevens, picks, k + 1);
} else {
picks[k] += 1;
pick(sevens, picks, k);
picks[k] -= 1;
pick(sevens, picks, k + 1);
}
}
public static boolean judge(int[] sevens, int[] picks) {
int sum = 0;
for (int i = 0; i < picks.length; i++) {
sum += sevens[i] * picks[i];
}
if (sum == 1000000) {
return true;
} else {
return false;
}
}
public static int getParts(int[] picks) {
int parts = 0;
for (int i = picks.length - 1; i >= 0; i--) {
parts += picks[i];
}
return parts;
}
再上课堂上老师教学的简便方法,将目标1000000转换城7进制表达式,直接输出。从那天起,笔者又重新感受到了被数学支配的恐惧 (摊手)。
/**
* 7进制,每位不超过5
*/
public class Main {
public static void main(String[] args) {
char[] c = Integer.toString(1000000, 7).toCharArray();
int count = 0;
for (char ch : c){
count += (ch-'0');
}
System.out.println(count);
}
}
第二题
用天平称重时,我们希望用尽可能少的砝码组合称出尽可能多的重量。
如果只有5个砝码,重量分别是1,3,9,27,81
则它们可以组合称出1到121之间任意整数重量(砝码允许放在左右两个盘中)。
本题目要求编程实现:对用户给定的重量,给出砝码组合方案。
例如:用户输入:
5
程序输出:
9-3-1
用户输入:
19
程序输出:
27-9+1要求程序输出的组合总是大数在前小数在后。
可以假设用户的输入的数字符合范围1~121。
解法:
先上暴力,数字组合+,-,不拿,得算式,验证结果。
public class Main {
public static void main(String[] args) {
int[] fama = {1, 3, 9, 27, 81};
int[] status = {0,0,0,0,0};
int n = 19;
f1(fama, status, 0, n)
}
/**
* 暴力法,全组合
*
* @param fama
* @param status
* @param k
* @param goal
*/
public static void f1(int[] fama, int[] status, int k, int goal) {
if (k == -1) {
if (goal == 0) {
boolean first = true;
for (int i = status.length - 1; i >= 0; i--) {
int num = fama[i] * status[i];
if (num != 0) {
if (num > 0 && !first) {
System.out.print("+");
}
System.out.print(num);
first = false;
}
}
}
return;
}
status[k] = 1;
f1(fama, status, k - 1, goal - fama[k]);
status[k] = -1;
f1(fama, status, k - 1, goal + fama[k]);
status[k] = 0;
f1(fama, status, k - 1, goal);
}
}
非递归方法初稿,代码可能看起来比较多,但是思路清晰
public class Main {
public static void main(String[] args) {
int[] fama = {1, 3, 9, 27, 81};
int n = 19;
System.out.println(f2(fama, n));
}
/**
* 将n化为3进制数,并且不允许当n = 2,当发生时继续取余得到-1
*
* @param fama
* @param n
*/
public static String f2(int[] fama, int n) {
StringBuilder result = new StringBuilder();
/*
求每个砝码状态
*/
while (n != 0) {
int remainder = n % 3;
if (remainder < 2) {
/*
余数为0,1的情况
*/
n /= 3;
} else {
/*
余数为2的情况
*/
n = n / 3 + 1;
remainder -= 3;
}
result.insert(0, ",").insert(0, remainder);
}
/*
补零
*/
int[] status = new int[5];
String[] statusB = result.toString().split(",");
for (int i = 0; i < status.length - statusB.length; i++) {
result.insert(0, "0,");
}
/*
转换,装填
*/
statusB = result.toString().split(",");
for (int i = 0; i < status.length; i++) {
status[i] = Integer.valueOf(statusB[i]);
}
/*
根据status,拼接出算式
*/
StringBuilder stringBuilder = new StringBuilder();
for (int i = fama.length - 1; i >= 0; i--) {
switch (status[fama.length - 1 - i]) {
case -1:
stringBuilder.append("-").append(fama[i]);
break;
case 0:
break;
case 1:
stringBuilder.append("+").append(fama[i]);
break;
}
}
stringBuilder.replace(0, 1, "");
return stringBuilder.toString();
}
}
再来最后一稿,先上思路伪代码
f(int[] famas, int n)
首先找到>=n的那个砝码k
如果X(k)=n
返回 n
如果X(k)>n
subSum=[X(0)+X(1)+…+X(k)]
如果subSum小于n //小数字全加起来都不够表示n,说明要靠大的减
返回 X(k) + [f(n-subSum)] //这里需要注意,下层返回的算式是不带括号的,所以如果上层是减的话,需要进行一次转换
如果subSum大于等于n //小数字全加起来够表示n
返回 X(k-1) + “+” + [f(n-subSum)]reverse(String s)
将加减互换得到s1
返回 “-” + s1
public class Main {
static int[] famas = {1, 3, 9, 27, 81};
public static void main(String[] args) {
for (int i = 1; i <= 121; i++) {
System.out.println(i + ":" + f(i));
}
}
public static String f(int n) {
int a = 1;
int index = 0;
while (a < n) {
a *= 3;
index++;
}
/*
a 只会 >= n
*/
if (a == n) return String.valueOf(n);
else {//a>n
int subSum = 0;
for (int i = 0; i < index; i++) {
subSum += famas[i];
}
if (subSum >= n) {
int thinnerOne = famas[index - 1];
return thinnerOne + "+" + f(n - thinnerOne);
} else {
return a + reverse(f(a - n));
}
}
}
public static String reverse(String s) {
s = s.replace("+", "#").
replace("-", "+").
replace("#", "-");
return "-" + s;
}
}
第三题
有3堆硬币,分别是3,4,5
二人轮流取硬币。
每人每次只能从某一堆上取任意数量。
不能弃权。
取到最后一枚硬币的为赢家。
求先取硬币一方有无必胜的招法。
解法:
尼姆堆问题关键在于求当前状态的Nim-sum,如果在自己手上的Nim-sum为0,则一定输;如果非0,则一定可以将0的局面给对手,则一定赢。
为什么得出这个结论呢?因为考虑到Nim-sum计算的特性(全堆异或),如果一开始给到对手的是0局面,则对手不管怎么动都会促成一个非0的局面,则你接下来反手一定可以给对方一个0的局面,到最后的情况就是0xor0xor0….即全0局面,胜。
所以我们要创造一个必胜的局面,就需要首先己方的初始状态Nim-sum一定非0,且走出的第一步一定要促成0的局面
public class Main {
public static void main(String[] args) {
int n = 3;
int[] heaps = new int[n];
heaps[0] = 3;
heaps[1] = 4;
heaps[2] = 5;
List<Integer[]> methods = new ArrayList<>();
for (int i = 0; i < n; i++) {
for (int j = 0; j <= heaps[i]; j++) {
heaps[i] -= j;
if (isNimSum0(heaps)) {
//这种下手方法必胜
Integer[] method = new Integer[2];
method[0] = i + 1;
method[1] = j;
//意为从i堆中拿走j个
methods.add(method);
}
heaps[i] += j;
}
}
for (Integer[] method : methods) {
System.out.println(Arrays.asList(method));
}
}
public static boolean isNimSum0(int[] heaps) {
int num = heaps[0];
for (int i = 1; i < heaps.length; i++) {
num = num ^ heaps[i];
}
return num == 0;
}
}
第四题
如果两个数很大,怎样求最大公约数,最小公倍数?
如果是n个数呢?比如1000个数的最小公倍数
解法:
使用BigInteger类的gcd()方法求最大公约数
public static String gcd1(int a, int b) {
BigInteger bigInteger = new BigInteger(String.valueOf(a));
return bigInteger.gcd(new BigInteger(String.valueOf(b))).toString();
}
使用BigInteger类的multiply()方法求乘积,用divide()方法除以上面得出的最大公约数,得最小公倍数
第五题
从昏迷中醒来,小明发现自己被关在X星球的废矿车里。
矿车停在平直的废弃的轨道上。
他的面前是两个按钮,分别写着“F”和“B”。
小明突然记起来,这两个按钮可以控制矿车在轨道上前进和后退。
按F,会前进97米。按B会后退127米。
透过昏暗的灯光,小明看到自己前方1米远正好有个监控探头。
他必须设法使得矿车正好停在摄像头的下方,才有机会争取同伴的援助。
或许,通过多次操作F和B可以办到。
矿车上的动力已经不太足,黄色的警示灯在默默闪烁…
每次进行 F 或 B 操作都会消耗一定的能量。
小明飞快地计算,至少要多少次操作,才能把矿车准确地停在前方1米远的地方。
请填写为了达成目标,最少需要操作的次数。
解法:
此题就是求不定方程97x+127y = 1的整数解
已知扩展欧几里德定理
Ax+By = gcd(A,B)一定有解
且
Bx’+(A%B)y’ = gcd(B, A%B)
由以上两式子,且根据欧几里德定理gcd(A,B) = gcd(B, A%B)
可得Ax+By = Bx’+(A%B)y’
所以
Ax+By = Bx’+(A%B)y’ = Bx’ +[A–(A/B)*B]y’ = Bx’ + Ay’ –(A/B)*By’ = Ay’+B[x’–(A/B)y’]
即
A [x] + B [y] = A [y’] + B {[x’–(A/B)y’]}
得出恒等关
x = y’ , y = [x’ – (A/B)y’]
即当下层求得一对x’, y’时,上层即可通过恒等式得出当前层的x, y继续返回上层
此时递归关系得出
由于gcd算法最终递归出口为b == 0
所以递归出口条件为b == 0
且当b == 0时, Ax = gcd(A,0) = A
得此时x = 1, y = 上层传入的b = 0这道题代码倒是不怎么重要,重要的是要知道扩展欧几里德定理
从那天起,笔者又重新感受到了被数学按在地上摩擦的恐惧 (摊手)。
当然要是题目条件允许的话,可以使用暴力求解,比如从-1000,-1000试到1000,1000,不行就再大点(逃
public class Main {
/**
* 遇到求解不定方程,首先可以想到暴力法
* 然后就是将方程化为Ax+By = gcd(A,B)*m
*
* @param args
*/
public static void main(String[] args) {
int[] xy = new int[2];
e_gcd(97, 127, xy);
System.out.println(xy[0] + " " + xy[1]);
}
/**
* 扩展欧几里德算法求解不定方程 Ax + By = gcd(A,B)*m (m是任意整数,这里只给出1的情况)
* 可以求解Ax + By = gcd(A,B),然后将得出的x,y分别乘m就好
* 再扩展的Ax + By = n的一般情况笔者暂时未思考
*
* @param a 上层向下传入的A
* @param b 上层向下传入的B
* @param xy 接收下层返回的x,y
* @return
*/
private static void e_gcd(int a, int b, int[] xy) {
if (b == 0) {
xy[0] = 1;
xy[1] = 0;
return;
}
e_gcd(b, a % b, xy);
int x = xy[0];
int y = xy[1];
xy[0] = y;
xy[1] = x - a / b * y;
}
}
第六题
如果求 1/2 + 1/3 + 1/4 + 1/5 + 1/6 + …. + 1/100 = ?
要求绝对精确,不能有误差。
解法:
自己定义分数计算类,不要使用浮点
public class Main {
public static void main(String[] args) {
Fraction sum = new Fraction(new BigInteger(String.valueOf(1)), new BigInteger(String.valueOf(2)));
for (int i = 3; i <= 100; i++) {
sum.add(new Fraction(new BigInteger(String.valueOf(1)), new BigInteger(String.valueOf(i))));
}
System.out.println(sum);
}
}
class Fraction {
private BigInteger numerator;
private BigInteger denominator;
public Fraction(BigInteger numerator, BigInteger denominator) {
this.numerator = numerator;
this.denominator = denominator;
}
public Fraction add(Fraction b) {
this.numerator = this.numerator.multiply(b.denominator).add(this.denominator.multiply(b.numerator));
this.denominator = this.denominator.multiply(b.denominator);
reduction(this);
return this;
}
public Fraction subtract(Fraction b) {
this.numerator = this.numerator.multiply(b.denominator).subtract(this.denominator.multiply(b.numerator));
this.denominator = this.denominator.multiply(b.denominator);
reduction(this);
return this;
}
public Fraction multiply(Fraction b) {
this.numerator = this.numerator.multiply(b.numerator);
this.denominator = this.denominator.multiply(b.denominator);
reduction(this);
return this;
}
public Fraction divide(Fraction b) {
this.numerator = this.numerator.multiply(b.denominator);
this.denominator = this.denominator.multiply(b.numerator);
reduction(this);
return this;
}
public Fraction reduction(Fraction r) {
BigInteger greatestCommonDivisor = r.numerator.gcd(r.denominator);
r.numerator = r.numerator.divide(greatestCommonDivisor);
r.denominator = r.denominator.divide(greatestCommonDivisor);
return r;
}
@Override
public String toString() {
return this.numerator + "/" + this.denominator;
}
}
第七题
第1个素数是2,第2个素数是3,…
求第100002(十万零二)个素数
解法:
使用埃拉托斯特尼筛法,在这个基础上,我们还需要注意一个点——数组长度设置,这里笔者引用了素数范围的AKS证明:This is an algorithm to test whether or not a given integer is prime.
It’s called the AKS primality test https://en.m.wikipedia.org/wiki/AKS_primality_test
And can be done in polynomial time, which is usually considered a decent amount of time.
Now if you’re trying to compute the nth prime, it has been proven that the nth prime must be greater than
nln(n)+n(ln(ln(n))−1)
and less than
nln(n)+nln(ln(n))
When n≥6.
So if you’re searching for the nth prime, I’d look in this gap.
public class Main {
public static void main(String[] args) {
long start = Calendar.getInstance().getTimeInMillis();
int n = 100002;
System.out.println(getNumberNPrime(n));
long end = Calendar.getInstance().getTimeInMillis();
System.out.println((end - start) + "ms");
}
public static int getNumberNPrime(int n) {
int size = n < 6 ? 12 : (int) (Math.ceil(n * Math.log(n) + n * Math.log(Math.log(n))));
int[] nums = new int[size];
Arrays.fill(nums, 0);
double len = Math.sqrt(nums.length);
for (int i = 2; i < len; i++) {
if (nums[i] == 1) continue;
for (int j = i * i; j < nums.length; j += i) {
nums[j] = 1;
}
}
int count = 0;
int index = -1;
for (int i = 2; i < nums.length; i++) {
if (nums[i] == 0) {
count++;
}
if (count == n) {
index = i;
break;
}
}
return index;
}
}
该算法经过笔者的细(随)心(意)优化后,计算100002th素数在笔者电脑上只需要要34ms,10000000th只需要3s