2016Java-B组省赛
一、煤球数目(找规律)
煤球数目
有一堆煤球,堆成三角棱锥形。具体:
第一层放1个,
第二层3个(排列成三角形),
第三层6个(排列成三角形),
第四层10个(排列成三角形),
....
如果一共有100层,共有多少个煤球?
请填表示煤球总数目的数字。
注意:你提交的应该是一个整数,不要填写任何多余的内容或说明性文字。
3 - 1 = 2 ; 6 - 3 = 3 ; 10 - 6 = 4
public class Main {
public static void main(String[] args) {
int ans = 20;
int cnt = 4;
int before = 10;
for (int i = 5; i <= 100; i++) {
cnt++;
before += cnt;
ans += before;
}
System.out.println(ans);
}
}
答案:171700
二、生日蜡烛(模拟)
生日蜡烛
某君从某年开始每年都举办一次生日party,并且每次都要吹熄与年龄相同根数的蜡烛。
现在算起来,他一共吹熄了236根蜡烛。
请问,他从多少岁开始过生日party的?
请填写他开始过生日party的年龄数。
注意:你提交的应该是一个整数,不要填写任何多余的内容或说明性文字。
模拟
public class Main {
public static void main(String[] args) {
int ans = 0;
for (int i = 1; i <= 50; i++) {
ans = i;
for (int j = i + 1; j <= 50; j++) {
ans += j;
if (ans == 236) {
System.out.println(i);
break;
}
}
if (ans == 236) break;
}
}
}
答案:26
三、凑算式(全排列、模拟)
凑算式
B DEF
A + --- + ------- = 10
C GHI
(如果显示有问题,可以参见【图1.jpg】)
这个算式中A~I代表1~9的数字,不同的字母代表不同的数字。
比如:
6+8/3+952/714 就是一种解法,
5+3/1+972/486 是另一种解法。
这个算式一共有多少种解法?
注意:你提交应该是个整数,不要填写任何多余的内容或说明性文字。
A * C + B + DEF * C/GHI = C * 10
转换成乘法,避免分数加减
A * C * GHI + B * GHI + DEF * C = C * 10 * GHI
import java.util.LinkedList;
public class Main {
static LinkedList<Integer> tmp = new LinkedList<>();
static int[] vis = new int[10];
static int ans = 0;
public static void main(String[] args) {
dfs();
System.out.println(ans);
}
static void dfs() {
if (tmp.size() == 9) {
int def = tmp.get(3) * 100 + tmp.get(4) * 10 + tmp.get(5);
int ghi = tmp.get(6) * 100 + tmp.get(7) * 10 + tmp.get(8);
int a = tmp.get(0);
int b = tmp.get(1);
int c = tmp.get(2);
if (a * c * ghi + b * ghi + def * c == c * 10 * ghi) {
ans++;
System.out.printf("%d %d %d %d %d\n", a, b, c, def, ghi);
}
return;
}
if (tmp.size() > 9) {
return;
}
for (int i = 1; i <= 9; i++) {
if (vis[i] == 1) {
continue;
}
vis[i] = 1;
tmp.add(i);
dfs();
tmp.removeLast();
vis[i] = 0;
}
}
}
答案:29
四、五、程序填空题
六、方格填数(全排列、搜索)
方格填数
如下的10个格子
+--+--+--+
| | | |
+--+--+--+--+
| | | | |
+--+--+--+--+
| | | |
+--+--+--+
(如果显示有问题,也可以参看【图1.jpg】)
填入0~9的数字。要求:连续的两个数字不能相邻。
(左右、上下、对角都算相邻)
一共有多少种可能的填数方案?
请填写表示方案数目的整数。
注意:你提交的应该是一个整数,不要填写任何多余的内容或说明性文字。
填入0~9的数字。要求:连续的两个数字不能相邻。
(左右、上下、对角都算相邻)
+--+--+--+
| 5 | 6 | 9 |
+--+--+--+--+
| 1 | 4 | 7 | 10 |
+--+--+--+--+
| 2 | 3 | 8 |
+--+--+--+
按照上面的顺序,生成全排列填数。关键在于check判断条件。
import java.util.LinkedList;
public class Main {
static int ans = 0;
static LinkedList<Integer> tmp = new LinkedList<>();
static int[] vis = new int[10];
public static void main(String[] args) {
// 先把0-9的全排列求出来,然后看check
dfs();
System.out.println(ans);
}
static void dfs() {
if (tmp.size() == 10) {
int a = tmp.get(0);
int b = tmp.get(1);
int c = tmp.get(2);
int d = tmp.get(3);
int e = tmp.get(4);
int f = tmp.get(5);
int g = tmp.get(6);
int h = tmp.get(7);
int i = tmp.get(8);
int j = tmp.get(9);
boolean flag = true;
if (Math.abs(a - b) == 1 || Math.abs(b - c) == 1 || Math.abs(c - d) == 1 || Math.abs(a - d) == 1
|| Math.abs(a - c) == 1 || Math.abs(b - d) == 1 || Math.abs(a - e) == 1 || Math.abs(d - e) == 1
|| Math.abs(e - f) == 1 || Math.abs(d - f) == 1 || Math.abs(g - f) == 1 || Math.abs(g - e) == 1
|| Math.abs(g - d) == 1 || Math.abs(g - c) == 1 || Math.abs(h - g) == 1 || Math.abs(h - c) == 1
|| Math.abs(h - d) == 1 || Math.abs(i - f) == 1 || Math.abs(i - g) == 1 || Math.abs(i - j) == 1
|| Math.abs(j - f) == 1 || Math.abs(j - g) == 1 || Math.abs(j - h) == 1) {
flag = false;
}
if (flag) {
ans++;
}
return;
}
if (tmp.size() > 10) {
return;
}
for (int i = 0; i <= 9; i++) {
if (vis[i] == 1) {
continue;
}
vis[i] = 1;
tmp.add(i);
dfs();
vis[i] = 0;
tmp.removeLast();
}
}
}
答案:1580
看一道之前的题目:寒假作业
这道题是找1-13排列中的12个数,如果直接暴力搜索,时间开销很大,所以可以通过剪枝缩减时间,第一个等式不满足了就不用往下找了。
import java.util.LinkedList;
public class Main {
static LinkedList<Integer> tmp = new LinkedList<>();
static int ans = 0;
static int[] vis = new int[14];
public static void main(String[] args) {
dfs();
System.out.println(ans);
}
static void dfs() {
// 剪枝
if (tmp.size() == 3 && tmp.get(0) + tmp.get(1) != tmp.get(2)) {
return;
}
// 剪枝
if (tmp.size() == 6 && tmp.get(3) - tmp.get(4) != tmp.get(5)) {
return;
}
if (tmp.size() == 12) {
if (tmp.get(0) + tmp.get(1) == tmp.get(2) && tmp.get(3) - tmp.get(4) == tmp.get(5)
&& tmp.get(6) * tmp.get(7) == tmp.get(8) && tmp.get(9) == tmp.get(10) * tmp.get(11)) {
System.out.println(tmp);
ans++;
}
return;
}
if (tmp.size() > 12) {
return;
}
for (int i = 1; i <= 13; i++) {
if (vis[i] == 1) {
continue;
}
vis[i] = 1;
tmp.add(i);
dfs();
tmp.removeLast();
vis[i] = 0;
}
}
}
答案:64
做了这道题,一定要学会之后在进行暴力搜索时,学会剪枝。
七、※※※剪邮票(搜索、连通性检测)、※※剪格子(n选m全排列(去重))
剪邮票
如【图1.jpg】, 有12张连在一起的12生肖的邮票。
现在你要从中剪下5张来,要求必须是连着的。
(仅仅连接一个角不算相连)
比如,【图2.jpg】,【图3.jpg】中,粉红色所示部分就是合格的剪取。
请你计算,一共有多少种不同的剪取方法。
请填写表示方案数目的整数。
注意:你提交的应该是一个整数,不要填写任何多余的内容或说明性文字。
先看一下剪格子
下意识是用dfs解,但是否可行?
暂且不探讨这道题,先来看看DFS的搜索路径,给定3x3格子,从上到下,从左到右依次为0-8,用dfs搜索来看看路径(路径长度为4)如何:
import java.util.LinkedList;
public class Main {
static LinkedList<Integer> tmp = new LinkedList<>();
static int ans = 0;
static int[][] vis = new int[4][4];
static int[] xx = {-1,1,0,0};
static int[] yy = {0,0,-1,1};
public static void main(String[] args) {
int[][] map = new int[3][3];
int index = 0;
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
map[i][j] = index;
index++;
}
}
vis[0][0] = 1;
tmp.add(map[0][0]);
dfs(map, 0, 0);
}
static void dfs(int[][] map, int x, int y) {
if (tmp.size() == 4) {
System.out.println(tmp);
return;
}
for (int i = 0; i < 4; i++) {
int tmpx = xx[i] + x;
int tmpy = yy[i] + y;
if (tmpx < 0 || tmpy < 0 || tmpx >= 3 || tmpy >= 3 || vis[tmpx][tmpy] == 1) {
continue;
}
vis[tmpx][tmpy] = 1;
tmp.add(map[tmpx][tmpy]);
dfs(map, tmpx, tmpy);
tmp.removeLast();
vis[tmpx][tmpy] = 0;
}
}
}
规定从左上角0出发,得到如下结果:
把图画出来,会发现,dfs遍历的结果没有T字型!,例如:0124,这是为什么?DFS是一搜到底,再往回回溯,0124这种情况是遍历不到的,它要么走012到5,要么走01到4,不可能有从01到4再到2,或者01到2再到4。所以如果这种题用dfs遍历去解,肯定是错误的!
如果用BFS,会是怎样的遍历路径?
import java.util.LinkedList;
import java.util.Queue;
class node {
int x, y;
LinkedList<Integer> anser;
node() {};
node(int x, int y) {this.x = x; this.y = y;}
}
public class Main {
static int ans = 0;
static int[][] vis = new int[4][4];
static int[] xx = {-1,1,0,0};
static int[] yy = {0,0,-1,1};
public static void main(String[] args) {
int[][] map = new int[3][3];
int index = 0;
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
map[i][j] = index;
index++;
}
}
vis[0][1] = 1;
Queue<node> Q = new LinkedList<>();
LinkedList<Integer> start = new LinkedList<>();
node b = new node(0, 1);
b.anser = start;
b.anser.add(map[0][1]);
// 开始结点装入Q
Q.offer(b);
while (!Q.isEmpty()) {
node temp = Q.poll();
for (int i = 0; i < 4; i++) {
node tmp = new node();
tmp.x = temp.x + xx[i];
tmp.y = temp.y + yy[i];
if (tmp.x < 0 || tmp.y < 0 || tmp.x >= 3 || tmp.y >= 3 || vis[tmp.x][tmp.y] == 1) {
continue;
}
vis[tmp.x][tmp.y] = 1;
tmp.anser = temp.anser;
tmp.anser.add(map[tmp.x][tmp.y]);
System.out.println(tmp.anser);
Q.offer(tmp);
}
}
}
}
分析代码下面代码可以知道:
每次都是把当前结点四个方向可能的值,加入队列中,队列是先进先出,所以会优先把当前结点的四个方向遍历完再接着遍历,而不是和dfs一样,一直深入的遍历。一定要把DFS和BFS的遍历方向搞清楚。
回到之前的题目,如果按照dfs方式,可以写出下列代码,由于测试样例没有考虑完全(没有T字型裁剪),所以导致普通的dfs可以通过测试。
import java.util.LinkedList;
import java.util.Scanner;
public class Main {
static LinkedList<Integer> tmp = new LinkedList<>();
static int ans = 99;
static int m, n;
static int[][] vis = new int[20][20];
static int[][] map = new int[20][20];
static int[] xx = {-1,1,0,0};
static int[] yy = {0,0,-1,1};
static int sum = 0;
public static void main(String[] args) {
Scanner scan = new Scanner(System.in);
m = scan.nextInt();
n = scan.nextInt();
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
map[i][j] = scan.nextInt();
sum += map[i][j];
}
}
sum /= 2;
vis[0][0] = 1;
dfs(map, 0, 0, map[0][0], 1);
System.out.println(ans);
}
static void dfs(int[][] map, int x, int y, int cnt, int step) {
if (cnt == sum) {
ans = Math.min(ans, step);
return;
}
for (int i = 0; i < 4; i++) {
int tmpx = x + xx[i];
int tmpy = y + yy[i];
if (tmpx < 0 || tmpy < 0 || tmpx >= n || tmpy >= m || vis[tmpx][tmpy] == 1) {
continue;
}
vis[tmpx][tmpy] = 1;
dfs(map, tmpx, tmpy, cnt + map[tmpx][tmpy], step + 1);
vis[tmpx][tmpy] = 0;
}
}
}
上面的代码能通过测试,完全是偶然,回到本题,就必须得考虑T字型剪纸,使用普通的dfs解决本题是不行的,它无法解决T字型问题。
测评样例没有考虑T字形问题
之前的想法是:以每个点为起点,dfs选5张,再去重,但这是错误的!
正解:枚举所有的5张牌的组合,从12个格子里面抽5个,看它们是不是一个连通块
现在问题转换成求12选5(组合),再check是否连通。
12选5,如果直接求12选5的组合,可能需要先求出12的排列,再选前5个,还需要去重。显然这么做是有问题的,时间开销太大。
换种思路,12选5,只要能够选出5个就行,那么我们用一个一维数组,长度为12,里面有5个1,7个0,5个1代表要选的5个数,我们只用遍历这12个元素,找它们的全排列即可。(每次为1的地方就是剪纸的一种方式)
连通块检测:
// 连通块检测
static void dfss(int[][] g, int x, int y) {
for (int i = 0; i < 4; i++) {
int tmpx = x + xx[i];
int tmpy = y + yy[i];
if (tmpx < 0 || tmpy < 0 || tmpx >= 3 || tmpy >= 4 || g[tmpx][tmpy] == 0) {
continue;
}
g[tmpx][tmpy] = 0;
dfss(g, tmpx, tmpy);
// 连通块检测不需要回溯!
}
}
......
int cnt = 0;
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 4; j++) {
if (g[i][j] == 1) {
g[i][j] = 0;
// 连通性检测
dfss(g, i, j);
cnt++;
}
}
}
// 只用了一次遍历,说明它们都是连通的
if (cnt == 1) {
ans++;
}
之前说的12选5,只往里面塞了5个1,但是有多个重复的0,和重复的1,会导致全排列重复,我们需要去重(这里和之前回溯专题很类似)。
注意,dfs不能生成T型剪纸,但是dfs可以检测T型剪纸(就是检测连通块)
import java.util.*;
public class Main {
static LinkedList<Integer> tmp = new LinkedList<>();
static int[] vis = new int[13];
static int[] xx = new int[] {-1,1,0,0};
static int ans = 0;
static int[] yy = new int[] {0,0,-1,1};
public static void main(String[] args) {
int[] a = new int[] {0,0,0,0,0,0,0,1,1,1,1,1};
dfs(a);
System.out.println(ans);
}
// 连通块检测
static void dfss(int[][] g, int x, int y) {
for (int i = 0; i < 4; i++) {
int tmpx = x + xx[i];
int tmpy = y + yy[i];
if (tmpx < 0 || tmpy < 0 || tmpx >= 3 || tmpy >= 4 || g[tmpx][tmpy] == 0) {
continue;
}
g[tmpx][tmpy] = 0;
dfss(g, tmpx, tmpy);
// 连通块检测不需要回溯!
}
}
static void dfs(int[] a) {
if (tmp.size() == 12) {
System.out.println(tmp);
int[][] g = new int[3][4];
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 4; j++) {
// 把一维数组转成二维数组
if (tmp.get(i * 4 + j) == 1) {
g[i][j] = 1;
} else {
g[i][j] = 0;
}
}
}
int cnt = 0;
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 4; j++) {
if (g[i][j] == 1) {
g[i][j] = 0;
dfss(g, i, j);
cnt++;
}
}
}
// 只用了一次遍历,说明它们都是连通的
if (cnt == 1) {
ans++;
}
// 再用dfs进行连通性检测
return;
}
if (tmp.size() > 12) {
return;
}
for (int i = 0; i < 12; i++) {
// 全排列去重
if (i > 0 && a[i] == a[i - 1] && vis[i - 1] == 0) {
continue;
}
if (vis[i] == 1) {
continue;
}
vis[i] = 1;
tmp.add(a[i]);
dfs(a);
tmp.removeLast();
vis[i] = 0;
}
}
}
全排列去重核心代码:
if (i > 0 && a[i] == a[i - 1] && vis[i - 1] == 0) {
continue;
}
如果是求子集、组合,去重代码:
if (i > start && nums[i - 1] == nums[i]) {
continue;
}
搞懂这些,最好是通过画图,debug来理解整个过程,不要死记硬背。
答案:116
八、四平方和(循环剪枝)
直接四循环会超时,可以变成三循环,确定前三个后,第四个数可以直接算出。这也可以算成剪枝的方案。
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner scan = new Scanner(System.in);
int n = scan.nextInt();
f(n);
}
static void f(int n) {
for (int i = 0; i <= Math.sqrt(n); i++) {
for (int j = i; j <= Math.sqrt(n); j++) {
for (int k = j; k <= Math.sqrt(n); k++) {
int tmp = (int)Math.sqrt(n - i * i - j * j - k * k);
if (n == i * i + j * j + k * k + tmp * tmp) {
System.out.printf("%d %d %d %d", i, j, k, tmp);
return;
}
}
}
}
}
}
九、※取球博弈(记忆型递归)
持有奇数个球的一方获胜,如果两人都是奇数则平局。
对于每堆球N,每个人都有n个取法,加入N大于每种取法的取球数,那么先手取球剩余球数可能为:N - n1,N - n2,N - n3。同样,后手也有三种可能。 对于这种存在重复子问题的情况,考虑使用递归解决,使用递归要明确参数和终止条件。
递归参数无非就是,剩余球数,我有的球数,你有的球数,f(N, me, you),但是要注意,一旦身份互换后,me就变成了you,you就变成了me。
注意博弈问题,会有身份互换,me变成you,you变成me。
可以写出下列递归代码:
import java.util.*;
public class Main {
static int[] n = new int[3];
public static void main(String[] args) {
Scanner scan = new Scanner(System.in);
n[0] = scan.nextInt();
n[1] = scan.nextInt();
n[2] = scan.nextInt();
// 排序方便后续结算
Arrays.sort(n);
for (int i = 0; i < 5; i++) {
int num = scan.nextInt();
char res = f(num, 0, 0);
System.out.print(res + " ");
}
}
/**
* 参数代表当前取球人面临的局面
* @param num 球的总数
* @param me 我方持有的数目
* @param you 对手持有的数目
* @return
*/
static char f(int num, int me, int you) {
// 不够取
if (num < n[0]) {
if ((me & 1) == 1 && (you & 1) == 0) {
// 我是奇数,对手是偶数,我就赢了
return '+';
} else if ((me & 1) == 0 && (you & 1) == 1) {
// 我是偶数,对手是奇数,我就输了
return '-';
} else {
// 都是偶数,或者都是奇数,就平局
return '0';
}
}
boolean ping = false;
for (int i = 0; i < 3; i++) {
if (num >= n[i]) {
// 注意换位
// 这里拿到的是对手的输赢情况
char res = f(num - n[i], you, me + n[i]);
if (res == '-') {
// 对手输了,我们就赢了
return '+';
}
if (res == '0') {
ping = true;
}
}
}
// 如果能走到这里,那就不存在对手输的情况
// 那么是否存在平局的情况?
if (ping) {
return '0';
} else {
// 不存在对手输,也不平,那就是输了
return '-';
}
}
}
上面的代码在球多时,会超时,因为会进行多次重复计算,例如:取球顺序123和取球顺序321,本质是一样的。所以要变成记忆型递归。
不需要考虑最后的me、you的球数,只需要考虑奇偶性即可(这也是为了缩小空间消耗),用三维数组存储已有的状态,改成记忆型递归,只需要在return的地方存储数据,在递归函数开始的地方查找cache。
import java.util.*;
public class Main {
static int[] n = new int[3];
static char[][][] cache = new char[1000][2][2];
public static void main(String[] args) {
Scanner scan = new Scanner(System.in);
n[0] = scan.nextInt();
n[1] = scan.nextInt();
n[2] = scan.nextInt();
// 排序方便后续结算
Arrays.sort(n);
for (int i = 0; i < 5; i++) {
int num = scan.nextInt();
char res = f(num, 0, 0);
System.out.print(res + " ");
}
}
/**
* 参数代表当前取球人面临的局面
* @param num 球的总数
* @param me 我方持有的数目 => 实质关注的是数目的奇偶性
* @param you 对手持有的数目 => 实质关注的是数目的奇偶性
* @return
*/
static char f(int num, int me, int you) {
// 不够取
if (num < n[0]) {
if ((me & 1) == 1 && (you & 1) == 0) {
// 我是奇数,对手是偶数,我就赢了
return '+';
} else if ((me & 1) == 0 && (you & 1) == 1) {
// 我是偶数,对手是奇数,我就输了
return '-';
} else {
// 都是偶数,或者都是奇数,就平局
return '0';
}
}
// 如果cache中找到了
if (cache[num][me][you] != '\0') {
return cache[num][me][you];
}
boolean ping = false;
for (int i = 0; i < 3; i++) {
if (num >= n[i]) {
// 注意换位
// 这里拿到的是对手的输赢情况
// 记录的是奇偶情况,如果n[i]是奇数,me拿了之后应该为1 - me(改变奇偶性)
int tmp = me;
if (n[i] % 2 != 0) {
tmp = 1 - tmp;
}
char res = f(num - n[i], you, tmp);
if (res == '-') {
// 对手输了,我们就赢了
cache[num][me][you] = '+';
return '+';
}
if (res == '0') {
ping = true;
}
}
}
// 如果能走到这里,那就不存在对手输的情况
// 那么是否存在平局的情况?
if (ping) {
cache[num][me][you] = '0';
return '0';
} else {
// 不存在对手输,也不平,那就是输了
cache[num][me][you] = '-';
return '-';
}
}
}
此类博弈类问题,要学会划分状态,一步一步地做。
十、压缩变换(模拟拿一半)
先看数据规模,直接模拟肯定超时。(果然,只能拿一半分,对我来说够啦!正解需要区间树,加快寻找不同数的种类数。)
import java.util.*;
public class Main {
public static void main(String[] args) {
Scanner scan = new Scanner(System.in);
Map<Integer, Integer> map = new HashMap<>();
int n = scan.nextInt();
int[] nums = new int[n];
for (int i = 0; i < n; i++) {
nums[i] = scan.nextInt();
}
// 如果这个数字没有出现过,刚将数字变换成它的相反数
// 如果数字出现过,则看它在原序列中最后的一次出现后面(且在当前数前面)出现了几种数字
// 用这个种类数替换原来的数字
int[] ans = new int[n];
for (int i = 0; i < n; i++) {
if (!map.containsKey(nums[i])) {
ans[i] = -nums[i];
} else {
int index = map.get(nums[i]);
int cnt = 0;
Set<Integer> tmp = new HashSet<>();
for (int j = index + 1; j < i; j++) {
if (tmp.contains(nums[j])) {
continue;
} else {
cnt++;
tmp.add(nums[j]);
}
}
ans[i] = cnt;
}
System.out.print(ans[i] + " ");
map.put(nums[i], i);
}
}
}