2020_java蓝桥_暑假训练_进制(补)+博弈问题

🌸博弈训练

贴在前面,想了解什么是博弈论,博弈论的基本游戏和基本解法可查看韦泽鑫の博客想要成为富婆的我每天都在努力の博客

🍁Excel地址

Excel单元格的地址表示很有趣,它使用字母来表示列号,比如:

A表示第1列,

B表示第2列,

Z表示第26列,

AA表示第27列,

AB表示第28列,

BA表示第53列,

….

当然Excel的最大列号是有限度的,所以转换起来不难。

如果我们想把这种表示法一般化,可以把很大的数字转换为很长的字母序列呢?

本题目既是要求对输入的数字, 输出其对应的Excel地址表示方式

例如,

1
2
3
4
5
6
7
8
输入:
26
程序应该输出:
Z
输入:
2054
则程序应该输出:
BZZ

我们约定,输入的整数范围[1,2147483647]

🍂分析

此题是进制与整除的变种,需要理解题意有时甚至可以逆向思维推导

可找出以下规律

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
a=1;
b=2;
z=26;
aa=1*26+1=27
ab=1*26+2=28;
az=1*26+26=52;
ba=2*26+1=53;
bb=2*26+2=54;
za=26*26+1=676;
zz=26*26+26=702
aaa=1*26*26+1*26+1=703;
aab=1*26*26+1*26+2=704;
aaz=1*26*26+1*26+26=728;
aba=1*26*26+2*26+1=729;
azz=1*26*26+26*26+26=1378;
baa=2*26*26+1*26+1=1379;
bab=2*26*26+1*26+2=1379;
baz=2*26*26+1*26+26=1404;
bba=2*26*26+2*26+1=1405;
....................
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Excel_address {
public static void main(String[] args) {
Scanner input = new Scanner(System.in);
int n = input.nextInt();
System.out.println(chage(n));
input.close();
}
private static String chage(int n) {
StringBuilder str = new StringBuilder();
while(n > 0) {
if(n % 26 == 0) { //n除以26能整除说明最后一位必定是Z
str.append("Z");
n /= 26;
n--; //前面一行已经除过26了,所以要把那一份26减掉
}else {
str.append((char)('A' + n % 26 - 1));
n /= 26;
}
}
return str.reverse().toString();
}
}

🍁高僧斗法

古时丧葬活动中经常请高僧做法事。仪式结束后,有时会有“高僧斗法”的趣味节目,以舒缓压抑的气氛。 节目大略步骤为:先用粮食(一般是稻米)在地上“画”出若干级台阶(表示N级浮屠)。又有若干小和尚随机地“站”在某个台阶上。最高一级台阶必须站人,其它任意。

两位参加斗法的法师分别指挥某个小和尚向上走任意多级的台阶,但会被站在高级台阶上的小和尚阻挡,不能越过。

两个小和尚也不能站在同一台阶,也不能向低级台阶移动。

两法师轮流发出指令,最后所有小和尚必然会都挤在高段台阶,再也不能向上移动。轮到哪个法师指挥时无法继续移动,则游戏结束,该法师认输。

对于已知的台阶数和小和尚的分布位置,请你计算先发指令的法师该如何决策才能保证胜出。

输入数据为一行用空格分 尚的位置。台阶序号从1算起,所以最后一个小和尚的位置即是台阶的总数。(N<100, 台阶总数<1000)

输出为一行用空格分开的两个整数: A B, 表示把A位置的小和尚移动到B位置。

若有多个解,输出A值较小的解,若无解则输出-1。

例如:

1
2
3
4
5
6
7
8
9
用户输入:
1 5 9
则程序输出:
1 4
再如:
用户输入:
1 5 8 10
则程序输出:
1 3

🍂分析

🍂尼姆博奕(Nimm Game)

有三堆各若干个物品,两个人轮流从某一堆取任意多的物品,规定每次至少取一个,多者不限,最后取光者得胜。这种情况最有意思,它与二进制有密切关系,我们用(a,b,c)表示某种局势,首先(0,0,0)显然是奇异局势,无论谁面对奇异局势,都必然失败。第二种奇异局势是(0,n,n),只要与对手拿走一样多的物品,最后都将导致(0,0,0)。仔细分析一下,(1,2,3)也是奇异局势,无论对手如何拿,接下来都可以变为(0,n,n)的情形。计算机算法里面有一种叫做按位模2加,也叫做异或的运算(相异为1,相同为0),我们用符号(+)表示这种运算。这种运算和一般加法不同的一点是1+1=0。先看(1,2,3)的按位模2加的结果:

1 =二进制01

2 =二进制10

