最近想要参加今年的CodeM竞赛,所以把去年的题做了一遍,顺便写个题解,做个记录。
资格赛有A-F,共6道题,资格赛不限时,而且只要完成一道题就可以了。
A、数码
题意:给定两个整数 l 和 r ,对于所有满足1 ≤ l ≤ x ≤ r ≤ 10^9 的 x ,把 x 的所有约数全部写下来。对于每个写下来的数,只保留最高位的那个数码。求1~9每个数码出现的次数。
1 4
4 2 1 1 0 0 0 0 0
这道题只要照着题目意思写代码,就可以通过50%的样例。但是,因为时间复杂度太大,并不能通过所有样例。
解题思路:
这道题一个很关键的点是,在[1,x]中,能够整除y的数有x/y个。
所以,将求[left,right]的问题,拆成求[1,right]-[1,left-1]的问题就变得简单很多。
下面是计算[1,x]的代码:
private static void compute(long[] a, int x) {
for (int i = 1; i <= 9; i++) { // i为约数y的最高位数字
for (long j = 1; i * j <= x; j *= 10) { //构造以i为最高位数字的约数
//约数y 从 [i*j,min((i+1)*j-1,x)],[1,1],[10,19],[100,199]...
// 例如:[10,19](i=1,j=10,x>19)
long start = i * j;
long end = Math.min((i + 1) * j - 1, x);
long slip;
for (long y = start; y <= end; y += slip) {
long mul = x / y; //[1,x]中能够整除y的个数为x/y
long remain = x - mul * y; //大于y却不能整除y的有remain个
//slip为在[start,end]区间中与y值具有相同mul的值的个数
slip = 1 + Math.min(remain / mul, end - y);
a[i] += mul * slip;
}
}
}
}
B、音乐研究
题意:在第二个数组中找到一个长度和第一个数组相等且是连续的子序列,使得它们的 difference 最小。两个等长数组的 difference 定义为:difference = SUM(a[i] - b[i])2 (1 ≤ i ≤ n)
2 1 2 4 3 1 2 4
0
这道题比较简单,用两个for循环就可以解决。
int min = Integer.MAX_VALUE;
for (int i = 0; i < len2 - len1; i++) {
int sum = 0;
int k = i;
for (int j = 0; j < len1; j++) {
sum += (a[j] - b[k]) * (a[j] - b[k]);
k++;
}
if (min > sum) {
min = sum;
}
}
System.out.println(min);
C、锦标赛
题意:比赛有 n 个人参加(其中 n 为2的幂),每个参赛者根据资格赛和预赛、复赛的成绩,会有不同的积分。比赛采取锦标赛赛制,分轮次进行,设某一轮有m 个人参加,那么参赛者会被分为 m/2 组,每组恰好 2 人,m/2 组的人分别厮杀。我们假定积分高的人肯定获胜,若积分一样,则随机产生获胜者。获胜者获得参加下一轮的资格,输的人被淘汰。重复这个过程,直至决出冠军。现在请问,参赛者小美最多可以活到第几轮(初始为第0轮,小美是第一个参赛者)?
4 4 1 2 3
2
先求比小美积分少或者积分与小美一样的人数(这里设为n-1),小美最多能在这部分人中到达最后一轮,log2(n)是小美最多能够活到的轮数。在java中求log2(n),需要求log(n)/log(2)。
int n = sc.nextInt();
int[] score = new int[n];
score[0] = sc.nextInt();
int tag = score[0];
int greater = 1;
for (int i = 1; i < n; i++) {
score[i] = sc.nextInt();
if (score[i] <= tag) {
greater += 1;
}
}
//int after=(int)(Math.log(greater)/Math.log(2));
int after = 0;
while (greater > 1) {
greater /= 2;
after += 1;
}
System.out.println(after);
D、送外卖
题意:给出n个点,从0到n-1编号,从i号点出发按照方案a可以到达i+ai号点,按照方案b可以到达i+bi号点,不允许走出这n个点,你需要判断是否存在从0号点出发到达n-1号点的方案,如果不存在输出“No solution!”,否则输出字典序最小的方案,如果字典序最小的方案长度无限,则输出“Infinity!”。
7 5 -3 6 5 -5 -1 6 -6 1 4 -2 0 -2 0
abbbb
这是一道我连题目都看不懂的题,下面解释一下例子的意思。
初始时从0号出发,选择a0,则到达0+a0=5号点,在5号选择b5,则到达5+b5=5-2=3号点,在3号点选择b3,到达3-2=1号点,在1号点选择b1,到达1+1=2号点,在2号点选择b2,到达2+b2=6,即到达n-1号点,所以,输出的最小字典序方案为abbbb。
解题思路:首先使用一个数组G将所有可以到达t(t = 0~n-1)点的点都存起来,然后使用bfs从后向前遍历G,求得一个vis数组,若vis[0]被访问过,则说明存在一条从0到n-1的路径,否则无解。从起点0开始根据vis求字典序最小的路径,先查看a方案是否是可以到达n-1点的方案,若是,再查看a方案到达的点是否被再次访问过,若是则说明路径中存在环路,则输出“Infinity!”。同理,对方案b也一样。
下面是代码:
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
while (sc.hasNext()) {
int n = sc.nextInt();
int[] a = new int[n];
int[] b = new int[n];
String[] G = new String[n];
String str = "";
int[] vis = new int[n];
int[] vis2 = new int[n];
for (int i = 0; i < n; i++)
G[i] = "\t";
for (int i = 0; i < n; i++) {
a[i] = sc.nextInt();
int t = i + a[i];
if (t >= 0 && t < n) {
//G存储可以到达t点的所有点(t = 0~n-1)
G[t] += i + "\t";
}
}
for (int i = 0; i < n; i++) {
b[i] = sc.nextInt();
int t = i + b[i];
if (t >= 0 && t < n) {
G[t] += i + "\t";
}
}
//System.out.println(Arrays.toString(G));
//从后向前使用广度优先搜索,求搜索路径
rev_bfs(n - 1, G, vis);
if (vis[0] == 0) {
//从后向前搜索,未能到达初始点0
System.out.println("No solution!");
} else {
//能够到达初始点0,则求最小字典序方案
int p = 0;
vis2[0] = 1; //前向访问数组
boolean infflag = false;
for (int x = 0; x != n - 1 && !infflag; ) {
int nxt = x + a[x];
if (nxt >= 0 && nxt < n && vis[nxt] == 1) {
if (vis2[nxt] == 0) {
vis2[nxt] = 1;
str += 'a';
} else {
//若已访问过,说明存在环,使得方案无限
infflag = true;
}
x = nxt;
} else {
nxt = x + b[x];
if (nxt >= 0 && nxt < n && vis[nxt] == 1) {
if (vis2[nxt] == 0) {
vis2[nxt] = 1;
str += 'b';
} else {
infflag = true;
}
} else {
System.out.println("No solution!");
}
x = nxt;
}
}
System.out.println(infflag ? "Infinity!" : str);
}
}
}
private static void rev_bfs(int p, String[] G, int[] vis) {
Queue q = new ArrayDeque<>();
vis[p] = 1;
q.add(p);
while (!q.isEmpty()) {
//System.out.println(q.toString());
int x = q.poll();
String[] str = G[x].trim().split("\t");
for (int i = 0; i < str.length; i++) {
int t;
try {
t = Integer.valueOf(str[i]);
} catch (Exception e) {
continue;
}
if (vis[t] == 0) {
vis[t] = 1;
q.add(t);
}
}
}
}
E、围棋
解题思路:根据题意模拟。这里的三个错误:miss 1 是重复放子,比较好盘判断,只要棋盘上相应位置已经存在棋子,再想要将棋子放到那里,就是miss 1。miss 2 是将棋子放在禁放位上,即,将棋子放入后,周围环绕着它的都是与它不同色的棋子,除非,将棋子放入后可以将它周围一个异色棋子给拿出来(因为这个异色棋子周围环绕的都是和它异色的棋子),用DFS检查这个棋子周围是否都是异色棋子。miss 3 是判断当前棋盘局面是否和历史棋盘局面一样,可以利用hash判断当前棋盘局面是否出现过。
package CodeM;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Scanner;
class hascode {
long has0;
long has1;
public hascode(long has0, long has1) {
this.has0 = has0;
this.has1 = has1;
}
}
public class GameGo_2017 {
static int[][] dir = {{-1, 0}, {0, -1}, {1, 0}, {0, 1}};
static int goNum = 19;
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
while (n-- > 0) {
int Num = sc.nextInt();
char[][] go = new char[goNum][goNum];
for (int i = 0; i < go.length; i++) {
// inital go board
for (int j = 0; j < go.length; j++)
go[i][j] = '.';
}
//use st to store the hashcode of go board,
// to compare the next go board and the past go board.
ArrayList st = new ArrayList<>();
check(go, st);
while (Num-- > 0) {
String op = sc.next();
int x = sc.nextInt();
int y = sc.nextInt();
x -= 1;
y -= 1;
//System.out.println("first : "+op);
char a = op.charAt(0);
char b = a == 'B' ? 'W' : 'B';
if (go[x][y] != '.') {
//exist one point.
System.out.println("miss 1");
} else {
//use nextgo to get the next step
//if this step can be done then cur equals to nextgo
//otherwise print the mistake.
char[][] nextgo = new char[goNum][goNum];
copy(nextgo, go);
/*System.out.println("==============================");
for (int i = 0; i < goNum; i++) {
System.out.println(Arrays.toString(nextgo[i]));
}*/
nextgo[x][y] = a;
//use vis[][] to distinguish one poist is visited or not.
int[][] vis = new int[goNum][goNum];
//check this step is conformed to the 2nd or not
//that is whether exist a or not,if not that is the 2nd mistake.
boolean isOk = dfs(x, y, a, vis, nextgo);
for (int i = 0; i < dir.length; i++) {
int tx = x + dir[i][0];
int ty = y + dir[i][1];
//!dfs(tx,ty,b,vis,nextgo) means there are all a around [tx,ty]
//according the go rules,must pick the go point.
if (tx >= 0 && tx < goNum && ty >= 0 && ty < goNum &&
vis[tx][ty] == 0 && nextgo[tx][ty] == b
&& !dfs(tx, ty, b, vis, nextgo)) {
pick(tx, ty, b, nextgo);
isOk = true;
}
}
if (isOk) {
// if this step will not appear the first two mistakes
// now check if the same as the the past one go board.
// use hashcode to compare.
if (check(nextgo, st)) {
//if this step is pass the check,then the cur is
// the same as the next
copy(go, nextgo);
} else System.out.println("miss 3");
} else System.out.println("miss 2" );
}
}
//show();
for (int i = 0; i < goNum; i++) {
for (int j = 0; j < goNum; j++) {
System.out.print(go[i][j]);
}
System.out.println();
}
}
}
private static boolean dfs(int x, int y, char op, int[][] vis, char[][] next) {
//check whether this point is surrounded by different points
if (next[x][y] == '.') return true;
if (next[x][y] != op) return false;
vis[x][y] = 1;
boolean isOk = false;
for (int i = 0; i < dir.length; i++) {
int tx = x + dir[i][0];
int ty = y + dir[i][1];
if (tx >= 0 && tx < goNum && ty >= 0 && ty < goNum && vis[tx][ty] == 0) {
isOk |= dfs(tx, ty, op, vis, next);
}
}
return isOk;
}
private static void pick(int x, int y, char op, char[][] next) {
next[x][y] = '.';
for (int i = 0; i < dir.length; i++) {
int tx = x + dir[i][0];
int ty = y + dir[i][1];
if (tx >= 0 && tx < goNum && ty >= 0 && ty < goNum && next[tx][ty] == op)
pick(tx, ty, op, next);
}
}
private static boolean check1(char[][] next, ArrayList st) {
long has1 = 0, has2 = 0;
for (int i = 0; i < goNum; i++) {
for (int j = 0; j < goNum; j++) {
has1 = has1 * 131 + next[i][j];
has2 = has2 * 137 + next[i][j];
}
}
for (hascode has : st) {
if (has1 == has.has0 && has2 == has.has1)
return false;
}
hascode hasCode = new hascode(has1, has2);
st.add(hasCode);
return true;
}
private static boolean check(char[][] next, ArrayList st) {
long has1 = 0;
for (int i = 0; i < goNum; i++) {
for (int j = 0; j < goNum; j++) {
has1 = has1 * 131 + next[i][j];
//has2 = has2 * 137 + next[i][j];
}
}
if (st.contains(has1)){
return false;
}
//hascode hasCode = new hascode(has1, has2);
st.add(has1);
return true;
}
private static void copy(char[][] next, char[][] go) {
for (int i = 0; i < goNum; i++)
for (int j = 0; j < goNum; j++)
next[i][j] = go[i][j];
}
}
F、优惠券
题意:按照时间顺序给出m条记录,每条记录可能是购买了x号优惠券,可能是使用了x号优惠券,也可能是未知,要求出最大的s使得第1到第s-1条记录是正确的,如果所有记录都正确那么输出-1。
这道题除了题目的提到的规则外,还有一个隐形规则:在优惠券没有使用的情况下,不允许重复购买相同编号的优惠券。这意味着在某编号优惠券数量大于1时,需要在这两个操作之间找到一个未知操作,作为优惠券的使用操作,使拥有的优惠券数量都不大于1.
package CodeM;
import java.util.ArrayList;
import java.util.Scanner;
public class coupon_2017ans {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int maxN = 100005;
int m = sc.nextInt();
int res = -1;
ArrayList mark = new ArrayList<>();
int[] cnt = new int[maxN];
int[] la = new int[maxN];
for (int i = 1; i <= m; i++) {
String op = sc.next();
if (op.equals("?")) {
mark.add(i);
continue;
}
int x = sc.nextInt();
cnt[x] += (op.equals("I") ? 1 : -1);
if (cnt[x] < 0 || cnt[x] > 1) {
int t = find(la[x], mark);
if (t == -1) {
res = i;
break;
}
mark.remove(t);
cnt[x] = cnt[x] < 0 ? 0 : 1;
//cnt[x]=Math.min(Math.max(cnt[x],0),1);
}
la[x] = i;
}
System.out.println(res);
}
private static int find(int la, ArrayList mark) {
if (mark.size() == 0) return -1;
int low = 0, high = mark.size() - 1;
while (low < high) {
int mid = (low + high) / 2;
int t = mark.get(mid);
if (t <= la) {
low = mid + 1;
} else {
high = mid;
}
}
//int res=low==mark.size()?-1:low;
int res;
if (mark.get(low) > la) {
res = low;
} else res = -1;
return res;
}
}
花了好几天终于把这篇文给写完了,同时也做完了这几道题。
下面来做个总结。
A)顺着题意写代码是本能,但是需要考虑时间复杂度的问题,求模操作需要耗费大量时间,可以考虑通过构造约数而不是寻找约数来减少时间开销;
B)按照本能写代码,庆幸时间未超出范围;
C)本质上是一个简单数学题;
D)这是一个题目都看不懂的题,找了许多资料,终于知道要求了。求路径还是需要搜索算法,可以是BFS也可以是DFS,当然还需要考虑前向后向的问题。这个题在利用搜索算法求路径后,又增加了求字典序最小的路径,这又需要遍历可行路径求其最优解;
E)因为相同棋子相邻会合并成团,所以需要用到DFS求这个团周围的棋子分布,这个题让我觉得比较新奇的是对二维数组使用hash算法,计算数组hash值,通过比较hash值来判断当前棋盘局面与历史棋盘局面是否相同。
F)这又是一个我无法看出题意的题。在求上次优惠券操作后的最近一次未知操作的编号时,我自己写了一个二分查找用来查找不小于la的最小值下标。刚写完的时候做了测试,是可以用的,后来发现里几个比较大的问题,还花了挺多时间来调这个程序。主要是忽略t==la时的操作,还有缺少超出mark范围的判断。
参考: