2020年 第11届 蓝桥杯 Java B组 第2次模拟赛真题详解及小结


  • 注意:部分代码及程序 源自 蓝桥杯 官网视频(历年真题解析) 郑未老师
  1. 2013年 第04届 蓝桥杯 Java B组 省赛真题详解及小结
  2. 2014年 第05届 蓝桥杯 Java B组 省赛真题详解及小结
  3. 2015年 第06届 蓝桥杯 Java B组 省赛真题详解及小结
  4. 2016年 第07届 蓝桥杯 Java B组 省赛真题详解及小结
  5. 2017年 第08届 蓝桥杯 Java B组 省赛真题详解及小结
  6. 2018年 第09届 蓝桥杯 Java B组 省赛真题详解及小结
  7. 2019年 第10届 蓝桥杯 Java B组 省赛真题详解及小结
  8. 2020年 第11届 蓝桥杯 Java B组 第1次模拟赛真题详解及小结(校内模拟)
  9. 2020年 第11届 蓝桥杯 Java B组 第2次模拟赛真题详解及小结
  10. 2020年 第11届 蓝桥杯 C/C++ B组 省赛真题详解及小结【第1场省赛 2020.7.5】【Java版】
  11. 2020年 第11届 蓝桥杯 Java B组 省赛真题详解及小结【第1场省赛 2020.7.5】
  12. 2020年 第11届 蓝桥杯 Java C组 省赛真题详解及小结【第1场省赛 2020.7.5】

目   录

一、12.5MB

二、最多边数

三、单词重排

解法一:全排列 + Set去重

解法二:dfs

四、括号序列

解法一:手工计算

解法二:递归

五、反倍数

六、凯撒加密

解法一:字符串.toCharArray()

解法二:add()函数

七、螺旋

八、摆动序列

解法1:dfs

优化1:修改递归式

优化2:改为递推

九、通电

十、植树

解法一

优化

小结


   模拟赛(第二轮)真题解析-官方讲解视频

一、12.5MB

【问题描述】

在计算机存储中,12.5MB是多少字节?

【答案提交】

这是一道结果填空的题,你只需要算出结果后提交即可。本题的结果为一个整数,在提交答案时只填写这个整数,填写多余的内容将无法得分。

【答案】:13107200

【解析】:1K=1024字节   ∴ 12.5*1024*1024

    

  • 1Byte(字节) = 8bit(位)   1K = 1024Byte(字节)   
  • 字节也叫baiByte,是计算机数据的基本存储单位,在电脑du里一个中zhi文字占两个字节。

二、最多边数

【问题描述】

一个包含有2019个结点的有向图,最多包含多少条边?(不允许有重边)

【答案提交】

这是一道结果填空的题,你只需要算出结果后提交即可。本题的结果为一个整数,在提交答案时只填写这个整数,填写多余的内容将无法得分。

【答案】:4074342

【解析】:任意两点组成边,边有向,一来一回算两条边。       所以是n中选2的组合数乘以2级n*(n-1)

           最大边数 = C_{n}^{2} * 2 ,∴ 最大边数 = ( 2019 * 2018 / 2 ) * 2 = 4074342

      计算器 计算 即可!

三、单词重排

【问题描述】

将LANQIAO中的字母重新排列,可以得到不同的单词,如LANQIAO、AAILNOQ等,注意这7个字母都要被用上,单词不一定有具体的英文意义。

请问,总共能排列出多少个不同的单词。

【答案提交】

这是一道结果填空的题,你只需要算出结果后提交即可。本题的结果为一个整数,在提交答案时只填写这个整数,填写多余的内容将无法得分。

【答案】:2520

【解析】:全排列 + 去重 + 计数

解法一:全排列 + Set去重

package simulationMatch_11_2020_2;

import java.util.HashSet;
import java.util.Set;

public class _03_单词重排 {

	static Set<String> set = new HashSet<String>(); // 不包含重复元素

	public static void main(String[] args) {

		char[] str = { 'L', 'A', 'N', 'Q', 'I', 'A', 'O' };
		f(str, 0);
		for(String x:set) {
			System.out.println(x);
		}
		System.out.println(set.size());
	}

	public static void f(char[] charArray, int k) {
		if (k == charArray.length) {
			String s = new String(charArray);
			set.add(s);
		}
		for (int i = k; i < charArray.length; i++) {
			char temp = charArray[i];
			charArray[i] = charArray[k];
			charArray[k] = temp;
			f(charArray, k + 1);
			temp = charArray[i];
			charArray[i] = charArray[k];
			charArray[k] = temp;
		}
	}

}

解法二:dfs