3 =二进制11 (+)

———————

0 =二进制00 (注意不进位)

对于奇异局势(0,n,n)也一样,结果也是0。

任何奇异局势(a,b,c)都有a(+)b(+)c =0。

如果我们面对的是一个非奇异局势(a,b,c),要如何变为奇异局势呢?假设 a < b< c,我们只要将 c 变为 a(+)b,即可,因为有如下的运算结果:a(+)b(+)(a(+)b)=(a(+)a)(+)(b(+)b)=0(+)0=0。要将c 变为a(+)b,只要从 c中减去 c-(a(+)b)即可。

此题便是尼姆博弈的典型例子,只要找到怎样改变使可移动的台阶数之间异或相加之后结果为0就是必赢局面,如果没有移动之前异或相加的值就是0,那么是必输局面,先求出n个和尚中每个和尚可以向上移动的台阶数量,再将和尚两两分组,1和2、3和4、……、2i-1和2i,只需要求出两两分组之间台阶数的异或相加值就可以判断是否为必赢局面,因为移动3可以通过移动2达到同样的异或结果,同理移动4也可以通过移动5达到同样的异或结果,所以有影响的只有两两组合之间的台阶数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
public class Nimm_Game {
public static void main(String[] args) {
Scanner input = new Scanner(System.in);
String[] nim = input.nextLine().split(" ");
int[] address = new int[nim.length]; // 用来存放和尚站的阶梯的位置
for(int i = 0; i < address.length; i++) {
address[i] = Integer.parseInt(nim[i]);
}
game(address);
input.close();
}
private static void game(int[] address) {
//用于存放和尚之间的阶梯数量
int[] count = new int[address.length];
for(int i = 1; i < address.length; i++) {
count[i - 1] = address[i] - address[i - 1] - 1;
}
int sum = 0; // 用于记录走的步数的异或和
for(int i = 0; i < count.length; i += 2) {
sum ^= count[i]; //异或和
}
if(sum == 0) {// 如果异或和结果为0,则必输
System.out.println("必输!");
}else {
for(int i = 0; i < address.length; i++) { // 所有和尚的移动,一个一个的进行枚举
for(int j = 1; j <= count[i]; j++) { //从第一节台阶开始进行枚举
count[i] -= j;
if(i > 0) {
count[i - 1] += j;
}
sum = 0; //每走一步重新计算异或值
for(int k = 0; k < count.length; k += 2) {
sum ^= count[k];
}
if(sum == 0) {
System.out.println(address[i] + " " + (address[i] + j));
break;
}
//回溯
count[i] += j;
if(i > 0) {
count[i - 1] -= j;
}
}
if(sum == 0) {//特判,可加可不加
break;
}
}
}
}
}

🍁高斯日记

大数学家高斯有个好习惯:无论如何都要记日记。

他的日记有个与众不同的地方,他从不注明年月日,而是用一个整数代替,比如:4210

后来人们知道,那个整数就是日期,它表示那一天是高斯出生后的第几天。这或许也是个好习惯,它时时刻刻提醒着主人:日子又过去一天,还有多少时光可以用于浪费呢

高斯出生于:1777年4月30日。

在高斯发现的一个重要定理的日记上标注着:5343,因此可算出那天是:1791年12月15日。

高斯获得博士学位的那天日记上标着:8113

请你算出高斯获得博士学位的年月日。

🍂分析

此题没什么好分析的,单纯的暴力模拟日期,一天天的加上去就ok了(能用Excel首用Excel,能用API首用API)

利用API

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Gauss_Diary {
public static void main(String[] args) {
getDate();
}
private static void getDate() {
Calendar c = Calendar.getInstance();
c.set(1777, 04, 30);
c.add(Calendar.DATE, 8113);

int year = c.get(Calendar.YEAR);
int month = c.get(Calendar.MONTH);
String m = "";
if(month < 10) {
m = "0" + month;
}else {
m += month;
}
int day = c.get(Calendar.DATE);
System.out.println(year + "-" + m + "-" + day);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
public class Gauss_Diary {
public static void main(String[] args) {
getDate2();
}
private static void getDate2() {
int year = 1777, month = 4, day = 30;
int num = 1;
while(num < 8113) {
if(month == 12 && day == 31) {
year++;
month = 1;
day = 1;
}else if(day == getDays(year, month)) {
month++;
day = 1;
}else {
day++;
}
num++;
}
String m = "";
if(month < 10) {
m = "0" + month;
}else {
m += month;
}
System.out.println(year + "-" + m + "-" + day);
}
/**
* 判断闰年
* @param year
* @return
*/
private static boolean isleap(int year) {
return (year % 4 == 0 && year % 100 != 0) || year % 400 == 0;
}
/**
* 计算当年某月的天数
* @param year
* @param month
* @return
*/
private static int getDays(int year, int month) {
int days;
switch(month) {
case 2: {
if(isleap(year)) {
days = 29;
}else {
days = 28;
}
break;
}
case 4:
case 6:
case 9:
case 11: {
days = 30;
break;
}
default :
days = 31;
}
return days;
}
}

🍁古代赌局

俗话说:十赌九输。因为大多数赌局的背后都藏有阴谋。

不过也不尽然,有些赌局背后藏有的是:“阳谋”。

有一种赌局是这样的:桌子上放六个匣子,编号是1至6。

多位参与者(以下称玩家)可以把任意数量的钱押在某个编号的匣子上。

所有玩家都下注后,庄家同时掷出3个骰子(骰子上的数字都是1至6)。

输赢规则如下:

1.若只有1个骰子上的数字与玩家所押注的匣子号相同,则玩家拿回自己的押注,庄家按他押注的数目赔付(即1比1的赔率)。

2.若2个骰子上的数字与玩家所押注的匣子号相同,则玩家拿回自己的押注,庄家按他押注的数目的2倍赔付(即1比2的赔率)。

3.若3个骰子上的数字都与玩家押注的匣子号相同,则玩家拿回自己的押注,庄家按他押注的数目的10倍赔付(即1比10的赔率)。

乍一看起来,好像规则对玩家有利,庄家吃亏。但经过大量实战,会发现局面很难说,于是怀疑是否庄家做了手脚,庄家则十分爽快地说:可以由玩家提供骰子,甚至也可以由玩家来投掷骰子。

你的任务是:通过编程模拟该过程。模拟50万次,假定只有1个玩家,他每次的押注都是1元钱,其押注的匣子号是随机的。再假定庄家有足够的资金用于赔付。最后计算出庄家的盈率(庄家盈利金额/押注总金额)。

🍂分析

直接模拟赌博过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class Ancient_Gambling {
static int sum = 0; //押注总钱数
public static void main(String[] args) {
int m = 1; // 押注都是1元钱
int n; // 押注匣子号
int a; // 第一个骰子
int b; // 第二个骰子
int c; // 第三个骰子
for(int i = 0; i < 500000; i++) {
n = (int)(Math.random() * 6 + 1);
a = (int)(Math.random() * 6 + 1);
b = (int)(Math.random() * 6 + 1);
c = (int)(Math.random() * 6 + 1);
gamble(m, n, a, b, c);
}
double f = sum / 500000.0;
System.out.println(f);
}
private static void gamble(int m, int n, int a, int b, int c) {
if(n == a && n == b && n == c) {
sum -= m * 10;
}else if((n == a && n == b) || (n == a && n == c) || (n == b && n == c)) {
sum -= m * 2;
}else if(n == a || n == b || n == c) {
sum -= m;
}else {
sum += m;
}
}
}

🍁国庆节

1949年的国庆节(10月1日)是星期六。

今年(2012)的国庆节是星期一。

那么,从建国到现在,有几次国庆节正好是星期日呢?

只要答案,不限手段!

可以用windows日历,windows计算器,Excel公式,。。。。。

当然,也可以编程!

🍂普通解法

利用当年10月1日到1949年10月2日的总天数计算模以7是否为0,如果是则代表那天为星期日

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public class National_day {
public static void main(String[] args) {
int count = 0;
// 1949年的国庆节(10月2日)是星期日。
// 得到这年10月2号后的的剩余天数
int sum = getCal(1949, 12, 31) - getCal(1949, 10, 2);
for(int i = 1950; i < 2012; i++) {
sum += getCal(i, 10, 1);
if(sum % 7 == 0) {
System.out.println(i + "年10月1日");
count++;
}
}
System.out.println("总数:" + count);
}
/**
* 判断闰年
* @param year
* @return
*/
private static boolean isleap(int year) {
return (year % 4 == 0 && year % 100 != 0) || year % 400 == 0;
}
/**
* 计算当年当日前的总天数
* @param year
* @param month
* @param day
* @return
*/
private static int getCal(int year, int month, int day) {
int[][] days = {{0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31},{0, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}};
int run = 0;//默认不是闰年
if(isleap(year)) {
run = 1;
}
int sum = day;
for(int i = 0; i < month; i++) {
sum += days[run][i];
}
return sum;
}
}

🍂解法二:基姆拉尔森计算公式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class National_day {
public static void main(String[] args) {
search();
}
/**
* 基姆拉尔森计算公式
*/
private static void search() {
int count = 0;
for(int i = 1950; i < 2012; i++) {
if(jim(i, 10, 1) == 6) {
System.out.println(i + "年10月1日");
count++;
}
}
System.out.println("总数:" + count);
}
private static int jim(int year, int month, int day) {
if(month == 1 || month == 2) {
month += 12;
year--;
}
return (day + 2 * month + 3 * (month + 1) / 5 + year + year / 4 - year / 100 + year / 400) % 7;
}
}

🍁火柴游戏

这是一个纵横火柴棒游戏。

如表

3 |
2
1
A B C D

在3x4的格子中,游戏的双方轮流放置火柴棒。

其规则是:

  1. 不能放置在已经放置了火柴棒的地方(即只能在空格中放置)。

  2. 火柴棒的方向只能是竖直或水平放置。

  3. 火柴棒不能与其它格子中的火柴“连通”。

所谓连通是指两根火柴棒可以连成一条直线,且中间没有其它不同方向的火柴“阻拦”。

例如:

表所示的局面下,可以在C2位置竖直放置(为了方便描述格子位置,图中左、下都添加了标记),但不能水平放置,因为会与A2连通。

同样道理,B2,B3,D2此时两种方向都不可以放置。

但如果C2竖直放置后,D2就可以水平放置了,因为不再会与A2连通(受到了C2的阻挡)。

  1. 游戏双方轮流放置火柴,不可以弃权,也不可以放多根。

如某一方无法继续放置,则该方为负(输的一方)。

游戏开始时可能已经放置了多根火柴。

你的任务是:编写程序,读入初始状态,计算出对自己最有利的放置方法并输出放置后的局面。

图1的局面表示为:

1
2
3
4
5
6
7
00-1

-000

0100

即用“0”表示空闲位置,用“1”表示竖直放置,用“-”表示水平放置。

解法不唯一,找到任意解法即可。

例如,局面:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
0111
-000
-000
的解:
-111
-000
-000

再例如,局面:
1111
----
0010
的解:
1111
----
0110

🍂分析

1
思考中。。

🍁取球游戏

今盒里有n个小球,A、B两人轮流从盒中取球。

每个人都可以看到另一个人取了多少个,也可以看到盒中还剩下多少个。

两人都很聪明,不会做出错误的判断。

每个人从盒子中取出的球的数目必须是:1,3,7或者8个。

轮到某一方取球时不能弃权!

A先取球,然后双方交替取球,直到取完。

被迫拿到最后一个球的一方为负方(输方)

编程确定出在双方都不判断失误的情况下,对于特定的初始球数,A是否能赢?

🍂分析

已知每人最多能取1,3,7或者8个,那么,球总数=乙必输的情况+甲拿(1,3,7或者8个),球为1时甲必输,以此类推(可以正向迭代和逆向递归),正向:将10000以内的输赢情况都存入数组,然后查表。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class Ball_Game {

public static void main(String[] args) {
Scanner input = new Scanner(System.in);
int n = input.nextInt();
List<Integer> list = new ArrayList<>();
for(int i = 0; i < n; i++) {
list.add(input.nextInt());
}
ball(list);
input.close();
}
private static void ball(List<Integer> list) {
int[] take = {1, 3, 7, 8};
int[] res = new int[10010];
for(int i = 1; i <= 10000; i++) {
if(res[i] == 0) {
for(int j = 0; j < 4; j++) {
res[i + take[j]] = 1;
}
}
}
for(int i = 0; i < list.size(); i++) {
System.out.println("结果:" + res[list.get(i)]);
}
}
}

逆向:球数是n时,只要满足n-1,n-3,n-7和n-8有一个数字对于乙玩家来说是必输的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public class Ball_Game {

public static void main(String[] args) {
Scanner input = new Scanner(System.in);
int n = input.nextInt();
List<Integer> list = new ArrayList<>();
for(int i = 0; i < n; i++) {
list.add(input.nextInt());
}
ball2(list);
input.close();
}
private static void ball2(List<Integer> list) {
for(int i = 0; i < list.size(); i++) {
if(test(list.get(i))) {
System.out.println(1);
}else {
System.out.println(0);
}
}
}
private static boolean test(int n) {
// TODO Auto-generated method stub
if(n >= 1) {
switch(n) {
case 1:
return false;// 剩1个球,则输
case 3:
return false;// 剩3个球,则输
case 7:
return false;// 剩7个球,则输
case 8:
return true;// 剩8个球,则赢
// 如果不是不是1,3,7,8则 选择权交给B,B任然调用该函数,不过返回值需要取反
// 而此时A可以选的只有 1 3 7 8 所以用num减去之 逐个测试即可
default:
return (!test(n - 8) || !test(n - 7) || !test(n - 3) || !test(n - 1));
}
}else {
return false;
}
}
}

🍁填字母游戏

K大师在纸上画了一行n个格子,要小明和他交替往其中填入字母。

  1. 轮到某人填的时候,只能在某个空 格中填入L或O

  2. 谁先让字母组成了“LOL”的字样,谁获胜。

  3. 如果所有格子都填满了,仍无法组成LOL,则平局。

小明试验了几次都输了,他很惭愧,希望你能用计算机帮他解开这个谜。

本题的输入格式为:

第一行,数字n(n<10),表示下面有n个初始局面。

接下来,n行,每行一个串,表示开始的局面。

比如:“**”, 表示有6个空格。“L** **”, 表示左边是一个字母L,它的右边是4个空格。

要求输出n个数字,表示对每个局面,如果小明先填,当K大师总是用最强着法的时候,小明的最好结果。

1 表示能赢

-1 表示必输

0 表示可以逼平

例如,

1
2
3
4
5
6
7
8
9
10
11
12
输入:
4
***
L**L
L**L***L
L*****L

则程序应该输出:
0
-1
1
1

🍂分析

利用回溯,一步一步的进行递归尝试,每走一步进行一次递归,将结果放入哈希表(利用记忆化哈希表减少递归查找的时间复杂度)。具体详情可查看代码注释

下面介绍带平局的博弈类解法魔板:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
main函数里调用dfs (a)
给定一个局面a,dfs(a)是计算我方通过最优步骤走是输还是赢还是平局。
利用map容器记录局面胜负平的状态可减少重复局面的运算提高算法效率
胜负平
dfs (局面a) {
边界值处理
for (所有可走的局面) {
试着走一步,得到一个新局面b
交给对手走,即判断dfs (b)的返回值
恢复局面
如果返回值为负,则我必赢,即这步是我的最优走法
如果返回值为赢,则我必输,不可取
如果返回值为平,则我至少可以逼平对手,先存着这个结果
}
如果算的可以逼平,则返回平
否则返回负|
}

解法代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
public class Alphabet_Game {
public static void main(String[] args) {
Scanner input = new Scanner(System.in);
int n = input.nextInt();
input.nextLine();
for(int i = 0; i < n; i++) {
String str = input.nextLine();
System.out.println(dfs(str.toCharArray()));
}
//System.out.println(dfs("L**L".toCharArray()));
input.close();
}
private static Map<String, Integer> map = new HashMap<>();
private static int dfs(char[] a) {
// TODO Auto-generated method stub
String s = new String(a);
if(map.containsKey(s)) {
return map.get(s);
}
if(s.contains("LOL")) { //如果对手给我的局面是包含了”LOL"的情况,那我就输了
return -1;
}
if(!s.contains("*")) { //如果对手给我的局面是没有空格可以填的情况,那我没赢也没输,返回平局
return 0;
}
boolean ping = false; //先假设我跟对手之间不可能出现平局

for(int i = 0; i < a.length; i++) { //开始循环,对有空格进行试填
if(a[i] == '*') { //循环开始,先判断遍历到的地方是不是空格,这里是空格的情况,
a[i] = 'L'; //自己进行尝试填写
//自己下完后交给对手进行判断
//此时switch()中传入的dfs(a)已经不是最开始传入的a数组了
//而是试填L后的a数组
int res = dfs(a);
//存入哈希表
map.put(String.valueOf(a), res);
switch(res) {
case -1: //如果对手返回-1,那么就是我赢了,我就返回1
return 1;
//如果对手返回0就是对手有可把我逼平的方法,此时我不是直接返回0是因为
//我继续下下一个空格可能有将对手赢了的可能,所以我先将ping置为true
case 0:
ping = true;
}
//回到我自己下
a[i] = 'O';
//交给对手
res = dfs(a);
map.put(String.valueOf(a), res);
//回溯,因为你们操作的是同一个数组
a[i] = '*';
switch(res) {
case -1:
return 1;
case 0:
ping = true;
}

}
}
//最后返回如果对手没有返回输的话,就是我赢不了对手了,
//这个时候我再来看看是否有平局的可能
if(ping) {
return 0;
}
//平局都没有,那我就输了
return -1;
}
}

最后,不经历风雨,怎能在计算机的大山之顶看见彩虹呢! 无论怎样,相信明天一定会更好!!!!!

-------------本文结束感谢您的阅读-------------
云澈 wechat
扫一扫,用手机访问哦
坚持原创技术分享,您的支持将鼓励我继续创作!
0%