本文目录
1. 天平称重问题
问题描述:
用天平称重时,我们希望用尽可能少的砝码组合称出尽可能多的重量。如果只有5个砝码,重量分别是1,3,9,27,81 则它们可以组合称出1到121之间任意整数重量(砝码允许放在左右两个盘中)。
本题目要求编程实现:对用户给定的重量,给出砝码组合方案。
例如:
用户输入:
5
程序输出:
9-3-1
用户输入:
19
程序输出:
27-9+1要求程序输出的组合总是大数在前小数在后。可以假设用户的输入的数字符合范围1~121。
思路分析:
金典数论进制问题。对于任意一个数转换为3进制,对于转换后的三进制数的每一数,如果是0表示不选;为1表示放在砝码区;为2则向高位进1,然后转换为-1,表示放在物体区;为3则同样向高位进1,然后本位转化为0。
代码:
package 数学问题;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Scanner;
/**
* @author: DreamCode
* @file: 天平称重.java
* @time: 2022年3月19日-下午4:46:06
*/
public class 天平称重 {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
int n = scanner.nextInt();
String string = Integer.toString(n, 3); // 转换为3进制字符串
char[] arr = new StringBuffer(string).reverse().toString().toCharArray(); // 需将进制反转,方便低位向高位进位
List<Integer> list = new ArrayList<>(); // 使用链表,方便每次在头部进行插入元素
for (int i = 0; i < arr.length; i++) {
if (arr[i] == '2') { // 进制为2,向高位进1,向容器插入-1
list.add(0, -1);
if (i == arr.length - 1) {// 当前已经是最后一位,直接再次向容器插入1即可
list.add(0, 1);
} else {
++arr[i + 1];
}
} else if (arr[i] == '3') {// 进制为3,向高位进1,向容器插入0
list.add(0, 0);
if (i == arr.length - 1) {// 当前已经是最后一位,直接再次向容器插入1即可
list.add(0, 1);
} else {
++arr[i + 1];
}
} else { // 进制为1或者0,向容器直接0
list.add(0, arr[i] - '0');
}
}
// 根据容器进行构造最终答案 10
int len = list.size();
for (int i = 0; i < len; i++) {
int num = list.get(i);
if (num == 1) {
if (i == 0)
System.out.print((int) Math.pow(3, len - i - 1));
else
System.out.print("+"+(int) Math.pow(3, len - i - 1));
} else if (num == -1) {
System.out.print("-" + (int) Math.pow(3, len - i - 1));
}
}
}
}
2. Nim游戏
问题描述:
有若干堆石子,每堆石子的数量都是有限的,合法的移动是“选择一堆石子并拿走若干颗(不能不拿)”,如果轮到某个人时所有的石子堆都已经被拿空了,则判负(因为他此刻没有任何合法的移动,几个数字做模2的加法,如果为0,无论怎么拿结果都不为0 ,如果不为0 ,总有办法改变一个数字把它变成0
011
100
101
010
现在不为0,我们改变第二位就可以将结果变为0
思路分析:
数组元素累计异或,对于先手的人,结果非0必赢,结果为0必输。因为先手的人可以通过变换将异或结果变为0,则后手的人面临的情形必定为0,
代码:
package 数学问题;
/**
* @author: DreamCode
* @file: Nim游戏.java
* @time: 2022年3月20日-上午9:12:39
*/
public class Nim游戏 {
public static void main(String[] args) {
int[] arr = { 15, 5, 10 };
boolean res = solve(arr);
System.out.println(res);
}
private static boolean solve(int[] arr) {
int res = 0;
for (int i = 0; i < arr.length; i++) { //将所有数分别异或
res ^= arr[i];
}
if (res != 0) //异或结果不为0,则先手的人胜
return true;
return false; //否则先手的人输
}
}
3. 阶梯尼姆博弈问题
问题描述:
Georgia和Bob轮流选择一个棋子向左移动,每次可以移动一个及以上任意多格,但是不允许反超其他的棋子,也不允许将两个棋子放在同一个格子内。无法进行移动操作的一方失败,问谁会赢?第一行输入测试样例组数,第二行输入石子个数,第三行输入每个石子的位置。
Sample Input
2
3
1 2 3
8
1 5 6 7 9 12 14 17Sample Output
Bob will win
Georgia will win
思路分析:
将两两分为一堆,计算每堆之间的差【如果数目为奇数个,则第一个数与1之间计算差】;将所有的差做异或,结果不为0则先手的人胜,否则先手的人输。因为异或结果不为0,先手的人总能通过移动部分棋子将结果变为0
代码:
package 数学问题;
import java.util.Arrays;
import java.util.Scanner;
/**
* @author: DreamCode
* @file: 阶梯尼姆博弈.java
* @time: 2022年3月20日-上午9:30:14
*/
public class 阶梯尼姆博弈 {
public static void main(String[] args) {
Scanner scanner=new Scanner(System.in);
int N = scanner.nextInt();
while(N!=0) {
int m = scanner.nextInt();
int[] arr = new int[m];
for(int i=0;i<m;i++) {
arr[i]=scanner.nextInt();
}
boolean res = solve(arr);
if(res)
System.out.println("Georgia will win");
else
System.out.println("Bob will win");
N--;
}
}
private static boolean solve(int[] arr) {
int len = arr.length;
Arrays.sort(arr); //先将数组按照棋子位置从小到大排序
int[] rec = new int[len/2+1]; //记录每两个棋子之间的距离,最多有len/2+1
Arrays.fill(rec, 0); //初始化记录数组全为0
int index=0;
if((len&1)==1) { //考虑棋子个数为奇数的情况,第一个棋子会与1做距离的差。组成堆的数目为len/2+1
for(int i=0;i<len;i+=2) { // 0 2 4 6 ...
rec[index++]=(i==0?arr[i]-1:arr[i]-arr[i-1]-1); //0 21 43 65...
}
}else { //考虑棋子个数为偶数的情况,刚好可以组成len/2堆
for(int i=1;i<len;i+=2) {
rec[index++]=arr[i]-arr[i-1]-1;
}
}
int sum=0;
for(int num:rec) { //分别做异或
sum^=num;
}
if(sum!=0) //结果不为0,先手胜
return true;
return false; //记过为0,先手输
}
}
4. 高僧斗法
问题描述:
古时丧葬活动中经常请高僧做法事。仪式结束后,有时会有“高僧斗法”的趣味节目,以舒缓压抑的气氛。节目大略步骤为:先用粮食(一般是稻米)在地上“画”出若干级台阶(表示N级浮屠)。又有若干小和尚随机地“站”在某个台阶上。最高一级台阶必须站人,其它任意。两位参加斗法的法师分别指挥某个小和尚向上走任意多级的台阶,但会被站在高级台阶上的小和尚阻挡,不能越过。两个小和尚也不能站在同一台阶,也不能向低级台阶移动。两法师轮流发出指令,最后所有小和尚必然会都挤在高段台阶,再也不能向上移动。轮到哪个法师指挥时无法继续移动,则游戏结束,该法师认输。对于已知的台阶数和小和尚的分布位置,请你计算先发指令的法师该如何决策才能保证胜出。
输入数据为一行用空格分开的N个整数,表示小和尚的位置。台阶序号从1算起,所以最后一个小和尚的位置即是台阶的总数。(N<100, 台阶总数<1000)
输出为一行用空格分开的两个整数: A B, 表示把A位置的小和尚移动到B位置。若有多个解,输出A值较小的解,若无解则输出-1。
例如:
用户输入:
1 5 9
则程序输出:
1 4
思路解析:
分别计算两两成堆之间【01,23,45…】的间隔,总共可产生n/2个堆;将所有堆的差做异或,结果非0则先手必赢,从前到后暴力枚举移动小和尚台阶的距离,找出将非0转换为0的方法即为答案
代码:
package 数学问题;
import java.util.Scanner;
/**
* @author: DreamCode
* @file: 高僧斗法_第四_决赛_B组.java
* @time: 2022年3月20日-上午9:44:40
*/
public class 高僧斗法_第四_决赛_B组 {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
String string = scanner.nextLine();
String[] sArr = string.split(" ");
int[] arr = new int[sArr.length];
for (int i = 0; i < sArr.length; i++) {
arr[i] = Integer.valueOf(sArr[i]);
}
int sum = Nim(arr);
if (sum == 0) { // 先手必输
System.out.println("-1");
} else { //先手必赢
for (int i = 0; i < arr.length - 1; i++) { //从前到后遍历每一个数
for (int j = 1; arr[i] < arr[i + 1]; j++) { //遍历该数移动的步数
arr[i] += j;
if (arr[i] >= arr[i + 1]) //如果移动的数目超过了下一个数的位置,则说明依靠移动当前数无解,直接跳出
break;
int res = Nim(arr);
if (res == 0) { //当前移动步数能将结果转换为0
System.out.println((arr[i] - j) + " " + (arr[i]));
return;
} else { //当前步数不能将结果转换为0
arr[i] -= j; //回溯
}
}
}
}
}
private static int Nim(int[] arr) {
int len = arr.length;
int[] rec = new int[len / 2 + 1];
int index = 0;
for (int i = 1; i < len; i += 2) { //1 3 5...
rec[index++] = arr[i] - arr[i - 1] - 1; //10 32 54...
}
int sum = 0;
for (int i = 0; i < index; i++) {
sum ^= rec[i];
}
return sum;
}
}
5. 欧几里得算法
问题描述:
欧几里得算法(辗转相除法)解决最大公约数与最小公倍数问题
代码:
/**
* 欧几里德算法算法,即辗转相除法,求最大公约数
*/
public static long gcd(long m, long n) {
return n == 0 ? m : gcd(n, m % n);
}
/**
* 最小公倍数lowest common multiple (LCM)
*/
private static long lcm(long a, long b) {
return a * b / gcd(a, b);
}
6. 扩展欧几里得算法
思路解析:
基础:
扩展欧几里得算法(裴蜀【贝祖】等式),对任意整数a,b和他们的最大公约数d,关于未知数x和y的线性方程:
ax+by=m
方程有整数解当且仅当m是d的倍数。如果方程有解,必然有无穷多的整数解(可以理解为 ax+by=m 的方程在坐标卓上有无数的整数坐标点)。
推广:
若方程 ax+by=1 有整数解,当且仅当a、b互为素数,因为此时的m为1,a与b互为素数时,其最大公约数d为1,这样才能满足m是d的倍数这一约束;
扩展欧几里得在原来欧几里得算法的基础上,除了能求出最大公约数,还能求出方程 ax+by=gcd(a,b) 的第一组整数解,递归核心代码:
x=y1;
y=x1-a/b*y1;
对于方程 ax+by=m 求出第一组整数解后,推广求其它整数解:
x = x0+(b/gcd)*t;
y = y0+(a/gcd)*t;
同时ax+by=m获取第一个正整数解为
X = (X % b + b) % b; (这里的b为正整数,若为负数需要取其绝对值)
代码:
package 数学问题;
/**
* @author: DreamCode
* @file: 扩展欧几里得算法.java
* @time: 2022年3月20日-下午3:55:06
*/
public class 扩展欧几里得算法 {
/**
* 扩展欧几里得算法 x=k1 y=x1-a/b*k1
*/
static long x;
static long y;
/**
* 扩展欧几里得 调用完成后x,y是ax+by=gcd(a,b)的解
*/
public static long ext_gcd(long a, long b) {
if (b == 0) {
x = 1;
y = 0;
return a;
}
long res = ext_gcd(b, a % b);
long x1 = x; // 备份x
x = y; // 更新x
y = x1 - a / b * y; // 更新y
return res;
}
/**
* 线性方程 ax+by=m 当m是gcd(a,b)倍数时有解(且有无数组解) 等价于ax = m mod b
*/
public static long linearEquation(long a, long b, long m) throws Exception {
long d = ext_gcd(a, b);
if (m % d != 0) {
throw new Exception(m + " % " + "gcd(" + a + "," + b + ")" + " != 0~~无解");
} else {
long n = m / d;
y *= n;
x *= n;
return d;
}
}
public static void main(String[] args) {
try {
long res = linearEquation(12, 42, 6);
System.out.println("a, b 最大公约数: " + res);
System.out.println("x: " + x + "\ty: " + y);
} catch (Exception e) {
System.out.println("无解");
}
}
}
7. 一步之遥
问题描述:
从昏迷中醒来,小明发现自己被关在X星球的废矿车里。矿车停在平直的废弃的轨道上。他的面前是两个按钮,分别写着“F”和“B”。小明突然记起来,这两个按钮可以控制矿车在轨道上前进和后退。按F,会前进97米。按B会后退127米。透过昏暗的灯光,小明看到自己前方1米远正好有个监控探头。他必须设法使得矿车正好停在摄像头的下方,才有机会争取同伴的援助。
或许,通过多次操作F和B可以办到。矿车上的动力已经不太足,黄色的警示灯在默默闪烁…
每次进行 F 或 B 操作都会消耗一定的能量。小明飞快地计算,至少要多少次操作,才能把矿车准确地停在前方1米远的地方。请填写为了达成目标,最少需要操作的次数。
思路分析:
典型的扩展欧几里得问题,a=97,b=-127,m=1,求出其第一组解再取绝对值之和即为答案
97x-127y=1
代码:
package 数学问题;
/**
* @author: DreamCode
* @file: 一步之遥.java
* @time: 2022年3月20日-下午4:38:31
*/
public class 一步之遥 {
static long x;
static long y;
public static void main(String[] args) {
long gcd = ext_gcd(97, -127);
System.out.println(Math.abs(x)+Math.abs(y));
}
private static long ext_gcd(long a, long b) {
// TODO 扩展欧几里得算法
if (b == 0) {
x = 1;
y = 0;
return a;
}
long res = ext_gcd(b, a % b);
long x1 = x;
x = y;
y = x1 - a / b * y;
return res;
}
}
8. 青蛙约会
问题描述:
两只青蛙在网上相识了,它们聊得很开心,于是觉得很有必要见一面。它们很高兴地发现它们住在同一条纬度线上,于是它们约定各自朝西跳,直到碰面为止。可是它们出发之前忘记了一件很重要的事情,既没有问清楚对方的特征,也没有约定见面的具体位置。不过青蛙们都是很乐观的,它们觉得只要一直朝着某个方向跳下去,总能碰到对方的。但是除非这两只青蛙在同一时间跳到同一点上,不然是永远都不可能碰面的。为了帮助这两只乐观的青蛙,你被要求写一个程序来判断这两只青蛙是否能够碰面,会在什么时候碰面。
我们把这两只青蛙分别叫做青蛙A和青蛙B,并且规定纬度线上东经0度处为原点,由东往西为正方向,单位长度1米,这样我们就得到了一条首尾相接的数轴。设青蛙A的出发点坐标是x,青蛙B的出发点坐标是y。青蛙A一次能跳m米,青蛙B一次能跳n米,两只青蛙跳一次所花费的时间相同。纬度线总长L米。现在要你求出它们跳了几次以后才会碰面。
思路分析:
典型的扩展欧几里得算法问题,模运算的应用。当满足公式:(x+t×m)%L==(y+t×n)%L时,取得该公式的第一组解即为答案。
推出公式:
x+t×m = k1×L
y+t×n = k2×L
两式做差得:(x-y)+t×(m-n)=(k1-k2)×L,可以得到(k1-k2)×L+t×(n-m) = x-y,从而方程ax+by=c,其中a=L,b=n-m,c=x-y
同时ax+by=c获取第一个正整数解为
X = (X % b + b) % b; (这里的b为正整数,若为负数需要取其绝对值)
代码:
package 数学问题;
import java.util.Scanner;
/**
* @author: DreamCode
* @file: 青蛙的约会.java
* @time: 2022年3月21日-上午10:08:31
*/
public class 青蛙的约会 {
static long X;
static long Y;
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
long x, y, m, n, L;
x = scanner.nextLong();
y = scanner.nextLong();
m = scanner.nextLong();
n = scanner.nextLong();
L = scanner.nextLong();
long c = y - x;
long a = m-n;
long b = L;
long d = ext_gcd(a, b); //求出的解是ax+by=1的第一组解
X = X * (c / d); //转换后才是ax+by=c的第一组解
Y = Y * (c / d);
if (b < 0)
b = Math.abs(b);
X = (X % b + b) % b;
System.out.println(X);
}
private static long ext_gcd(long a, long b) {
// TODO 扩展欧几里得算法
if (b == 0) {
X = 1;
Y = 0;
return a;
}
long res = ext_gcd(b, a % b);
long X1 = X;
X = Y;
Y = X1 - (a / b) * Y;
return res;
}
}
9. 模的逆元
问题描述:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8M4oXi50-1649577686332)(C:\Users\MI\AppData\Roaming\Typora\typora-user-images\image-20220405155546091.png)]
(A/B)%9973,求余除法不满足交换性,可改为求B关于9973的逆元x,这样结果等价于Ax%9973,等价于x*A%9973,等价于xn%9973
思路解析:
ax≡1(mod n),gcd(a,n)=1时有解,ax+ny=1,这时求出来的x为a关于n的逆元
代码:
package 数学问题;
import java.util.Scanner;
/**
* @author: DreamCode
* @file: 模的逆元.java
* @time: 2022年3月22日-下午2:56:27
*/
public class 模的逆元 {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int T = sc.nextInt();
for (int i = 0; i < T; i++) {
int A = sc.nextInt();
int B = sc.nextInt();
// 求B关于9973的逆元x:B*x+9973*y=1
int x = My_EXT_GCD.inverse(B, 9973);
System.out.println((x*A)%9973);
}
}
private static class My_EXT_GCD {
static int x;
static int y;
static int ext_gcd(int a, int b) { // 扩展欧几里得
if (b == 0) {
x = 1;
y = 0;
return a;
}
int res = ext_gcd(b, a % b);
int x1 = x;
x = y;
y = x1 - a / b * y;
return res;
}
public static int inverse(int a, int b) {
// 求线性方程ax+by=1的解
int d = ext_gcd(a, b);
x = (x % b + b) % b; // 将x变为大于0
return x;
}
}
}
10. 同余方程组
问题描述:
假设同余方程组
x=a1(mod m[1])
x=a2(mod m[2])
…
x=ar(mod m[r])
求解非负正整数解x
思路解析:
对于模线性方程组,在意进行方程组的合并,求出合并后的方程的解,这样就可以得出方程组的解,不管这样的方程有多少个,通过两两合并求得所有的解;因此,我们求解的时候只要通过两个方程就可以了
代码:
package 数学问题;
import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;
/**
* @author: DreamCode
* @file: 同余方程组.java
* @time: 2022年3月22日-下午3:22:41
*/
public class 同余方程组 {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
List<long[]> aList = new ArrayList<>();
List<Long> dList = new ArrayList<>();
while (scanner.hasNext()) {
long[] a = { scanner.nextLong(), scanner.nextLong(), scanner.nextLong() };
long d = scanner.nextLong();
if (a[0] == -1 && a[1] == -1 && a[2] == -1 && d == -1)
break;
aList.add(a);
dList.add(d);
}
for (int i = 0; i < aList.size(); i++) {
long[] a = aList.get(i);
long[] m = { 23, 28, 33 };
long d = dList.get(i);
long res = MyExtGcd.linearEquationGroup(a, m);
while (res <= d) {
res += 21252;
}
System.out.println("Case " + (i) + ": the next triple peak occurs in " + (res - d) + " days.");
}
}
private static class MyExtGcd {
static long x;
static long y;
static long ext_gcd(long a, long a2) {
if (a2 == 0) {
x = 1;
y = 0;
return a;
}
long res = ext_gcd(a2, a % a2);
long x1 = x;
x = y;
y = x1 - a / a2 * y;
return res;
}
public static long linearEquationGroup(long[] a, long[] m) {
// TODO 求同余方程组
for (int i = 1; i < a.length; i++) {
long d = ext_gcd(m[i - 1], m[i]);
x *= (a[i] - a[i - 1]) / d;
long x0 = a[i - 1] + m[i - 1] * x;
long lcm = (m[i - 1] * m[i]) / d;
a[i] = (x0 % lcm + lcm) % lcm;
m[i] = lcm;
}
// 合并完之后,只有一个方程 : x ≡ a[len-1] (% m[len-1])
return a[a.length - 1] % m[m.length - 1];
}
}
}
11. 素数_埃氏筛法
问题描述:
求第十万零二个素数
思路解析:
自然数n之内的素数所占的比例为1/ln(n)。建立记录数组,初始默认都为素数,若当前位置已经确定不是素数,则跳过当前循环;如果当前数为素数,则他的K倍肯定都不是素数,则将它的K倍都进行标记
代码:
package 数学问题;
import java.util.Arrays;
/**
* @author: DreamCode
* @file: 素数_埃氏筛法.java
* @time: 2022年3月23日-下午3:05:44
*/
public class 素数_埃氏筛法 {
public static void main(String[] args) {
int n=2;
int N=100002;
while(n/Math.log(n)<N) { //n个数中含有素数n/log(n)个
n++;
}
int[] rec = new int[n];
Arrays.fill(rec, 0); //初始标记都为0;
int num=2; //从2开始
while(num<n) {
if(rec[num]!=0) {//当前位置已经确定不是素数了
num++;
continue;
}
//默认当前数为素数,则他的K倍肯定都不是素数
int k=2;
while(k*num<n) {
rec[k*num]=-1;
k++;
}
num++;
}
int counter=0;
for(int i=2;i<n;i++) {
if(rec[i]==0) { //当前数是素数
counter++;
}
if(counter==N) {
System.out.println(i);
return;
}
}
}
}
12. 快速幂运算
问题描述:
求一个数的n次幂
思路解析:
巧算 二进制数每一位置上的0或1 ,m=1010
代码:
package 数学问题;
/**
* @author: DreamCode
* @file: 二进制_快速幂运算.java
* @time: 2022年3月23日-下午3:43:49
*/
public class 二进制_快速幂运算 {
public static void main(String[] args) {
// TODO Auto-generated method stub
long ans = pow(2, 10);
System.out.println(ans);
}
private static long pow(long n, long m) {
if (n == 0)
return 1;
long pingFangShu = n; // n 的 1 次方
long result = 1;
while (m != 0) {
// 遇1累乘现在的幂
if ((m & 1) == 1)
result *= pingFangShu;
// 每移位一次,幂累乘方一次
pingFangShu = pingFangShu * pingFangShu;
// 右移一位
m >>= 1;
}
return result;
}
}
r++;
}
if(counter==N) {
System.out.println(i);
return;
}
}
}
}
# 12. 快速幂运算
**问题描述:**
> 求一个数的n次幂
**思路解析:**
> 巧算 二进制数每一位置上的0或1 ,m=1010
**代码:**
```java
package 数学问题;
/**
* @author: DreamCode
* @file: 二进制_快速幂运算.java
* @time: 2022年3月23日-下午3:43:49
*/
public class 二进制_快速幂运算 {
public static void main(String[] args) {
// TODO Auto-generated method stub
long ans = pow(2, 10);
System.out.println(ans);
}
private static long pow(long n, long m) {
if (n == 0)
return 1;
long pingFangShu = n; // n 的 1 次方
long result = 1;
while (m != 0) {
// 遇1累乘现在的幂
if ((m & 1) == 1)
result *= pingFangShu;
// 每移位一次,幂累乘方一次
pingFangShu = pingFangShu * pingFangShu;
// 右移一位
m >>= 1;
}
return result;
}
}