package simulationMatch_11_2020_2;

import java.util.HashSet;
import java.util.Set;

/**
 * @Author zhengwei
 * @Date 2020/5/17 5:32 PM
 * @Version 1.0
 */
public class _03_单词重排2 {
	private static char[] a = "LANQIAO".toCharArray();
	private static Set<String> ans = new HashSet<>();
	private static char[] tmp = new char[7];
	private static boolean[] vis = new boolean[7];

	public static void main(String[] args) {
		dfs(0);
		System.out.println(ans.size());
	}

	private static void dfs(int k) {
		if (k == 7) {
			ans.add(new String(tmp));
			return;
		}
		for (int i = 0; i < 7; i++) {
			if (!vis[i]) { // 没有被选入
				tmp[k] = a[i];
				vis[i] = true;
				dfs(k + 1); // 确定下一位
				vis[i] = false; // 回溯
			}
		}
	}
}

四、括号序列

【问题描述】

由1对括号,可以组成一种合法括号序列:()。

由2对括号,可以组成两种合法括号序列:()()、(())。

由4对括号组成的合法括号序列一共有多少种?

【答案提交】

这是一道结果填空的题,你只需要算出结果后提交即可。本题的结果为一个整数,在提交答案时只填写这个整数,填写多余的内容将无法得分。

【答案】:14

解法一:手工计算

统计“4对括号组成的合法括号序列”,可以分为4种情况:

  1. 括号不嵌套:()()()()
  2. 1个括号嵌套1个括号:(())()()、()(())()、()()(())、(())(())
  3. 1个括号嵌套2个括号:((()))()、()((()))、(()())()、()(()())
  4. 1个括号嵌套3个括号:(((())))、(()()())、(()(()))、((())())、((()()))

∴ 1+4+4+5

解法二:递归

典型的递归结构,每个位置有两种选择,要么左括号,要么右括号。

选左括号的条件:可选数>0 且 变化后存在的左括号数量始终大于等于已存在的右括号数量。

选右括号类似。

package simulationMatch_11_2020_2;

/**
 * @Author zhengwei
 * @Date 2020/5/17 5:42 PM
 * @Version 1.0
 */

public class _04_括号序列 {
	
	public static void main(String[] args) {
		System.out.println(solve(4, 4, 4));
	}

	/**
	 *
	 * @param n 对的数量
	 * @param l 剩余左括号数量
	 * @param r 剩余右括号的数量
	 * @return
	 */
	private static int solve(int n, int l, int r) {
		if (l == 0 && r == 0)
			return 1;
		int ans = 0;
		// # 要么选左括号,要么选右括号
		// # 选左括号的条件:l>0 且 变化后存在的左括号数量始终大于等于已存在的右括号数量
		if (l > 0 && n - (l - 1) >= n - r)
			ans += solve(n, l - 1, r);
		if (r > 0 && n - l >= n - (r - 1))
			ans += solve(n, l, r - 1);

		return ans;
	}
	
}

五、反倍数

【问题描述】

给定三个整数 a, b, c,如果一个整数既不是 a 的整数倍 也不是 b 的整数倍 还不是 c 的整数倍,则这个数称为反倍数。

请问在 1 至 n 中有多少个反倍数。

【输入格式】

输入的第一行包含一个整数 n。

第二行包含三个整数 a, b, c,相邻两个数之间用一个空格分隔。

【输出格式】

输出一行包含一个整数,表示答案。

【样例输入】

30

2 3 6

【样例输出】

10

【样例说明】

以下这些数满足要求:1, 5, 7, 11, 13, 17, 19, 23, 25, 29。

【评测用例规模与约定】

对于 40% 的评测用例,1 <= n <= 10000。

对于 80% 的评测用例,1 <= n <= 100000。

对于所有评测用例,1 <= n <= 1000000,1 <= a <= n,1 <= b <= n,1 <= c <= n。

【解析】:迭代 + check

package simulationMatch_11_2020_2;

import java.util.Scanner;

public class _05_反倍数 {

	public static void main(String[] args) {
		Scanner sc = new Scanner(System.in);
		int n = sc.nextInt();
		int a = sc.nextInt();
		int b = sc.nextInt();
		int c = sc.nextInt();
		int answer = 0;
		for (int i = 1; i <= n; i++) { // i < n + 1
			if (i % a != 0 && i % b != 0 && i % c != 0) {
				answer++;
			}
		}
		System.out.println(answer);
	}

}

六、凯撒加密

【问题描述】

给定一个单词,请使用凯撒密码将这个单词加密。

凯撒密码是一种替换加密的技术,单词中的所有字母都在字母表上向后偏移3位后被替换成密文。即a变为d,b变为e,...,w变为z,x变为a,y变为b,z变为c。

例如,lanqiao会变成odqtldr。

【输入格式】

输入一行,包含一个单词,单词中只包含小写英文字母。

【输出格式】

输出一行,表示加密后的密文。

【样例输入】

lanqiao

【样例输出】

odqtldr

【评测用例规模与约定】

对于所有评测用例,单词中的字母个数不超过100。

【解析】: 遍历 + 转换,再拼成字符串

解法一:字符串.toCharArray()

package simulationMatch_11_2020_2;

import java.util.Scanner;

public class _06_凯撒加密 {

	public static void main(String[] args) {
		Scanner sc = new Scanner(System.in);
		String str = sc.next();
		char[] temp = str.toCharArray();
		for (int i = 0; i < str.length(); i++) {
			if (temp[i] == 'x') {
				temp[i] = 'a';
			} else if (temp[i] == 'y') {
				temp[i] = 'b';
			} else if (temp[i] == 'z') {
				temp[i] = 'c';
			} else {
				temp[i] += 3;
			}
		}
		System.out.println(new String(temp)); // 字符数组转字符串
//		for (char x : temp) {
//			System.out.print(x + "");
//		}
	}

}

解法二:add()函数

package simulationMatch_11_2020_2;

import java.util.Scanner;

/**
 * @Author zhengwei
 * @Date 2020/5/17 5:51 PM
 * @Version 1.0
 */
public class _06_凯撒加密2 {
	private static char add(char letter) {
		if (letter < 'x')
			return (char) (letter + 3);
		else if (letter == 'x')
			return 'a';
		else if (letter == 'y')
			return 'b';
		else
			return 'c';
	}

	public static void main(String[] args) {
		Scanner sc = new Scanner(System.in);
		String s = sc.next();
		char[] ans = new char[s.length()];
		for (int i = 0; i < s.length(); i++) {
			ans[i] = add(s.charAt(i));
		}
		System.out.println(new String(ans));
	}
}

七、螺旋

【问题描述】

对于一个 n 行 m 列的表格,我们可以使用螺旋的方式给表格依次填上正整数,我们称填好的表格为一个螺旋矩阵。

例如,一个 4 行 5 列的螺旋矩阵如下:

1 2 3 4 5

14 15 16 17 6

13 20 19 18 7

12 11 10 9 8

【输入格式】

输入的第一行包含两个整数 n, m,分别表示螺旋矩阵的行数和列数。

第二行包含两个整数 r, c,表示要求的行号和列号。

【输出格式】

输出一个整数,表示螺旋矩阵中第 r 行第 c 列的元素的值。

【样例输入】

4 5

2 2

【样例输出】

15

【评测用例规模与约定】

对于 30% 的评测用例,2 <= n, m <= 20。

对于 70% 的评测用例,2 <= n, m <= 100。

对于所有评测用例,2 <= n, m <= 1000,1 <= r <= n,1 <= c <= m。

【解析】:按题意生成网格,再取单元格中的数据 

package simulationMatch_11_2020_2;

import java.util.Scanner;

public class _07_螺旋 {

	public static void main(String[] args) {
		Scanner sc = new Scanner(System.in);
		int n = sc.nextInt();
		int m = sc.nextInt();
		int r = sc.nextInt();
		int c = sc.nextInt();
		int[][] grid = new int[n][m];

		int num = 1;
		int up = 0, down = n, left = 0, right = m;
		while (true) {
			for (int col = left; col < right; col++) {
				grid[up][col] = num;
				num++;
			}
			up++;
			if (up == down)
				break;
			for (int row = up; row < down; row++) {
				grid[row][right - 1] = num;
				num++;
			}
			right--;
			if (left == right)
				break;
			for (int col = right - 1; col >= left; col--) {
				grid[down - 1][col] = num;
				num++;
			}
			down -= 1;
			if (up == down)
				break;
			for (int row = down - 1; row >= up; row--) {
				grid[row][left] = num;
				num++;
			}
			left++;
			if (left == right)
				break;
		}
		System.out.println(grid[r - 1][c - 1]);
	}

}
/**
int[][] array = new int[n][m];
int x = 1; // 递增数字
int index = 0; // 循环标志
int count = n / 2 + n % 2; // 循环结束判断标识
while (count > 0) {
	for (int i = index; i < m; i++) { // 第1行
		array[0][i] = x++;
	}
	for (int i = 1; i < n; i++) { // 第m列(最后一列)
		array[i][m - 1] = x++;
	}
	for (int i = m; i > 0; i--) { // 第n行(最后一行)
		array[n - 1][i - 1] = (x++ - 1);
	}
	for (int i = n - 1; i > 0; i--) { // 第1列
		array[i][0] = (x++ - 2);
	}
	index++;
	for (int i = 0; i < n; i++) {
		for (int j = 0; j < m; j++) {
			System.out.print(array[i][j] + " ");
		}
		System.out.println();
	}
	System.out.println("------");
	count--;
}
*/

八、摆动序列

【问题描述】

如果一个序列的奇数项都比前一项大,偶数项都比前一项小,则称为一个摆动序列。即 a[2i]<a[2i-1], a[2i+1]<a[2i]。

小明想知道,长度为 m,每个数都是 1 到 n 之间的正整数的摆动序列一共有多少个。

【输入格式】

输入一行包含两个整数 m,n。

【输出格式】

输出一个整数,表示答案。答案可能很大,请输出答案除以10000的余数。

【样例输入】

3 4

【样例输出】

14

【样例说明】

以下是符合要求的摆动序列:

2 1 2

2 1 3

2 1 4

3 1 2

3 1 3

3 1 4

3 2 3

3 2 4

4 1 2

4 1 3

4 1 4

4 2 3

4 2 4

4 3 4

【评测用例规模与约定】

对于 20% 的评测用例,1 <= n, m <= 5;

对于 50% 的评测用例,1 <= n, m <= 10;

对于 80% 的评测用例,1 <= n, m <= 100;

对于所有评测用例,1 <= n, m <= 1000。

解法1:dfs

多数人都能想到:第1位,可选为[2,n]
选定第1位(last1),开始选第2位,可选为[1,last1-1],对所有last1结果求和
选定第2位(last2),开始选第3位,可选为[last2+1,n],对所有last2结果求和
……
选定奇数位(last),开始选下一个偶数位,可选为[1,last-1],对所有last结果求和
选定偶数位(last),开始选下一个奇数位,可选为[last+1,n],对所有last结果求和
……

递归式为:
dfs(last, k) = Σdfs(i, k + 1) | k为奇数,i from 1 to last-1
dfs(last, k) = Σdfs(i, k + 1) | k为偶数,i from last+1 to n

而递归起点(选定第一位,可选是2 to n)也是一个循环:

for i in range(2, n + 1):
    ans = (ans + dfs(i, 1)) % MOD

这里面有大量重复子问题,所以可以记忆型递归;但只能过80%的数据,因为复杂度是O(N³)

package simulationMatch_11_2020_2;

import java.util.Scanner;

public class _08_摆动序列 {

	private static final int MOD = 10000;
	private static int[][] mem = new int[1000][1000];
	private static int n, m;

	// 第k个数确定为last时,序列总数是多少
	// param last:确定的最后一个数
	// param k:last是第k个
	private static int dfs(int last, int k) {
		if (k == m)
			return 1;
		if (mem[last][k] != 0)
			return mem[last][k];
		// k是奇数,k+1是偶数,偶数位比前一个小
		if ((k & 1) == 1)
			for (int i = 1; i < last; i++)
				mem[last][k] = (mem[last][k] + dfs(i, k + 1)) % MOD;
		else
			for (int i = last + 1; i < n + 1; i++)
				mem[last][k] = (mem[last][k] + dfs(i, k + 1)) % MOD;
		return mem[last][k];
	}

	public static void main(String[] args) {
		Scanner sc = new Scanner(System.in);
		m = sc.nextInt();
		n = sc.nextInt();
		int ans = 0;
		// 第一位可以选2到n
		for (int i = 2; i < n + 1; i++)
			ans = (ans + dfs(i, 1)) % MOD;
		System.out.println(ans);
	}

}
/**
int answer = 0;
int array[] = new int[m];
for (int i = 0; i < m; i++) {
	f(array); // 对数组元素进行赋值
	if(m % 2 == 1) { // m是奇数
		for (int k = 2; k < m; k += 2) {
			if (array[k - 1] < array[k] && array[k] < array[k + 1]) {
				answer++;
			}
		}
	} else { // m是偶数
		for (int k = 0; k < m; k += 2) {
			if (array[k - 1] < array[k] && array[k] < array[k + 1]) {
				answer++;
			}
		}
	}
}
System.out.println(answer % 10000);
*/

优化1:修改递归式

这种在递归中加总的递归形态,往往可以通过优化递归式来改进,将递归式变成有汇总 or 集合的意义,就可以减少一层循环,从而把复杂度变为O(N²)

可以从递归起点的那个循环考虑,我们既然要加总第1位选2到n的这若干种情况的结果,为什么不用dfs(2,1)直接表示第一位选[2,n]这所有情况的和呢?这就是集合的概念了。

更通用地:
k为奇数时,dfs(x,k)表示第k位选[x,n]这若干种情况的种数和;
k为偶数时,dfs(x,k)表示第k位选[1,x]这若干种情况的种数和;
但是怎么拆呢?技巧是拆成一个元素+一个(少了该元素的)小集合,小集合动一个变量(往往代表规模),那拆出来的元素往往可以转换成另外一个集合。

就本题来说,k为奇数时,dfs(x,k)可以这样拆:
在这里插入图片描述

k为偶数时,dfs(x,k)可以这样拆:

在这里插入图片描述 
那么可得递归式:

dfs(x, k) = dfs(x+1, k) + dfs(x-1,k+1) | k为奇数
dfs(x, k) = dfs(x-1, k) + dfs(x+1,k+1) | k为偶数

递归起点为:ans = dfs(2, 1)

package simulationMatch_11_2020_2;

import java.util.Scanner;

public class _08_摆动序列2_优化1 {
	private static final int MOD = 10000;
	private static int[][] mem = new int[1000][1000];
	private static int n, m;

	// 第k个数确定为last时,序列总数是多少
	// param last:确定的最后一个数
	// param k:last是第k个
	private static int dfs(int last, int k) {
		if (last < 1 || last > n)
			return 0;
		if (k == m) {
			// 奇数,return 大于等于last的个数;偶数,小于等于last的个数
			if ((k & 1) == 1) {
				mem[last][k] = n - last + 1;
			} else {
				mem[last][k] = last;
			}
			return mem[last][k];
		}

		if (mem[last][k] != 0)
			return mem[last][k];
		if ((k & 1) == 1)
			// 注意看这里的拆解:当前函数的含义是第k位选last~n的序列数总和,切成两块
			// 1:第k位选(last+1)到n的序列数总和,函数含义不变,第一个参数变为last+1==》dfs(last + 1, k)
			// 2:第k位固定为last,那么第k+1位的选择是从1到last-1(因k+1是偶数)==》dfs(last - 1, k + 1)
			mem[last][k] = (dfs(last + 1, k) + dfs(last - 1, k + 1)) % MOD;
		else // 偶数
			mem[last][k] = (dfs(last - 1, k) + dfs(last + 1, k + 1)) % MOD;
		return mem[last][k];
	}

	public static void main(String[] args) {
		Scanner sc = new Scanner(System.in);
		m = sc.nextInt();
		n = sc.nextInt();
		System.out.println(dfs(2, 1));
	}

}

优化2:改为递推

为什么还要优化呢?因为这样递归层次太深,会超出栈空间限制。

递推是递归的逆过程,因此我们观察上述递归函数的出口,就知道怎么初始化dp数组,再按照与递归相逆的顺序逐步生成递推数组。

递归出口:

if (k == m) {
  // 奇数,return 大于等于last的个数;偶数,小于等于last的个数
  if ((k & 1) == 1) {
    mem[last][k] = n - last + 1;
  } else {
    mem[last][k] = last;
  }
  return mem[last][k];
}

转变为数组初始化:

//初始化最后一列
for (int x = 1; x < n + 1; x++) {
  if ((m & 1) == 1)
    dp[x][m] = n - x + 1;
  else
    dp[x][m] = x;
}

递归中,列数k是逐渐增大,直至最后一列,那么递推中,应该从最后一列反推到第一列。

package simulationMatch_11_2020_2;

import java.util.Scanner;

public class _08_摆动序列2_优化2 {
	private static final int MOD = 10000;
	private static int[][] dp;
	private static int n, m;

	public static void main(String[] args) {
		Scanner sc = new Scanner(System.in);
		m = sc.nextInt();
		n = sc.nextInt();
		dp = new int[n + 1][m + 1];
		// 初始化最后一列
		for (int x = 1; x < n + 1; x++) {
			if ((m & 1) == 1)
				dp[x][m] = n - x + 1;
			else
				dp[x][m] = x;
		}

		for (int k = m - 1; k > 0; k--) {
			// # 奇数,x从大到小遍历
			if ((k & 1) == 1)
				for (int x = n; x > 0; x--)
					dp[x][k] = ((x + 1 <= n ? dp[x + 1][k] : 0) + dp[x - 1][k + 1]) % MOD;
			// # 偶数,x从小到大遍历
			else
				for (int x = 1; x < n + 1; x++)
					dp[x][k] = (dp[x - 1][k] + (x + 1 <= n ? dp[x + 1][k + 1] : 0)) % MOD;
		}
		System.out.println(dp[2][1]);

	}

}

九、通电

【问题描述】

2015年,全中国实现了户户通电。作为一名电力建设者,小明正在帮助一带一路上的国家通电。

这一次,小明要帮助 n 个村庄通电,其中 1 号村庄正好可以建立一个发电站,所发的电足够所有村庄使用。

现在,这 n 个村庄之间都没有电线相连,小明主要要做的是架设电线连接这些村庄,使得所有村庄都直接或间接的与发电站相通。

小明测量了所有村庄的位置(坐标)和高度,如果要连接两个村庄,小明需要花费两个村庄之间的坐标距离加上高度差的平方,形式化描述为坐标为 (x_1, y_1) 高度为 h_1 的村庄与坐标为 (x_2, y_2) 高度为 h_2 的村庄之间连接的费用为

sqrt((x_1-x_2)*(x_1-x_2)+(y_1-y_2)*(y_1-y_2))+(h_1-h_2)*(h_1-h_2)。

在上式中 sqrt 表示取括号内的平方根。请注意括号的位置,高度的计算方式与横纵坐标的计算方式不同。

由于经费有限,请帮助小明计算他至少要花费多少费用才能使这 n 个村庄都通电。

【输入格式】

输入的第一行包含一个整数 n ,表示村庄的数量。

接下来 n 行,每个三个整数 x, y, h,分别表示一个村庄的横、纵坐标和高度,其中第一个村庄可以建立发电站。

【输出格式】

输出一行,包含一个实数,四舍五入保留 2 位小数,表示答案。

【样例输入】

4

1 1 3

9 9 7

8 8 6

4 5 4

【样例输出】

17.41

【评测用例规模与约定】

对于 30% 的评测用例,1 <= n <= 10;

对于 60% 的评测用例,1 <= n <= 100;

对于所有评测用例,1 <= n <= 1000,0 <= x, y, h <= 10000。

【解析】:最小生成树

可以说是最小生成树的裸题了——连通==树,代价最小==最小生成
用Kruskal算法一气呵成。
不过要进行一些处理:
每个村庄看做是一个顶点,编号存储
两两组成边,用费用做边的权重

做好数据处理,然后就是排序,从小到大把边添加到最小生成树的边集(也不用真正添加,符合的边把代价累加就行,不符合的边忽略)

符合不符合,当然要用并查集了。

并查集 一定要掌握。

package simulationMatch_11_2020_2;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Scanner;

public class _09_通电 {

	public static void main(String[] args) {
		Scanner sc = new Scanner(System.in);
		int n = sc.nextInt();
		// 接下来 n 行,每个三个整数 x, y, h
		int[][] data = new int[n + 1][3];
		for (int i = 1; i <= n; i++) {
			data[i][0] = sc.nextInt();
			data[i][1] = sc.nextInt();
			data[i][2] = sc.nextInt();
		}
		// # 将原始数据处理成边集,每两个点一条边,计算代价
		List<Edge> edges = new ArrayList<>(n * n);
		for (int i = 1; i <= n - 1; i++) {
			for (int j = i + 1; j <= n; j++) {
				edges.add(new Edge(i, j, cost(data[i], data[j])));
			}
		}
		// 对边集排序
		Collections.sort(edges);
		// 初始化并查集工具
		UF uf = new UF(n);

		int edge_cnt = 0;
		double ans = 0;
		for (Edge e : edges) {
			if (uf.find(e.x) != uf.find(e.y)) {
				uf.union(e.x, e.y);
				edge_cnt++;
				ans += e.cost;
				if (edge_cnt == n - 1)
					break;
			}
		}
		System.out.printf("%.2f", ans);
	}

	/**
	 * 封装并查集操作
	 */
	private static class UF {
		int n;
		int[] parent;

		public UF(int n) {
			this.n = n;
			parent = new int[n + 1];
			for (int i = 1; i <= n; i++) {
				parent[i] = i;
			}
		}

		int find(int x) {
			if (parent[x] == x)
				return x;
			HashSet<Integer> path = new HashSet<>();
			while (parent[x] != x) {
				path.add(x);
				x = parent[x];
			}
			for (Integer xx : path) {
				parent[xx] = x;
			}
			return x;
		}

		void union(int a, int b) {
			parent[find(b)] = find(a);
		}
	}

	/**
	 * 计算a,b两个村庄的建设代价
	 * 
	 * @param a
	 * @param b
	 * @return
	 */
	private static double cost(int[] a, int[] b) {
		return Math.sqrt((a[0] - b[0]) * (a[0] - b[0]) + 
				(a[1] - b[1]) * (a[1] - b[1])) + (a[2] - b[2]) * (a[2] - b[2]);
	}

	/**
	 * 注意实现Comparable接口
	 */
	private static class Edge implements Comparable<Edge> {
		int x;
		int y;
		double cost;

		public Edge(int x, int y, double cost) {
			this.x = x;
			this.y = y;
			this.cost = cost;
		}

		@Override
		public int compareTo(Edge o) {
			return this.cost < o.cost ? -1 : (this.cost == o.cost ? 0 : 1);
		}
	}

}

十、植树

【问题描述】

小明和朋友们一起去郊外植树,他们带了一些在自己实验室精心研究出的小树苗。

小明和朋友们一共有 n 个人,他们经过精心挑选,在一块空地上每个人挑选了一个适合植树的位置,总共 n 个。他们准备把自己带的树苗都植下去。

然而,他们遇到了一个困难:有的树苗比较大,而有的位置挨太近,导致两棵树植下去后会撞在一起。

他们将树看成一个圆,圆心在他们找的位置上。如果两棵树对应的圆相交,这两棵树就不适合同时植下(相切不受影响),称为两棵树冲突。

小明和朋友们决定先合计合计,只将其中的一部分树植下去,保证没有互相冲突的树。他们同时希望这些树所能覆盖的面积和(圆面积和)最大。

【输入格式】

输入的第一行包含一个整数 n ,表示人数,即准备植树的位置数。

接下来 n 行,每行三个整数 x, y, r,表示一棵树在空地上的横、纵坐标和半径。

【输出格式】

输出一行包含一个整数,表示在不冲突下可以植树的面积和。由于每棵树的面积都是圆周率的整数倍,请输出答案除以圆周率后的值(应当是一个整数)。

【样例输入】

6

1 1 2

1 4 2

1 7 2

4 1 2

4 4 2

4 7 2

【样例输出】

12

【评测用例规模与约定】

对于 30% 的评测用例,1 <= n <= 10;

对于 60% 的评测用例,1 <= n <= 20;

对于所有评测用例,1 <= n <= 30,0 <= x, y <= 1000,1 <= r <= 1000。

解法一

【解析】:

每个圆可以选也可以不选,但不知道哪种决策结果最大,只能先考虑暴力搜索每种情况,总可选数为2的n次方的深度优先搜索。

某一个圆在准备选入的时候,可以判断是否与之前已入选的圆冲突,如果冲突了,这条分支就可以不继续了,这可以视为剪枝;但是判断是否冲突的check函数要遍历已入选的圆,复杂度依然高;

【代码1】在n等于30的时候会吃不消。

package simulationMatch_11_2020_2;

import java.util.Scanner;

public class _10_植树 {
	static Scanner sc = new Scanner(System.in);
	static int n;
	static int ans = 0;
	static Tree[] trees;
	static int[][] adjaTable;

	public static void main(String[] args) {
		n = sc.nextInt();
		initTrees();
		initAdjaTable();
		dfs(0, 0);
		System.out.println(ans);
	}

	/**
	 * 初始化邻接矩阵
	 */
	private static void initAdjaTable() {
		adjaTable = new int[n][n];
		for (int i = 0; i < n - 1; i++) {
			for (int j = i + 1; j < n; j++) {
				if (trees[i].intersected(trees[j])) {
					adjaTable[i][j] = 1;
					adjaTable[j][i] = 1;
				}
			}
		}
	}

	/**
	 * 初始化每棵树并加入数组
	 */
	private static void initTrees() {
		trees = new Tree[n];
		for (int i = 0; i < n; i++) {
			trees[i] = new Tree(sc.nextInt(), sc.nextInt(), sc.nextInt());
		}
	}

	private static void dfs(int sum, int index) {
		// 边界
		if (index == n) {
			ans = Math.max(ans, sum);
			return;
		}
		// 2.选这棵树(是有条件的)
		if (ok(index)) {
			trees[index].selected = true;
			int r = trees[index].r;
			dfs(sum + r * r, index + 1);
			trees[index].selected = false; // 回溯
		}
		// 1.不选当前这棵树
		trees[index].selected = false;
		dfs(sum, index + 1);

	}

	private static boolean ok(int index) {
		for (int i = 0; i < index; i++) {
			// i被选入,且i与当前准备入选的index相交,则index代表的树不能入选
			if (trees[i].selected && adjaTable[i][index] == 1)
				return false;
		}
		return true;
	}

	private static class Tree {
		int x, y, r;
		boolean selected; // 是否入选

		public Tree(int x, int y, int r) {
			this.x = x;
			this.y = y;
			this.r = r;
		}

		/** 与另一颗树是否相交 */
		public boolean intersected(Tree other) {
			int dis = (this.x - other.x) * (this.x - other.x) + (this.y - other.y) * (this.y - other.y);
			return dis < (this.r + other.r) * (this.r + other.r);
		}
	}

}

优化

优化的关键点在于用类似贪心的办法(但不是贪心):将圆按半径从大到小排序,这样优先考虑半径大的圆的选与不选问题;另外把“选”这个分支放在“不选”这个分支前面执行,这样我们相信会尽早地遇到最优解。

基于这个假设,在递归之前我们可以以O(N)的复杂度存储所有圆的“半径的平方”的后缀和,计为数组s;在递归函数dfs中,参数sum代表index之前的选择策略所得到的sum,s[index]代表包括index索引及之后续所有圆的半径的平方和,如果sum+s[index]小于等于已经求得的ans,那就不必进行任何后续的选择试探了,可立即退出递归。

实测,【代码2】在n=30时能秒出结果。

package simulationMatch_11_2020_2;

import java.time.Duration;
import java.time.Instant;
import java.util.Arrays;
import java.util.Scanner;

public class _10_植树2 {
	static Scanner sc = new Scanner(System.in);
	static int n;
	static int ans = 0;
	static Tree[] trees;
	static int[][] adjaTable;
	/** 半径的平方后缀和 */
	static int[] suffix;

	public static void main(String[] args) {
		// Instant now = Instant.now();
		n = sc.nextInt();
		initTrees();
		initSuffix();
		initAdjaTable();
		dfs(0, 0);
		System.out.println(ans);
		// System.err.println("Duration:" + Duration.between(now,
		// Instant.now()).toMillis());
	}

	private static void initSuffix() {
		suffix = new int[n];

		suffix[n - 1] = trees[n - 1].pow_r;
		for (int i = n - 2; i >= 0; i--) {
			// 后缀和加当前项的平方
			suffix[i] = suffix[i + 1] + trees[i].pow_r;
		}
	}

	/**
	 * 初始化邻接矩阵
	 */
	private static void initAdjaTable() {
		adjaTable = new int[n][n];
		for (int i = 0; i < n - 1; i++) {
			for (int j = i + 1; j < n; j++) {
				if (trees[i].intersected(trees[j])) {
					adjaTable[i][j] = 1;
					adjaTable[j][i] = 1;
				}
			}
		}
	}

	/**
	 * 初始化每棵树并加入数组
	 */
	private static void initTrees() {
		trees = new Tree[n];
		for (int i = 0; i < n; i++) {
			trees[i] = new Tree(sc.nextInt(), sc.nextInt(), sc.nextInt());
		}
		// !!!!排序
		Arrays.sort(trees);
	}

	private static void dfs(int sum, int index) {
		// 边界
		if (index == n) {
			ans = Math.max(ans, sum);
			return;
		}
		// !!!如果index之前的sum加上自index开始的半径的平方和小于ans,则没必要继续
		if (sum + suffix[index] <= ans)
			return;
		// 2.选这棵树(是有条件的)
		if (ok(index)) {
			trees[index].selected = true;
			dfs(sum + trees[index].pow_r, index + 1);
			trees[index].selected = false; // 回溯
		}
		// 1.不选当前这棵树
		trees[index].selected = false;
		dfs(sum, index + 1);

	}

	private static boolean ok(int index) {
		for (int i = 0; i < index; i++) {
			// i被选入,且i与当前准备入选的index相交,则index代表的树不能入选
			if (trees[i].selected && adjaTable[i][index] == 1)
				return false;
		}
		return true;
	}

	private static class Tree implements Comparable<Tree> {
		int x, y, r, pow_r;
		boolean selected; // 是否入选

		public Tree(int x, int y, int r) {
			this.x = x;
			this.y = y;
			this.r = r;
			pow_r = r * r;
		}

		/** 与另一颗树是否相交 */
		public boolean intersected(Tree other) {
			int dis = (this.x - other.x) * (this.x - other.x) + (this.y - other.y) * (this.y - other.y);
			return dis < (this.r + other.r) * (this.r + other.r);
		}

		@Override
		public int compareTo(Tree o) {
			return this.r - o.r;
		}
	}
}

小结

Python组 第9题 练功

扩展多少步达到目标,一般用bfs求解。

bfs、dfs:走过的路 不再走。

BufferedWriter:对输出进行优化。缓冲式输出,内存积累到一定量,才向控制台进行输出。

猜你喜欢

转载自blog.csdn.net/weixin_44949135/article/details/108203734